[
  {
    "path": ".clang-format",
    "content": "---\n# This file is centrally managed in https://github.com/<organization>/.github/\n# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in\n# the above-mentioned repo.\n\n# Generated from CLion C/C++ Code Style settings\nBasedOnStyle: LLVM\nAccessModifierOffset: -2\nAlignAfterOpenBracket: DontAlign\nAlignConsecutiveAssignments: false\nAlignOperands: Align\nAllowAllArgumentsOnNextLine: false\nAllowAllConstructorInitializersOnNextLine: false\nAllowAllParametersOfDeclarationOnNextLine: false\nAllowShortBlocksOnASingleLine: Always\nAllowShortCaseLabelsOnASingleLine: false\nAllowShortFunctionsOnASingleLine: All\nAllowShortIfStatementsOnASingleLine: WithoutElse\nAllowShortLambdasOnASingleLine: All\nAllowShortLoopsOnASingleLine: true\nAlignTrailingComments: false\nAlwaysBreakAfterReturnType: All\nAlwaysBreakTemplateDeclarations: MultiLine\nBreakBeforeBraces: Custom\nBraceWrapping:\n  AfterCaseLabel: false\n  AfterClass: false\n  AfterControlStatement: Never\n  AfterEnum: false\n  AfterFunction: false\n  AfterNamespace: false\n  AfterObjCDeclaration: false\n  AfterUnion: false\n  BeforeCatch: true\n  BeforeElse: true\n  IndentBraces: false\n  SplitEmptyFunction: false\n  SplitEmptyRecord: true\nBreakBeforeBinaryOperators: None\nBreakBeforeTernaryOperators: false\nBreakConstructorInitializers: AfterColon\nBreakInheritanceList: AfterColon\nColumnLimit: 0\nCompactNamespaces: false\nContinuationIndentWidth: 2\nIndentCaseLabels: true\nIndentPPDirectives: BeforeHash\nIndentWidth: 2\nKeepEmptyLinesAtTheStartOfBlocks: false\nMaxEmptyLinesToKeep: 1\nNamespaceIndentation: All\nObjCSpaceAfterProperty: true\nObjCSpaceBeforeProtocolList: true\nPointerAlignment: Right\nReflowComments: true\nSpaceAfterCStyleCast: true\nSpaceAfterLogicalNot: false\nSpaceAfterTemplateKeyword: true\nSpaceBeforeAssignmentOperators: true\nSpaceBeforeCpp11BracedList: true\nSpaceBeforeCtorInitializerColon: false\nSpaceBeforeInheritanceColon: false\nSpaceBeforeParens: ControlStatements\nSpaceBeforeRangeBasedForLoopColon: true\nSpaceInEmptyParentheses: false\nSpacesBeforeTrailingComments: 2\nSpacesInAngles: Never\nSpacesInCStyleCastParentheses: false\nSpacesInContainerLiterals: false\nSpacesInParentheses: false\nSpacesInSquareBrackets: false\nTabWidth: 2\nCpp11BracedListStyle: false\nUseTab: Never\n"
  },
  {
    "path": ".codeql-prebuild-cpp-Linux.sh",
    "content": "# install dependencies for C++ analysis\nset -e\n\nchmod +x ./scripts/linux_build.sh\n./scripts/linux_build.sh --skip-package --ubuntu-test-repo\n\n# Delete CUDA\nrm -rf ./build/cuda\n\n# skip autobuild\necho \"skip_autobuild=true\" >> \"$GITHUB_OUTPUT\"\n"
  },
  {
    "path": ".codeql-prebuild-cpp-Windows.sh",
    "content": "# install dependencies for C++ analysis\nset -e\n\n# update pacman\npacman --noconfirm -Syu\n\ngcc_version=\"15.1.0-5\"\n\nbroken_deps=(\n  \"mingw-w64-ucrt-x86_64-gcc\"\n  \"mingw-w64-ucrt-x86_64-gcc-libs\"\n)\n\ntarballs=\"\"\nfor dep in \"${broken_deps[@]}\"; do\n  tarball=\"${dep}-${gcc_version}-any.pkg.tar.zst\"\n\n  # download and install working version\n  wget https://repo.msys2.org/mingw/ucrt64/${tarball}\n\n  tarballs=\"${tarballs} ${tarball}\"\ndone\n\n# install broken dependencies\nif [ -n \"$tarballs\" ]; then\n  pacman -U --noconfirm ${tarballs}\nfi\n\n# install dependencies\ndependencies=(\n  \"git\"\n  \"mingw-w64-ucrt-x86_64-cmake\"\n  \"mingw-w64-ucrt-x86_64-cppwinrt\"\n  \"mingw-w64-ucrt-x86_64-curl-winssl\"\n  \"mingw-w64-ucrt-x86_64-MinHook\"\n  \"mingw-w64-ucrt-x86_64-miniupnpc\"\n  \"mingw-w64-ucrt-x86_64-nlohmann-json\"\n  \"mingw-w64-ucrt-x86_64-nodejs\"\n  \"mingw-w64-ucrt-x86_64-nsis\"\n  \"mingw-w64-ucrt-x86_64-onevpl\"\n  \"mingw-w64-ucrt-x86_64-openssl\"\n  \"mingw-w64-ucrt-x86_64-opus\"\n  \"mingw-w64-ucrt-x86_64-toolchain\"\n)\n\n# Note: mingw-w64-ucrt-x86_64-rust conflicts with fixed gcc-15.1.0-5\n# We install Rust via rustup instead\n\npacman -Syu --noconfirm --ignore=\"$(IFS=,; echo \"${broken_deps[*]}\")\" \"${dependencies[@]}\"\n\n# install Rust via rustup (for Tauri GUI)\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"📦 Installing Rust via rustup (required for Tauri GUI)\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n\n# check if cargo already exists\nif command -v cargo &> /dev/null; then\n  echo \"✅ Rust already installed: $(cargo --version)\"\nelse\n  echo \"📥 Downloading and installing rustup...\"\n  \n  # download rustup-init for Windows\n  curl --proto '=https' --tlsv1.2 -sSf https://win.rustup.rs/x86_64 -o /tmp/rustup-init.exe\n  \n  # install rustup with defaults (non-interactive)\n  /tmp/rustup-init.exe -y --default-toolchain stable --profile minimal\n  \n  # add cargo to PATH for current session\n  export PATH=\"$HOME/.cargo/bin:$PATH\"\n  \n  # verify installation\n  if command -v cargo &> /dev/null; then\n    echo \"✅ Rust installed successfully: $(cargo --version)\"\n    echo \"   rustc: $(rustc --version)\"\n  else\n    echo \"❌ Rust installation failed!\"\n    echo \"   Please install manually from: https://rustup.rs/\"\n    exit 1\n  fi\nfi\n\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n\n# build\nmkdir -p build\ncmake \\\n  -B build \\\n  -G Ninja \\\n  -S . \\\n  -DBUILD_DOCS=OFF \\\n  -DBUILD_WERROR=ON\nninja -C build\n\n# skip autobuild\necho \"skip_autobuild=true\" >> \"$GITHUB_OUTPUT\""
  },
  {
    "path": ".codeql-prebuild-cpp-macOS.sh",
    "content": "# install dependencies for C++ analysis\nset -e\n\n# install dependencies\ndependencies=(\n  \"cmake\"\n  \"miniupnpc\"\n  \"ninja\"\n  \"node\"\n  \"openssl@3\"\n  \"opus\"\n  \"pkg-config\"\n)\nbrew install \"${dependencies[@]}\"\n\n# build\nmkdir -p build\ncd build || exit 1\ncmake \\\n  -DBOOST_USE_STATIC=OFF \\\n  -DBUILD_DOCS=OFF \\\n  -G \"Unix Makefiles\" ..\nmake -j\"$(sysctl -n hw.logicalcpu)\"\n\n# skip autobuild\necho \"skip_autobuild=true\" >> \"$GITHUB_OUTPUT\"\n"
  },
  {
    "path": ".coderabbit.yaml",
    "content": "# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json\nlanguage: zh-CN\nearly_access: true\n\nreviews:\n  profile: chill\n  high_level_summary: true\n  high_level_summary_in_walkthrough: true\n  collapse_walkthrough: false\n  sequence_diagrams: true\n  estimate_code_review_effort: true\n  assess_linked_issues: true\n  related_prs: true\n  poem: false\n  review_status: true\n  review_details: true\n\n  path_filters:\n    - \"!third-party/**\"\n    - \"!node_modules/**\"\n    - \"!build/**\"\n    - \"!cmake-*/**\"\n    - \"!docs/doxyconfig*\"\n    - \"!**/*.log\"\n    - \"!package-lock.json\"\n    - \"!**/*.obj\"\n    - \"!**/*.etl\"\n    - \"!**/*.csv\"\n    - \"!**/*.cat\"\n    - \"!**/*.bmp\"\n\n  path_instructions:\n    - path: \"src/**/*.{cpp,c,h}\"\n      instructions: >\n        Sunshine 核心 C++ 源码，自托管游戏串流服务器。审查要点：内存安全、\n        线程安全、RAII 资源管理、安全漏洞。注意预处理宏控制的平台相关代码。\n    - path: \"src/platform/**\"\n      instructions: >\n        平台抽象层代码（Windows/Linux/macOS）。确保各平台实现一致，\n        注意 Windows API 调用的错误处理和资源释放。\n    - path: \"cmake/**\"\n      instructions: >\n        CMake 构建系统文件。审查跨平台兼容性、现代 CMake 实践。\n    - path: \"src_assets/**/*.{vue,js,html}\"\n      instructions: >\n        基于 Vue.js 的 Web 配置面板。审查 XSS/CSRF 安全性、\n        组件设计、状态管理和可访问性。\n    - path: \"docker/**\"\n      instructions: >\n        Docker 配置文件。审查最小化基础镜像、层缓存、安全性。\n    - path: \"packaging/**\"\n      instructions: >\n        打包配置（Inno Setup、Flatpak、DEB 等）。审查安装路径、\n        权限设置和依赖声明的正确性。\n    - path: \"tests/**\"\n      instructions: >\n        测试文件。验证测试覆盖率、边界情况和断言正确性。\n\n  auto_review:\n    enabled: true\n    drafts: false\n    base_branches:\n      - master\n\n  tools:\n    cppcheck:\n      enabled: true\n    shellcheck:\n      enabled: true\n    yamllint:\n      enabled: true\n    markdownlint:\n      enabled: true\n\nchat:\n  auto_reply: true\n\nknowledge_base:\n  opt_out: false\n  learnings:\n    scope: auto\n"
  },
  {
    "path": ".dockerignore",
    "content": "# ignore hidden files\n.*\n\n# do not ignore .git, needed for versioning\n!/.git\n\n# do not ignore .rstcheck.cfg, needed to test building docs\n!/.rstcheck.cfg\n\n# ignore repo directories and files\ndocker/\ngh-pages-template/\nscripts/\ntools/\ncrowdin.yml\n\n# don't ignore linux build script\n!scripts/linux_build.sh\n\n# ignore dev directories\nbuild/\ncmake-*/\nvenv/\n\n# ignore artifacts\nartifacts/\n"
  },
  {
    "path": ".flake8",
    "content": "[flake8]\nfilename =\n    *.py,\n    *.pys\nmax-line-length = 120\nextend-exclude =\n    venv/\n"
  },
  {
    "path": ".gitattributes",
    "content": "# ensure dockerfiles are checked out with LF line endings\nDockerfile text eol=lf\n*.dockerfile text eol=lf\n\n# ensure flatpak lint json files are checked out with LF line endings\n*flatpak-lint-*.json text eol=lf\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: [\n  \"https://www.ifdian.net/a/qiin2333\",\n  \"https://www.ifdian.net/a/Yundi339\"\n]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report_en.yml",
    "content": "name: \"🐛 Bug Report (English)\"\ndescription: \"Report a bug to help us improve\"\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## 📋 Before You Submit\n        Thank you for taking the time to report this issue!\n        This project is **Foundation Sunshine**, a self-hosted game stream host for Moonlight.\n\n        **Please check before submitting:**\n        - [ ] I have searched existing issues to avoid duplicates\n        - [ ] This issue is specific to **Foundation Sunshine**, not other versions of Sunshine\n        - [ ] I have checked the [documentation](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB) for solutions\n        - [ ] I am using Windows 10/Windows 11 (Virtual Display feature requires Windows 10 22H2 or higher)\n        ---\n\n  - type: dropdown\n    id: bug-type\n    attributes:\n      label: \"🏷️ Bug Category\"\n      description: \"What type of bug is this?\"\n      options:\n        - \"📺 Video Streaming issues\"\n        - \"🎨 HDR Support issues\"\n        - \"🖥️ Virtual Display issues\"\n        - \"🎤 Remote Microphone issues\"\n        - \"🔊 Audio issues\"\n        - \"🌐 Network Connection issues\"\n        - \"🎮 Gamepad/Controller issues\"\n        - \"⚙️ Settings/Configuration issues\"\n        - \"💻 Web Control Panel issues\"\n        - \"💥 Crash/Freeze\"\n        - \"🔋 Performance/Encoding issues\"\n        - \"🔧 Pairing/Device Management issues\"\n        - \"❓ Other\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: describe-bug\n    attributes:\n      label: \"📝 Bug Description\"\n      description: \"Describe the bug clearly and concisely. What happened? What did you expect to happen?\"\n      placeholder: |\n        Example:\n        When I start streaming, the video output is black screen. The HDR display is enabled but the stream shows no content. I expect the stream to display the game content correctly.\n    validations:\n      required: true\n\n  - type: textarea\n    id: steps-reproduce\n    attributes:\n      label: \"🔄 Steps to Reproduce\"\n      description: \"Provide detailed steps to reproduce the issue\"\n      placeholder: |\n        1. Start Sunshine server\n        2. Connect from Moonlight client\n        3. Launch a game/application\n        4. Perform specific action that triggers the bug\n        5. Bug occurs\n    validations:\n      required: true\n\n  - type: textarea\n    id: affected-games\n    attributes:\n      label: \"🎯 Affected Games/Apps\"\n      description: \"Which games or applications are affected? Does it happen in all games or specific ones?\"\n      placeholder: |\n        - All games/applications\n        - Specific games (list them)\n        - Desktop streaming\n        - Only with HDR enabled\n    validations:\n      required: false\n\n  - type: dropdown\n    id: reproducibility\n    attributes:\n      label: \"🔁 Reproducibility\"\n      description: \"How often can you reproduce this issue?\"\n      options:\n        - \"Always (100%)\"\n        - \"Often (>50%)\"\n        - \"Sometimes (<50%)\"\n        - \"Rarely (<10%)\"\n        - \"Only once\"\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 🖥️ Host Server Information\n\n  - type: dropdown\n    id: host-os\n    attributes:\n      label: \"💻 Host Operating System\"\n      description: \"Operating system running Sunshine (Only Windows 10/Windows 11 supported)\"\n      options:\n        - \"Windows 10\"\n        - \"Windows 11\"\n    validations:\n      required: true\n\n  - type: input\n    id: os-version\n    attributes:\n      label: \"📌 Windows Specific Version\"\n      description: \"Please get the specific Windows version from your system (e.g., Windows 10 22H2, Windows 11 23H2, Windows 10 LTSC 2021, etc.)\"\n      placeholder: \"e.g. Windows 10 22H2 / Windows 11 23H2 / Windows 10 LTSC 2021\"\n    validations:\n      required: false\n\n  - type: input\n    id: sunshine-version\n    attributes:\n      label: \"☀️ Sunshine Version\"\n      description: \"Sunshine server version\"\n      placeholder: \"e.g. v0.23.1 / commit hash\"\n    validations:\n      required: true\n\n  - type: dropdown\n    id: gpu-vendor\n    attributes:\n      label: \"🎨 GPU Vendor\"\n      description: \"Your GPU manufacturer\"\n      options:\n        - \"NVIDIA\"\n        - \"AMD\"\n        - \"Intel\"\n        - \"Other/Unknown\"\n    validations:\n      required: true\n\n  - type: input\n    id: gpu-model\n    attributes:\n      label: \"🎨 GPU Model & Driver\"\n      description: \"GPU model and driver version\"\n      placeholder: \"e.g. RTX 4090 / Driver 545.92\"\n    validations:\n      required: false\n\n  - type: dropdown\n    id: encoder\n    attributes:\n      label: \"🔧 Video Encoder\"\n      description: \"Which encoder is being used?\"\n      options:\n        - \"NVENC (NVIDIA)\"\n        - \"AMD VCE\"\n        - \"Intel QSV\"\n        - \"Software (x264/x265)\"\n        - \"Auto/Unknown\"\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 📱 Client Information (Optional)\n\n  - type: input\n    id: client-device\n    attributes:\n      label: \"📱 Client Device\"\n      description: \"Moonlight client device and version (if relevant)\"\n      placeholder: \"e.g. Android Moonlight V+ v12.5.1 / Windows Moonlight-QT / iOS VoidLink\"\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 🔧 Configuration & Troubleshooting\n\n  - type: dropdown\n    id: settings-default\n    attributes:\n      label: \"⚙️ Configuration Modified?\"\n      description: \"Have you changed any Sunshine settings from default?\"\n      options:\n        - \"No, using default settings\"\n        - \"Yes, configuration modified\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: settings-adjusted-settings\n    attributes:\n      label: \"📋 Modified Configuration\"\n      description: \"If you modified settings, list them here (bitrate, encoder, HDR, display, etc.)\"\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 📎 Additional Information\n\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: \"📸 Screenshots/Videos\"\n      description: \"If applicable, add screenshots or screen recordings to help explain the problem\"\n      placeholder: \"Drag and drop images/videos here\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: \"📜 Log Output\"\n      description: \"If you have relevant log output, paste it here. Especially helpful for connection or crash issues\"\n      render: shell\n    validations:\n      required: false\n\n  - type: textarea\n    id: additional\n    attributes:\n      label: \"💬 Additional Information\"\n      description: \"What settings have been modified besides default settings?\"\n      placeholder: |\n        Example:\n        - Modified settings: Resolution, bitrate, encoder, etc.\n        - Network: Local LAN / Internet / VPN\n        - Connection type: WiFi 6 / 5GHz / Ethernet\n        - Display: Resolution, refresh rate, multi-monitor setup\n        - HDR: Display model, HDR settings\n        - Controller: Xbox / PlayStation / Other\n        - Special software: Antivirus, firewall, other streaming software\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report_zh.yml",
    "content": "name: \"🐛 问题报告（中文）\"\ndescription: \"报告问题帮助我们改进\"\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## 📋 提交前请注意\n        感谢您花时间报告这个问题！\n        此项目是 **Foundation Sunshine**，一个自托管的游戏串流服务器。\n\n        **提交前请检查**\n        - [ ] 我已搜索现有 issues，避免重复提交\n        - [ ] 此问题针对 **Foundation Sunshine**，不是其他版本的 Sunshine\n        - [ ] 我已查阅[使用文档](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB)寻找解决方案\n        - [ ] 我使用的是 Windows 10/Windows 11（虚拟显示器功能需要 Windows 10 22H2 及以上版本）\n        ---\n\n  - type: dropdown\n    id: bug-type\n    attributes:\n      label: \"🏷️ 问题类型\"\n      description: \"这是什么类型的问题？\"\n      options:\n        - \"📺 视频串流问题\"\n        - \"🎨 HDR 支持问题\"\n        - \"🖥️ 虚拟显示器问题\"\n        - \"🎤 远程麦克风问题\"\n        - \"🔊 音频问题\"\n        - \"🌐 网络连接问题\"\n        - \"🎮 手柄/控制器问题\"\n        - \"⚙️ 设置/配置问题\"\n        - \"💻 Web 控制面板问题\"\n        - \"💥 崩溃/卡死\"\n        - \"🔋 性能/编码问题\"\n        - \"🔧 配对/设备管理问题\"\n        - \"❓ 其他\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: describe-bug\n    attributes:\n      label: \"📝 问题描述\"\n      description: \"清晰简洁地描述问题。发生了什么？您期望发生什么？\"\n      placeholder: |\n        示例:\n        当我开始串流时，视频输出是黑屏。HDR 显示器已启用，但串流显示没有内容。我期望串流能正确显示游戏内容。\n    validations:\n      required: true\n\n  - type: textarea\n    id: steps-reproduce\n    attributes:\n      label: \"🔄 复现步骤\"\n      description: \"提供详细的复现步骤\"\n      placeholder: |\n        1. 启动 Sunshine 服务器\n        2. 从 Moonlight 客户端连接\n        3. 启动游戏/应用\n        4. 执行触发问题的特定操作\n        5. 问题出现\n    validations:\n      required: true\n\n  - type: textarea\n    id: affected-games\n    attributes:\n      label: \"🎯 受影响的游戏/应用\"\n      description: \"哪些游戏或应用受到影响？是所有游戏都有问题还是特定游戏？\"\n      placeholder: |\n        - 所有游戏/应用\n        - 特定游戏（请列出）\n        - 桌面串流\n        - 仅在启用 HDR 时\n    validations:\n      required: false\n\n  - type: dropdown\n    id: reproducibility\n    attributes:\n      label: \"🔁 可复现性\"\n      description: \"多久能复现一次此问题？\"\n      options:\n        - \"总是发生 (100%)\"\n        - \"经常发生 (>50%)\"\n        - \"有时发生 (<50%)\"\n        - \"很少发生 (<10%)\"\n        - \"只发生过一次\"\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 🖥️ 主机服务器信息\n\n  - type: dropdown\n    id: host-os\n    attributes:\n      label: \"💻 主机操作系统\"\n      description: \"运行 Sunshine 的操作系统（仅支持 Windows 10/Windows 11）\"\n      options:\n        - \"Windows 10\"\n        - \"Windows 11\"\n    validations:\n      required: true\n\n  - type: input\n    id: os-version\n    attributes:\n      label: \"📌 Windows 具体版本\"\n      description: \"请从系统中获取 Windows 的具体版本信息（如：Windows 10 22H2、Windows 11 23H2、Windows 10 LTSC 2021 等）\"\n      placeholder: \"例如: Windows 10 22H2 / Windows 11 23H2 / Windows 10 LTSC 2021\"\n    validations:\n      required: false\n\n  - type: input\n    id: sunshine-version\n    attributes:\n      label: \"☀️ Sunshine 版本\"\n      description: \"Sunshine 服务器版本\"\n      placeholder: \"例如: v0.23.1 / commit hash\"\n    validations:\n      required: true\n\n  - type: dropdown\n    id: gpu-vendor\n    attributes:\n      label: \"🎨 显卡厂商\"\n      description: \"您的显卡制造商\"\n      options:\n        - \"NVIDIA\"\n        - \"AMD\"\n        - \"Intel\"\n        - \"其他/未知\"\n    validations:\n      required: true\n\n  - type: input\n    id: gpu-model\n    attributes:\n      label: \"🎨 显卡型号及驱动\"\n      description: \"显卡型号和驱动版本\"\n      placeholder: \"例如: RTX 4090 / 驱动 545.92\"\n    validations:\n      required: false\n\n  - type: dropdown\n    id: encoder\n    attributes:\n      label: \"🔧 视频编码器\"\n      description: \"使用哪个编码器？\"\n      options:\n        - \"NVENC (NVIDIA)\"\n        - \"AMD VCE\"\n        - \"Intel QSV\"\n        - \"软件编码 (x264/x265)\"\n        - \"自动/未知\"\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 📱 客户端信息（可选）\n\n  - type: input\n    id: client-device\n    attributes:\n      label: \"📱 客户端设备\"\n      description: \"Moonlight 客户端设备和版本（如相关）\"\n      placeholder: \"例如: Android Moonlight V+ v12.5.1 / Windows Moonlight-QT / iOS VoidLink\"\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 🔧 配置与故障排除\n\n  - type: dropdown\n    id: settings-default\n    attributes:\n      label: \"⚙️ 是否修改过配置？\"\n      description: \"您是否修改过默认配置？\"\n      options:\n        - \"否，使用默认设置\"\n        - \"是，已修改配置\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: settings-adjusted-settings\n    attributes:\n      label: \"📋 已修改的配置\"\n      description: \"如果修改了设置，请列出（码率、编码器、HDR、显示器等）\"\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 📎 附加信息\n\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: \"📸 截图/视频\"\n      description: \"如有必要，请添加截图或录屏来帮助说明问题\"\n      placeholder: \"在此处拖放图片/视频\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: \"📜 日志输出\"\n      description: \"如有相关日志输出，请粘贴在此。对于连接或崩溃问题特别有帮助\"\n      render: shell\n    validations:\n      required: false\n\n  - type: textarea\n    id: additional\n    attributes:\n      label: \"💬 其他补充\"\n      description: \"除了默认设置外还修改了什么内容？\"\n      placeholder: |\n        示例:\n        - 修改的设置: 分辨率、码率、编码器等\n        - 网络: 本地局域网 / 外网 / VPN\n        - 连接类型: WiFi 6 / 5GHz / 有线\n        - 显示器: 分辨率、刷新率、多显示器设置\n        - HDR: 显示器型号、HDR 设置\n        - 手柄: Xbox / PlayStation / 其他\n        - 特殊软件: 杀毒软件、防火墙、其他串流软件\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request_en.yml",
    "content": "name: \"✨ Feature Request (English)\"\ndescription: \"Suggest a new feature or improvement\"\ntitle: \"[Feature]: \"\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## 💡 Feature Request\n        Thank you for taking the time to suggest a new feature!\n        This project is **Foundation Sunshine**, a self-hosted game stream host for Moonlight.\n        \n        **⚠️ Important: This template is only for submitting Sunshine server-side feature requests. Client-side (Moonlight) issues should not be submitted here.**\n\n        **Please check before submitting:**\n        - [ ] I have searched existing issues and feature requests to avoid duplicates\n        - [ ] This feature is not already available in the latest version\n        - [ ] This feature is specific to the server-side (Sunshine), not the client-side (Moonlight)\n        - [ ] I am using Windows 10/Windows 11 (Virtual Display feature requires Windows 10 22H2 or higher)\n        ---\n\n  - type: dropdown\n    id: feature-type\n    attributes:\n      label: \"🏷️ Feature Category\"\n      description: \"What type of feature is this?\"\n      options:\n        - \"🎨 HDR Support\"\n        - \"🖥️ Virtual Display\"\n        - \"🎤 Remote Microphone\"\n        - \"📺 Video Encoding/Streaming\"\n        - \"🔊 Audio Processing\"\n        - \"🌐 Network/Connection\"\n        - \"💻 Web Control Panel\"\n        - \"⚙️ Configuration/Settings\"\n        - \"🔧 Device/Pairing Management\"\n        - \"🎮 Controller/Input Support\"\n        - \"🔋 Performance Optimization\"\n        - \"📊 Monitoring/Logging\"\n        - \"🔒 Security/Authentication\"\n        - \"❓ Other\"\n    validations:\n      required: false\n\n  - type: dropdown\n    id: priority\n    attributes:\n      label: \"📊 Priority\"\n      description: \"How important is this feature to you?\"\n      options:\n        - \"🔴 Critical - Can't use app without it\"\n        - \"🟠 High - Significantly improves experience\"\n        - \"🟡 Medium - Nice to have\"\n        - \"🟢 Low - Minor improvement\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: problem\n    attributes:\n      label: \"😤 Problem Statement\"\n      description: \"Is your feature request related to a problem? Describe the frustration\"\n      placeholder: |\n        Example:\n        When streaming ends, my game doesn't close and continues running in the background.\n    validations:\n      required: true\n\n  - type: textarea\n    id: solution\n    attributes:\n      label: \"💡 Proposed Solution\"\n      description: \"Describe the solution you'd like. Be as specific as possible\"\n      placeholder: |\n        Example:\n        I need a feature that can execute specified background commands when streaming ends. It would be much better if this feature could be supported.\n    validations:\n      required: true\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: \"🔄 Alternative Solutions\"\n      description: \"Have you considered any alternative solutions or workarounds?\"\n      placeholder: |\n        Example:\n        1. Use third-party scripts to execute commands when streaming ends\n        2. Implement similar functionality through Task Scheduler\n        3. Manually manage game processes\n    validations:\n      required: false\n\n  - type: textarea\n    id: use-case\n    attributes:\n      label: \"🎯 Use Case\"\n      description: \"Describe a specific scenario where this feature would be useful\"\n      placeholder: |\n        Example:\n        When streaming ends, I need to automatically close the game process to free up system resources. This feature would help me manage multiple streaming sessions more efficiently.\n    validations:\n      required: false\n\n  - type: dropdown\n    id: willing-to-test\n    attributes:\n      label: \"🧪 Willing to Test?\"\n      description: \"Would you be willing to test this feature if implemented?\"\n      options:\n        - \"Yes, I can test and provide feedback\"\n        - \"Maybe, depends on timing\"\n        - \"No\"\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 📎 Additional Information\n\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: \"📸 Mockups/References\"\n      description: \"Add mockups, sketches, or reference screenshots from other apps that have similar features\"\n      placeholder: \"Drag and drop images here\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: additional\n    attributes:\n      label: \"💬 Additional Information\"\n      description: \"Log information, what settings have been modified besides default settings?\"\n      placeholder: |\n        Example:\n        - Log level: Verbose/Debug\n        - Modified settings: Resolution, bitrate, encoder, etc.\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 🖥️ System Information (Optional)\n\n  - type: dropdown\n    id: host-os\n    attributes:\n      label: \"💻 Host Operating System\"\n      description: \"Your host OS (Only Windows 10/Windows 11 supported)\"\n      options:\n        - \"Windows 10\"\n        - \"Windows 11\"\n    validations:\n      required: false\n\n  - type: input\n    id: os-version\n    attributes:\n      label: \"📌 Windows Specific Version\"\n      description: \"Please get the specific Windows version from your system (e.g., Windows 10 22H2, Windows 11 23H2, Windows 10 LTSC 2021, etc.)\"\n      placeholder: \"e.g. Windows 10 22H2 / Windows 11 23H2 / Windows 10 LTSC 2021\"\n    validations:\n      required: false\n\n  - type: dropdown\n    id: gpu-vendor\n    attributes:\n      label: \"🎨 GPU Vendor\"\n      description: \"Your GPU manufacturer (if relevant to the feature)\"\n      options:\n        - \"NVIDIA\"\n        - \"AMD\"\n        - \"Intel\"\n        - \"Not applicable\"\n        - \"Other/Unknown\"\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request_zh.yml",
    "content": "name: \"✨ 功能建议（中文）\"\ndescription: \"建议新功能或改进\"\ntitle: \"[Feature]: \"\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## 💡 功能建议\n        感谢您花时间提出新功能建议！\n        此项目是 **Foundation Sunshine**，一个自托管的游戏串流服务器。\n        \n        **⚠️ 重要提示：此模板仅用于提交 Sunshine 服务器端的功能建议，客户端（Moonlight）相关问题请勿在此提交。**\n\n        **提交前请检查**\n        - [ ] 我已搜索现有 issues，避免重复\n        - [ ] 此功能在最新版本中尚不可用\n        - [ ] 此功能针对服务器端（Sunshine），而非客户端（Moonlight）\n        - [ ] 我使用的是 Windows 10/Windows 11（虚拟显示器功能需要 Windows 10 22H2 及以上版本）\n        ---\n\n  - type: dropdown\n    id: feature-type\n    attributes:\n      label: \"🏷️ 功能类别\"\n      description: \"这是什么类型的功能？\"\n      options:\n        - \"� 界面体验\"\n        - \"🎨 HDR 支持\"\n        - \"🖥️ 虚拟显示器\"\n        - \"🎤 远程麦克风\"\n        - \"📺 视频编码/串流\"\n        - \"🔊 音频处理\"\n        - \"🌐 网络/连接\"\n        - \"💻 控制面板\"\n        - \"⚙️ 配置/设置\"\n        - \"🔧 设备/配对管理\"\n        - \"🎮 控制器/输入支持\"\n        - \"🔋 性能优化\"\n        - \"📊 监控/日志\"\n        - \"🔒 安全/认证\"\n        - \"❓ 其他\"\n    validations:\n      required: false\n\n  - type: dropdown\n    id: priority\n    attributes:\n      label: \"📊 优先级\"\n      description: \"这个功能对您有多重要？\"\n      options:\n        - \"🔴 关键 - 没有它无法使用\"\n        - \"🟠 高 - 显著改善体验\"\n        - \"🟡 中 - 有了更好\"\n        - \"🟢 低 - 小改进\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: problem\n    attributes:\n      label: \"😤 问题描述\"\n      description: \"您的功能请求是否与某个问题相关？请描述您遇到的困扰\"\n      placeholder: |\n        示例:\n        当我串流结束时，我的游戏没有关闭，而是继续在后台运行。\n    validations:\n      required: true\n\n  - type: textarea\n    id: solution\n    attributes:\n      label: \"💡 建议的解决方案\"\n      description: \"描述您希望的解决方案。请尽可能具体\"\n      placeholder: |\n        示例:\n        我需要一个功能，当串流结束时，能够执行指定后台命令，如果能够支持这个功能会好得多。\n    validations:\n      required: true\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: \"🔄 替代方案\"\n      description: \"您是否考虑过任何替代解决方案或变通办法？\"\n      placeholder: |\n        示例:\n        1. 使用第三方脚本在串流结束时执行命令\n        2. 通过任务计划程序实现类似功能\n        3. 手动管理游戏进程\n    validations:\n      required: false\n\n  - type: textarea\n    id: use-case\n    attributes:\n      label: \"🎯 使用场景\"\n      description: \"描述一个这个功能会很有用的具体场景\"\n      placeholder: |\n        示例:\n        当串流结束时，我需要自动关闭游戏进程以释放系统资源。这个功能将帮助我更高效地管理多个串流会话。\n    validations:\n      required: false\n\n  - type: dropdown\n    id: willing-to-test\n    attributes:\n      label: \"🧪 愿意测试吗？\"\n      description: \"如果实现了此功能，您愿意帮忙测试吗？\"\n      options:\n        - \"是，我可以测试并提供反馈\"\n        - \"也许，取决于时间\"\n        - \"否\"\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 📎 附加信息\n\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: \"📸 效果图/参考\"\n      description: \"添加效果图、草图，或其他有类似功能的应用的参考截图\"\n      placeholder: \"在此处拖放图片\"\n    validations:\n      required: false\n\n  - type: textarea\n    id: additional\n    attributes:\n      label: \"💬 其他补充\"\n      description: \"日志信息、除了默认设置外还修改了什么内容？\"\n      placeholder: |\n        示例:\n        - 日志级别: 详细/调试\n        - 修改的设置: 分辨率、码率、编码器等\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## 🖥️ 系统信息（可选）\n\n  - type: dropdown\n    id: host-os\n    attributes:\n      label: \"💻 主机操作系统\"\n      description: \"您的主机操作系统（仅支持 Windows 10/Windows 11）\"\n      options:\n        - \"Windows 10\"\n        - \"Windows 11\"\n    validations:\n      required: false\n\n  - type: input\n    id: os-version\n    attributes:\n      label: \"📌 Windows 具体版本\"\n      description: \"请从系统中获取 Windows 的具体版本信息（如：Windows 10 22H2、Windows 11 23H2、Windows 10 LTSC 2021 等）\"\n      placeholder: \"例如: Windows 10 22H2 / Windows 11 23H2 / Windows 10 LTSC 2021\"\n    validations:\n      required: false\n\n  - type: dropdown\n    id: gpu-vendor\n    attributes:\n      label: \"🎨 显卡厂商\"\n      description: \"您的显卡制造商（如与功能相关）\"\n      options:\n        - \"NVIDIA\"\n        - \"AMD\"\n        - \"Intel\"\n        - \"不适用\"\n        - \"其他/未知\"\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/RELEASE_TEMPLATE.md",
    "content": "## What's Changed\n\n### ✨ New Features\n* **feat:** 功能描述 by [@用户名](https://github.com/用户名) in [#PR号](https://github.com/qiin2333/Sunshine-Foundation/pull/PR号)\n\n### 🔧 Improvements & Refactors\n* **refactor:** 改进描述 by [@用户名](https://github.com/用户名) in [#PR号](https://github.com/qiin2333/Sunshine-Foundation/pull/PR号)\n* **chore:** 任务描述 by [@用户名](https://github.com/用户名) in [#PR号](https://github.com/qiin2333/Sunshine-Foundation/pull/PR号)\n\n### 🐛 Bug Fixes\n* **fix:** 修复描述 by [@用户名](https://github.com/用户名) in [#PR号](https://github.com/qiin2333/Sunshine-Foundation/pull/PR号)\n\n---\n\n## ⚠️ 注意事项\n* 版本特定的注意事项，例如驱动更新要求、配置变更等\n\n---\n\n## 🎉 New Contributors\n* [@用户名](https://github.com/用户名) made their first contribution in [#PR号](https://github.com/qiin2333/Sunshine-Foundation/pull/PR号)\n\n---\n\n**Full Changelog**: https://github.com/qiin2333/Sunshine-Foundation/compare/PREVIOUS_TAG...CURRENT_TAG\n\n---\n\n## 👥 Contributors\n\n| <a href=\"https://github.com/用户名\"><img src=\"https://avatars.githubusercontent.com/用户名?s=80\" width=\"80\" height=\"80\" alt=\"用户名\"/><br/><sub><b>用户名</b></sub></a><br/><sub>合并次数 merges</sub> |\n| :---: |\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Build and Release\n\non:\n  pull_request:\n    branches:\n      - master\n    types:\n      - opened\n      - synchronize\n      - reopened\n  push:\n    branches:\n      - master\n  release:\n    types: [published]\n  workflow_dispatch:\n\nconcurrency:\n  group: '${{ github.workflow }}-${{ github.ref }}'\n  cancel-in-progress: true\n\njobs:\n  github_env:\n    name: GitHub Env Debug\n    runs-on: ubuntu-latest\n    steps:\n      - name: Dump github context\n        run: echo \"$GITHUB_CONTEXT\"\n        shell: bash\n        env:\n          GITHUB_CONTEXT: ${{ toJson(github) }}\n\n  setup_release:\n    name: Setup Release\n    outputs:\n      publish_release: ${{ steps.setup_release_auto.outputs.publish_release || steps.setup_release_release.outputs.publish_release || steps.setup_release_manual.outputs.publish_release }}\n      release_body: ${{ steps.setup_release_auto.outputs.release_body || steps.setup_release_release.outputs.release_body || steps.setup_release_manual.outputs.release_body }}\n      release_commit: ${{ steps.setup_release_auto.outputs.release_commit || steps.setup_release_release.outputs.release_commit || steps.setup_release_manual.outputs.release_commit }}\n      release_generate_release_notes: ${{ steps.setup_release_auto.outputs.release_generate_release_notes || steps.setup_release_release.outputs.release_generate_release_notes || steps.setup_release_manual.outputs.release_generate_release_notes }}\n      release_tag: ${{ steps.setup_release_auto.outputs.release_tag || steps.setup_release_release.outputs.release_tag || steps.setup_release_manual.outputs.release_tag }}\n      release_version: ${{ steps.setup_release_auto.outputs.release_version || steps.setup_release_release.outputs.release_version || steps.setup_release_manual.outputs.release_version }}\n    permissions:\n      contents: write  # read does not work to check squash and merge details\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Debug GitHub Event\n        run: |\n          echo \"GitHub Event Name: ${{ github.event_name }}\"\n          echo \"Has commits field: ${{ contains(toJson(github.event), 'commits') }}\"\n\n      - name: Setup Release (Auto)\n        id: setup_release_auto\n        if: github.event_name != 'workflow_dispatch' && github.event_name != 'release'\n        uses: LizardByte/setup-release-action@v2025.426.225\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Setup Release (Release event)\n        id: setup_release_release\n        if: github.event_name == 'release'\n        env:\n          RELEASE_BODY: ${{ github.event.release.body }}\n        run: |\n          TAG=\"${{ github.event.release.tag_name }}\"\n          SUFFIX=$([ \"${{ github.event.release.target_commitish }}\" = \"master\" ] && echo \"\" || echo \"-dev\")\n          echo \"publish_release=true\" >> $GITHUB_OUTPUT\n          {\n            echo \"release_body<<RELEASE_BODY_EOF\"\n            echo \"$RELEASE_BODY\"\n            echo \"RELEASE_BODY_EOF\"\n          } >> $GITHUB_OUTPUT\n          echo \"release_commit=${{ github.event.release.target_commitish }}\" >> $GITHUB_OUTPUT\n          echo \"release_generate_release_notes=false\" >> $GITHUB_OUTPUT\n          echo \"release_tag=${TAG}${SUFFIX}\" >> $GITHUB_OUTPUT\n          echo \"release_version=${TAG}${SUFFIX}\" >> $GITHUB_OUTPUT\n\n      - name: Setup Release (Manual)\n        id: setup_release_manual\n        if: github.event_name == 'workflow_dispatch'\n        run: |\n          SUFFIX=$([ \"${{ github.ref_name }}\" = \"master\" ] && echo \"\" || echo \"-dev\")\n          echo \"publish_release=false\" >> $GITHUB_OUTPUT\n          echo \"release_body=Manual build (${{ github.ref_name }})\" >> $GITHUB_OUTPUT\n          echo \"release_commit=${{ github.sha }}\" >> $GITHUB_OUTPUT\n          echo \"release_generate_release_notes=false\" >> $GITHUB_OUTPUT\n          echo \"release_tag=manual-$(date +%Y%m%d-%H%M%S)${SUFFIX}\" >> $GITHUB_OUTPUT\n          echo \"release_version=manual-$(date +%Y%m%d-%H%M%S)${SUFFIX}\" >> $GITHUB_OUTPUT\n\n  build_win:\n    name: Windows\n    needs: setup_release\n    runs-on: windows-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event_name == 'release' && github.event.release.tag_name || github.sha }}\n          submodules: recursive\n\n      - name: Setup Dependencies Windows\n        uses: msys2/setup-msys2@v2\n        with:\n          msystem: ucrt64\n          update: true\n          install: >-\n            wget\n            curl\n\n      - name: Update Windows dependencies\n        env:\n          gcc_version: '15.1.0-5'\n          opus_version: '1.6.1-1'\n        shell: msys2 {0}\n        run: |\n          broken_deps=(\n            \"mingw-w64-ucrt-x86_64-gcc\"\n            \"mingw-w64-ucrt-x86_64-gcc-libs\"\n          )\n\n          tarballs=\"\"\n          for dep in \"${broken_deps[@]}\"; do\n            tarball=\"${dep}-${gcc_version}-any.pkg.tar.zst\"\n\n            # download and install working version\n            wget https://repo.msys2.org/mingw/ucrt64/${tarball}\n\n            tarballs=\"${tarballs} ${tarball}\"\n          done\n\n          # download pinned opus version\n          opus_tarball=\"mingw-w64-ucrt-x86_64-opus-${opus_version}-any.pkg.tar.zst\"\n          wget https://repo.msys2.org/mingw/ucrt64/${opus_tarball}\n\n          # install broken dependencies\n          if [ -n \"$tarballs\" ]; then\n            pacman -U --noconfirm ${tarballs}\n          fi\n\n          # install dependencies\n          dependencies=(\n            \"git\"\n            \"mingw-w64-ucrt-x86_64-cmake\"\n            \"mingw-w64-ucrt-x86_64-ninja\"\n            \"mingw-w64-ucrt-x86_64-cppwinrt\"\n            \"mingw-w64-ucrt-x86_64-curl-winssl\"\n            \"mingw-w64-ucrt-x86_64-graphviz\"\n            \"mingw-w64-ucrt-x86_64-MinHook\"\n            \"mingw-w64-ucrt-x86_64-miniupnpc\"\n            \"mingw-w64-ucrt-x86_64-nlohmann-json\"\n            # \"mingw-w64-ucrt-x86_64-nodejs\"  # Replaced by actions/setup-node (vite 8 requires MSVC Node.js)\n            # \"mingw-w64-ucrt-x86_64-nsis\"  # Replaced by Inno Setup\n            \"mingw-w64-ucrt-x86_64-onevpl\"\n            \"mingw-w64-ucrt-x86_64-openssl\"\n            \"mingw-w64-ucrt-x86_64-toolchain\"\n            \"mingw-w64-ucrt-x86_64-autotools\"\n          )\n\n          pacman -Syu --noconfirm --ignore=\"$(IFS=,; echo \"${broken_deps[*]}\")\" \"${dependencies[@]}\"\n\n          # install pinned opus after Syu to prevent upgrade\n          pacman --noconfirm -U ${opus_tarball}\n\n      - name: Verify Build Tools\n        shell: msys2 {0}\n        run: |\n          echo \"Verifying build tools are installed...\"\n          which cmake || (echo \"cmake not found\" && exit 1)\n          which ninja || (echo \"ninja not found\" && exit 1)\n          which gcc || (echo \"gcc not found\" && exit 1)\n          \n          echo \"All build tools verified successfully\"\n          echo \"  CMake: $(cmake --version | head -1)\"\n          echo \"  Ninja: $(ninja --version)\"\n          echo \"  GCC: $(gcc --version | head -1)\"\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - name: Cache npm dependencies\n        uses: actions/cache@v4\n        with:\n          path: node_modules\n          key: npm-${{ hashFiles('package.json') }}\n\n      - name: Build Web UI\n        shell: pwsh\n        run: |\n          npm install\n          New-Item -ItemType Directory -Force -Path build | Out-Null\n          $env:SUNSHINE_SOURCE_ASSETS_DIR = \"${{ github.workspace }}\\src_assets\"\n          $env:SUNSHINE_ASSETS_DIR = \"${{ github.workspace }}\\build\"\n          npm run build\n\n      - name: Build Windows\n        shell: msys2 {0}\n        env:\n          BRANCH: ${{ github.head_ref || github.ref_name }}\n          BUILD_VERSION: ${{ needs.setup_release.outputs.release_tag }}.杂鱼\n          COMMIT: ${{ needs.setup_release.outputs.release_commit }}\n          GITHUB_TOKEN: ${{ secrets.DRIVER_DOWNLOAD_TOKEN }}\n        run: |\n          mkdir -p build\n          cmake \\\n            -B build \\\n            -G Ninja \\\n            -S . \\\n            -DBUILD_DOCS=OFF \\\n            -DBUILD_WEB_UI=OFF \\\n            -DSUNSHINE_ASSETS_DIR=assets \\\n            -DSUNSHINE_PUBLISHER_NAME='${{ github.repository_owner }}' \\\n            -DSUNSHINE_PUBLISHER_WEBSITE='https://github.com/qiin2333/Sunshine-Foundation' \\\n            -DSUNSHINE_PUBLISHER_ISSUE_URL='https://github.com/qiin2333/Sunshine-Foundation/issues'\n          ninja -C build\n\n      - name: Cache Inno Setup\n        id: inno-cache\n        uses: actions/cache@v4\n        with:\n          path: C:\\Program Files (x86)\\Inno Setup 6\n          key: inno-setup-6\n\n      - name: Install Inno Setup\n        if: steps.inno-cache.outputs.cache-hit != 'true'\n        shell: pwsh\n        run: |\n          # Download and install Inno Setup 6 (silent install)\n          $url = \"https://jrsoftware.org/download.php/is.exe\"\n          $installer = \"$env:TEMP\\innosetup.exe\"\n          Invoke-WebRequest -Uri $url -OutFile $installer\n          Start-Process -FilePath $installer -ArgumentList '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-' -Wait\n\n      - name: Add Inno Setup to PATH\n        shell: pwsh\n        run: |\n          echo \"C:\\Program Files (x86)\\Inno Setup 6\" >> $env:GITHUB_PATH\n\n      - name: Package Windows\n        shell: msys2 {0}\n        run: |\n          mkdir -p artifacts\n          cd build\n\n          # package - 生成 Inno Setup 安装包和 ZIP 便携版\n          # 先安装到 staging 目录\n          cmake --install . --prefix ./inno_staging\n          \n          # 运行 Inno Setup 编译器\n          \"/c/Program Files (x86)/Inno Setup 6/ISCC.exe\" sunshine_installer.iss\n          \n          # 生成 ZIP 便携版\n          cpack -G ZIP --config ./CPackConfig.cmake --verbose\n\n          # move\n          mv ./cpack_artifacts/Sunshine.exe ../artifacts/sunshine-windows-installer.exe\n          mv ./cpack_artifacts/Sunshine.zip ../artifacts/sunshine-windows-portable.zip\n\n      - name: Generate Checksums\n        shell: pwsh\n        run: |\n          # 生成 SHA256 校验和\n          .\\scripts\\generate-checksums.ps1 -Path .\\artifacts -Output \"SHA256SUMS.txt\"\n\n      - name: Package Windows Debug Info\n        working-directory: build\n        run: |\n          # use .dbg file extension for binaries to avoid confusion with real packages\n          Get-ChildItem -File -Recurse | `\n            % { Rename-Item -Path $_.PSPath -NewName $_.Name.Replace(\".exe\",\".dbg\") }\n\n          # save the binaries with debug info\n          7z -r `\n            \"-xr!CMakeFiles\" `\n            \"-xr!cpack_artifacts\" `\n            a \"../artifacts/sunshine-win32-debuginfo.7z\" \"*.dbg\"\n\n      - name: Rename release assets\n        shell: msys2 {0}\n        run: |\n          # Format tag to vYEAR.DATE where DATE is zero-padded to 4 digits\n          TAG=\"${{ needs.setup_release.outputs.release_tag }}\"\n          NEWTAG=\"$TAG\"\n\n          if [[ \"$TAG\" =~ ^v([0-9]{4})\\.([0-9]+) ]]; then\n            YEAR=\"${BASH_REMATCH[1]}\"\n            DATE_PART=\"${BASH_REMATCH[2]}\"\n            DATE_PADDED=$(printf \"%04d\" \"$DATE_PART\")\n            NEWTAG=\"v${YEAR}.${DATE_PADDED}\"\n          fi\n\n          # 重命名安装包和便携版\n          mv artifacts/sunshine-windows-installer.exe \"artifacts/Sunshine.${NEWTAG}.WindowsInstaller.exe\"\n          mv artifacts/sunshine-windows-portable.zip \"artifacts/Sunshine.${NEWTAG}.WindowsPortable.zip\"\n\n      - name: Upload Artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: sunshine-windows-r${{ github.run_number }}\n          path: |\n            artifacts/Sunshine.*.WindowsInstaller.exe\n            artifacts/Sunshine.*.WindowsPortable.zip\n            artifacts/SHA256SUMS.txt\n            artifacts/checksums.json\n          if-no-files-found: error\n\n      - name: Create/Update GitHub Release\n        if: needs.setup_release.outputs.publish_release == 'true'\n        uses: LizardByte/create-release-action@v2025.426.1549\n        with:\n          allowUpdates: true\n          body: ${{ needs.setup_release.outputs.release_body }}\n          generateReleaseNotes: ${{ needs.setup_release.outputs.release_generate_release_notes }}\n          name: ${{ needs.setup_release.outputs.release_tag }}.杂鱼\n          prerelease: true\n          tag: ${{ needs.setup_release.outputs.release_tag }}.杂鱼\n          token: ${{ secrets.GH_BOT_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/sign-and-repackage.yml",
    "content": "# 签名并重新打包工作流（仅用于正式发布版本）\n# 流程：\n# 1. 编译当前分支的代码（只编译一次）\n# 2. 生成未签名的 ZIP 和 Inno Setup staging 目录\n# 3. 将所有 EXE/DLL 文件提交到 SignPath 签名\n# 4. 用签名文件重新打包 ZIP 便携版\n# 5. 用签名文件替换 Inno Setup staging 目录中的文件并重新打包\n# 6. 签名最终的 Inno Setup 安装包\n# 注意：只在正式发布（非预发布）时运行，确保代码版本和签名文件一致\n\nname: Sign and Repackage (Release Only)\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n    inputs:\n      ref:\n        description: \"Branch or tag to checkout (e.g., master, v2025.1116)\"\n        required: true\n        default: \"master\"\n      release-tag:\n        description: \"Release tag for naming the output files\"\n        required: true\n\njobs:\n  sign-and-repackage:\n    name: Sign Files and Repackage\n    runs-on: windows-latest\n    # 只在正式发布时运行（不是预发布）\n    if: ${{ (github.event_name == 'release' && !github.event.release.prerelease) || github.event_name == 'workflow_dispatch' }}\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event.inputs.ref || github.ref }}\n          submodules: recursive\n\n      - name: Setup Dependencies Windows\n        uses: msys2/setup-msys2@v2\n        with:\n          msystem: ucrt64\n          update: true\n          install: >-\n            wget\n\n      - name: Update Windows dependencies\n        env:\n          gcc_version: '15.1.0-5'\n        shell: msys2 {0}\n        run: |\n          broken_deps=(\n            \"mingw-w64-ucrt-x86_64-gcc\"\n            \"mingw-w64-ucrt-x86_64-gcc-libs\"\n          )\n\n          tarballs=\"\"\n          for dep in \"${broken_deps[@]}\"; do\n            tarball=\"${dep}-${gcc_version}-any.pkg.tar.zst\"\n\n            # download and install working version\n            wget https://repo.msys2.org/mingw/ucrt64/${tarball}\n\n            tarballs=\"${tarballs} ${tarball}\"\n          done\n\n          # install broken dependencies\n          if [ -n \"$tarballs\" ]; then\n            pacman -U --noconfirm ${tarballs}\n          fi\n\n          # install dependencies\n          dependencies=(\n            \"git\"\n            \"mingw-w64-ucrt-x86_64-cmake\"\n            \"mingw-w64-ucrt-x86_64-ninja\"\n            \"mingw-w64-ucrt-x86_64-cppwinrt\"\n            \"mingw-w64-ucrt-x86_64-curl-winssl\"\n            \"mingw-w64-ucrt-x86_64-graphviz\"\n            \"mingw-w64-ucrt-x86_64-MinHook\"\n            \"mingw-w64-ucrt-x86_64-miniupnpc\"\n            \"mingw-w64-ucrt-x86_64-nlohmann-json\"\n            \"mingw-w64-ucrt-x86_64-nodejs\"\n            # \"mingw-w64-ucrt-x86_64-nsis\"  # Replaced by Inno Setup\n            \"mingw-w64-ucrt-x86_64-onevpl\"\n            \"mingw-w64-ucrt-x86_64-openssl\"\n            \"mingw-w64-ucrt-x86_64-opus\"\n            \"mingw-w64-ucrt-x86_64-toolchain\"\n          )\n\n          # Note: mingw-w64-ucrt-x86_64-rust conflicts with fixed gcc-15.1.0-5\n          # We install Rust via rustup in a separate step\n\n          pacman -Syu --noconfirm --ignore=\"$(IFS=,; echo \"${broken_deps[*]}\")\" \"${dependencies[@]}\"\n\n      - name: Install Rust (for Tauri GUI)\n        shell: msys2 {0}\n        run: |\n          echo \"Installing Rust via rustup...\"\n\n          # Rust installs to Windows user directory\n          WINDOWS_USER=$(cmd //c \"echo %USERNAME%\" | tr -d '\\r')\n          CARGO_BIN=\"/c/Users/${WINDOWS_USER}/.cargo/bin\"\n          export PATH=\"$CARGO_BIN:$PATH\"\n\n          # Check if cargo already exists\n          if command -v cargo &> /dev/null; then\n            echo \"Rust already installed: $(cargo --version)\"\n          else\n            # Download and install rustup\n            curl --proto '=https' --tlsv1.2 -sSf https://win.rustup.rs/x86_64 -o /tmp/rustup-init.exe\n            /tmp/rustup-init.exe -y --default-toolchain stable --profile minimal\n            \n            # Refresh PATH\n            sleep 3\n            export PATH=\"$CARGO_BIN:$PATH\"\n            \n            # Verify installation\n            if [ -f \"$CARGO_BIN/cargo.exe\" ]; then\n              echo \"Rust installed successfully: $(cargo --version)\"\n            else\n              echo \"Warning: Rust installed but cargo not found at $CARGO_BIN\"\n              exit 1\n            fi\n          fi\n\n      - name: Verify Build Tools\n        shell: msys2 {0}\n        run: |\n          echo \"Verifying build tools are installed...\"\n          which cmake || (echo \"cmake not found\" && exit 1)\n          which ninja || (echo \"ninja not found\" && exit 1)\n          which gcc || (echo \"gcc not found\" && exit 1)\n          \n          # verify Rust is in PATH\n          WINDOWS_USER=$(cmd //c \"echo %USERNAME%\" | tr -d '\\r')\n          CARGO_BIN=\"/c/Users/${WINDOWS_USER}/.cargo/bin\"\n          export PATH=\"$CARGO_BIN:$PATH\"\n          \n          which cargo || (echo \"cargo not found\" && exit 1)\n          \n          echo \"All build tools verified successfully\"\n          echo \"  CMake: $(cmake --version | head -1)\"\n          echo \"  Ninja: $(ninja --version)\"\n          echo \"  GCC: $(gcc --version | head -1)\"\n          echo \"  Cargo: $(cargo --version)\"\n\n      - name: Build Windows\n        shell: msys2 {0}\n        env:\n          BRANCH: master\n          BUILD_VERSION: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release-tag }}.杂鱼\n          COMMIT: ${{ github.sha }}\n        run: |\n          # add Rust to PATH for Tauri GUI build\n          WINDOWS_USER=$(cmd //c \"echo %USERNAME%\" | tr -d '\\r')\n          CARGO_BIN=\"/c/Users/${WINDOWS_USER}/.cargo/bin\"\n          export PATH=\"$CARGO_BIN:$PATH\"\n\n          mkdir -p build\n          cmake \\\n            -B build \\\n            -G Ninja \\\n            -S . \\\n            -DBUILD_DOCS=OFF \\\n            -DSUNSHINE_ASSETS_DIR=assets \\\n            -DSUNSHINE_PUBLISHER_NAME='${{ github.repository_owner }}' \\\n            -DSUNSHINE_PUBLISHER_WEBSITE='https://github.com/qiin2333/Sunshine-Foundation' \\\n            -DSUNSHINE_PUBLISHER_ISSUE_URL='https://github.com/qiin2333/Sunshine-Foundation/issues'\n          ninja -C build\n          ninja -C build sunshine-control-panel\n\n      # Install Inno Setup\n      - name: Install Inno Setup\n        shell: pwsh\n        run: |\n          $url = \"https://jrsoftware.org/download.php/is.exe\"\n          $installer = \"$env:TEMP\\innosetup.exe\"\n          Invoke-WebRequest -Uri $url -OutFile $installer\n          Start-Process -FilePath $installer -ArgumentList '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-' -Wait\n          echo \"C:\\Program Files (x86)\\Inno Setup 6\" >> $env:GITHUB_PATH\n\n      # 生成未签名的打包产物\n      - name: Package unsigned files\n        shell: msys2 {0}\n        run: |\n          cd build\n\n          # 生成 ZIP 便携版（包含所有文件）\n          cpack -G ZIP --verbose\n\n          # 生成 Inno Setup staging 目录\n          echo \"Generating Inno Setup staging directory...\"\n          cmake --install . --prefix ./inno_staging\n\n          cd ..\n          \n          # 列出生成的文件\n          echo \"Generated files:\"\n          ls -lh build/cpack_artifacts/\n\n      # 解压 Portable ZIP 获取所有需要签名的文件\n      - name: Extract files for signing\n        shell: bash\n        run: |\n          PORTABLE=$(find build/cpack_artifacts -name \"*.zip\" | head -n 1)\n          if [ -n \"$PORTABLE\" ]; then\n            echo \"Extracting: $PORTABLE\"\n            mkdir -p unsigned-files\n            7z x \"$PORTABLE\" -o\"unsigned-files\" -y -aoa\n            echo \"Extracted files for signing:\"\n            ls -laR unsigned-files/\n          else\n            echo \"No portable ZIP found\"\n            exit 1\n          fi\n\n      # 上传所有未签名的文件到 SignPath\n      - name: Upload unsigned files for signing\n        id: upload-unsigned\n        uses: actions/upload-artifact@v4\n        with:\n          name: files-for-signing\n          path: unsigned-files/\n\n      # 提交到 SignPath 签名（使用生产策略）\n      - name: Submit to SignPath for signing\n        uses: signpath/github-action-submit-signing-request@v1\n        with:\n          api-token: ${{ secrets.SIGNPATH_API_TOKEN }}\n          organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}\n          project-slug: Sunshine-Foundation\n          signing-policy-slug: release-signing\n          artifact-configuration-slug: windows-portable\n          github-artifact-id: \"${{ steps.upload-unsigned.outputs.artifact-id }}\"\n          output-artifact-directory: signed-files\n          wait-for-completion: true\n          wait-for-completion-timeout-in-seconds: 600\n          service-unavailable-timeout-in-seconds: 600\n\n      # 验证签名\n      - name: Verify all signatures\n        shell: pwsh\n        run: |\n          Write-Host \"Verifying signatures...\"\n          $signedFiles = Get-ChildItem -Path \"signed-files\" -Recurse -File -Include *.exe,*.dll\n\n          foreach ($file in $signedFiles) {\n            Write-Host \"`n=== $($file.Name) ===\"\n            $signature = Get-AuthenticodeSignature $file.FullName\n            Write-Host \"Status: $($signature.Status)\"\n            \n            if ($signature.Status -ne \"Valid\") {\n              Write-Error \"Signature invalid for: $($file.Name)\"\n              exit 1\n            }\n          }\n          Write-Host \"`n✓ All signatures are VALID!\" -ForegroundColor Green\n\n      # 使用签名后的文件重新打包 ZIP 便携版\n      - name: Repackage signed Portable ZIP\n        shell: bash\n        run: |\n          cd signed-files\n          # 使用与输入一致的版本号\n          if [ -n \"${{ github.event.inputs.release-tag }}\" ]; then\n            VERSION=\"${{ github.event.inputs.release-tag }}\"\n          elif [ -n \"${{ github.event.release.tag_name }}\" ]; then\n            VERSION=\"${{ github.event.release.tag_name }}\"\n          else\n            VERSION=\"v$(date +%Y.%m%d)\"\n          fi\n          7z a \"../Sunshine-${VERSION}-Windows-Portable-Signed.zip\" * -y\n          cd ..\n          echo \"Created signed portable package:\"\n          ls -lh Sunshine-*-Portable-Signed.zip\n\n      # 使用签名文件重新打包 Inno Setup 安装包\n      - name: Repackage signed Inno Setup installer\n        shell: msys2 {0}\n        run: |\n          echo \"Repackaging Inno Setup installer with signed files...\"\n\n          STAGING_DIR=\"build/inno_staging\"\n\n          # 检查 staging 目录是否存在\n          if [ ! -d \"$STAGING_DIR\" ]; then\n            echo \"Error: Inno Setup staging directory not found: $STAGING_DIR\"\n            ls -laR build/\n            exit 1\n          fi\n\n          # 替换为签名后的文件\n          SIGNED_SRC=\"signed-files/Sunshine\"\n          \n          echo \"Replacing with signed files...\"\n          cp -fv \"$SIGNED_SRC/sunshine.exe\"                 \"$STAGING_DIR/sunshine.exe\"\n          cp -fv \"$SIGNED_SRC/zlib1.dll\"                    \"$STAGING_DIR/zlib1.dll\"\n          cp -fv \"$SIGNED_SRC/tools/sunshinesvc.exe\"        \"$STAGING_DIR/tools/sunshinesvc.exe\"\n          cp -fv \"$SIGNED_SRC/tools/qiin-tabtip.exe\"        \"$STAGING_DIR/tools/qiin-tabtip.exe\"\n          cp -fv \"$SIGNED_SRC/tools/device-toggler.exe\"     \"$STAGING_DIR/tools/device-toggler.exe\"\n          cp -fv \"$SIGNED_SRC/tools/DevManView.exe\"         \"$STAGING_DIR/tools/DevManView.exe\"\n          cp -fv \"$SIGNED_SRC/tools/restart64.exe\"          \"$STAGING_DIR/tools/restart64.exe\"\n          cp -fv \"$SIGNED_SRC/tools/SetDpi.exe\"             \"$STAGING_DIR/tools/SetDpi.exe\"\n          cp -fv \"$SIGNED_SRC/tools/setreg.exe\"             \"$STAGING_DIR/tools/setreg.exe\"\n          cp -fv \"$SIGNED_SRC/tools/audio-info.exe\"         \"$STAGING_DIR/tools/audio-info.exe\"\n          cp -fv \"$SIGNED_SRC/tools/dxgi-info.exe\"          \"$STAGING_DIR/tools/dxgi-info.exe\"\n          cp -fv \"$SIGNED_SRC/assets/gui/sunshine-gui.exe\"  \"$STAGING_DIR/assets/gui/sunshine-gui.exe\"\n\n          # 运行 Inno Setup 编译器重新打包\n          echo \"Running ISCC to repackage installer...\"\n          \"/c/Program Files (x86)/Inno Setup 6/ISCC.exe\" \"build/sunshine_installer.iss\"\n\n          # 使用与 Portable ZIP 一致的版本号\n          if [ -n \"${{ github.event.inputs.release-tag }}\" ]; then\n            VERSION=\"${{ github.event.inputs.release-tag }}\"\n          elif [ -n \"${{ github.event.release.tag_name }}\" ]; then\n            VERSION=\"${{ github.event.release.tag_name }}\"\n          else\n            VERSION=\"v$(date +%Y.%m%d)\"\n          fi\n          \n          mv -fv \"build/cpack_artifacts/Sunshine.exe\" \"Sunshine-${VERSION}-Windows-Installer-Signed.exe\"\n          echo \"Created signed installer:\"\n          ls -lh Sunshine-*-Installer-Signed.exe\n\n      # 签名最终的 Inno Setup 安装包\n      - name: Upload installer for final signing\n        id: upload-installer\n        uses: actions/upload-artifact@v4\n        with:\n          name: installer-for-final-signing\n          path: Sunshine-*-Installer-Signed.exe\n\n      - name: Sign installer\n        uses: signpath/github-action-submit-signing-request@v1\n        with:\n          api-token: ${{ secrets.SIGNPATH_API_TOKEN }}\n          organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}\n          project-slug: Sunshine-Foundation\n          signing-policy-slug: release-signing\n          artifact-configuration-slug: windows-installer\n          github-artifact-id: \"${{ steps.upload-installer.outputs.artifact-id }}\"\n          output-artifact-directory: final-signed\n          wait-for-completion: true\n          wait-for-completion-timeout-in-seconds: 600\n          service-unavailable-timeout-in-seconds: 600\n\n      # 验证最终签名\n      - name: Verify final installer signature\n        shell: pwsh\n        run: |\n          $installer = Get-ChildItem \"final-signed\" -Filter \"*.exe\" | Select-Object -First 1\n          if ($installer) {\n            Write-Host \"Verifying installer signature...\"\n            $signature = Get-AuthenticodeSignature $installer.FullName\n            Write-Host \"Status: $($signature.Status)\"\n            if ($signature.Status -eq \"Valid\") {\n              Write-Host \"✓ Installer signature is VALID!\" -ForegroundColor Green\n            }\n          }\n\n      # 上传最终的签名文件\n      - name: Upload final signed packages\n        uses: actions/upload-artifact@v4\n        with:\n          name: sunshine-windows-fully-signed\n          path: |\n            Sunshine-*-Portable-Signed.zip\n            final-signed/*.exe\n          if-no-files-found: error\n\n      # 发布到 GitHub Release\n      - name: Create Release\n        uses: softprops/action-gh-release@v1\n        with:\n          tag_name: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release-tag }}\n          name: ${{ github.event_name == 'release' && github.event.release.name || github.event.inputs.release-tag }} (Signed)\n          files: |\n            Sunshine-*-Portable-Signed.zip\n            final-signed/*.exe\n          draft: true\n          prerelease: true\n          allowUpdates: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test-signpath.yml",
    "content": "# SignPath 测试工作流\n# 用于手动测试 SignPath 签名功能\n\nname: Test SignPath Signing\n\non:\n  workflow_dispatch:\n    inputs:\n      artifact-name:\n        description: 'Artifact name to sign'\n        required: false\n        default: 'sunshine-windows'\n      run-id:\n        description: 'Run ID to download artifact from (leave empty for latest)'\n        required: false\n\njobs:\n  test-sign:\n    name: Test SignPath\n    runs-on: windows-latest\n    \n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      \n      # 如果提供了 run-id，从指定的 run 下载\n      - name: Download specific artifact\n        if: ${{ github.event.inputs.run-id != '' }}\n        uses: actions/download-artifact@v4\n        with:\n          name: ${{ github.event.inputs.artifact-name }}\n          path: artifacts\n          run-id: ${{ github.event.inputs.run-id }}\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n      \n      # 如果没有提供 run-id，尝试从最近的成功构建下载\n      - name: Download latest artifact\n        if: ${{ github.event.inputs.run-id == '' }}\n        uses: dawidd6/action-download-artifact@v3\n        with:\n          workflow: main.yml\n          name: ${{ github.event.inputs.artifact-name }}\n          path: artifacts\n          check_artifacts: true\n          search_artifacts: true\n      \n      - name: List downloaded files\n        shell: bash\n        run: |\n          echo \"Downloaded files:\"\n          ls -lR artifacts/\n      \n      # 查找文件\n      - name: Find installer file\n        id: find-installer\n        shell: bash\n        run: |\n          INSTALLER=$(find artifacts -name \"*.exe\" -name \"*Installer*\" | head -n 1)\n          if [ -z \"$INSTALLER\" ]; then\n            echo \"No installer found\"\n            echo \"has-installer=false\" >> $GITHUB_OUTPUT\n          else\n            echo \"Found installer: $INSTALLER\"\n            echo \"installer-file=$INSTALLER\" >> $GITHUB_OUTPUT\n            echo \"has-installer=true\" >> $GITHUB_OUTPUT\n          fi\n      \n      - name: Find portable file\n        id: find-portable\n        shell: bash\n        run: |\n          PORTABLE=$(find artifacts -name \"*.zip\" -name \"*Portable*\" | head -n 1)\n          if [ -z \"$PORTABLE\" ]; then\n            echo \"No portable package found\"\n            echo \"has-portable=false\" >> $GITHUB_OUTPUT\n          else\n            echo \"Found portable: $PORTABLE\"\n            echo \"portable-file=$PORTABLE\" >> $GITHUB_OUTPUT\n            echo \"has-portable=true\" >> $GITHUB_OUTPUT\n          fi\n      \n      # 解压 Portable ZIP 以减少一层嵌套\n      - name: Extract Portable ZIP\n        if: ${{ steps.find-portable.outputs.has-portable == 'true' }}\n        shell: bash\n        run: |\n          mkdir -p artifacts/portable-extracted\n          7z x \"${{ steps.find-portable.outputs.portable-file }}\" -o\"artifacts/portable-extracted\"\n          echo \"Extracted portable files:\"\n          ls -laR artifacts/portable-extracted/\n      \n      # 为 Installer 创建单独的 artifact\n      - name: Upload Installer for SignPath\n        id: upload-installer\n        if: ${{ steps.find-installer.outputs.has-installer == 'true' }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: installer-for-signing\n          path: ${{ steps.find-installer.outputs.installer-file }}\n      \n      # 为 Portable 创建单独的 artifact（上传解压后的文件夹）\n      - name: Upload Portable for SignPath\n        id: upload-portable\n        if: ${{ steps.find-portable.outputs.has-portable == 'true' }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: portable-for-signing\n          path: artifacts/portable-extracted/\n      \n      # 测试签名 - Installer\n      - name: Test SignPath - Installer\n        if: ${{ steps.find-installer.outputs.has-installer == 'true' }}\n        uses: signpath/github-action-submit-signing-request@v1\n        with:\n          api-token: ${{ secrets.SIGNPATH_API_TOKEN }}\n          organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}\n          project-slug: Sunshine-Foundation\n          signing-policy-slug: test-signing\n          artifact-configuration-slug: windows-installer\n          github-artifact-id: '${{ steps.upload-installer.outputs.artifact-id }}'\n          output-artifact-directory: artifacts/signed/installer\n          wait-for-completion: true\n          wait-for-completion-timeout-in-seconds: 600\n          service-unavailable-timeout-in-seconds: 600\n      \n      # 测试签名 - Portable\n      - name: Test SignPath - Portable\n        if: ${{ steps.find-portable.outputs.has-portable == 'true' }}\n        uses: signpath/github-action-submit-signing-request@v1\n        with:\n          api-token: ${{ secrets.SIGNPATH_API_TOKEN }}\n          organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}\n          project-slug: Sunshine-Foundation\n          signing-policy-slug: test-signing\n          artifact-configuration-slug: windows-portable\n          github-artifact-id: '${{ steps.upload-portable.outputs.artifact-id }}'\n          output-artifact-directory: artifacts/signed/portable\n          wait-for-completion: true\n          wait-for-completion-timeout-in-seconds: 600\n          service-unavailable-timeout-in-seconds: 600\n      \n      # 列出签名后的文件\n      - name: List signed files\n        shell: bash\n        run: |\n          echo \"Signed files:\"\n          if [ -d \"artifacts/signed\" ]; then\n            ls -laR artifacts/signed/\n          else\n            echo \"No signed files directory found\"\n          fi\n      \n      # 验证签名\n      - name: Verify Signatures\n        shell: pwsh\n        run: |\n          if (Test-Path \"artifacts/signed\") {\n            Write-Host \"Verifying signatures...\"\n            \n            # 递归搜索所有签名文件\n            $signedFiles = Get-ChildItem -Path \"artifacts/signed\" -Recurse -File\n            \n            if ($signedFiles.Count -eq 0) {\n              Write-Warning \"No signed files found\"\n            } else {\n              Write-Host \"Found $($signedFiles.Count) signed file(s)\"\n              \n              foreach ($file in $signedFiles) {\n                Write-Host \"`n=== Checking: $($file.Name) ===\"\n                Write-Host \"Path: $($file.FullName)\"\n                \n                $signature = Get-AuthenticodeSignature $file.FullName\n                \n                Write-Host \"Status: $($signature.Status)\"\n                if ($signature.SignerCertificate) {\n                  Write-Host \"Signer: $($signature.SignerCertificate.Subject)\"\n                }\n                if ($signature.TimeStamperCertificate) {\n                  Write-Host \"Timestamp: $($signature.TimeStamperCertificate.Subject)\"\n                }\n                \n                if ($signature.Status -eq \"Valid\") {\n                  Write-Host \"✓ Signature is VALID\" -ForegroundColor Green\n                } elseif ($signature.Status -eq \"NotSigned\") {\n                  Write-Warning \"⚠ File is NOT SIGNED\"\n                } else {\n                  Write-Warning \"⚠ Signature status: $($signature.Status)\"\n                }\n              }\n            }\n          } else {\n            Write-Warning \"Signed files directory not found\"\n          }\n      \n      # 上传已签名的文件\n      - name: Upload Signed Files\n        uses: actions/upload-artifact@v4\n        with:\n          name: signpath-test-signed\n          path: artifacts/signed/\n          if-no-files-found: warn\n\n"
  },
  {
    "path": ".github/workflows/translate-simple.yml",
    "content": "name: Simple README Translation\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - 'README.md'\n  workflow_dispatch:\n\njobs:\n  translate-simple:\n    name: Simple README Translation\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      pull-requests: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Setup Python\n        uses: actions/setup-python@v4\n        with:\n          python-version: '3.9'\n\n      - name: Install translation dependencies\n        run: |\n          pip install requests\n\n      - name: Create translation script\n        run: |\n          cat > translate_simple.py << 'EOF'\n          import os\n          import re\n          import requests\n\n          # 项目名和术语保护列表\n          PROTECTED_TERMS = [\n              'Sunshine', 'README', 'GitHub', 'CI', 'API', 'Markdown', 'OpenAI', 'DeepL', 'Google Translate',\n              # 可在此添加更多术语\n          ]\n\n          def mask_terms(text):\n              for term in PROTECTED_TERMS:\n                  text = re.sub(rf'(?<![`\\w]){re.escape(term)}(?![`\\w])', f'@@@{term}@@@', text)\n              return text\n\n          def unmask_terms(text):\n              for term in PROTECTED_TERMS:\n                  text = text.replace(f'@@@{term}@@@', term)\n              return text\n\n          def translate_with_deepseek(text, target_lang):\n              # 使用 DeepSeek API 进行翻译\n              api_key = os.getenv('DEEPSEEK_API_KEY')\n              if not api_key:\n                  raise Exception('DEEPSEEK_API_KEY 环境变量未设置')\n              url = 'https://api.deepseek.com/v1/chat/completions'\n              prompt = f\"请将以下 Markdown 内容翻译为{target_lang}，但不要翻译项目名和术语：{', '.join(PROTECTED_TERMS)}。保持原有格式、链接和图片。\\n\\n{text}\"\n              headers = {\n                  'Authorization': f'Bearer {api_key}',\n                  'Content-Type': 'application/json'\n              }\n              payload = {\n                  \"model\": \"deepseek-chat\",\n                  \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n                  \"temperature\": 0.2\n              }\n              resp = requests.post(url, headers=headers, json=payload)\n              resp.raise_for_status()\n              result = resp.json()\n              return result['choices'][0]['message']['content']\n\n          def translate_readme():\n              with open('README.md', 'r', encoding='utf-8') as f:\n                  content = f.read()\n\n              languages = [\n                  ('en', 'English'),\n                  ('fr', 'French'),\n                  ('de', 'German'),\n                  ('ja', 'Japanese')\n              ]\n\n              for lang_code, lang_name in languages:\n                  try:\n                      if lang_code == 'zh_CN':\n                          translated_content = content\n                      else:\n                          masked = mask_terms(content)\n                          translated = translate_with_deepseek(masked, lang_name)\n                          translated = unmask_terms(translated)\n                          # 去除 DeepSeek 返回的多余提示，只保留第一个 Markdown 标题及后面内容\n                          lines = translated.splitlines()\n                          for idx, line in enumerate(lines):\n                              if line.strip().startswith('#'):\n                                translated_content = '\\n'.join(lines[idx:])\n                                break\n                          else:\n                            translated_content = translated.strip()\n\n                      filename = f'README.{lang_code}.md'\n                      with open(filename, 'w', encoding='utf-8') as f:\n                          f.write(translated_content)\n                      print(f\"✓ Translated to {lang_name} ({lang_code})\")\n                  except Exception as e:\n                      print(f\"✗ Failed to translate to {lang_name}: {e}\")\n\n          if __name__ == \"__main__\":\n              translate_readme()\n          EOF\n\n      - name: Run translation\n        env:\n          DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}\n        run: python translate_simple.py\n\n      - name: Add language selector to README\n        run: |\n          # Check if language selector already exists\n          if grep -q \"多语言支持\" README.md; then\n            echo \"Language selector already exists, skipping...\"\n          else\n            # Create a backup of the original README\n            cp README.md README.md.backup\n            \n            # Create the language selector content\n            cat > language_selector.md << 'EOF'\n          \n          ## 🌐 多语言支持 / Multi-language Support\n\n          <div align=\"center\">\n\n          [![English](https://img.shields.io/badge/English-README.en.md-blue?style=for-the-badge)](README.en.md)\n          [![中文简体](https://img.shields.io/badge/中文简体-README.zh--CN.md-red?style=for-the-badge)](README.md)\n          [![Français](https://img.shields.io/badge/Français-README.fr.md-green?style=for-the-badge)](README.fr.md)\n          [![Deutsch](https://img.shields.io/badge/Deutsch-README.de.md-yellow?style=for-the-badge)](README.de.md)\n          [![日本語](https://img.shields.io/badge/日本語-README.ja.md-purple?style=for-the-badge)](README.ja.md)\n\n          </div>\n\n          ---\n\n          EOF\n            \n            # Insert language selector after the first line (title)\n            head -n 1 README.md > README.md.new\n            cat language_selector.md >> README.md.new\n            tail -n +2 README.md >> README.md.new\n            \n            # Replace the original file\n            mv README.md.new README.md\n            \n            # Clean up\n            rm language_selector.md\n            \n            echo \"✓ Added language selector to main README\"\n          fi\n\n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v5\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          title: \"docs: add multi-language README translations with language selector\"\n          body: |\n            ## 自动翻译更新\n            \n            此PR包含以下语言的README翻译：\n            \n            - English (en)\n            - French (fr)\n            - German (de)\n            - Japanese (ja)\n            \n            ### 翻译说明\n            - 使用Google Translate API自动翻译\n            - 保持原始Markdown格式\n            - 保留所有链接和图片\n            - 建议人工检查翻译质量\n            \n            ### 生成的文件\n            - `README.en.md` - 英语版本\n            - `README.fr.md` - 法语版本\n            - `README.de.md` - 德语版本\n            - `README.ja.md` - 日语版本\n            \n            ### 新增功能\n            - 在主README文件中添加了语言选择器\n            - 用户可以通过徽章快速访问不同语言版本\n            - 支持5种语言的快速切换\n            \n            ---\n            \n            *此PR由CI自动生成，当README.md文件更新时触发*\n          branch: docs/translations\n          delete-branch: true\n          commit-message: \"docs: add multi-language README translations with language selector [skip ci]\"\n\n      - name: Create summary\n        run: |\n          echo \"## Translation Summary\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"The following README translations were generated:\" >> $GITHUB_STEP_SUMMARY\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          for file in README.*.md; do\n            if [ -f \"$file\" ]; then\n              lang=$(echo $file | sed 's/README\\.\\(.*\\)\\.md/\\1/')\n              echo \"- $lang\" >> $GITHUB_STEP_SUMMARY\n            fi\n          done\n          echo \"\" >> $GITHUB_STEP_SUMMARY\n          echo \"✓ Language selector added to main README\" >> $GITHUB_STEP_SUMMARY"
  },
  {
    "path": ".github/workflows/update-driver-versions.yml",
    "content": "# Update driver version pins in cmake/packaging/FetchDriverDeps.cmake\n#\n# Driver binaries are no longer committed to the repo.\n# CMake downloads them at configure time from GitHub Releases.\n# This workflow updates the version pins and creates a PR.\n\nname: Update Driver Versions\n\non:\n  workflow_dispatch:\n    inputs:\n      component:\n        description: 'Which driver to update'\n        required: true\n        type: choice\n        options:\n          - vmouse\n          - vdd\n          - nefcon\n          - all\n      version:\n        description: 'Release tag (e.g. v1.1.0). Leave empty for latest.'\n        required: false\n        default: ''\n        type: string\n  repository_dispatch:\n    types: [driver-release]\n\njobs:\n  update-version:\n    name: Update Driver Version Pin\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      pull-requests: write\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Resolve versions and update pins\n        shell: bash\n        env:\n          GH_TOKEN: ${{ github.token }}\n        run: |\n          CMAKE_FILE=\"cmake/packaging/FetchDriverDeps.cmake\"\n\n          COMPONENT=\"${{ github.event.inputs.component }}\"\n          VERSION=\"${{ github.event.inputs.version }}\"\n\n          # Override with repository_dispatch values\n          if [ \"${{ github.event_name }}\" = \"repository_dispatch\" ]; then\n            COMPONENT=\"${{ github.event.client_payload.component }}\"\n            VERSION=\"${{ github.event.client_payload.version }}\"\n            echo \"Triggered by repository_dispatch: component=$COMPONENT version=$VERSION\"\n          fi\n\n          resolve_latest() {\n            local repo=\"$1\"\n            local tag\n            tag=$(gh api \"repos/${repo}/releases/latest\" --jq '.tag_name' 2>/dev/null || echo \"\")\n            echo \"$tag\"\n          }\n\n          update_pin() {\n            local var_name=\"$1\"\n            local new_version=\"$2\"\n            if [ -z \"$new_version\" ]; then\n              echo \"Warning: empty version for ${var_name}, skipping\"\n              return\n            fi\n            echo \"Updating ${var_name} to ${new_version}\"\n            sed -i \"s|set(${var_name} \\\"[^\\\"]*\\\"|set(${var_name} \\\"${new_version}\\\"|\" \"$CMAKE_FILE\"\n          }\n\n          declare -A REPOS=(\n            [vmouse]=\"AlkaidLab/ZakoVirtualMouse\"\n            [vdd]=\"qiin2333/zako-vdd\"\n            [nefcon]=\"nefarius/nefcon\"\n          )\n          declare -A VARS=(\n            [vmouse]=\"VMOUSE_DRIVER_VERSION\"\n            [vdd]=\"VDD_DRIVER_VERSION\"\n            [nefcon]=\"NEFCON_VERSION\"\n          )\n\n          if [ \"$COMPONENT\" = \"all\" ]; then\n            COMPONENTS=(vmouse vdd nefcon)\n          else\n            COMPONENTS=(\"$COMPONENT\")\n          fi\n\n          SUMMARY=\"\"\n          for comp in \"${COMPONENTS[@]}\"; do\n            repo=\"${REPOS[$comp]}\"\n            var=\"${VARS[$comp]}\"\n            ver=\"$VERSION\"\n\n            if [ -z \"$ver\" ]; then\n              ver=$(resolve_latest \"$repo\")\n            fi\n\n            if [ -n \"$ver\" ]; then\n              update_pin \"$var\" \"$ver\"\n              SUMMARY=\"${SUMMARY}\\n- ${comp}: ${ver}\"\n            else\n              echo \"Warning: could not resolve version for ${comp}\"\n            fi\n          done\n\n          echo \"SUMMARY<<EOF\" >> \"$GITHUB_ENV\"\n          echo -e \"$SUMMARY\" >> \"$GITHUB_ENV\"\n          echo \"EOF\" >> \"$GITHUB_ENV\"\n\n          echo \"Updated CMake file:\"\n          grep -E \"^set\\((VMOUSE_DRIVER_VERSION|VDD_DRIVER_VERSION|NEFCON_VERSION)\" \"$CMAKE_FILE\"\n\n      - name: Create PR\n        uses: peter-evans/create-pull-request@v6\n        with:\n          commit-message: \"chore: update driver version pins\"\n          title: \"Update driver versions\"\n          body: |\n            Automated update of driver version pins in `FetchDriverDeps.cmake`.\n\n            Updated versions:\n            ${{ env.SUMMARY }}\n\n            CMake will download the new driver binaries at configure time.\n          branch: update-driver-versions\n          delete-branch: true\n"
  },
  {
    "path": ".gitignore",
    "content": "# Prerequisites\n*.d\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Compiled Dynamic libraries\n*.so\n*.dylib\n\n# Fortran module files\n*.mod\n*.smod\n\n# Compiled Static libraries\n*.lai\n*.la\n*.a\n*.lib\n\n# Executables\n*.out\n*.app\n\n# JetBrains IDE\n.idea/\n\n# VSCode IDE\n.vscode/\n.vs/\n\n# build directories\nbuild/\ncmake-*/\ndocs/doxyconfig*\n\n# Build logs\nbuild.log\n*.log\n\n# CMake user presets (personal overrides)\nCMakeUserPresets.json\n\n# npm\nnode_modules/\npackage-lock.json\n\n# Translations\n*.mo\n*.pot\n\n# Dummy macOS files\n.DS_Store\n\n# Python\n*.pyc\nvenv/\n\n# Dev notes (analysis docs, guide generation, images)\n_dev_notes/\n\n# Temp HID trace files\ntemphidtrace*\n\n# Windows NUL device artifact\nnul\n\n# Driver binaries (downloaded at build time via FetchDriverDeps.cmake)\nsrc_assets/windows/misc/vdd/driver/*.dll\nsrc_assets/windows/misc/vdd/driver/*.inf\nsrc_assets/windows/misc/vdd/driver/*.cat\nsrc_assets/windows/misc/vdd/driver/*.cer\nsrc_assets/windows/misc/vdd/driver/*.zip\nsrc_assets/windows/misc/vdd/driver/*.exe\nsrc_assets/windows/misc/vmouse/driver/*.dll\nsrc_assets/windows/misc/vmouse/driver/*.inf\nsrc_assets/windows/misc/vmouse/driver/*.cat\n\n# Generated files\nsrc_assets/common/assets/web/welcome.html\nsrc_assets/common/assets/web/dist/\n# Local dev scripts\nbuild.sh\n\n# Vulkan HDR layer source (standalone tool, not part of Sunshine)\nsrc/vk_layer_hdr_inject/\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"packaging/linux/flatpak/deps/flatpak-builder-tools\"]\n\tpath = packaging/linux/flatpak/deps/flatpak-builder-tools\n\turl = https://github.com/flatpak/flatpak-builder-tools.git\n\tbranch = master\n[submodule \"packaging/linux/flatpak/deps/shared-modules\"]\n\tpath = packaging/linux/flatpak/deps/shared-modules\n\turl = https://github.com/flathub/shared-modules.git\n\tbranch = master\n[submodule \"third-party/build-deps\"]\n\tpath = third-party/build-deps\n\turl = https://github.com/LizardByte/build-deps.git\n\tbranch = dist\n[submodule \"third-party/doxyconfig\"]\n\tpath = third-party/doxyconfig\n\turl = https://github.com/LizardByte/doxyconfig.git\n\tbranch = master\n[submodule \"third-party/googletest\"]\n\tpath = third-party/googletest\n\turl = https://github.com/google/googletest.git\n\tbranch = main\n[submodule \"third-party/inputtino\"]\n\tpath = third-party/inputtino\n\turl = https://github.com/games-on-whales/inputtino.git\n\tbranch = stable\n[submodule \"third-party/moonlight-common-c\"]\n\tpath = third-party/moonlight-common-c\n\turl = https://github.com/moonlight-stream/moonlight-common-c.git\n\tbranch = master\n[submodule \"third-party/nanors\"]\n\tpath = third-party/nanors\n\turl = https://github.com/sleepybishop/nanors.git\n\tbranch = master\n[submodule \"third-party/nvenc-headers/1100\"]\n\tpath = third-party/nvenc-headers/1100\n\turl = https://github.com/FFmpeg/nv-codec-headers.git\n\tbranch = sdk/11.0\n[submodule \"third-party/nvenc-headers/1200\"]\n\tpath = third-party/nvenc-headers/1200\n\turl = https://github.com/FFmpeg/nv-codec-headers.git\n\tbranch = sdk/12.0\n[submodule \"third-party/nvenc-headers/1202\"]\n\tpath = third-party/nvenc-headers/1202\n\turl = https://github.com/FFmpeg/nv-codec-headers.git\n\tbranch = master\n[submodule \"third-party/nvapi\"]\n\tpath = third-party/nvapi\n\turl = https://github.com/NVIDIA/nvapi.git\n\tbranch = main\n[submodule \"third-party/Simple-Web-Server\"]\n\tpath = third-party/Simple-Web-Server\n\turl = https://github.com/LizardByte-infrastructure/Simple-Web-Server.git\n\tbranch = master\n[submodule \"third-party/TPCircularBuffer\"]\n\tpath = third-party/TPCircularBuffer\n\turl = https://github.com/michaeltyson/TPCircularBuffer.git\n\tbranch = master\n[submodule \"third-party/tray\"]\n\tpath = third-party/tray\n\turl = https://github.com/LizardByte/tray.git\n\tbranch = master\n[submodule \"third-party/ViGEmClient\"]\n\tpath = third-party/ViGEmClient\n\turl = https://github.com/LizardByte/Virtual-Gamepad-Emulation-Client.git\n\tbranch = master\n[submodule \"third-party/wayland-protocols\"]\n\tpath = third-party/wayland-protocols\n\turl = https://github.com/LizardByte-infrastructure/wayland-protocols.git\n\tbranch = main\n[submodule \"third-party/wlr-protocols\"]\n\tpath = third-party/wlr-protocols\n\turl = https://github.com/LizardByte-infrastructure/wlr-protocols.git\n\tbranch = master\n[submodule \"src_assets/common/sunshine-control-panel\"]\n\tpath = src_assets/common/sunshine-control-panel\n\turl = https://github.com/qiin2333/sunshine-control-panel.git\n\tbranch = master\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"printWidth\": 120,\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"prettier.spaceBeforeFunctionParen\": true\n}\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "---\n# .readthedocs.yaml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\nversion: 2\n\nbuild:\n  os: ubuntu-24.04\n  tools:\n    python: \"miniconda-latest\"\n  commands:\n    - |\n      if [ -f readthedocs_build.sh ]; then\n        doxyconfig_dir=\".\"\n      else\n        doxyconfig_dir=\"./third-party/doxyconfig\"\n      fi\n      chmod +x \"${doxyconfig_dir}/readthedocs_build.sh\"\n      export DOXYCONFIG_DIR=\"${doxyconfig_dir}\"\n      \"${doxyconfig_dir}/readthedocs_build.sh\"\n\n# using conda, we can get newer doxygen and graphviz than ubuntu provide\n# https://github.com/readthedocs/readthedocs.org/issues/8151#issuecomment-890359661\nconda:\n  environment: third-party/doxyconfig/environment.yml\n\nsubmodules:\n  include: all\n  recursive: true\n"
  },
  {
    "path": ".rstcheck.cfg",
    "content": "# configuration file for rstcheck, an rst linting tool\n# https://rstcheck.readthedocs.io/en/latest/usage/config\n\n[rstcheck]\nignore_directives =\n    doxygenfile,\n    include,\n    mdinclude,\n    tab,\n    todo,\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.20)\n# `CMAKE_CUDA_ARCHITECTURES` requires 3.18\n# `set_source_files_properties` requires 3.18\n# `cmake_path(CONVERT ... TO_NATIVE_PATH_LIST ...)` requires 3.20\n# todo - set this conditionally\n\nproject(Sunshine VERSION 0.0.0\n        DESCRIPTION \"Self-hosted game stream host for Moonlight\"\n        HOMEPAGE_URL \"https://alkaidlab.com/sunshine\")\n\nset(PROJECT_LICENSE \"GPL-3.0-only\")\n\nset(PROJECT_FQDN \"sunshine.alkaidlab.com\")\n\nset(PROJECT_BRIEF_DESCRIPTION \"GameStream host for Moonlight\")  # must be <= 35 characters\n\nset(PROJECT_LONG_DESCRIPTION \"Offering low latency, cloud gaming server capabilities with support for AMD, Intel, \\\nand Nvidia GPUs for hardware encoding. Software encoding is also available. You can connect to Sunshine from any \\\nMoonlight client on a variety of devices. A web UI is provided to allow configuration, and client pairing, from \\\nyour favorite web browser. Pair from the local server or any mobile device.\")\n\nif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    message(STATUS \"Setting build type to 'Release' as none was specified.\")\n    set(CMAKE_BUILD_TYPE \"Release\" CACHE STRING \"Choose the type of build.\" FORCE)\nendif()\n\n# set the module path, used for includes\nset(CMAKE_MODULE_PATH \"${CMAKE_SOURCE_DIR}/cmake\")\n\n# export compile_commands.json\nset(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n\n# set version info for this build\ninclude(${CMAKE_MODULE_PATH}/prep/build_version.cmake)\n\n# cmake build flags\ninclude(${CMAKE_MODULE_PATH}/prep/options.cmake)\n\n# initial prep\ninclude(${CMAKE_MODULE_PATH}/prep/init.cmake)\n\n# configure special package files, such as sunshine.desktop, Flatpak manifest, Portfile , etc.\ninclude(${CMAKE_MODULE_PATH}/prep/special_package_configuration.cmake)\n\n# Exit early if END_BUILD is ON, i.e. when only generating package manifests\nif(${END_BUILD})\n    return()\nendif()\n\n# project constants\ninclude(${CMAKE_MODULE_PATH}/prep/constants.cmake)\n\n# load macros\ninclude(${CMAKE_MODULE_PATH}/macros/common.cmake)\n\n# load dependencies\ninclude(${CMAKE_MODULE_PATH}/dependencies/common.cmake)\n\n# setup compile definitions\ninclude(${CMAKE_MODULE_PATH}/compile_definitions/common.cmake)\n\n# target definitions\ninclude(${CMAKE_MODULE_PATH}/targets/common.cmake)\n\n# packaging\ninclude(${CMAKE_MODULE_PATH}/packaging/common.cmake)\n"
  },
  {
    "path": "CMakePresets.json",
    "content": "{\n  \"version\": 6,\n  \"cmakeMinimumRequired\": {\n    \"major\": 3,\n    \"minor\": 20,\n    \"patch\": 0\n  },\n  \"configurePresets\": [\n    {\n      \"name\": \"dev-win\",\n      \"displayName\": \"Windows Dev (Ninja + MSYS2)\",\n      \"description\": \"Local Windows development build. Run inside MSYS2 UCRT64 shell.\",\n      \"generator\": \"Ninja\",\n      \"binaryDir\": \"${sourceDir}/build\",\n      \"cacheVariables\": {\n        \"BUILD_DOCS\": \"OFF\",\n        \"SUNSHINE_ASSETS_DIR\": \"assets\",\n        \"FETCH_DRIVER_DEPS\": \"OFF\"\n      }\n    }\n  ],\n  \"buildPresets\": [\n    {\n      \"name\": \"dev\",\n      \"displayName\": \"Dev Build\",\n      \"configurePreset\": \"dev-win\"\n    }\n  ]\n}\n"
  },
  {
    "path": "DOCKER_README.md",
    "content": "# Docker\n\n## Important note\nStarting with v0.18.0, tag names have changed. You may no longer use `latest`, `master`, `vX.X.X`.\n\n## Build your own containers\nThis image provides a method for you to easily use the latest Sunshine release in your own docker projects. It is not\nintended to use as a standalone container at this point, and should be considered experimental.\n\n```dockerfile\nARG SUNSHINE_VERSION=latest\nARG SUNSHINE_OS=ubuntu-22.04\nFROM lizardbyte/sunshine:${SUNSHINE_VERSION}-${SUNSHINE_OS}\n\n# install Steam, Wayland, etc.\n\nENTRYPOINT steam && sunshine\n```\n\n### SUNSHINE_VERSION\n- `latest`, `master`, `vX.X.X`\n- commit hash\n\n### SUNSHINE_OS\nSunshine images are available with the following tag suffixes, based on their respective base images.\n\n- `archlinux`\n- `debian-bookworm`\n- `fedora-39`\n- `fedora-40`\n- `ubuntu-22.04`\n- `ubuntu-24.04`\n\n### Tags\nYou must combine the `SUNSHINE_VERSION` and `SUNSHINE_OS` to determine the tag to pull. The format should be\n`<SUNSHINE_VERSION>-<SUNSHINE_OS>`. For example, `latest-ubuntu-24.04`.\n\nSee all our available tags on [docker hub](https://hub.docker.com/r/lizardbyte/sunshine/tags) or\n[ghcr](https://github.com/LizardByte/Sunshine/pkgs/container/sunshine/versions) for more info.\n\n## Where used\nThis is a list of docker projects using Sunshine. Something missing? Let us know about it!\n\n- [Games on Whales](https://games-on-whales.github.io)\n\n## Port and Volume mappings\nExamples are below of the required mappings. The configuration file will be saved to `/config` in the container.\n\n### Using docker run\nCreate and run the container (substitute your `<values>`):\n\n```bash\ndocker run -d \\\n  --device /dev/dri/ \\\n  --name=<image_name> \\\n  --restart=unless-stopped \\\n  --ipc=host \\\n  -e PUID=<uid> \\\n  -e PGID=<gid> \\\n  -e TZ=<timezone> \\\n  -v <path to data>:/config \\\n  -p 47984-47990:47984-47990/tcp \\\n  -p 48010:48010 \\\n  -p 47998-48000:47998-48000/udp \\\n  <image>\n```\n\n### Using docker-compose\nCreate a `docker-compose.yml` file with the following contents (substitute your `<values>`):\n\n```yaml\nversion: '3'\nservices:\n  <image_name>:\n    image: <image>\n    container_name: sunshine\n    restart: unless-stopped\n    volumes:\n      - <path to data>:/config\n    environment:\n      - PUID=<uid>\n      - PGID=<gid>\n      - TZ=<timezone>\n    ipc: host\n    ports:\n      - \"47984-47990:47984-47990/tcp\"\n      - \"48010:48010\"\n      - \"47998-48000:47998-48000/udp\"\n```\n\n### Using podman run\nCreate and run the container (substitute your `<values>`):\n\n```bash\npodman run -d \\\n  --device /dev/dri/ \\\n  --name=<image_name> \\\n  --restart=unless-stopped \\\n  --userns=keep-id \\\n  -e PUID=<uid> \\\n  -e PGID=<gid> \\\n  -e TZ=<timezone> \\\n  -v <path to data>:/config \\\n  -p 47984-47990:47984-47990/tcp \\\n  -p 48010:48010 \\\n  -p 47998-48000:47998-48000/udp \\\n  <image>\n```\n\n### Parameters\nYou must substitute the `<values>` with your own settings.\n\nParameters are split into two halves separated by a colon. The left side represents the host and the right side the\ncontainer.\n\n**Example:** `-p external:internal` - This shows the port mapping from internal to external of the container.\nTherefore `-p 47990:47990` would expose port `47990` from inside the container to be accessible from the host's IP on\nport `47990` (e.g. `http://<host_ip>:47990`). The internal port must be `47990`, but the external port may be changed\n(e.g. `-p 8080:47990`). All the ports listed in the `docker run` and `docker-compose` examples are required.\n\n\n| Parameter                   | Function             | Example Value      | Required |\n|-----------------------------|----------------------|--------------------|----------|\n| `-p <port>:47990`           | Web UI Port          | `47990`            | True     |\n| `-v <path to data>:/config` | Volume mapping       | `/home/sunshine`   | True     |\n| `-e PUID=<uid>`             | User ID              | `1001`             | False    |\n| `-e PGID=<gid>`             | Group ID             | `1001`             | False    |\n| `-e TZ=<timezone>`          | Lookup [TZ value][1] | `America/New_York` | False    |\n\nFor additional configuration, it is recommended to reference the *Games on Whales*\n[sunshine config](https://github.com/games-on-whales/gow/blob/2e442292d79b9d996f886b8a03d22b6eb6bddf7b/compose/streamers/sunshine.yml).\n\n[1]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones\n\n#### User / Group Identifiers:\nWhen using data volumes (-v flags) permissions issues can arise between the host OS and the container. To avoid this\nissue you can specify the user PUID and group PGID. Ensure the data volume directory on the host is owned by the same\nuser you specify.\n\nIn this instance `PUID=1001` and `PGID=1001`. To find yours use id user as below:\n\n```bash\n$ id dockeruser\nuid=1001(dockeruser) gid=1001(dockergroup) groups=1001(dockergroup)\n```\n\nIf you want to change the PUID or PGID after the image has been built, it will require rebuilding the image.\n\n## Supported Architectures\n\nSpecifying `lizardbyte/sunshine:latest-<SUNSHINE_OS>` or `ghcr.io/lizardbyte/sunshine:latest-<SUNSHINE_OS>` should\nretrieve the correct image for your architecture.\n\nThe architectures supported by these images are shown in the table below.\n\n| tag suffix      | amd64/x86_64 | arm64/aarch64 |\n|-----------------|--------------|---------------|\n| archlinux       | ✅            | ❌             |\n| debian-bookworm | ✅            | ✅             |\n| fedora-39       | ✅            | ❌             |\n| fedora-40       | ✅            | ❌             |\n| ubuntu-22.04    | ✅            | ✅             |\n| ubuntu-24.04    | ✅            | ✅             |\n\n<div class=\"section_buttons\">\n\n| Previous                       |                                                 Next |\n|:-------------------------------|-----------------------------------------------------:|\n| [Changelog](docs/changelog.md) | [Third-Party Packages](docs/third_party_packages.md) |\n\n</div>\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "NOTICE",
    "content": "©2018 Valve Corporation. Steam and the Steam logo are trademarks and/or\nregistered trademarks of Valve Corporation in the U.S. and/or other countries. All\nrights reserved."
  },
  {
    "path": "README.de.md",
    "content": "# Sunshine Foundation Edition\n\n## 🌐 Mehrsprachige Unterstützung / Multi-language Support\n\n<div align=\"center\">\n\n[![English](https://img.shields.io/badge/English-README.en.md-blue?style=for-the-badge)](README.en.md)\n[![中文简体](https://img.shields.io/badge/中文简体-README.zh--CN.md-red?style=for-the-badge)](README.md)\n[![Français](https://img.shields.io/badge/Français-README.fr.md-green?style=for-the-badge)](README.fr.md)\n[![Deutsch](https://img.shields.io/badge/Deutsch-README.de.md-yellow?style=for-the-badge)](README.de.md)\n[![日本語](https://img.shields.io/badge/日本語-README.ja.md-purple?style=for-the-badge)](README.ja.md)\n\n</div>\n\n---\n\nEin Fork basierend auf LizardByte/Sunshine, bietet vollständige Dokumentationsunterstützung [Read the Docs](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB).\n\n**Sunshine-Foundation** ist ein selbst gehosteter Game-Stream-Host für Moonlight. Diese Fork-Version hat erhebliche Verbesserungen gegenüber dem ursprünglichen Sunshine vorgenommen und konzentriert sich darauf, das Spiel-Streaming-Erlebnis für verschiedene Streaming-Endgeräte und Windows-Hosts zu verbessern:\n\n### 🌟 Kernfunktionen\n- **Vollständige HDR-Pipeline-Unterstützung** - Dualformat HDR10 (PQ) + HLG Kodierung mit adaptiven Metadaten für eine breitere Geräteabdeckung\n- **Virtuelle Anzeige** - Integriertes virtuelles Display-Management, ermöglicht das Erstellen und Verwalten virtueller Displays ohne zusätzliche Software\n- **Entferntes Mikrofon** - Unterstützt das Empfangen von Client-Mikrofonen und bietet hochwertige Sprachdurchleitung\n- **Erweiterte Systemsteuerung** - Intuitive Web-Oberfläche zur Konfiguration mit Echtzeit-Überwachung und Verwaltung\n- **Niedrige Latenzübertragung** - Optimierte Encoder-Verarbeitung unter Nutzung der neuesten Hardware-Fähigkeiten\n- **Intelligente Paarung** - Intelligentes Management von Profilen für gepaarte Geräte\n\n### 🎬 Vollständige HDR-Pipeline-Architektur\n\n**Dual-Format HDR-Kodierung: HDR10 (PQ) + HLG Parallelunterstützung**\n\nHerkömmliche Streaming-Lösungen unterstützen nur HDR10 (PQ) mit absoluter Luminanzzuordnung, was erfordert, dass das Client-Display die EOTF-Parameter und Spitzenhelligkeit der Quelle genau reproduziert. Wenn die Fähigkeiten des Empfangsgeräts unzureichend sind oder die Helligkeitsparameter nicht übereinstimmen, treten Tone-Mapping-Artefakte wie abgeschnittene Schatten und überbelichtete Lichter auf.\n\nFoundation Sunshine führt HLG-Unterstützung (Hybrid Log-Gamma, ITU-R BT.2100) auf der Kodierungsebene ein. Dieser Standard verwendet eine relative Luminanzzuordnung mit folgenden technischen Vorteilen:\n- **Szenenreferenzierte Luminanzanpassung**: HLG verwendet eine relative Luminanzkurve, die es dem Display ermöglicht, automatisch Tone Mapping basierend auf seiner eigenen Spitzenhelligkeit durchzuführen — die Erhaltung von Schattendetails auf Geräten mit niedriger Helligkeit ist PQ deutlich überlegen\n- **Sanfter Highlight-Roll-Off**: Die hybride Log-Gamma-Transferfunktion von HLG bietet einen graduellen Roll-Off in Highlight-Bereichen und vermeidet die Banding-Artefakte, die durch hartes Clipping bei PQ verursacht werden\n- **Native SDR-Abwärtskompatibilität**: HLG-Signale können von SDR-Displays direkt als Standard-BT.709-Inhalt dekodiert werden, ohne zusätzliches Tone Mapping\n\n**Einzelbild-Luminanzanalyse und adaptive Metadatengenerierung**\n\nDie Kodierungspipeline integriert ein Echtzeit-Luminanzanalysemodul auf der GPU-Seite, das über Compute Shader für jedes Einzelbild folgende Operationen ausführt:\n- **MaxFALL / MaxCLL Einzelbild-Berechnung**: Echtzeit-Berechnung des maximalen Inhaltslichtpegels (MaxCLL) und des maximalen durchschnittlichen Bildlichtpegels (MaxFALL) auf Einzelbildebene, dynamisch in HEVC/AV1 SEI/OBU-Metadaten injiziert\n- **Robuste Ausreißerfilterung**: Perzentilbasierte Abschneidestrategie zur Eliminierung extremer Luminanzpixel (z.B. Spiegelreflexionen), um zu verhindern, dass isolierte Leuchtpunkte die globale Luminanzreferenz anheben und zu einer allgemeinen Bildverdunkelung führen\n- **Interframe-Exponentialglättung**: EMA-Filterung (Exponentieller gleitender Durchschnitt) auf Luminanzstatistiken über aufeinanderfolgende Frames, zur Beseitigung von Helligkeitsflimmern durch abrupte Metadatenänderungen bei Szenenwechseln\n\n**Vollständige HDR-Metadaten-Durchleitung**\n\nUnterstützt die vollständige Durchleitung von statischen HDR10-Metadaten (Mastering Display Info + Content Light Level), dynamischen HDR Vivid-Metadaten und HLG-Transfercharakteristik-Kennungen. Dies stellt sicher, dass die von NVENC / AMF / QSV-Encodern ausgegebenen Bitstreams vollständige Farbvolumen- und Luminanzinformationen gemäß der CTA-861-Spezifikation enthalten, sodass Client-Decoder die HDR-Absicht der Quelle präzise reproduzieren können.\n\n### 🖥️ Integriertes virtuelles Display (Erfordert Win10 22H2 oder neuer)\n- Dynamische Erstellung und Entfernung virtueller Displays\n- Unterstützung für benutzerdefinierte Auflösungen und Bildwiederholraten\n- Verwaltung von Mehrfachanzeigekonfigurationen\n- Echtzeit-Konfigurationsänderungen ohne Neustart\n\n\n## Empfohlene Moonlight-Clients\n\nFür das beste Streaming-Erlebnis wird die Verwendung der folgenden optimierten Moonlight-Clients empfohlen (aktiviert Set-Boni):\n\n### 🖥️ Windows(X86_64, Arm64), MacOS, Linux Clients\n[![Moonlight-PC](https://img.shields.io/badge/Moonlight-PC-red?style=for-the-badge&logo=windows)](https://github.com/qiin2333/moonlight-qt)\n\n### 📱 Android Client\n[![Enhanced Edition Moonlight-Android](https://img.shields.io/badge/Enhanced_Edition-Moonlight--Android-green?style=for-the-badge&logo=android)](https://github.com/qiin2333/moonlight-android/releases/tag/shortcut)\n[![Crown Edition Moonlight-Android](https://img.shields.io/badge/Crown_Edition-Moonlight--Android-blue?style=for-the-badge&logo=android)](https://github.com/WACrown/moonlight-android)\n\n### 📱 iOS Client\n[![Voidlink Moonlight-iOS](https://img.shields.io/badge/Voidlink-Moonlight--iOS-lightgrey?style=for-the-badge&logo=apple)](https://github.com/The-Fried-Fish/VoidLink)\n\n\n### 🛠️ Weitere Ressourcen\n[awesome-sunshine](https://github.com/LizardByte/awesome-sunshine)\n\n## Systemanforderungen\n\n\n> [!WARNING]\n> Diese Tabellen werden kontinuierlich aktualisiert. Bitte kaufen Sie Hardware nicht nur basierend auf diesen Informationen.\n\n\n<table>\n    <caption id=\"minimum_requirements\">Mindestanforderungen</caption>\n    <tr>\n        <th>Komponente</th>\n        <th>Anforderung</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">GPU</td>\n        <td>AMD: VCE 1.0 oder höher, siehe: <a href=\"https://github.com/obsproject/obs-amd-encoder/wiki/Hardware-Support\">obs-amd Hardware-Unterstützung</a></td>\n    </tr>\n    <tr>\n        <td>Intel: VAAPI-kompatibel, siehe: <a href=\"https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html\">VAAPI Hardware-Unterstützung</a></td>\n    </tr>\n    <tr>\n        <td>Nvidia: Grafikkarte mit NVENC-Unterstützung, siehe: <a href=\"https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new\">NVENC-Unterstützungsmatrix</a></td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">CPU</td>\n        <td>AMD: Ryzen 3 oder höher</td>\n    </tr>\n    <tr>\n        <td>Intel: Core i3 oder höher</td>\n    </tr>\n    <tr>\n        <td>RAM</td>\n        <td>4GB oder mehr</td>\n    </tr>\n    <tr>\n        <td rowspan=\"5\">Betriebssystem</td>\n        <td>Windows: 10 22H2+ (Windows Server unterstützt keine virtuellen Gamepads)</td>\n    </tr>\n    <tr>\n        <td>macOS: 12+</td>\n    </tr>\n    <tr>\n        <td>Linux/Debian: 12+ (bookworm)</td>\n    </tr>\n    <tr>\n        <td>Linux/Fedora: 39+</td>\n    </tr>\n    <tr>\n        <td>Linux/Ubuntu: 22.04+ (jammy)</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">Netzwerk</td>\n        <td>Host: 5GHz, 802.11ac</td>\n    </tr>\n    <tr>\n        <td>Client: 5GHz, 802.11ac</td>\n    </tr>\n</table>\n\n<table>\n    <caption id=\"4k_suggestions\">Empfohlene Konfiguration für 4K</caption>\n    <tr>\n        <th>Komponente</th>\n        <th>Anforderung</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">GPU</td>\n        <td>AMD: Video Coding Engine 3.1 oder höher</td>\n    </tr>\n    <tr>\n        <td>Intel: HD Graphics 510 oder höher</td>\n    </tr>\n    <tr>\n        <td>Nvidia: GeForce GTX 1080 oder höhere Modelle mit mehreren Encodern</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">CPU</td>\n        <td>AMD: Ryzen 5 oder höher</td>\n    </tr>\n    <tr>\n        <td>Intel: Core i5 oder höher</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">Netzwerk</td>\n        <td>Host: CAT5e Ethernet oder besser</td>\n    </tr>\n    <tr>\n        <td>Client: CAT5e Ethernet oder besser</td>\n    </tr>\n</table>\n\n## Technischer Support\n\nLösungsweg bei Problemen:\n1. Konsultieren Sie die [Nutzungsdokumentation](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB) [LizardByte-Dokumentation](https://docs.lizardbyte.dev/projects/sunshine/latest/)\n2. Aktivieren Sie den detaillierten Log-Level in den Einstellungen, um relevante Informationen zu finden\n3. [Treten Sie der QQ-Gruppe bei, um Hilfe zu erhalten](https://qm.qq.com/cgi-bin/qm/qr?k=5qnkzSaLIrIaU4FvumftZH_6Hg7fUuLD&jump_from=webapi)\n4. [Benutze zwei Buchstaben!](https://uuyc.163.com/)\n\n**Problemrückmeldung-Labels:**\n- `hdr-support` - Probleme im Zusammenhang mit HDR\n- `virtual-display` - Probleme mit virtuellen Displays\n- `config-help` - Probleme im Zusammenhang mit der Konfiguration\n\n## 📚 Entwicklerdokumentation\n\n- **[Build-Anleitung](docs/building.md)** - Anleitung zum Kompilieren und Erstellen des Projekts\n- **[Konfigurationshandbuch](docs/configuration.md)** - Erläuterung der Laufzeit-Konfigurationsoptionen\n- **[WebUI-Entwicklung](docs/WEBUI_DEVELOPMENT.md)** - Vollständige Anleitung zur Entwicklung der Vue 3 + Vite Web-Oberfläche\n\n## Community beitreten\n\nWir begrüßen die Teilnahme an Diskussionen und Code-Beiträgen!\n[![QQ-Gruppe beitreten](https://pub.idqqimg.com/wpa/images/group.png 'QQ-Gruppe beitreten')](https://qm.qq.com/cgi-bin/qm/qr?k=WC2PSZ3Q6Hk6j8U_DG9S7522GPtItk0m&jump_from=webapi&authKey=zVDLFrS83s/0Xg3hMbkMeAqI7xoHXaM3sxZIF/u9JW7qO/D8xd0npytVBC2lOS+z)\n\n## Star-Verlauf\n\n[![Star-Verlauf Diagramm](https://api.star-history.com/svg?repos=qiin2333/Sunshine-Foundation&type=Date)](https://www.star-history.com/#qiin2333/Sunshine-Foundation&Date)\n\n---\n\n**Sunshine Foundation Edition - Macht Game-Streaming eleganter**"
  },
  {
    "path": "README.en.md",
    "content": "# Sunshine Foundation Edition\n\n## 🌐 Multi-language Support\n\n<div align=\"center\">\n\n[![English](https://img.shields.io/badge/English-README.en.md-blue?style=for-the-badge)](README.en.md)\n[![简体中文](https://img.shields.io/badge/简体中文-README.zh--CN.md-red?style=for-the-badge)](README.md)\n[![Français](https://img.shields.io/badge/Français-README.fr.md-green?style=for-the-badge)](README.fr.md)\n[![Deutsch](https://img.shields.io/badge/Deutsch-README.de.md-yellow?style=for-the-badge)](README.de.md)\n[![日本語](https://img.shields.io/badge/日本語-README.ja.md-purple?style=for-the-badge)](README.ja.md)\n\n</div>\n\n---\n\nA fork based on LizardByte/Sunshine, providing comprehensive documentation support [Read the Docs](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB).\n\n**Sunshine-Foundation** is a self-hosted game stream host for Moonlight. This forked version introduces significant improvements over the original Sunshine, focusing on enhancing the game streaming experience for various streaming terminal devices connected to a Windows host:\n\n### 🌟 Core Features\n- **Full HDR Pipeline Support** - Dual-format HDR10 (PQ) + HLG encoding with adaptive metadata, covering a wider range of endpoint devices\n- **Virtual Display** - Built-in virtual display management, allowing creation and management of virtual displays without additional software\n- **Remote Microphone** - Supports receiving client microphones, providing high-quality voice passthrough\n- **Advanced Control Panel** - Intuitive web control interface with real-time monitoring and configuration management\n- **Low-Latency Transmission** - Optimized encoding processing leveraging the latest hardware capabilities\n- **Smart Pairing** - Intelligent management of pairing devices with corresponding profiles\n\n### 🎬 Full HDR Pipeline Architecture\n\n**Dual-Format HDR Encoding: HDR10 (PQ) + HLG Parallel Support**\n\nConventional streaming solutions only support HDR10 (PQ) absolute luminance mapping, which requires the client display to precisely match the source EOTF parameters and peak brightness. When the receiving device has insufficient capabilities or mismatched brightness parameters, tone mapping artifacts such as crushed blacks and clipped highlights occur.\n\nFoundation Sunshine introduces HLG (Hybrid Log-Gamma, ITU-R BT.2100) support at the encoding layer. This standard employs relative luminance mapping with the following technical advantages:\n- **Scene-Referred Luminance Adaptation**: HLG uses a relative luminance curve, allowing the display to automatically perform tone mapping based on its own peak brightness — shadow detail retention on low-brightness devices is significantly superior to PQ\n- **Smooth Highlight Roll-Off**: HLG's hybrid log-gamma transfer function provides gradual roll-off in highlight regions, avoiding the banding artifacts caused by PQ's hard clipping\n- **Native SDR Backward Compatibility**: HLG signals can be directly decoded by SDR displays as standard BT.709 content without additional tone mapping\n\n**Per-Frame Luminance Analysis and Adaptive Metadata Generation**\n\nThe encoding pipeline integrates a real-time luminance analysis module on the GPU side, executing the following via Compute Shaders on each frame:\n- **Per-Frame MaxFALL / MaxCLL Computation**: Real-time calculation of frame-level Maximum Content Light Level (MaxCLL) and Maximum Frame-Average Light Level (MaxFALL), dynamically injected into HEVC/AV1 SEI/OBU metadata\n- **Robust Outlier Filtering**: Percentile-based truncation strategy to discard extreme luminance pixels (e.g., specular highlights), preventing isolated bright spots from inflating the global luminance reference and causing overall image dimming\n- **Inter-Frame Exponential Smoothing**: EMA (Exponential Moving Average) filtering applied to luminance statistics across consecutive frames, eliminating brightness flicker caused by abrupt metadata changes during scene transitions\n\n**Complete HDR Metadata Passthrough**\n\nSupports full passthrough of HDR10 static metadata (Mastering Display Info + Content Light Level), HDR Vivid dynamic metadata, and HLG transfer characteristic identifiers, ensuring that bitstreams output by NVENC / AMF / QSV encoders carry complete color volume and luminance information compliant with the CTA-861 specification, enabling client decoders to accurately reproduce the source HDR intent.\n\n### 🖥️ Virtual Display Integration (Requires Windows 10 22H2 or newer)\n- Dynamic virtual display creation and destruction\n- Custom resolution and refresh rate support\n- Multi-display configuration management\n- Real-time configuration changes without restarting\n\n## Recommended Moonlight Clients\n\nFor the best streaming experience (activating set bonuses), it is recommended to use the following optimized Moonlight clients:\n\n### 🖥️ Windows (X86_64, Arm64), macOS, Linux Clients\n[![Moonlight-PC](https://img.shields.io/badge/Moonlight-PC-red?style=for-the-badge&logo=windows)](https://github.com/qiin2333/moonlight-qt)\n\n### 📱 Android Clients\n[![Enhanced Edition Moonlight-Android](https://img.shields.io/badge/Enhanced_Edition-Moonlight--Android-green?style=for-the-badge&logo=android)](https://github.com/qiin2333/moonlight-android/releases/tag/shortcut)\n[![Crown Edition Moonlight-Android](https://img.shields.io/badge/Crown_Edition-Moonlight--Android-blue?style=for-the-badge&logo=android)](https://github.com/WACrown/moonlight-android)\n\n### 📱 iOS Client\n[![Voidlink Moonlight-iOS](https://img.shields.io/badge/Voidlink-Moonlight--iOS-lightgrey?style=for-the-badge&logo=apple)](https://github.com/The-Fried-Fish/VoidLink)\n\n### 🛠️ Additional Resources\n[awesome-sunshine](https://github.com/LizardByte/awesome-sunshine)\n\n## System Requirements\n\n> [!WARNING]\n> These tables are continuously updated. Please do not purchase hardware based solely on this information.\n\n<table>\n    <caption id=\"minimum_requirements\">Minimum Requirements</caption>\n    <tr>\n        <th>Component</th>\n        <th>Requirement</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">GPU</td>\n        <td>AMD: VCE 1.0 or later, see: <a href=\"https://github.com/obsproject/obs-amd-encoder/wiki/Hardware-Support\">obs-amd hardware support</a></td>\n    </tr>\n    <tr>\n        <td>Intel: VAAPI compatible, see: <a href=\"https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html\">VAAPI hardware support</a></td>\n    </tr>\n    <tr>\n        <td>Nvidia: Graphics card with NVENC support, see: <a href=\"https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new\">NVENC support matrix</a></td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">CPU</td>\n        <td>AMD: Ryzen 3 or higher</td>\n    </tr>\n    <tr>\n        <td>Intel: Core i3 or higher</td>\n    </tr>\n    <tr>\n        <td>RAM</td>\n        <td>4GB or more</td>\n    </tr>\n    <tr>\n        <td rowspan=\"5\">Operating System</td>\n        <td>Windows: 10 22H2+ (Windows Server does not support virtual gamepads)</td>\n    </tr>\n    <tr>\n        <td>macOS: 12+</td>\n    </tr>\n    <tr>\n        <td>Linux/Debian: 12+ (bookworm)</td>\n    </tr>\n    <tr>\n        <td>Linux/Fedora: 39+</td>\n    </tr>\n    <tr>\n        <td>Linux/Ubuntu: 22.04+ (jammy)</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">Network</td>\n        <td>Host: 5GHz, 802.11ac</td>\n    </tr>\n    <tr>\n        <td>Client: 5GHz, 802.11ac</td>\n    </tr>\n</table>\n\n<table>\n    <caption id=\"4k_suggestions\">4K Recommended Configuration</caption>\n    <tr>\n        <th>Component</th>\n        <th>Requirement</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">GPU</td>\n        <td>AMD: Video Coding Engine 3.1 or later</td>\n    </tr>\n    <tr>\n        <td>Intel: HD Graphics 510 or higher</td>\n    </tr>\n    <tr>\n        <td>Nvidia: GeForce GTX 1080 or higher models with multiple encoders</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">CPU</td>\n        <td>AMD: Ryzen 5 or higher</td>\n    </tr>\n    <tr>\n        <td>Intel: Core i5 or higher</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">Network</td>\n        <td>Host: CAT5e Ethernet or better</td>\n    </tr>\n    <tr>\n        <td>Client: CAT5e Ethernet or better</td>\n    </tr>\n</table>\n\n## Technical Support\n\nTroubleshooting path when encountering issues:\n1. Check the [Usage Documentation](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB) [LizardByte Documentation](https://docs.lizardbyte.dev/projects/sunshine/latest/)\n2. Enable detailed log level in settings to find relevant information\n3. [Join the QQ group for help](https://qm.qq.com/cgi-bin/qm/qr?k=5qnkzSaLIrIaU4FvumftZH_6Hg7fUuLD&jump_from=webapi)\n4. [Use two letters!](https://uuyc.163.com/)\n\n**Issue Feedback Labels:**\n- `hdr-support` - HDR-related issues\n- `virtual-display` - Virtual display issues\n- `config-help` - Configuration-related issues\n\n## 📚 Development Documentation\n\n- **[Building Instructions](docs/building.md)** - Project compilation and building instructions\n- **[Configuration Guide](docs/configuration.md)** - Runtime configuration options explanation\n- **[WebUI Development](docs/WEBUI_DEVELOPMENT.md)** - Complete guide for Vue 3 + Vite web interface development\n\n## Join the Community\n\nWe welcome everyone to participate in discussions and contribute code!\n[![Join QQ Group](https://pub.idqqimg.com/wpa/images/group.png 'Join QQ Group')](https://qm.qq.com/cgi-bin/qm/qr?k=WC2PSZ3Q6Hk6j8U_DG9S7522GPtItk0m&jump_from=webapi&authKey=zVDLFrS83s/0Xg3hMbkMeAqI7xoHXaM3sxZIF/u9JW7qO/D8xd0npytVBC2lOS+z)\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=qiin2333/Sunshine-Foundation&type=Date)](https://www.star-history.com/#qiin2333/Sunshine-Foundation&Date)\n\n---\n\n**Sunshine Foundation Edition - Making Game Streaming More Elegant**\n```"
  },
  {
    "path": "README.fr.md",
    "content": "# Sunshine Foundation\n\n## 🌐 Support multilingue / Multi-language Support\n\n<div align=\"center\">\n\n[![English](https://img.shields.io/badge/English-README.en.md-blue?style=for-the-badge)](README.en.md)\n[![中文简体](https://img.shields.io/badge/简体中文-README.zh--CN.md-red?style=for-the-badge)](README.md)\n[![Français](https://img.shields.io/badge/Français-README.fr.md-green?style=for-the-badge)](README.fr.md)\n[![Deutsch](https://img.shields.io/badge/Deutsch-README.de.md-yellow?style=for-the-badge)](README.de.md)\n[![日本語](https://img.shields.io/badge/日本語-README.ja.md-purple?style=for-the-badge)](README.ja.md)\n\n</div>\n\n---\n\nFork basé sur LizardByte/Sunshine, offrant une documentation complète [Lire la documentation](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB).\n\n**Sunshine-Foundation** est un hôte de streaming de jeu auto-hébergé pour Moonlight. Cette version forkée apporte des améliorations significatives par rapport à Sunshine original, en se concentrant sur l'amélioration de l'expérience de streaming de jeu entre divers appareils terminaux et l'hôte Windows :\n\n### 🌟 Fonctionnalités principales\n- **Support HDR Full Pipeline** - Double encodage HDR10 (PQ) + HLG avec métadonnées adaptatives, couvrant un plus large éventail d'appareils\n- **Écran virtuel** - Gestion intégrée des écrans virtuels, permettant de créer et gérer des écrans virtuels sans logiciel supplémentaire\n- **Microphone distant** - Prise en charge de la réception du microphone client, offrant une fonction de transmission vocale de haute qualité\n- **Panneau de contrôle avancé** - Interface de contrôle Web intuitive avec surveillance en temps réel et gestion de configuration\n- **Transmission à faible latence** - Traitement de codage optimisé exploitant les dernières capacités matérielles\n- **Appairage intelligent** - Gestion intelligente des profils correspondants aux appareils appairés\n\n### 🎬 Architecture complète du pipeline HDR\n\n**Double format d'encodage HDR : HDR10 (PQ) + HLG en parallèle**\n\nLes solutions de streaming conventionnelles ne prennent en charge que le HDR10 (PQ) avec mappage de luminance absolue, ce qui exige que l'écran client reproduise précisément les paramètres EOTF et la luminosité maximale de la source. Lorsque les capacités de l'appareil récepteur sont insuffisantes ou que les paramètres de luminosité ne correspondent pas, des artefacts de tone mapping apparaissent, tels que la perte de détails dans les ombres et l'écrêtage des hautes lumières.\n\nFoundation Sunshine introduit la prise en charge du HLG (Hybrid Log-Gamma, ITU-R BT.2100) au niveau de l'encodage. Ce standard utilise un mappage de luminance relatif avec les avantages techniques suivants :\n- **Adaptation de luminance référencée à la scène** : Le HLG utilise une courbe de luminance relative, permettant à l'écran d'effectuer automatiquement le tone mapping en fonction de sa propre luminosité maximale — la préservation des détails d'ombre sur les appareils à faible luminosité est significativement supérieure au PQ\n- **Roll-off progressif des hautes lumières** : La fonction de transfert log-gamma hybride du HLG fournit un roll-off graduel dans les zones de haute luminosité, évitant les artefacts de banding causés par l'écrêtage dur du PQ\n- **Compatibilité ascendante native SDR** : Les signaux HLG peuvent être directement décodés par les écrans SDR comme du contenu standard BT.709 sans traitement de tone mapping supplémentaire\n\n**Analyse de luminance image par image et génération adaptative de métadonnées**\n\nLe pipeline d'encodage intègre un module d'analyse de luminance en temps réel côté GPU, exécutant via des Compute Shaders sur chaque image :\n- **Calcul MaxFALL / MaxCLL par image** : Calcul en temps réel du niveau de lumière maximal du contenu (MaxCLL) et du niveau de lumière moyen maximal par image (MaxFALL), injectés dynamiquement dans les métadonnées HEVC/AV1 SEI/OBU\n- **Filtrage robuste des valeurs aberrantes** : Stratégie de troncature par percentile pour éliminer les pixels de luminance extrême (ex. réflexions spéculaires), empêchant les points lumineux isolés d'élever la référence de luminance globale et de provoquer un assombrissement global de l'image\n- **Lissage exponentiel inter-images** : Filtrage EMA (Moyenne Mobile Exponentielle) appliqué aux statistiques de luminance sur les images consécutives, éliminant le scintillement de luminosité causé par les changements brusques de métadonnées lors des transitions de scène\n\n**Transmission complète des métadonnées HDR**\n\nPrise en charge de la transmission complète des métadonnées statiques HDR10 (Mastering Display Info + Content Light Level), des métadonnées dynamiques HDR Vivid et des identifiants de caractéristiques de transfert HLG, garantissant que les flux de bits produits par les encodeurs NVENC / AMF / QSV transportent des informations complètes de volume colorimétrique et de luminance conformes à la spécification CTA-861, permettant aux décodeurs clients de reproduire fidèlement l'intention HDR de la source.\n\n### 🖥️ Intégration d'écran virtuel (nécessite Windows 10 22H2 ou plus récent)\n- Création et destruction dynamique d'écrans virtuels\n- Prise en charge des résolutions et taux de rafraîchissement personnalisés\n- Gestion de configuration multi-écrans\n- Modifications de configuration en temps réel sans redémarrage\n\n## Clients Moonlight recommandés\n\nIl est recommandé d'utiliser les clients Moonlight suivants optimisés pour une expérience de streaming optimale (activation des propriétés du set) :\n\n### 🖥️ Clients Windows(X86_64, Arm64), MacOS, Linux\n[![Moonlight-PC](https://img.shields.io/badge/Moonlight-PC-red?style=for-the-badge&logo=windows)](https://github.com/qiin2333/moonlight-qt)\n\n### 📱 Client Android\n[![Édition renforcée Moonlight-Android](https://img.shields.io/badge/Édition_renforcée-Moonlight--Android-green?style=for-the-badge&logo=android)](https://github.com/qiin2333/moonlight-android/releases/tag/shortcut)\n[![Édition Crown Moonlight-Android](https://img.shields.io/badge/Édition_Crown-Moonlight--Android-blue?style=for-the-badge&logo=android)](https://github.com/WACrown/moonlight-android)\n\n### 📱 Client iOS\n[![Terminal Void Moonlight-iOS](https://img.shields.io/badge/Voidlink-Moonlight--iOS-lightgrey?style=for-the-badge&logo=apple)](https://github.com/The-Fried-Fish/VoidLink)\n\n### 🛠️ Autres ressources\n[awesome-sunshine](https://github.com/LizardByte/awesome-sunshine)\n\n## Configuration système requise\n\n> [!WARNING]\n> Ces tableaux sont continuellement mis à jour. Veuillez ne pas acheter de matériel uniquement sur la base de ces informations.\n\n<table>\n    <caption id=\"minimum_requirements\">Configuration minimale requise</caption>\n    <tr>\n        <th>Composant</th>\n        <th>Exigence</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">GPU</td>\n        <td>AMD : VCE 1.0 ou version ultérieure, voir : <a href=\"https://github.com/obsproject/obs-amd-encoder/wiki/Hardware-Support\">obs-amd support matériel</a></td>\n    </tr>\n    <tr>\n        <td>Intel : Compatible VAAPI, voir : <a href=\"https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html\">Support matériel VAAPI</a></td>\n    </tr>\n    <tr>\n        <td>Nvidia : Carte graphique supportant NVENC, voir : <a href=\"https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new\">Matrice de support nvenc</a></td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">CPU</td>\n        <td>AMD : Ryzen 3 ou supérieur</td>\n    </tr>\n    <tr>\n        <td>Intel : Core i3 ou supérieur</td>\n    </tr>\n    <tr>\n        <td>RAM</td>\n        <td>4GB ou plus</td>\n    </tr>\n    <tr>\n        <td rowspan=\"5\">Système d'exploitation</td>\n        <td>Windows : 10 22H2+ (Windows Server ne prend pas en charge les manettes de jeu virtuelles)</td>\n    </tr>\n    <tr>\n        <td>macOS : 12+</td>\n    </tr>\n    <tr>\n        <td>Linux/Debian : 12+ (bookworm)</td>\n    </tr>\n    <tr>\n        <td>Linux/Fedora : 39+</td>\n    </tr>\n    <tr>\n        <td>Linux/Ubuntu : 22.04+ (jammy)</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">Réseau</td>\n        <td>Hôte : 5GHz, 802.11ac</td>\n    </tr>\n    <tr>\n        <td>Client : 5GHz, 802.11ac</td>\n    </tr>\n</table>\n\n<table>\n    <caption id=\"4k_suggestions\">Configuration recommandée pour la 4K</caption>\n    <tr>\n        <th>Composant</th>\n        <th>Exigence</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">GPU</td>\n        <td>AMD : Video Coding Engine 3.1 ou supérieur</td>\n    </tr>\n    <tr>\n        <td>Intel : HD Graphics 510 ou supérieur</td>\n    </tr>\n    <tr>\n        <td>Nvidia : GeForce GTX 1080 ou modèles supérieurs avec encodeurs multiples</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">CPU</td>\n        <td>AMD : Ryzen 5 ou supérieur</td>\n    </tr>\n    <tr>\n        <td>Intel : Core i5 ou supérieur</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">Réseau</td>\n        <td>Hôte : Ethernet CAT5e ou supérieur</td>\n    </tr>\n    <tr>\n        <td>Client : Ethernet CAT5e ou supérieur</td>\n    </tr>\n</table>\n\n## Support technique\n\nProcédure de résolution des problèmes :\n1. Consultez la [documentation d'utilisation](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB) [Documentation LizardByte](https://docs.lizardbyte.dev/projects/sunshine/latest/)\n2. Activez le niveau de journalisation détaillé dans les paramètres pour trouver des informations pertinentes\n3. [Rejoignez le groupe QQ pour obtenir de l'aide](https://qm.qq.com/cgi-bin/qm/qr?k=5qnkzSaLIrIaU4FvumftZH_6Hg7fUuLD&jump_from=webapi)\n4. [Utilisez deux lettres !](https://uuyc.163.com/)\n\n**Étiquettes de signalement des problèmes :**\n- `hdr-support` - Problèmes liés au HDR\n- `virtual-display` - Problèmes d'écran virtuel\n- `config-help` - Problèmes de configuration\n\n## 📚 Documentation de développement\n\n- **[Instructions de compilation](docs/building.md)** - Instructions pour compiler et construire le projet\n- **[Guide de configuration](docs/configuration.md)** - Description des options de configuration d'exécution\n- **[Développement WebUI](docs/WEBUI_DEVELOPMENT.md)** - Guide complet du développement de l'interface Web Vue 3 + Vite\n\n## Rejoignez la communauté\n\nNous accueillons favorablement les discussions et les contributions de code !\n[![Rejoindre le groupe QQ](https://pub.idqqimg.com/wpa/images/group.png 'Rejoindre le groupe QQ')](https://qm.qq.com/cgi-bin/qm/qr?k=WC2PSZ3Q6Hk6j8U_DG9S7522GPtItk0m&jump_from=webapi&authKey=zVDLFrS83s/0Xg3hMbkMeAqI7xoHXaM3sxZIF/u9JW7qO/D8xd0npytVBC2lOS+z)\n\n## Historique des stars\n\n[![Graphique d'historique des stars](https://api.star-history.com/svg?repos=qiin2333/Sunshine-Foundation&type=Date)](https://www.star-history.com/#qiin2333/Sunshine-Foundation&Date)\n\n---\n\n**Sunshine Foundation - Rendre le streaming de jeux plus élégant**\n```"
  },
  {
    "path": "README.ja.md",
    "content": "# Sunshine 基地版\n\n## 🌐 多言語サポート / Multi-language Support\n\n<div align=\"center\">\n\n[![English](https://img.shields.io/badge/English-README.en.md-blue?style=for-the-badge)](README.en.md)\n[![中文简体](https://img.shields.io/badge/中文简体-README.zh--CN.md-red?style=for-the-badge)](README.md)\n[![Français](https://img.shields.io/badge/Français-README.fr.md-green?style=for-the-badge)](README.fr.md)\n[![Deutsch](https://img.shields.io/badge/Deutsch-README.de.md-yellow?style=for-the-badge)](README.de.md)\n[![日本語](https://img.shields.io/badge/日本語-README.ja.md-purple?style=for-the-badge)](README.ja.md)\n\n</div>\n\n---\n\nLizardByte/Sunshineをベースにしたフォークで、完全なドキュメントサポートを提供します [Read the Docs](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB)。\n\n**Sunshine-Foundation** はMoonlight用のセルフホスト型ゲームストリームホストです。このフォークバージョンはオリジナルのSunshineに基づき、様々なストリーミング端末とWindowsホスト間のゲームストリーミング体験を向上させることに重点を置いた大幅な改良が加えられています：\n\n### 🌟 コア機能\n- **HDR フルパイプラインサポート** - HDR10 (PQ) + HLG デュアルフォーマットエンコーディングとアダプティブメタデータにより、幅広い端末デバイスをカバー\n- **仮想ディスプレイ** - 内蔵の仮想ディスプレイ管理により、追加ソフトウェアなしで仮想ディスプレイの作成と管理が可能\n- **リモートマイク** - クライアントマイクの受信をサポートし、高音質の音声パススルー機能を提供\n- **高度なコントロールパネル** - 直感的なWebコントロールインターフェースで、リアルタイム監視と設定管理を提供\n- **低遅延伝送** - 最新のハードウェア能力を活用した最適化されたエンコード処理\n- **インテリジェントペアリング** - ペアリングデバイスの対応プロファイルをインテリジェントに管理\n\n### 🎬 HDR フルパイプライン技術アーキテクチャ\n\n**デュアルフォーマット HDR エンコーディング：HDR10 (PQ) + HLG 並列サポート**\n\n従来のストリーミングソリューションは HDR10 (PQ) の絶対輝度マッピングのみをサポートしており、クライアントディスプレイがソース側の EOTF パラメータとピーク輝度を正確に再現することが求められます。受信デバイスの能力が不十分な場合やパラメータが一致しない場合、暗部ディテールの消失やハイライトクリッピングなどのトーンマッピングアーティファクトが発生します。\n\nFoundation Sunshine はエンコーディング層に HLG（Hybrid Log-Gamma、ITU-R BT.2100）サポートを追加しました。この規格は相対輝度マッピングを採用し、以下の技術的優位性を備えています：\n- **シーン参照型輝度適応**：HLG は相対輝度カーブに基づいており、ディスプレイ側が自身のピーク輝度に基づいて自動的にトーンマッピングを実行 — 低輝度デバイスでの暗部ディテール保持は PQ を大幅に上回る\n- **ハイライト領域のスムーズなロールオフ**：HLG のハイブリッド対数ガンマ伝達関数は高輝度領域で漸進的なロールオフを提供し、PQ のハードクリッピングによるバンディングアーティファクトを回避\n- **ネイティブ SDR 後方互換性**：HLG 信号は SDR ディスプレイで標準 BT.709 コンテンツとして直接デコード可能。追加のトーンマッピング処理は不要\n\n**フレームごとの輝度分析とアダプティブメタデータ生成**\n\nエンコーディングパイプラインは GPU 側にリアルタイム輝度分析モジュールを統合し、Compute Shader を介して各フレームに対して以下を実行します：\n- **MaxFALL / MaxCLL フレーム単位計算**：フレームレベルの最大コンテンツ輝度（MaxCLL）とフレーム平均輝度（MaxFALL）をリアルタイムで計算し、HEVC/AV1 SEI/OBU メタデータに動的に注入\n- **ロバストな外れ値フィルタリング**：パーセンタイルベースの切断戦略により極端な輝度ピクセル（例：鏡面反射ハイライト）を除去。孤立した高輝度点がグローバル輝度参照を引き上げ、画面全体が暗くなることを防止\n- **フレーム間指数平滑化**：連続フレームの輝度統計値に EMA（指数移動平均）フィルタリングを適用し、シーン遷移時のメタデータ急変による輝度フリッカーを解消\n\n**完全な HDR メタデータパススルー**\n\nHDR10 静的メタデータ（Mastering Display Info + Content Light Level）、HDR Vivid ダイナミックメタデータ、および HLG 伝送特性識別子の完全なパススルーをサポートし、NVENC / AMF / QSV エンコーダが出力するビットストリームが CTA-861 仕様に準拠した完全な色域・輝度情報を含むことを保証します。これにより、クライアントデコーダがソース側の HDR インテントを正確に再現できます。\n\n### 🖥️ 仮想ディスプレイ統合 (win10 22H2 以降のシステムが必要）\n- 動的な仮想ディスプレイの作成と破棄\n- カスタム解像度とリフレッシュレートのサポート\n- マルチディスプレイ設定管理\n- 再起動不要のリアルタイム設定変更\n\n\n## 推奨されるMoonlightクライアント\n\n最適なストリーミング体験を得るために、以下の最適化されたMoonlightクライアントの使用を推奨します（セット効果を発動）：\n\n### 🖥️ Windows(X86_64, Arm64), MacOS, Linux クライアント\n[![Moonlight-PC](https://img.shields.io/badge/Moonlight-PC-red?style=for-the-badge&logo=windows)](https://github.com/qiin2333/moonlight-qt)\n\n### 📱 Androidクライアント\n[![威力加强版 Moonlight-Android](https://img.shields.io/badge/威力加强版-Moonlight--Android-green?style=for-the-badge&logo=android)](https://github.com/qiin2333/moonlight-android/releases/tag/shortcut)\n[![王冠版 Moonlight-Android](https://img.shields.io/badge/王冠版-Moonlight--Android-blue?style=for-the-badge&logo=android)](https://github.com/WACrown/moonlight-android)\n\n### 📱 iOSクライアント\n[![虚空终端 Moonlight-iOS](https://img.shields.io/badge/Voidlink-Moonlight--iOS-lightgrey?style=for-the-badge&logo=apple)](https://github.com/The-Fried-Fish/VoidLink)\n\n\n### 🛠️ その他のリソース \n[awesome-sunshine](https://github.com/LizardByte/awesome-sunshine)\n\n## システム要件\n\n\n> [!WARNING] \n> これらの表は継続的に更新中です。この情報のみに基づいてハードウェアを購入しないでください。\n\n\n<table>\n    <caption id=\"minimum_requirements\">最小システム要件</caption>\n    <tr>\n        <th>コンポーネント</th>\n        <th>要件</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">GPU</td>\n        <td>AMD: VCE 1.0以降、参照: <a href=\"https://github.com/obsproject/obs-amd-encoder/wiki/Hardware-Support\">obs-amdハードウェアサポート</a></td>\n    </tr>\n    <tr>\n        <td>Intel: VAAPI互換、参照: <a href=\"https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html\">VAAPIハードウェアサポート</a></td>\n    </tr>\n    <tr>\n        <td>Nvidia: NVENC対応グラフィックカード、参照: <a href=\"https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new\">nvencサポートマトリックス</a></td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">CPU</td>\n        <td>AMD: Ryzen 3以降</td>\n    </tr>\n    <tr>\n        <td>Intel: Core i3以降</td>\n    </tr>\n    <tr>\n        <td>RAM</td>\n        <td>4GB以上</td>\n    </tr>\n    <tr>\n        <td rowspan=\"5\">オペレーティングシステム</td>\n        <td>Windows: 10 22H2+ (Windows Serverは仮想ゲームパッドをサポートしません)</td>\n    </tr>\n    <tr>\n        <td>macOS: 12+</td>\n    </tr>\n    <tr>\n        <td>Linux/Debian: 12+ (bookworm)</td>\n    </tr>\n    <tr>\n        <td>Linux/Fedora: 39+</td>\n    </tr>\n    <tr>\n        <td>Linux/Ubuntu: 22.04+ (jammy)</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">ネットワーク</td>\n        <td>ホスト: 5GHz, 802.11ac</td>\n    </tr>\n    <tr>\n        <td>クライアント: 5GHz, 802.11ac</td>\n    </tr>\n</table>\n\n<table>\n    <caption id=\"4k_suggestions\">4K推奨構成</caption>\n    <tr>\n        <th>コンポーネント</th>\n        <th>要件</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">GPU</td>\n        <td>AMD: Video Coding Engine 3.1以降</td>\n    </tr>\n    <tr>\n        <td>Intel: HD Graphics 510以降</td>\n    </tr>\n    <tr>\n        <td>Nvidia: GeForce GTX 1080以降のマルチエンコーダ対応モデル</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">CPU</td>\n        <td>AMD: Ryzen 5以降</td>\n    </tr>\n    <tr>\n        <td>Intel: Core i5以降</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">ネットワーク</td>\n        <td>ホスト: CAT5eイーサネット以上</td>\n    </tr>\n    <tr>\n        <td>クライアント: CAT5eイーサネット以上</td>\n    </tr>\n</table>\n\n## テクニカルサポート\n\n問題が発生した場合の解決手順：\n1. [使用ドキュメント](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB) [LizardByteドキュメント](https://docs.lizardbyte.dev/projects/sunshine/latest/)を確認\n2. 設定で詳細なログレベルを有効にして関連情報を見つける\n3. [QQグループに参加してヘルプを入手](https://qm.qq.com/cgi-bin/qm/qr?k=5qnkzSaLIrIaU4FvumftZH_6Hg7fUuLD&jump_from=webapi)\n4. [二文字を使おう！](https://uuyc.163.com/)\n\n**問題フィードバックタグ：**\n- `hdr-support` - HDR関連の問題\n- `virtual-display` - 仮想ディスプレイの問題  \n- `config-help` - 設定関連の問題\n\n## 📚 開発ドキュメント\n\n- **[ビルド手順](docs/building.md)** - プロジェクトのコンパイルとビルド手順\n- **[設定ガイド](docs/configuration.md)** - 実行時設定オプションの説明\n- **[WebUI開発](docs/WEBUI_DEVELOPMENT.md)** - Vue 3 + Vite Webインターフェース開発完全ガイド\n\n## コミュニティに参加\n\nディスカッションとコード貢献を歓迎します！\n[![QQグループに参加](https://pub.idqqimg.com/wpa/images/group.png 'QQグループに参加')](https://qm.qq.com/cgi-bin/qm/qr?k=WC2PSZ3Q6Hk6j8U_DG9S7522GPtItk0m&jump_from=webapi&authKey=zVDLFrS83s/0Xg3hMbkMeAqI7xoHXaM3sxZIF/u9JW7qO/D8xd0npytVBC2lOS+z)\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=qiin2333/Sunshine-Foundation&type=Date)](https://www.star-history.com/#qiin2333/Sunshine-Foundation&Date)\n\n---\n\n**Sunshine基地版 - ゲームストリーミングをよりエレガントに**\n```"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n<img src=\"docs/poster.webp\" width=\"800\" alt=\"Foundation Sunshine\">\n\n<br>\n\n[![English](https://img.shields.io/badge/English-blue?style=flat-square)](README.en.md)\n[![中文简体](https://img.shields.io/badge/中文简体-red?style=flat-square)](README.md)\n[![Français](https://img.shields.io/badge/Français-green?style=flat-square)](README.fr.md)\n[![Deutsch](https://img.shields.io/badge/Deutsch-yellow?style=flat-square)](README.de.md)\n[![日本語](https://img.shields.io/badge/日本語-purple?style=flat-square)](README.ja.md)\n\n基于 [LizardByte/Sunshine](https://github.com/LizardByte/Sunshine) 的增强分支，专注于 Windows 游戏串流体验\n\n[使用文档](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB) · [LizardByte 文档](https://docs.lizardbyte.dev/projects/sunshine/latest/) · [QQ 交流群](https://qm.qq.com/cgi-bin/qm/qr?k=5qnkzSaLIrIaU4FvumftZH_6Hg7fUuLD&jump_from=webapi)\n\n</div>\n\n---\n\n### ░▒▓ 核心特性\n\n- **HDR 全链路** — 双格式编码 (PQ + HLG)・逐帧 GPU 亮度分析・HDR10+ / HDR Vivid 动态元数据・完整静态元数据透传\n- **虚拟显示器** — 深度集成 [ZakoVDD](https://github.com/qiin2333/zako-vdd)・5 种屏幕模式・Named Pipe 通信・多客户端 GUID 会话\n- **音频增强** — 7.1.4 环绕声 (12ch)・Opus DRED 丢包恢复・持续音频流・远程麦克风・虚拟扬声器位深匹配\n- **编码优化** — NVENC SDK 13.0・AMF QVBR/HQVBR・编码器结果缓存 (260x)・自适应下采样・Vulkan 编码器\n- **控制面板** — Tauri 2 + Vue 3 + Vite・深色模式・QR 配对・实时监控\n- **智能配对** — 客户端独立配置・自动匹配设备能力・虚拟鼠标驱动 (vmouse)\n\n### ░▒▓ 技术细节\n\n<details>\n<summary><b>HDR 全链路技术方案</b></summary>\n\n#### 双格式 HDR 编码：HDR10 (PQ) + HLG 并行支持\n\n传统串流方案仅支持 HDR10 (PQ) 绝对亮度映射，当终端设备能力不足或亮度参数不匹配时，会出现暗部细节丢失、高光截断等问题。\n\n因此在编码层加入了 HLG（Hybrid Log-Gamma, ITU-R BT.2100）支持，采用相对亮度映射：\n- **场景参考式亮度适配**：HLG 基于相对亮度曲线，显示端根据自身峰值亮度自动进行色调映射，低亮度设备上暗部细节保留显著优于 PQ\n- **高光区域平滑滚降**：HLG 的对数-伽马混合传输函数在高亮区域提供渐进式滚降，避免 PQ 硬截断导致的高光色阶断裂\n- **天然 SDR 向后兼容**：HLG 信号可直接被 SDR 显示器解码为标准 BT.709 画面，无需额外的色调映射处理\n\n**逐帧亮度分析与自适应元数据生成**\n\n在 GPU 端集成了实时亮度分析模块，通过 Compute Shader 对每帧画面执行：\n- **MaxFALL / MaxCLL 逐帧计算**：实时统计帧级最大内容亮度（MaxCLL）和帧平均亮度（MaxFALL），动态注入 HEVC/AV1 SEI/OBU 元数据\n- **异常值鲁棒过滤**：采用百分位截断策略剔除极端亮度像素（如高光镜面反射），防止孤立高亮点拉高全局亮度参考导致整体画面偏暗\n- **帧间指数平滑**：对连续帧的亮度统计值应用 EMA（指数移动平均）滤波，消除场景切换时元数据突变引发的亮度闪烁\n\n**完整 HDR 元数据透传**\n\nHDR10 静态元数据（Mastering Display Info + Content Light Level）完整透传，NVENC / AMF / QSV 编码输出的码流携带符合 CTA-861 规范的完整色彩容积与亮度信息。\n\n**HDR10+ / HDR Vivid 动态元数据注入**\n\n在 NVENC 编码管线中，基于逐帧亮度分析结果，自动生成并注入以下动态元数据 SEI：\n- **HDR10+ (ST 2094-40)**：携带场景级 MaxSCL / distribution percentiles / knee point 等色调映射参考，支持 Samsung/Panasonic 等 HDR10+ 认证电视精确色调映射\n- **HDR Vivid (CUVA T/UWA 005.3)**：ITU-T T.35 注册的中国超高清视频联盟(CUVA)标准，PQ 模式下提供绝对亮度色调映射、HLG 模式下提供场景参考相对亮度色调映射，覆盖国产终端生态\n\n</details>\n\n<details>\n<summary><b>虚拟显示器集成</b> (需 Windows 10 22H2+)</summary>\n\n深度集成 [ZakoVDD](https://github.com/qiin2333/zako-vdd) 虚拟显示器驱动：\n- 自定义分辨率和刷新率支持，10-bit HDR 色深\n- **5 种屏幕组合模式**：仅虚拟屏、仅物理屏、混合模式、镜像模式、扩展模式\n- Named Pipe 实时通信，串流开始/结束时自动创建/销毁虚拟显示器\n- 每个客户端独立绑定 VDD 会话（GUID），支持多客户端快速切换\n- 无需重启的实时配置更改\n\n</details>\n\n<details>\n<summary><b>音频增强</b></summary>\n\n- **7.1.4 环绕声 (12声道)**：Dolby Atmos 等沉浸式音频布局的完整声道映射\n- **Opus DRED 深度冗余**：基于神经网络的丢包恢复，100ms 冗余窗口在网络抖动时平滑补偿\n- **持续音频流**：无中断的音频流，无声时自动填充静音数据，避免音频设备反复初始化\n- **虚拟扬声器自动匹配**：自动检测并匹配 16bit/24bit 等位深格式的虚拟音频设备\n\n</details>\n\n<details>\n<summary><b>捕获与编码优化</b></summary>\n\n**捕获管线**\n- **Gamma-Aware 着色器**：根据 DXGI ColorSpace 自动选择 sRGB / 线性 Gamma 颜色转换\n- **高质量下采样**：双三次 (Bicubic) 插值，支持 fast / balanced / high_quality 三档\n- **动态分辨率检测**：实时感知显示器分辨率与旋转变化，编码器自适应调整\n- **GPU 亮度分析**：Compute Shader 两阶段规约、P95/P99 截断、帧间 EMA 时域平滑\n\n**NVENC**\n- **SDK 13.0**：精细化码率控制与 Look-ahead\n- **HDR 元数据 API**：NVENC SDK 12.2+ 原生 Mastering Display / Content Light Level 写入\n- **HDR10+ / HDR Vivid SEI**：逐帧自动生成 ST 2094-40 和 CUVA T.35 动态元数据\n- **SPS 码流规范**：H.264/HEVC SPS bitstream restrictions 完整写入\n\n**AMF (AMD)**\n- **QVBR / HQVBR / HQCBR**：高级码率控制，支持质量等级 UI 调节\n- **AV1 低延迟**：AV1 编码器无延迟影响的优化选项\n\n**通用**\n- **编码器结果缓存**：探测结果持久化，后续连接 26s → <100ms (260x 加速)\n- **自适应下采样**：支持双线性 / 双三次 / 高质量三档分辨率缩放，适配 4K 主机→1080p 串流场景\n- **Vulkan 编码器**：实验性 Vulkan 视频编码支持\n- **无锁证书链**：`shared_mutex` 替代 mutex，消除 TLS 队列开销\n\n</details>\n\n<br>\n\n---\n\n### ░▒▓ 推荐客户端\n\n搭配以下优化版 Moonlight 客户端可获得最佳体验（激活套装属性）\n\n- **PC** — [Moonlight-PC](https://github.com/qiin2333/moonlight-qt)（Windows · macOS · Linux）\n- **Android** — [威力加强版](https://github.com/qiin2333/moonlight-vplus) · [王冠版](https://github.com/WACrown/moonlight-android)\n- **iOS** — [VoidLink](https://github.com/The-Fried-Fish/VoidLink-previously-moonlight-zwm)\n- **鸿蒙** — [Moonlight V+](https://appgallery.huawei.com/app/detail?id=com.alkaidlab.sdream)\n\n更多资源：[awesome-sunshine](https://github.com/LizardByte/awesome-sunshine)\n\n<br>\n\n<details>\n<summary><b>░▒▓ 系统要求</b></summary>\n\n| 组件 | 最低要求 | 4K 推荐 |\n|------|----------|---------|\n| **GPU** | AMD VCE 1.0+ / Intel VAAPI / NVIDIA NVENC | AMD VCE 3.1+ / Intel HD 510+ / GTX 1080+ |\n| **CPU** | Ryzen 3 / Core i3 | Ryzen 5 / Core i5 |\n| **RAM** | 4 GB | 8 GB |\n| **系统** | Windows 10 22H2+ | Windows 10 22H2+ |\n| **网络** | 5GHz 802.11ac | CAT5e 以太网 |\n\nGPU 兼容性：[NVENC](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new) · [AMD VCE](https://github.com/obsproject/obs-amd-encoder/wiki/Hardware-Support) · [Intel VAAPI](https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html)\n\n</details>\n\n---\n\n### ░▒▓ 文档与支持\n\n[![Docs](https://img.shields.io/badge/使用文档-ff69b4?style=flat-square)](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB) [![LizardByte](https://img.shields.io/badge/LizardByte_文档-a78bfa?style=flat-square)](https://docs.lizardbyte.dev/projects/sunshine/latest/) [![QQ群](https://img.shields.io/badge/QQ_交流群-38bdf8?style=flat-square)](https://qm.qq.com/cgi-bin/qm/qr?k=5qnkzSaLIrIaU4FvumftZH_6Hg7fUuLD&jump_from=webapi)\n\n想帮杂鱼写代码? → [![Build](https://img.shields.io/badge/构建说明-34d399?style=flat-square)](docs/building.md) [![Config](https://img.shields.io/badge/配置指南-fbbf24?style=flat-square)](docs/configuration.md) [![WebUI](https://img.shields.io/badge/WebUI_开发-fb923c?style=flat-square)](docs/WEBUI_DEVELOPMENT.md)\n\n<br>\n\n<div align=\"center\">\n\n「 ░▒▓ 」\n\n<a href=\"https://github.com/qiin2333/foundation-sunshine/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=qiin2333/foundation-sunshine&max=100\" />\n</a>\n\n<br>\n\n[![加入QQ群](https://pub.idqqimg.com/wpa/images/group.png '加入QQ群')](https://qm.qq.com/cgi-bin/qm/qr?k=WC2PSZ3Q6Hk6j8U_DG9S7522GPtItk0m&jump_from=webapi&authKey=zVDLFrS83s/0Xg3hMbkMeAqI7xoHXaM3sxZIF/u9JW7qO/D8xd0npytVBC2lOS+z)\n\n[![Star History Chart](https://api.star-history.com/svg?repos=qiin2333/Sunshine-Foundation&type=Date)](https://www.star-history.com/#qiin2333/Sunshine-Foundation&Date)\n\n</div>\n"
  },
  {
    "path": "README.md.backup",
    "content": "# Sunshine 基地版\n\n基于LizardByte/Sunshine的分支，提供完整的文档支持 [Read the Docs](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB)。\n\n**Sunshine-Foundation**  is a self-hosted game stream host for Moonlight，本分支版本在原始Sunshine基础上进行了重大改进，专注于提高各种串流终端设备与windows主机接入的游戏串流体验：\n\n### 🌟 核心特性\n- **HDR友好支持** - 经过优化的HDR处理管线，提供真正的HDR游戏流媒体体验\n- **集成虚拟显示器** - 内置虚拟显示器管理，无需额外软件即可创建和管理虚拟显示器\n- **远程麦克风** - 支持接收客户端麦克风，提供高音质的语音直通功能\n- **高级控制面板** - 直观的Web控制界面，提供实时监控和配置管理\n- **低延迟传输** - 结合最新硬件能力优化的编码处理\n- **智能配对** - 智能管理配对设备的对应配置文件\n\n### 🖥️ 虚拟显示器集成 (需win10 22H2 及更新的系统）\n- 动态虚拟显示器创建和销毁\n- 自定义分辨率和刷新率支持\n- 多显示器配置管理\n- 无需重启的实时配置更改\n\n\n## 推荐的Moonlight客户端\n\n建议使用以下经过优化的Moonlight客户端获得最佳的串流体验（激活套装属性）：\n\n### 🖥️ Windows(X86_64, Arm64), MacOS, Linux 客户端\n[![Moonlight-PC](https://img.shields.io/badge/Moonlight-PC-red?style=for-the-badge&logo=windows)](https://github.com/qiin2333/moonlight-qt)\n\n### 📱 Android客户端\n[![威力加强版 Moonlight-Android](https://img.shields.io/badge/威力加强版-Moonlight--Android-green?style=for-the-badge&logo=android)](https://github.com/qiin2333/moonlight-android/releases/tag/shortcut)\n[![王冠版 Moonlight-Android](https://img.shields.io/badge/王冠版-Moonlight--Android-blue?style=for-the-badge&logo=android)](https://github.com/WACrown/moonlight-android)\n\n### 📱 iOS客户端\n[![真砖家版 Moonlight-iOS](https://img.shields.io/badge/真砖家版-Moonlight--iOS-lightgrey?style=for-the-badge&logo=apple)](https://github.com/TrueZhuangJia/moonlight-ios-NativeMultiTouchPassthrough)\n\n\n### 🛠️ 其他资源 \n[awesome-sunshine](https://github.com/LizardByte/awesome-sunshine)\n\n## 系统要求\n\n\n> [!WARNING] \n> 这些表格正在持续更新中。请不要仅基于此信息购买硬件。\n\n\n<table>\n    <caption id=\"minimum_requirements\">最低配置要求</caption>\n    <tr>\n        <th>组件</th>\n        <th>要求</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">GPU</td>\n        <td>AMD: VCE 1.0或更高版本，参见: <a href=\"https://github.com/obsproject/obs-amd-encoder/wiki/Hardware-Support\">obs-amd硬件支持</a></td>\n    </tr>\n    <tr>\n        <td>Intel: VAAPI兼容，参见: <a href=\"https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html\">VAAPI硬件支持</a></td>\n    </tr>\n    <tr>\n        <td>Nvidia: 支持NVENC的显卡，参见: <a href=\"https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new\">nvenc支持矩阵</a></td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">CPU</td>\n        <td>AMD: Ryzen 3或更高</td>\n    </tr>\n    <tr>\n        <td>Intel: Core i3或更高</td>\n    </tr>\n    <tr>\n        <td>RAM</td>\n        <td>4GB或更多</td>\n    </tr>\n    <tr>\n        <td rowspan=\"5\">操作系统</td>\n        <td>Windows: 10 22H2+ (Windows Server不支持虚拟游戏手柄)</td>\n    </tr>\n    <tr>\n        <td>macOS: 12+</td>\n    </tr>\n    <tr>\n        <td>Linux/Debian: 12+ (bookworm)</td>\n    </tr>\n    <tr>\n        <td>Linux/Fedora: 39+</td>\n    </tr>\n    <tr>\n        <td>Linux/Ubuntu: 22.04+ (jammy)</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">网络</td>\n        <td>主机: 5GHz, 802.11ac</td>\n    </tr>\n    <tr>\n        <td>客户端: 5GHz, 802.11ac</td>\n    </tr>\n</table>\n\n<table>\n    <caption id=\"4k_suggestions\">4K推荐配置</caption>\n    <tr>\n        <th>组件</th>\n        <th>要求</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">GPU</td>\n        <td>AMD: Video Coding Engine 3.1或更高</td>\n    </tr>\n    <tr>\n        <td>Intel: HD Graphics 510或更高</td>\n    </tr>\n    <tr>\n        <td>Nvidia: GeForce GTX 1080或更高的具有多编码器的型号</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">CPU</td>\n        <td>AMD: Ryzen 5或更高</td>\n    </tr>\n    <tr>\n        <td>Intel: Core i5或更高</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">网络</td>\n        <td>主机: CAT5e以太网或更好</td>\n    </tr>\n    <tr>\n        <td>客户端: CAT5e以太网或更好</td>\n    </tr>\n</table>\n\n## 技术支持\n\n遇到问题时的解决路径：\n1. 查看 [使用文档](https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=YTpMj5JNNdB5hEKJhhqlSB) [LizardByte文档](https://docs.lizardbyte.dev/projects/sunshine/latest/)\n2. 在设置中打开详细的日志等级找到相关信息\n3. [加入QQ交流群获取帮助](https://qm.qq.com/cgi-bin/qm/qr?k=5qnkzSaLIrIaU4FvumftZH_6Hg7fUuLD&jump_from=webapi)\n4. [使用两个字母！](https://uuyc.163.com/)\n\n**问题反馈标签：**\n- `hdr-support` - HDR相关问题\n- `virtual-display` - 虚拟显示器问题  \n- `config-help` - 配置相关问题\n\n## 加入社区\n\n我们欢迎大家参与讨论和贡献代码！\n[![加入QQ群](https://pub.idqqimg.com/wpa/images/group.png '加入QQ群')](https://qm.qq.com/cgi-bin/qm/qr?k=WC2PSZ3Q6Hk6j8U_DG9S7522GPtItk0m&jump_from=webapi&authKey=zVDLFrS83s/0Xg3hMbkMeAqI7xoHXaM3sxZIF/u9JW7qO/D8xd0npytVBC2lOS+z)\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=qiin2333/Sunshine-Foundation&type=Date)](https://www.star-history.com/#qiin2333/Sunshine-Foundation&Date)\n\n---\n\n**Sunshine基地版 - 让游戏串流更简单**\n"
  },
  {
    "path": "cmake/FindLIBCAP.cmake",
    "content": "# - Try to find Libcap\n# Once done this will define\n#\n#  LIBCAP_FOUND - system has Libcap\n#  LIBCAP_INCLUDE_DIRS - the Libcap include directory\n#  LIBCAP_LIBRARIES - the libraries needed to use Libcap\n#  LIBCAP_DEFINITIONS - Compiler switches required for using Libcap\n\n# Use pkg-config to get the directories and then use these values\n# in the find_path() and find_library() calls\nfind_package(PkgConfig)\npkg_check_modules(PC_LIBCAP libcap)\n\nset(LIBCAP_DEFINITIONS ${PC_LIBCAP_CFLAGS})\n\nfind_path(LIBCAP_INCLUDE_DIRS sys/capability.h PATHS ${PC_LIBCAP_INCLUDEDIR} ${PC_LIBCAP_INCLUDE_DIRS})\nfind_library(LIBCAP_LIBRARIES NAMES libcap.so PATHS ${PC_LIBCAP_LIBDIR} ${PC_LIBCAP_LIBRARY_DIRS})\nmark_as_advanced(LIBCAP_INCLUDE_DIRS LIBCAP_LIBRARIES)\n\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(LIBCAP REQUIRED_VARS LIBCAP_LIBRARIES LIBCAP_INCLUDE_DIRS)\n"
  },
  {
    "path": "cmake/FindLIBDRM.cmake",
    "content": "# - Try to find Libdrm\n# Once done this will define\n#\n#  LIBDRM_FOUND - system has Libdrm\n#  LIBDRM_INCLUDE_DIRS - the Libdrm include directory\n#  LIBDRM_LIBRARIES - the libraries needed to use Libdrm\n#  LIBDRM_DEFINITIONS - Compiler switches required for using Libdrm\n\n# Use pkg-config to get the directories and then use these values\n# in the find_path() and find_library() calls\nfind_package(PkgConfig)\npkg_check_modules(PC_LIBDRM libdrm)\n\nset(LIBDRM_DEFINITIONS ${PC_LIBDRM_CFLAGS})\n\nfind_path(LIBDRM_INCLUDE_DIRS drm.h PATHS ${PC_LIBDRM_INCLUDEDIR} ${PC_LIBDRM_INCLUDE_DIRS} PATH_SUFFIXES libdrm)\nfind_library(LIBDRM_LIBRARIES NAMES libdrm.so PATHS ${PC_LIBDRM_LIBDIR} ${PC_LIBDRM_LIBRARY_DIRS})\nmark_as_advanced(LIBDRM_INCLUDE_DIRS LIBDRM_LIBRARIES)\n\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(LIBDRM REQUIRED_VARS LIBDRM_LIBRARIES LIBDRM_INCLUDE_DIRS)\n"
  },
  {
    "path": "cmake/FindLibva.cmake",
    "content": "# - Try to find Libva\n# This module defines the following variables:\n#\n# * LIBVA_FOUND - The component was found\n# * LIBVA_INCLUDE_DIRS - The component include directory\n# * LIBVA_LIBRARIES - The component library Libva\n# * LIBVA_DRM_LIBRARIES - The component library Libva DRM\n\n# Use pkg-config to get the directories and then use these values in the\n# find_path() and find_library() calls\n# cmake-format: on\n\nfind_package(PkgConfig QUIET)\nif(PKG_CONFIG_FOUND)\n    pkg_check_modules(_LIBVA libva)\n    pkg_check_modules(_LIBVA_DRM libva-drm)\nendif()\n\nfind_path(\n        LIBVA_INCLUDE_DIR\n        NAMES va/va.h va/va_drm.h\n        HINTS ${_LIBVA_INCLUDE_DIRS}\n        PATHS /usr/include /usr/local/include /opt/local/include)\n\nfind_library(\n        LIBVA_LIB\n        NAMES ${_LIBVA_LIBRARIES} libva\n        HINTS ${_LIBVA_LIBRARY_DIRS}\n        PATHS /usr/lib /usr/local/lib /opt/local/lib)\n\nfind_library(\n        LIBVA_DRM_LIB\n        NAMES ${_LIBVA_DRM_LIBRARIES} libva-drm\n        HINTS ${_LIBVA_DRM_LIBRARY_DIRS}\n        PATHS /usr/lib /usr/local/lib /opt/local/lib)\n\ninclude(FindPackageHandleStandardArgs)\nfind_package_handle_standard_args(Libva REQUIRED_VARS LIBVA_INCLUDE_DIR LIBVA_LIB LIBVA_DRM_LIB)\nmark_as_advanced(LIBVA_INCLUDE_DIR LIBVA_LIB LIBVA_DRM_LIB)\n\nif(LIBVA_FOUND)\n    set(LIBVA_INCLUDE_DIRS ${LIBVA_INCLUDE_DIR})\n    set(LIBVA_LIBRARIES ${LIBVA_LIB})\n    set(LIBVA_DRM_LIBRARIES ${LIBVA_DRM_LIB})\n\n    if(NOT TARGET Libva::va)\n        if(IS_ABSOLUTE \"${LIBVA_LIBRARIES}\")\n            add_library(Libva::va UNKNOWN IMPORTED)\n            set_target_properties(Libva::va PROPERTIES IMPORTED_LOCATION \"${LIBVA_LIBRARIES}\")\n        else()\n            add_library(Libva::va INTERFACE IMPORTED)\n            set_target_properties(Libva::va PROPERTIES IMPORTED_LIBNAME \"${LIBVA_LIBRARIES}\")\n        endif()\n\n        set_target_properties(Libva::va PROPERTIES INTERFACE_INCLUDE_DIRECTORIES \"${LIBVA_INCLUDE_DIRS}\")\n    endif()\n\n    if(NOT TARGET Libva::drm)\n        if(IS_ABSOLUTE \"${LIBVA_DRM_LIBRARIES}\")\n            add_library(Libva::drm UNKNOWN IMPORTED)\n            set_target_properties(Libva::drm PROPERTIES IMPORTED_LOCATION \"${LIBVA_DRM_LIBRARIES}\")\n        else()\n            add_library(Libva::drm INTERFACE IMPORTED)\n            set_target_properties(Libva::drm PROPERTIES IMPORTED_LIBNAME \"${LIBVA_DRM_LIBRARIES}\")\n        endif()\n\n        set_target_properties(Libva::drm PROPERTIES INTERFACE_INCLUDE_DIRECTORIES \"${LIBVA_INCLUDE_DIRS}\")\n    endif()\n\nendif()\n"
  },
  {
    "path": "cmake/FindSystemd.cmake",
    "content": "# - Try to find Systemd\n# Once done this will define\n#\n# SYSTEMD_FOUND - system has systemd\n# SYSTEMD_USER_UNIT_INSTALL_DIR - the systemd system unit install directory\n# SYSTEMD_SYSTEM_UNIT_INSTALL_DIR - the systemd user unit install directory\n\nIF (NOT WIN32)\n\n    find_package(PkgConfig QUIET)\n    if(PKG_CONFIG_FOUND)\n        pkg_check_modules(SYSTEMD \"systemd\")\n    endif()\n\n    if (SYSTEMD_FOUND)\n        execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE}\n            --variable=systemduserunitdir systemd\n            OUTPUT_VARIABLE SYSTEMD_USER_UNIT_INSTALL_DIR)\n\n        string(REGEX REPLACE \"[ \\t\\n]+\" \"\" SYSTEMD_USER_UNIT_INSTALL_DIR\n            \"${SYSTEMD_USER_UNIT_INSTALL_DIR}\")\n\n        execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE}\n            --variable=systemdsystemunitdir systemd\n            OUTPUT_VARIABLE SYSTEMD_SYSTEM_UNIT_INSTALL_DIR)\n\n        string(REGEX REPLACE \"[ \\t\\n]+\" \"\" SYSTEMD_SYSTEM_UNIT_INSTALL_DIR\n            \"${SYSTEMD_SYSTEM_UNIT_INSTALL_DIR}\")\n\n        mark_as_advanced(SYSTEMD_USER_UNIT_INSTALL_DIR SYSTEMD_SYSTEM_UNIT_INSTALL_DIR)\n\n    endif ()\n\nENDIF ()\n"
  },
  {
    "path": "cmake/FindUdev.cmake",
    "content": "# - Try to find Udev\n# Once done this will define\n#\n# UDEV_FOUND - system has udev\n# UDEV_RULES_INSTALL_DIR - the udev rules install directory\n\nIF (NOT WIN32)\n\n    find_package(PkgConfig QUIET)\n    if(PKG_CONFIG_FOUND)\n        pkg_check_modules(UDEV \"udev\")\n    endif()\n\n    if (UDEV_FOUND)\n        execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE}\n            --variable=udevdir udev\n            OUTPUT_VARIABLE UDEV_RULES_INSTALL_DIR)\n\n        string(REGEX REPLACE \"[ \\t\\n]+\" \"\" UDEV_RULES_INSTALL_DIR\n            \"${UDEV_RULES_INSTALL_DIR}\")\n\n        set(UDEV_RULES_INSTALL_DIR \"${UDEV_RULES_INSTALL_DIR}/rules.d\")\n\n        mark_as_advanced(UDEV_RULES_INSTALL_DIR)\n\n    endif ()\n\nENDIF ()\n"
  },
  {
    "path": "cmake/FindWayland.cmake",
    "content": "# Try to find Wayland on a Unix system\n#\n# This will define:\n#\n#   WAYLAND_FOUND        - True if Wayland is found\n#   WAYLAND_LIBRARIES    - Link these to use Wayland\n#   WAYLAND_INCLUDE_DIRS - Include directory for Wayland\n#   WAYLAND_DEFINITIONS  - Compiler flags for using Wayland\n#\n# In addition the following more fine grained variables will be defined:\n#\n#   Wayland_Client_FOUND  WAYLAND_CLIENT_INCLUDE_DIRS  WAYLAND_CLIENT_LIBRARIES\n#   Wayland_Server_FOUND  WAYLAND_SERVER_INCLUDE_DIRS  WAYLAND_SERVER_LIBRARIES\n#   Wayland_EGL_FOUND     WAYLAND_EGL_INCLUDE_DIRS     WAYLAND_EGL_LIBRARIES\n#   Wayland_Cursor_FOUND  WAYLAND_CURSOR_INCLUDE_DIRS  WAYLAND_CURSOR_LIBRARIES\n#\n# Copyright (c) 2013 Martin Gräßlin <mgraesslin@kde.org>\n#               2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>\n#\n# Redistribution and use is allowed according to the terms of the BSD license.\n# For details see the accompanying COPYING-CMAKE-SCRIPTS file.\n\nIF (NOT WIN32)\n\n    # Use pkg-config to get the directories and then use these values\n    # in the find_path() and find_library() calls\n    find_package(PkgConfig)\n    PKG_CHECK_MODULES(PKG_WAYLAND QUIET wayland-client wayland-server wayland-egl wayland-cursor)\n\n    set(WAYLAND_DEFINITIONS ${PKG_WAYLAND_CFLAGS})\n\n    find_path(WAYLAND_CLIENT_INCLUDE_DIRS NAMES wayland-client.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})\n    find_library(WAYLAND_CLIENT_LIBRARIES NAMES wayland-client   HINTS ${PKG_WAYLAND_LIBRARY_DIRS})\n    if(WAYLAND_CLIENT_INCLUDE_DIRS AND WAYLAND_CLIENT_LIBRARIES)\n        set(Wayland_Client_FOUND TRUE)  # cmake-lint: disable=C0103\n    else()\n        set(Wayland_Client_FOUND FALSE)  # cmake-lint: disable=C0103\n    endif()\n    mark_as_advanced(WAYLAND_CLIENT_INCLUDE_DIRS WAYLAND_CLIENT_LIBRARIES)\n\n    find_path(WAYLAND_CURSOR_INCLUDE_DIRS NAMES wayland-cursor.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})\n    find_library(WAYLAND_CURSOR_LIBRARIES NAMES wayland-cursor   HINTS ${PKG_WAYLAND_LIBRARY_DIRS})\n    if(WAYLAND_CURSOR_INCLUDE_DIRS AND WAYLAND_CURSOR_LIBRARIES)\n        set(Wayland_Cursor_FOUND TRUE)  # cmake-lint: disable=C0103\n    else()\n        set(Wayland_Cursor_FOUND FALSE)  # cmake-lint: disable=C0103\n    endif()\n    mark_as_advanced(WAYLAND_CURSOR_INCLUDE_DIRS WAYLAND_CURSOR_LIBRARIES)\n\n    find_path(WAYLAND_EGL_INCLUDE_DIRS    NAMES wayland-egl.h    HINTS ${PKG_WAYLAND_INCLUDE_DIRS})\n    find_library(WAYLAND_EGL_LIBRARIES    NAMES wayland-egl      HINTS ${PKG_WAYLAND_LIBRARY_DIRS})\n    if(WAYLAND_EGL_INCLUDE_DIRS AND WAYLAND_EGL_LIBRARIES)\n        set(Wayland_EGL_FOUND TRUE)  # cmake-lint: disable=C0103\n    else()\n        set(Wayland_EGL_FOUND FALSE)  # cmake-lint: disable=C0103\n    endif()\n    mark_as_advanced(WAYLAND_EGL_INCLUDE_DIRS WAYLAND_EGL_LIBRARIES)\n\n    find_path(WAYLAND_SERVER_INCLUDE_DIRS NAMES wayland-server.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})\n    find_library(WAYLAND_SERVER_LIBRARIES NAMES wayland-server   HINTS ${PKG_WAYLAND_LIBRARY_DIRS})\n    if(WAYLAND_SERVER_INCLUDE_DIRS AND WAYLAND_SERVER_LIBRARIES)\n        set(Wayland_Server_FOUND TRUE)  # cmake-lint: disable=C0103\n    else()\n        set(Wayland_Server_FOUND FALSE)  # cmake-lint: disable=C0103\n    endif()\n    mark_as_advanced(WAYLAND_SERVER_INCLUDE_DIRS WAYLAND_SERVER_LIBRARIES)\n\n    set(WAYLAND_INCLUDE_DIRS ${WAYLAND_CLIENT_INCLUDE_DIRS} ${WAYLAND_SERVER_INCLUDE_DIRS}\n            ${WAYLAND_EGL_INCLUDE_DIRS} ${WAYLAND_CURSOR_INCLUDE_DIRS})\n    set(WAYLAND_LIBRARIES ${WAYLAND_CLIENT_LIBRARIES} ${WAYLAND_SERVER_LIBRARIES}\n            ${WAYLAND_EGL_LIBRARIES} ${WAYLAND_CURSOR_LIBRARIES})\n    mark_as_advanced(WAYLAND_INCLUDE_DIRS WAYLAND_LIBRARIES)\n\n    list(REMOVE_DUPLICATES WAYLAND_INCLUDE_DIRS)\n\n    include(FindPackageHandleStandardArgs)\n\n    find_package_handle_standard_args(Wayland REQUIRED_VARS WAYLAND_LIBRARIES WAYLAND_INCLUDE_DIRS HANDLE_COMPONENTS)\n\nENDIF ()\n"
  },
  {
    "path": "cmake/compile_definitions/common.cmake",
    "content": "# common compile definitions\n# this file will also load platform specific definitions\n\nlist(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-sign-compare)\n# Wall - enable all warnings\n# Werror - treat warnings as errors\n# Wno-maybe-uninitialized/Wno-uninitialized - disable warnings for maybe uninitialized variables\n# Wno-sign-compare - disable warnings for signed/unsigned comparisons\n# Wno-restrict - disable warnings for memory overlap\nif(CMAKE_CXX_COMPILER_ID STREQUAL \"GNU\")\n    # GCC specific compile options\n\n    # GCC 12 and higher will complain about maybe-uninitialized\n    if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12)\n        list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-maybe-uninitialized)\n\n        # Disable the bogus warning that may prevent compilation (only for GCC 12).\n        # See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105651.\n        if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13)\n            list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-restrict)\n        endif()\n    endif()\nelseif(CMAKE_CXX_COMPILER_ID STREQUAL \"Clang\")\n    # Clang specific compile options\n\n    # Clang doesn't actually complain about this this, so disabling for now\n    # list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-uninitialized)\nendif()\nif(BUILD_WERROR)\n    list(APPEND SUNSHINE_COMPILE_OPTIONS -Werror)\nendif()\n\n# setup assets directory\nif(NOT SUNSHINE_ASSETS_DIR)\n    set(SUNSHINE_ASSETS_DIR \"assets\")\nendif()\n\n# platform specific compile definitions\nif(WIN32)\n    include(${CMAKE_MODULE_PATH}/compile_definitions/windows.cmake)\nelseif(UNIX)\n    include(${CMAKE_MODULE_PATH}/compile_definitions/unix.cmake)\n\n    if(APPLE)\n        include(${CMAKE_MODULE_PATH}/compile_definitions/macos.cmake)\n    else()\n        include(${CMAKE_MODULE_PATH}/compile_definitions/linux.cmake)\n    endif()\nendif()\n\nconfigure_file(\"${CMAKE_SOURCE_DIR}/src/version.h.in\" version.h @ONLY)\ninclude_directories(\"${CMAKE_CURRENT_BINARY_DIR}\")  # required for importing version.h\n\nset(SUNSHINE_TARGET_FILES\n        \"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Input.h\"\n        \"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Rtsp.h\"\n        \"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/RtspParser.c\"\n        \"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/src/Video.h\"\n        \"${CMAKE_SOURCE_DIR}/third-party/tray/src/tray.h\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/display_device.h\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/parsed_config.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/parsed_config.h\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/session.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/session.h\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/settings.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/settings.h\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/to_string.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/to_string.h\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/vdd_utils.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/display_device/vdd_utils.h\"\n        \"${CMAKE_SOURCE_DIR}/src/upnp.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/upnp.h\"\n        \"${CMAKE_SOURCE_DIR}/src/cbs.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/utility.h\"\n        \"${CMAKE_SOURCE_DIR}/src/uuid.h\"\n        \"${CMAKE_SOURCE_DIR}/src/config.h\"\n        \"${CMAKE_SOURCE_DIR}/src/config.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/entry_handler.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/entry_handler.h\"\n        \"${CMAKE_SOURCE_DIR}/src/file_handler.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/file_handler.h\"\n        \"${CMAKE_SOURCE_DIR}/src/globals.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/globals.h\"\n        \"${CMAKE_SOURCE_DIR}/src/logging.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/logging.h\"\n        \"${CMAKE_SOURCE_DIR}/src/main.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/main.h\"\n        \"${CMAKE_SOURCE_DIR}/src/crypto.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/crypto.h\"\n        \"${CMAKE_SOURCE_DIR}/src/webhook_format.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/webhook_format.h\"\n        \"${CMAKE_SOURCE_DIR}/src/webhook_httpsclient.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/webhook_httpsclient.h\"\n        \"${CMAKE_SOURCE_DIR}/src/webhook.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/webhook.h\"\n        \"${CMAKE_SOURCE_DIR}/src/nvhttp.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/nvhttp.h\"\n        \"${CMAKE_SOURCE_DIR}/src/abr.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/abr.h\"\n        \"${CMAKE_SOURCE_DIR}/src/httpcommon.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/httpcommon.h\"\n        \"${CMAKE_SOURCE_DIR}/src/confighttp.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/confighttp.h\"\n        \"${CMAKE_SOURCE_DIR}/src/rtsp.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/rtsp.h\"\n        \"${CMAKE_SOURCE_DIR}/src/stream.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/stream.h\"\n        \"${CMAKE_SOURCE_DIR}/src/video.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/video.h\"\n        \"${CMAKE_SOURCE_DIR}/src/video_colorspace.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/video_colorspace.h\"\n        \"${CMAKE_SOURCE_DIR}/src/input.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/input.h\"\n        \"${CMAKE_SOURCE_DIR}/src/audio.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/audio.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/common.h\"\n        \"${CMAKE_SOURCE_DIR}/src/process.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/process.h\"\n        \"${CMAKE_SOURCE_DIR}/src/network.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/network.h\"\n        \"${CMAKE_SOURCE_DIR}/src/move_by_copy.h\"\n        \"${CMAKE_SOURCE_DIR}/src/system_tray.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/system_tray.h\"\n        \"${CMAKE_SOURCE_DIR}/src/system_tray_i18n.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/system_tray_i18n.h\"\n        \"${CMAKE_SOURCE_DIR}/src/task_pool.h\"\n        \"${CMAKE_SOURCE_DIR}/src/thread_pool.h\"\n        \"${CMAKE_SOURCE_DIR}/src/thread_safe.h\"\n        \"${CMAKE_SOURCE_DIR}/src/sync.h\"\n        \"${CMAKE_SOURCE_DIR}/src/round_robin.h\"\n        \"${CMAKE_SOURCE_DIR}/src/stat_trackers.h\"\n        \"${CMAKE_SOURCE_DIR}/src/stat_trackers.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/rswrapper.h\"\n        \"${CMAKE_SOURCE_DIR}/src/rswrapper.c\"\n        ${PLATFORM_TARGET_FILES})\n\nif(NOT SUNSHINE_ASSETS_DIR_DEF)\n    set(SUNSHINE_ASSETS_DIR_DEF \"${SUNSHINE_ASSETS_DIR}\")\nendif()\nlist(APPEND SUNSHINE_DEFINITIONS SUNSHINE_ASSETS_DIR=\"${SUNSHINE_ASSETS_DIR_DEF}\")\n\nlist(APPEND SUNSHINE_DEFINITIONS SUNSHINE_TRAY=${SUNSHINE_TRAY})\n\n# Publisher metadata - escape spaces for proper compilation\nstring(REPLACE \" \" \"_\" SUNSHINE_PUBLISHER_NAME_SAFE \"${SUNSHINE_PUBLISHER_NAME}\")\nlist(APPEND SUNSHINE_DEFINITIONS SUNSHINE_PUBLISHER_NAME=\"${SUNSHINE_PUBLISHER_NAME_SAFE}\")\nlist(APPEND SUNSHINE_DEFINITIONS SUNSHINE_PUBLISHER_WEBSITE=\"${SUNSHINE_PUBLISHER_WEBSITE}\")\nlist(APPEND SUNSHINE_DEFINITIONS SUNSHINE_PUBLISHER_ISSUE_URL=\"${SUNSHINE_PUBLISHER_ISSUE_URL}\")\n\ninclude_directories(\"${CMAKE_SOURCE_DIR}\")\n\ninclude_directories(\n        SYSTEM\n        \"${CMAKE_SOURCE_DIR}/third-party\"\n        \"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet/include\"\n        \"${CMAKE_SOURCE_DIR}/third-party/nanors\"\n        \"${CMAKE_SOURCE_DIR}/third-party/nanors/deps/obl\"\n        ${FFMPEG_INCLUDE_DIRS}\n        ${Boost_INCLUDE_DIRS}  # has to be the last, or we get runtime error on macOS ffmpeg encoder\n)\n\nlist(APPEND SUNSHINE_EXTERNAL_LIBRARIES\n        ${MINIUPNP_LIBRARIES}\n        ${CMAKE_THREAD_LIBS_INIT}\n        enet\n        nlohmann_json::nlohmann_json\n        opus\n        ${FFMPEG_LIBRARIES}\n        ${Boost_LIBRARIES}\n        ${OPENSSL_LIBRARIES}\n        ${PLATFORM_LIBRARIES})\n"
  },
  {
    "path": "cmake/compile_definitions/linux.cmake",
    "content": "# linux specific compile definitions\n\nadd_compile_definitions(SUNSHINE_PLATFORM=\"linux\")\n\n# AppImage\nif(${SUNSHINE_BUILD_APPIMAGE})\n    # use relative assets path for AppImage\n    string(REPLACE \"${CMAKE_INSTALL_PREFIX}\" \".${CMAKE_INSTALL_PREFIX}\" SUNSHINE_ASSETS_DIR_DEF ${SUNSHINE_ASSETS_DIR})\nendif()\n\n# cuda\nset(CUDA_FOUND OFF)\nif(${SUNSHINE_ENABLE_CUDA})\n    include(CheckLanguage)\n    check_language(CUDA)\n\n    if(CMAKE_CUDA_COMPILER)\n        set(CUDA_FOUND ON)\n        enable_language(CUDA)\n\n        message(STATUS \"CUDA Compiler Version: ${CMAKE_CUDA_COMPILER_VERSION}\")\n        set(CMAKE_CUDA_ARCHITECTURES \"\")\n\n        # https://tech.amikelive.com/node-930/cuda-compatibility-of-nvidia-display-gpu-drivers/\n        if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 6.5)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 10)\n        elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 6.5)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 50 52)\n        endif()\n\n        if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 7.0)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 11)\n        elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER 7.6)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 60 61 62)\n        endif()\n\n        # https://docs.nvidia.com/cuda/archive/9.2/cuda-compiler-driver-nvcc/index.html\n        if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 9.0)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 20)\n        elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 9.0)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 70)\n        endif()\n\n        # https://docs.nvidia.com/cuda/archive/10.0/cuda-compiler-driver-nvcc/index.html\n        if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 10.0)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 72 75)\n        endif()\n\n        # https://docs.nvidia.com/cuda/archive/11.0/cuda-compiler-driver-nvcc/index.html\n        if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 11.0)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 30)\n        elseif(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 11.0)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 80)\n        endif()\n\n        # https://docs.nvidia.com/cuda/archive/11.8.0/cuda-compiler-driver-nvcc/index.html\n        if(CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 11.8)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 86 87 89 90)\n        endif()\n\n        if(CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 12.0)\n            list(APPEND CMAKE_CUDA_ARCHITECTURES 35)\n        endif()\n\n        # sort the architectures\n        list(SORT CMAKE_CUDA_ARCHITECTURES COMPARE NATURAL)\n\n        # message(STATUS \"CUDA NVCC Flags: ${CUDA_NVCC_FLAGS}\")\n        message(STATUS \"CUDA Architectures: ${CMAKE_CUDA_ARCHITECTURES}\")\n    endif()\nendif()\nif(CUDA_FOUND)\n    include_directories(SYSTEM \"${CMAKE_SOURCE_DIR}/third-party/nvfbc\")\n    list(APPEND PLATFORM_TARGET_FILES\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.h\"\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.cu\"\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/cuda.cpp\"\n            \"${CMAKE_SOURCE_DIR}/third-party/nvfbc/NvFBC.h\")\n\n    add_compile_definitions(SUNSHINE_BUILD_CUDA)\nendif()\n\n# drm\nif(${SUNSHINE_ENABLE_DRM})\n    find_package(LIBDRM)\n    find_package(LIBCAP)\nelse()\n    set(LIBDRM_FOUND OFF)\n    set(LIBCAP_FOUND OFF)\nendif()\nif(LIBDRM_FOUND AND LIBCAP_FOUND)\n    add_compile_definitions(SUNSHINE_BUILD_DRM)\n    include_directories(SYSTEM ${LIBDRM_INCLUDE_DIRS} ${LIBCAP_INCLUDE_DIRS})\n    list(APPEND PLATFORM_LIBRARIES ${LIBDRM_LIBRARIES} ${LIBCAP_LIBRARIES})\n    list(APPEND PLATFORM_TARGET_FILES\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/kmsgrab.cpp\")\n    list(APPEND SUNSHINE_DEFINITIONS EGL_NO_X11=1)\nelseif(NOT LIBDRM_FOUND)\n    message(WARNING \"Missing libdrm\")\nelseif(NOT LIBDRM_FOUND)\n    message(WARNING \"Missing libcap\")\nendif()\n\n# evdev\ninclude(dependencies/libevdev_Sunshine)\n\n# vaapi\nif(${SUNSHINE_ENABLE_VAAPI})\n    find_package(Libva)\nelse()\n    set(LIBVA_FOUND OFF)\nendif()\nif(LIBVA_FOUND)\n    add_compile_definitions(SUNSHINE_BUILD_VAAPI)\n    include_directories(SYSTEM ${LIBVA_INCLUDE_DIR})\n    list(APPEND PLATFORM_LIBRARIES ${LIBVA_LIBRARIES} ${LIBVA_DRM_LIBRARIES})\n    list(APPEND PLATFORM_TARGET_FILES\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/vaapi.h\"\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/vaapi.cpp\")\nendif()\n\n# wayland\nif(${SUNSHINE_ENABLE_WAYLAND})\n    find_package(Wayland)\nelse()\n    set(WAYLAND_FOUND OFF)\nendif()\nif(WAYLAND_FOUND)\n    add_compile_definitions(SUNSHINE_BUILD_WAYLAND)\n\n    if(NOT SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS)\n        set(WAYLAND_PROTOCOLS_DIR \"${CMAKE_SOURCE_DIR}/third-party/wayland-protocols\")\n    else()\n        pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)\n        pkg_check_modules(WAYLAND_PROTOCOLS wayland-protocols REQUIRED)\n    endif()\n\n    GEN_WAYLAND(\"${WAYLAND_PROTOCOLS_DIR}\" \"unstable/xdg-output\" xdg-output-unstable-v1)\n    GEN_WAYLAND(\"${CMAKE_SOURCE_DIR}/third-party/wlr-protocols\" \"unstable\" wlr-export-dmabuf-unstable-v1)\n\n    include_directories(\n            SYSTEM\n            ${WAYLAND_INCLUDE_DIRS}\n            ${CMAKE_BINARY_DIR}/generated-src\n    )\n\n    list(APPEND PLATFORM_LIBRARIES ${WAYLAND_LIBRARIES})\n    list(APPEND PLATFORM_TARGET_FILES\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/wlgrab.cpp\"\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.h\"\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/wayland.cpp\")\nendif()\n\n# x11\nif(${SUNSHINE_ENABLE_X11})\n    find_package(X11)\nelse()\n    set(X11_FOUND OFF)\nendif()\nif(X11_FOUND)\n    add_compile_definitions(SUNSHINE_BUILD_X11)\n    include_directories(SYSTEM ${X11_INCLUDE_DIR})\n    list(APPEND PLATFORM_LIBRARIES ${X11_LIBRARIES})\n    list(APPEND PLATFORM_TARGET_FILES\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/x11grab.h\"\n            \"${CMAKE_SOURCE_DIR}/src/platform/linux/x11grab.cpp\")\nendif()\n\nif(NOT ${CUDA_FOUND}\n        AND NOT ${WAYLAND_FOUND}\n        AND NOT ${X11_FOUND}\n        AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND})\n        AND NOT ${LIBVA_FOUND})\n    message(FATAL_ERROR \"Couldn't find either cuda, wayland, x11, (libdrm and libcap), or libva\")\nendif()\n\n# tray icon\nif(${SUNSHINE_ENABLE_TRAY})\n    pkg_check_modules(APPINDICATOR ayatana-appindicator3-0.1)\n    if(APPINDICATOR_FOUND)\n        list(APPEND SUNSHINE_DEFINITIONS TRAY_AYATANA_APPINDICATOR=1)\n    else()\n        pkg_check_modules(APPINDICATOR appindicator3-0.1)\n        if(APPINDICATOR_FOUND)\n            list(APPEND SUNSHINE_DEFINITIONS TRAY_LEGACY_APPINDICATOR=1)\n        endif ()\n    endif()\n    pkg_check_modules(LIBNOTIFY libnotify)\n    if(NOT APPINDICATOR_FOUND OR NOT LIBNOTIFY_FOUND)\n        set(SUNSHINE_TRAY 0)\n        message(WARNING \"Missing appindicator or libnotify, disabling tray icon\")\n        message(STATUS \"APPINDICATOR_FOUND: ${APPINDICATOR_FOUND}\")\n        message(STATUS \"LIBNOTIFY_FOUND: ${LIBNOTIFY_FOUND}\")\n    else()\n        include_directories(SYSTEM ${APPINDICATOR_INCLUDE_DIRS} ${LIBNOTIFY_INCLUDE_DIRS})\n        link_directories(${APPINDICATOR_LIBRARY_DIRS} ${LIBNOTIFY_LIBRARY_DIRS})\n\n        list(APPEND PLATFORM_TARGET_FILES \"${CMAKE_SOURCE_DIR}/third-party/tray/src/tray_linux.c\")\n        list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${APPINDICATOR_LIBRARIES} ${LIBNOTIFY_LIBRARIES})\n    endif()\nelse()\n    set(SUNSHINE_TRAY 0)\n    message(STATUS \"Tray icon disabled\")\nendif()\n\nif(${SUNSHINE_ENABLE_TRAY} AND ${SUNSHINE_TRAY} EQUAL 0 AND SUNSHINE_REQUIRE_TRAY)\n    message(FATAL_ERROR \"Tray icon is required\")\nendif()\n\nif(${SUNSHINE_USE_LEGACY_INPUT})  # TODO: Remove this legacy option after the next stable release\n    list(APPEND PLATFORM_TARGET_FILES \"${CMAKE_SOURCE_DIR}/src/platform/linux/input/legacy_input.cpp\")\nelse()\n    # These need to be set before adding the inputtino subdirectory in order for them to be picked up\n    set(LIBEVDEV_CUSTOM_INCLUDE_DIR \"${EVDEV_INCLUDE_DIR}\")\n    set(LIBEVDEV_CUSTOM_LIBRARY \"${EVDEV_LIBRARY}\")\n\n    add_subdirectory(\"${CMAKE_SOURCE_DIR}/third-party/inputtino\")\n    list(APPEND SUNSHINE_EXTERNAL_LIBRARIES inputtino::libinputtino)\n    file(GLOB_RECURSE INPUTTINO_SOURCES\n            ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.h\n            ${CMAKE_SOURCE_DIR}/src/platform/linux/input/inputtino*.cpp)\n    list(APPEND PLATFORM_TARGET_FILES ${INPUTTINO_SOURCES})\n\n    # build libevdev before the libinputtino target\n    if(EXTERNAL_PROJECT_LIBEVDEV_USED)\n        add_dependencies(libinputtino libevdev)\n    endif()\nendif()\n\nlist(APPEND PLATFORM_TARGET_FILES\n        \"${CMAKE_SOURCE_DIR}/src/platform/linux/publish.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/linux/graphics.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/linux/misc.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/linux/misc.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/linux/audio.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/linux/display_device.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/linux/input.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/linux/display_device.cpp\"\n        \"${CMAKE_SOURCE_DIR}/third-party/glad/src/egl.c\"\n        \"${CMAKE_SOURCE_DIR}/third-party/glad/src/gl.c\"\n        \"${CMAKE_SOURCE_DIR}/third-party/glad/include/EGL/eglplatform.h\"\n        \"${CMAKE_SOURCE_DIR}/third-party/glad/include/KHR/khrplatform.h\"\n        \"${CMAKE_SOURCE_DIR}/third-party/glad/include/glad/gl.h\"\n        \"${CMAKE_SOURCE_DIR}/third-party/glad/include/glad/egl.h\")\n\nlist(APPEND PLATFORM_LIBRARIES\n        dl\n        pulse\n        pulse-simple)\n\ninclude_directories(\n        SYSTEM\n        \"${CMAKE_SOURCE_DIR}/third-party/glad/include\")\n"
  },
  {
    "path": "cmake/compile_definitions/macos.cmake",
    "content": "# macos specific compile definitions\n\nadd_compile_definitions(SUNSHINE_PLATFORM=\"macos\")\n\nset(MACOS_LINK_DIRECTORIES\n        /opt/homebrew/lib\n        /opt/local/lib\n        /usr/local/lib)\n\nforeach(dir ${MACOS_LINK_DIRECTORIES})\n    if(EXISTS ${dir})\n        link_directories(${dir})\n    endif()\nendforeach()\n\nif(NOT BOOST_USE_STATIC AND NOT FETCH_CONTENT_BOOST_USED)\n    ADD_DEFINITIONS(-DBOOST_LOG_DYN_LINK)\nendif()\n\nlist(APPEND SUNSHINE_EXTERNAL_LIBRARIES\n        ${APP_KIT_LIBRARY}\n        ${APP_SERVICES_LIBRARY}\n        ${AV_FOUNDATION_LIBRARY}\n        ${CORE_MEDIA_LIBRARY}\n        ${CORE_VIDEO_LIBRARY}\n        ${FOUNDATION_LIBRARY}\n        ${VIDEO_TOOLBOX_LIBRARY})\n\nset(APPLE_PLIST_FILE \"${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/Info.plist\")\n\n# todo - tray is not working on macos\nset(SUNSHINE_TRAY 0)\n\nset(PLATFORM_TARGET_FILES\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/av_audio.m\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/av_img_t.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/av_video.m\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/display.mm\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/display_device.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/input.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/microphone.mm\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/misc.mm\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/misc.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/nv12_zero_device.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/macos/publish.cpp\"\n        \"${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.c\"\n        \"${CMAKE_SOURCE_DIR}/third-party/TPCircularBuffer/TPCircularBuffer.h\"\n        ${APPLE_PLIST_FILE})\n\nif(SUNSHINE_ENABLE_TRAY)\n    list(APPEND SUNSHINE_EXTERNAL_LIBRARIES\n            ${COCOA})\n    list(APPEND PLATFORM_TARGET_FILES\n            \"${CMAKE_SOURCE_DIR}/third-party/tray/src/tray_darwin.m\")\nendif()\n"
  },
  {
    "path": "cmake/compile_definitions/unix.cmake",
    "content": "# unix specific compile definitions\n# put anything here that applies to both linux and macos\n\nlist(APPEND SUNSHINE_EXTERNAL_LIBRARIES\n        ${CURL_LIBRARIES})\n\n# add install prefix to assets path if not already there\nif(NOT SUNSHINE_ASSETS_DIR MATCHES \"^${CMAKE_INSTALL_PREFIX}\")\n    set(SUNSHINE_ASSETS_DIR \"${CMAKE_INSTALL_PREFIX}/${SUNSHINE_ASSETS_DIR}\")\nendif()\n"
  },
  {
    "path": "cmake/compile_definitions/windows.cmake",
    "content": "# windows specific compile definitions\n\nadd_compile_definitions(SUNSHINE_PLATFORM=\"windows\")\n\nenable_language(RC)\nset(CMAKE_RC_COMPILER windres)\nset(CMAKE_RC_FLAGS \"${CMAKE_RC_FLAGS} --use-temp-file\")\nset(CMAKE_EXE_LINKER_FLAGS \"${CMAKE_EXE_LINKER_FLAGS} -static\")\n\n# gcc complains about misleading indentation in some mingw includes\nlist(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-misleading-indentation)\n\n# gcc15 complains about non-template type 'coroutine_handle' used as a template in Windows.Foundation.h\n# can remove after https://gcc.gnu.org/bugzilla/show_bug.cgi?id=120495 is available in mingw-w64\nlist(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-template-body)\n\n# see gcc bug 98723\nadd_definitions(-DUSE_BOOST_REGEX)\n\n# Fix for GCC 15 C++20 coroutine compatibility with WinRT\nif(CMAKE_CXX_COMPILER_ID STREQUAL \"GNU\" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15)\n    # Add compiler flags to handle WinRT coroutine issues with GCC 15\n    list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-template-body)\n    add_definitions(-DWINRT_LEAN_AND_MEAN)\n    # Force using experimental coroutine ABI for compatibility\n    add_definitions(-D_ALLOW_COROUTINE_ABI_MISMATCH)\nendif()\n\n# curl\nadd_definitions(-DCURL_STATICLIB)\ninclude_directories(SYSTEM ${CURL_STATIC_INCLUDE_DIRS})\nlink_directories(${CURL_STATIC_LIBRARY_DIRS})\n\n# miniupnpc\nadd_definitions(-DMINIUPNP_STATICLIB)\n\n# extra tools/binaries for audio/display devices\nadd_subdirectory(tools)  # todo - this is temporary, only tools for Windows are needed, for now\n\n# nvidia\ninclude_directories(SYSTEM \"${CMAKE_SOURCE_DIR}/third-party/nvapi\")\nfile(GLOB NVPREFS_FILES CONFIGURE_DEPENDS\n        \"${CMAKE_SOURCE_DIR}/third-party/nvapi/*.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/nvprefs/*.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/nvprefs/*.h\")\n\ninclude_directories(SYSTEM \"${CMAKE_SOURCE_DIR}/third-party/nvenc-headers\")\nfile(GLOB_RECURSE NVENC_SOURCES CONFIGURE_DEPENDS\n        \"${CMAKE_SOURCE_DIR}/src/nvenc/*.h\"\n        \"${CMAKE_SOURCE_DIR}/src/nvenc/*.cpp\")\n\n# amf\nfile(GLOB_RECURSE AMF_SOURCES CONFIGURE_DEPENDS\n        \"${CMAKE_SOURCE_DIR}/src/amf/*.h\"\n        \"${CMAKE_SOURCE_DIR}/src/amf/*.cpp\")\n\n# vigem\ninclude_directories(SYSTEM \"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include\")\n\n# sunshine icon\nif(NOT DEFINED SUNSHINE_ICON_PATH)\n    set(SUNSHINE_ICON_PATH \"${CMAKE_SOURCE_DIR}/sunshine.ico\")\nendif()\n\nconfigure_file(\"${CMAKE_SOURCE_DIR}/src/platform/windows/windows.rc.in\" windows.rc @ONLY)\n\n# set(SUNSHINE_TRAY 0)\n\nset(PLATFORM_TARGET_FILES\n        \"${CMAKE_CURRENT_BINARY_DIR}/windows.rc\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/publish.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/ftime_compat.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/misc.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/misc.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/win_dark_mode.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/win_dark_mode.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/input.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/virtual_mouse.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/virtual_mouse.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/dsu_server.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/dsu_server.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_base.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_vram.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_amd.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/mic_write.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/device_hdr_states.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/device_modes.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/device_topology.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/general_functions.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/settings_topology.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/settings_topology.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/settings.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/windows_utils.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/windows_utils.cpp\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/session_listener.h\"\n        \"${CMAKE_SOURCE_DIR}/src/platform/windows/display_device/session_listener.cpp\"\n        \"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp\"\n        \"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h\"\n        \"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h\"\n        \"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Util.h\"\n        \"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/km/BusShared.h\"\n        ${NVPREFS_FILES}\n        ${NVENC_SOURCES}\n        ${AMF_SOURCES})\n\nset(OPENSSL_LIBRARIES\n        libssl.a\n        libcrypto.a)\n\nlist(PREPEND PLATFORM_LIBRARIES\n        ${CURL_STATIC_LIBRARIES}\n        avrt\n        d3d11\n        D3DCompiler\n        dwmapi\n        dxgi\n        hid\n        iphlpapi\n        ksuser\n        libssp.a\n        libstdc++.a\n        libwinpthread.a\n        minhook::minhook\n        nlohmann_json::nlohmann_json\n        ntdll\n        ole32\n        PowrProf\n        setupapi\n        shlwapi\n        synchronization.lib\n        userenv\n        ws2_32\n        wsock32\n)\n\nif(SUNSHINE_ENABLE_TRAY)\n    list(APPEND PLATFORM_TARGET_FILES\n            \"${CMAKE_SOURCE_DIR}/third-party/tray/src/tray_windows.c\")\nendif()"
  },
  {
    "path": "cmake/dependencies/Boost_Sunshine.cmake",
    "content": "#\n# Loads the boost library giving the priority to the system package first, with a fallback to FetchContent.\n#\ninclude_guard(GLOBAL)\n\nset(BOOST_VERSION \"1.89.0\")\nset(BOOST_COMPONENTS\n        filesystem\n        locale\n        log\n        program_options\n        system\n)\n# system is not used by Sunshine, but by Simple-Web-Server, added here for convenience\n\n# algorithm, preprocessor, scope, and uuid are not used by Sunshine, but by libdisplaydevice, added here for convenience\nif(WIN32)\n    list(APPEND BOOST_COMPONENTS\n            algorithm\n            preprocessor\n            scope\n            uuid\n    )\nendif()\n\nif(BOOST_USE_STATIC)\n    set(Boost_USE_STATIC_LIBS ON)  # cmake-lint: disable=C0103\nendif()\n\nif (CMAKE_VERSION VERSION_GREATER_EQUAL \"3.30\")\n    cmake_policy(SET CMP0167 NEW)  # Get BoostConfig.cmake from upstream\nendif()\nfind_package(Boost CONFIG ${BOOST_VERSION} EXACT COMPONENTS ${BOOST_COMPONENTS})\nif(NOT Boost_FOUND)\n    message(STATUS \"Boost v${BOOST_VERSION} package not found in the system. Falling back to FetchContent.\")\n    include(FetchContent)\n\n    if (CMAKE_VERSION VERSION_GREATER_EQUAL \"3.24.0\")\n        cmake_policy(SET CMP0135 NEW)  # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24\n    endif()\n    if (CMAKE_VERSION VERSION_GREATER_EQUAL \"3.31.0\")\n        cmake_policy(SET CMP0174 NEW)  # Handle empty variables\n    endif()\n\n    # more components required for compiling boost targets\n    list(APPEND BOOST_COMPONENTS\n            asio\n            crc\n            format\n            process\n            property_tree)\n\n    set(BOOST_ENABLE_CMAKE ON)\n\n    # Limit boost to the required libraries only\n    set(BOOST_INCLUDE_LIBRARIES ${BOOST_COMPONENTS})\n    set(BOOST_URL \"https://github.com/boostorg/boost/releases/download/boost-${BOOST_VERSION}/boost-${BOOST_VERSION}-cmake.tar.xz\")  # cmake-lint: disable=C0301\n    set(BOOST_HASH \"SHA256=67acec02d0d118b5de9eb441f5fb707b3a1cdd884be00ca24b9a73c995511f74\")\n\n    if(CMAKE_VERSION VERSION_LESS \"3.24.0\")\n        FetchContent_Declare(\n                Boost\n                URL ${BOOST_URL}\n                URL_HASH ${BOOST_HASH}\n        )\n    elseif(APPLE AND CMAKE_VERSION VERSION_GREATER_EQUAL \"3.25.0\")\n        # add SYSTEM to FetchContent_Declare, this fails on debian bookworm\n        FetchContent_Declare(\n                Boost\n                URL ${BOOST_URL}\n                URL_HASH ${BOOST_HASH}\n                SYSTEM  # requires CMake 3.25+\n                OVERRIDE_FIND_PACKAGE  # requires CMake 3.24+, but we have a macro to handle it for other versions\n        )\n    elseif(CMAKE_VERSION VERSION_GREATER_EQUAL \"3.24.0\")\n        FetchContent_Declare(\n                Boost\n                URL ${BOOST_URL}\n                URL_HASH ${BOOST_HASH}\n                OVERRIDE_FIND_PACKAGE  # requires CMake 3.24+, but we have a macro to handle it for other versions\n        )\n    endif()\n\n    FetchContent_MakeAvailable(Boost)\n    set(FETCH_CONTENT_BOOST_USED TRUE)\n\n    add_definitions(-DBOOST_SYSTEM_USE_UTF8)\n    message(STATUS \"Boost.System UTF-8 support enabled: BOOST_SYSTEM_USE_UTF8\")\n\n    set(Boost_FOUND TRUE)  # cmake-lint: disable=C0103\n    set(Boost_INCLUDE_DIRS  # cmake-lint: disable=C0103\n            \"$<BUILD_INTERFACE:${Boost_SOURCE_DIR}/libs/headers/include>\")\n\n    if(WIN32)\n        # Windows build is failing to create .h file in this directory\n        file(MAKE_DIRECTORY ${Boost_BINARY_DIR}/libs/log/src/windows)\n    endif()\n\n    set(Boost_LIBRARIES \"\")  # cmake-lint: disable=C0103\n    foreach(component ${BOOST_COMPONENTS})\n        list(APPEND Boost_LIBRARIES \"Boost::${component}\")\n    endforeach()\nendif()\n\nmessage(STATUS \"Boost include dirs: ${Boost_INCLUDE_DIRS}\")\nmessage(STATUS \"Boost libraries: ${Boost_LIBRARIES}\")\n"
  },
  {
    "path": "cmake/dependencies/common.cmake",
    "content": "# load common dependencies\n# this file will also load platform specific dependencies\n\n# boost, this should be before Simple-Web-Server as it also depends on boost\ninclude(dependencies/Boost_Sunshine)\n\n# submodules\n# moonlight common library\nset(ENET_NO_INSTALL ON CACHE BOOL \"Don't install any libraries built for enet\")\nadd_subdirectory(\"${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet\")\n\n# web server\nadd_subdirectory(\"${CMAKE_SOURCE_DIR}/third-party/Simple-Web-Server\")\n\n# common dependencies\ninclude(\"${CMAKE_MODULE_PATH}/dependencies/nlohmann_json.cmake\")\nfind_package(OpenSSL REQUIRED)\nfind_package(PkgConfig REQUIRED)\nfind_package(Threads REQUIRED)\npkg_check_modules(CURL REQUIRED libcurl)\n\n# miniupnp\npkg_check_modules(MINIUPNP miniupnpc REQUIRED)\ninclude_directories(SYSTEM ${MINIUPNP_INCLUDE_DIRS})\n\n# ffmpeg pre-compiled binaries\nif(WIN32)\n    set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl MinHook)\nelseif(UNIX AND NOT APPLE)\n    set(FFMPEG_PLATFORM_LIBRARIES numa va va-drm va-x11 X11)\nendif()\n\nif(NOT DEFINED FFMPEG_PREPARED_BINARIES)\n    set(FFMPEG_PREPARED_BINARIES\n            \"${CMAKE_SOURCE_DIR}/third-party/build-deps/dist/${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}\")\n\n    # check if the directory exists\n    if(NOT EXISTS \"${FFMPEG_PREPARED_BINARIES}\")\n        message(FATAL_ERROR\n                \"FFmpeg pre-compiled binaries not found at ${FFMPEG_PREPARED_BINARIES}. \\\n                Please consider contributing to the LizardByte/build-deps repository. \\\n                Optionally, you can use the FFMPEG_PREPARED_BINARIES option to specify the path to the \\\n                system-installed FFmpeg libraries\")\n    endif()\n\n    if(EXISTS \"${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a\")\n        set(HDR10_PLUS_LIBRARY\n                \"${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a\")\n    endif()\n    set(FFMPEG_LIBRARIES\n            \"${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a\"\n            \"${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a\"\n            \"${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a\"\n            \"${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a\"\n            \"${FFMPEG_PREPARED_BINARIES}/lib/libSvtAv1Enc.a\"\n            \"${FFMPEG_PREPARED_BINARIES}/lib/libx264.a\"\n            \"${FFMPEG_PREPARED_BINARIES}/lib/libx265.a\"\n            ${HDR10_PLUS_LIBRARY}\n            ${FFMPEG_PLATFORM_LIBRARIES})\nelse()\n    if(EXISTS \"${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a\")\n        set(HDR10_PLUS_LIBRARY\n                \"${FFMPEG_PREPARED_BINARIES}/lib/libhdr10plus.a\")\n    endif()\n    set(FFMPEG_LIBRARIES\n        \"${FFMPEG_PREPARED_BINARIES}/lib/libavcodec.a\"\n        \"${FFMPEG_PREPARED_BINARIES}/lib/libswscale.a\"\n        \"${FFMPEG_PREPARED_BINARIES}/lib/libavutil.a\"\n        \"${FFMPEG_PREPARED_BINARIES}/lib/libcbs.a\"\n        \"${FFMPEG_PREPARED_BINARIES}/lib/libSvtAv1Enc.a\"\n        \"${FFMPEG_PREPARED_BINARIES}/lib/libx264.a\"\n        \"${FFMPEG_PREPARED_BINARIES}/lib/libx265.a\"\n        ${HDR10_PLUS_LIBRARY}\n        ${FFMPEG_PLATFORM_LIBRARIES})\nendif()\n\nset(FFMPEG_INCLUDE_DIRS\n        \"${FFMPEG_PREPARED_BINARIES}/include\")\n\n# platform specific dependencies\nif(WIN32)\n    include(\"${CMAKE_MODULE_PATH}/dependencies/windows.cmake\")\nelseif(UNIX)\n    include(\"${CMAKE_MODULE_PATH}/dependencies/unix.cmake\")\n\n    if(APPLE)\n        include(\"${CMAKE_MODULE_PATH}/dependencies/macos.cmake\")\n    else()\n        include(\"${CMAKE_MODULE_PATH}/dependencies/linux.cmake\")\n    endif()\nendif()"
  },
  {
    "path": "cmake/dependencies/libevdev_Sunshine.cmake",
    "content": "#\n# Loads the libevdev library giving the priority to the system package first, with a fallback to ExternalProject\n#\ninclude_guard(GLOBAL)\n\nset(LIBEVDEV_VERSION libevdev-1.13.2)\n\npkg_check_modules(PC_EVDEV libevdev)\nif(PC_EVDEV_FOUND)\n    find_path(EVDEV_INCLUDE_DIR libevdev/libevdev.h\n            HINTS ${PC_EVDEV_INCLUDE_DIRS} ${PC_EVDEV_INCLUDEDIR})\n    find_library(EVDEV_LIBRARY\n            NAMES evdev libevdev)\nelse()\n    include(ExternalProject)\n\n    ExternalProject_Add(libevdev\n            URL http://www.freedesktop.org/software/libevdev/${LIBEVDEV_VERSION}.tar.xz\n            PREFIX ${LIBEVDEV_VERSION}\n            CONFIGURE_COMMAND <SOURCE_DIR>/configure --prefix=<INSTALL_DIR>\n            BUILD_COMMAND \"make\"\n            INSTALL_COMMAND \"\"\n    )\n\n    ExternalProject_Get_Property(libevdev SOURCE_DIR)\n    message(STATUS \"libevdev source dir: ${SOURCE_DIR}\")\n    set(EVDEV_INCLUDE_DIR \"${SOURCE_DIR}\")\n\n    ExternalProject_Get_Property(libevdev BINARY_DIR)\n    message(STATUS \"libevdev binary dir: ${BINARY_DIR}\")\n    set(EVDEV_LIBRARY \"${BINARY_DIR}/libevdev/.libs/libevdev.a\")\n\n    # compile libevdev before sunshine\n    set(SUNSHINE_TARGET_DEPENDENCIES ${SUNSHINE_TARGET_DEPENDENCIES} libevdev)\n\n    set(EXTERNAL_PROJECT_LIBEVDEV_USED TRUE)\nendif()\n\nif(EVDEV_INCLUDE_DIR AND EVDEV_LIBRARY)\n    message(STATUS \"Found libevdev library: ${EVDEV_LIBRARY}\")\n    message(STATUS \"Found libevdev include directory: ${EVDEV_INCLUDE_DIR}\")\n\n    include_directories(SYSTEM ${EVDEV_INCLUDE_DIR})\n    list(APPEND PLATFORM_LIBRARIES ${EVDEV_LIBRARY})\nelse()\n    message(FATAL_ERROR \"Couldn't find or fetch libevdev\")\nendif()\n"
  },
  {
    "path": "cmake/dependencies/linux.cmake",
    "content": "# linux specific dependencies\n"
  },
  {
    "path": "cmake/dependencies/macos.cmake",
    "content": "# macos specific dependencies\n\nFIND_LIBRARY(APP_KIT_LIBRARY AppKit)\nFIND_LIBRARY(APP_SERVICES_LIBRARY ApplicationServices)\nFIND_LIBRARY(AV_FOUNDATION_LIBRARY AVFoundation)\nFIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia)\nFIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo)\nFIND_LIBRARY(FOUNDATION_LIBRARY Foundation)\nFIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox)\n\nif(SUNSHINE_ENABLE_TRAY)\n    FIND_LIBRARY(COCOA Cocoa REQUIRED)\nendif()\n"
  },
  {
    "path": "cmake/dependencies/nlohmann_json.cmake",
    "content": "#\n# Loads the nlohmann_json library giving the priority to the system package first, with a fallback to FetchContent.\n#\ninclude_guard(GLOBAL)\n\nfind_package(nlohmann_json 3.11 QUIET GLOBAL)\nif(NOT nlohmann_json_FOUND)\n    message(STATUS \"nlohmann_json v3.11.x package not found in the system. Falling back to FetchContent.\")\n    include(FetchContent)\n\n    if (CMAKE_VERSION VERSION_GREATER_EQUAL \"3.24.0\")\n        cmake_policy(SET CMP0135 NEW)  # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24\n    endif()\n    if (CMAKE_VERSION VERSION_GREATER_EQUAL \"3.31.0\")\n        cmake_policy(SET CMP0174 NEW)  # Handle empty variables\n    endif()\n\n    FetchContent_Declare(\n            json\n            URL      https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz\n            URL_HASH MD5=c23a33f04786d85c29fda8d16b5f0efd\n            DOWNLOAD_EXTRACT_TIMESTAMP\n    )\n    FetchContent_MakeAvailable(json)\nendif()\n"
  },
  {
    "path": "cmake/dependencies/unix.cmake",
    "content": "# unix specific dependencies\n# put anything here that applies to both linux and macos\n"
  },
  {
    "path": "cmake/dependencies/windows.cmake",
    "content": "# windows specific dependencies\n\n# nlohmann_json\nfind_package(nlohmann_json CONFIG 3.11 REQUIRED)\n\n# Make sure MinHook is installed\nfind_library(MINHOOK_LIBRARY libMinHook.a REQUIRED)\nfind_path(MINHOOK_INCLUDE_DIR MinHook.h PATH_SUFFIXES include REQUIRED)\n\nadd_library(minhook::minhook STATIC IMPORTED)\nset_property(TARGET minhook::minhook PROPERTY IMPORTED_LOCATION ${MINHOOK_LIBRARY})\ntarget_include_directories(minhook::minhook INTERFACE ${MINHOOK_INCLUDE_DIR})\n"
  },
  {
    "path": "cmake/macros/common.cmake",
    "content": "# common macros\n# this file will also load platform specific macros\n\n# platform specific macros\nif(WIN32)\n    include(${CMAKE_MODULE_PATH}/macros/windows.cmake)\nelseif(UNIX)\n    include(${CMAKE_MODULE_PATH}/macros/unix.cmake)\n\n    if(APPLE)\n        include(${CMAKE_MODULE_PATH}/macros/macos.cmake)\n    else()\n        include(${CMAKE_MODULE_PATH}/macros/linux.cmake)\n    endif()\nendif()\n\n# override find_package function\nmacro(find_package)  # cmake-lint: disable=C0103\n    string(TOLOWER \"${ARGV0}\" ARGV0_LOWER)\n    if(\n        ((\"${ARGV0_LOWER}\" STREQUAL \"boost\") AND DEFINED FETCH_CONTENT_BOOST_USED) OR\n        ((\"${ARGV0_LOWER}\" STREQUAL \"libevdev\") AND DEFINED EXTERNAL_PROJECT_LIBEVDEV_USED)\n    )\n        # Do nothing, as the package has already been fetched\n    else()\n        # Call the original find_package function\n        _find_package(${ARGV})\n    endif()\nendmacro()\n"
  },
  {
    "path": "cmake/macros/linux.cmake",
    "content": "# linux specific macros\n\n# GEN_WAYLAND: args = `filename`\nmacro(GEN_WAYLAND wayland_directory subdirectory filename)\n    file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated-src)\n\n    message(\"wayland-scanner private-code \\\n${wayland_directory}/${subdirectory}/${filename}.xml \\\n${CMAKE_BINARY_DIR}/generated-src/${filename}.c\")\n    message(\"wayland-scanner client-header \\\n${wayland_directory}/${subdirectory}/${filename}.xml \\\n${CMAKE_BINARY_DIR}/generated-src/${filename}.h\")\n    execute_process(\n            COMMAND wayland-scanner private-code\n            ${wayland_directory}/${subdirectory}/${filename}.xml\n            ${CMAKE_BINARY_DIR}/generated-src/${filename}.c\n            COMMAND wayland-scanner client-header\n            ${wayland_directory}/${subdirectory}/${filename}.xml\n            ${CMAKE_BINARY_DIR}/generated-src/${filename}.h\n\n            RESULT_VARIABLE EXIT_INT\n    )\n\n    if(NOT ${EXIT_INT} EQUAL 0)\n        message(FATAL_ERROR \"wayland-scanner failed\")\n    endif()\n\n    list(APPEND PLATFORM_TARGET_FILES\n            ${CMAKE_BINARY_DIR}/generated-src/${filename}.c\n            ${CMAKE_BINARY_DIR}/generated-src/${filename}.h)\nendmacro()\n"
  },
  {
    "path": "cmake/macros/macos.cmake",
    "content": "# macos specific macros\n\n# ADD_FRAMEWORK: args = `fwname`, `appname`\nmacro(ADD_FRAMEWORK fwname appname)\n    find_library(FRAMEWORK_${fwname}\n            NAMES ${fwname}\n            PATHS ${CMAKE_OSX_SYSROOT}/System/Library\n            PATH_SUFFIXES Frameworks\n            NO_DEFAULT_PATH)\n    if( ${FRAMEWORK_${fwname}} STREQUAL FRAMEWORK_${fwname}-NOTFOUND)\n        MESSAGE(ERROR \": Framework ${fwname} not found\")\n    else()\n        TARGET_LINK_LIBRARIES(${appname} \"${FRAMEWORK_${fwname}}/${fwname}\")\n        MESSAGE(STATUS \"Framework ${fwname} found at ${FRAMEWORK_${fwname}}\")\n    endif()\nendmacro(ADD_FRAMEWORK)\n"
  },
  {
    "path": "cmake/macros/unix.cmake",
    "content": "# unix specific macros\n# put anything here that applies to both linux and macos\n"
  },
  {
    "path": "cmake/macros/windows.cmake",
    "content": "# windows specific macros\n"
  },
  {
    "path": "cmake/packaging/ChineseSimplified.isl",
    "content": "; *** Inno Setup version 6.5.0+ Chinese Simplified messages ***\n;\n; To download user-contributed translations of this file, go to:\n;   https://jrsoftware.org/files/istrans/\n;\n; Note: When translating this text, do not add periods (.) to the end of\n; messages that didn't have them already, because on those messages Inno\n; Setup adds the periods automatically (appending a period would result in\n; two periods being displayed).\n;\n; Maintained by Zhenghan Yang\n; Email: 847320916@QQ.com\n; Translation based on network resource\n; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation\n;\n\n[LangOptions]\n; The following three entries are very important. Be sure to read and \n; understand the '[LangOptions] section' topic in the help file.\nLanguageName=简体中文\n; If Language Name display incorrect, uncomment next line\n; LanguageName=<7B80><4F53><4E2D><6587>\n; About LanguageID, to reference link:\n; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c\nLanguageID=$0804\n; About CodePage, to reference link:\n; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers\nLanguageCodePage=936\n; If the language you are translating to requires special font faces or\n; sizes, uncomment any of the following entries and change them accordingly.\n;DialogFontName=\n;DialogFontSize=9\n;DialogFontBaseScaleWidth=7\n;DialogFontBaseScaleHeight=15\n;WelcomeFontName=Segoe UI\n;WelcomeFontSize=14\n\n[Messages]\n\n; *** 应用程序标题\nSetupAppTitle=安装\nSetupWindowTitle=安装 - %1\nUninstallAppTitle=卸载\nUninstallAppFullTitle=%1 卸载\n\n; *** Misc. common\nInformationTitle=信息\nConfirmTitle=确认\nErrorTitle=错误\n\n; *** SetupLdr messages\nSetupLdrStartupMessage=现在将安装 %1。您想要继续吗？\nLdrCannotCreateTemp=无法创建临时文件。安装程序已中止\nLdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止\nHelpTextNote=\n\n; *** 启动错误消息\nLastErrorMessage=%1。%n%n错误 %2: %3\nSetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。\nSetupFileCorrupt=安装文件已损坏。请获取程序的新副本。\nSetupFileCorruptOrWrongVer=安装文件已损坏，或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。\nInvalidParameter=无效的命令行参数：%n%n%1\nSetupAlreadyRunning=安装程序正在运行。\nWindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。\nWindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。\nNotOnThisPlatform=此程序不能在 %1 上运行。\nOnlyOnThisPlatform=此程序只能在 %1 上运行。\nOnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中：%n%n%1\nWinVersionTooLowError=此程序需要 %1 版本 %2 或更高。\nWinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。\nAdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。\nPowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。\nSetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序，然后点击“确定”继续，或点击“取消”退出。\nUninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序，然后点击“确定”继续，或点击“取消”退出。\n\n; *** 启动问题\nPrivilegesRequiredOverrideTitle=选择安装程序模式\nPrivilegesRequiredOverrideInstruction=选择安装模式\nPrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限)，或仅为您安装。\nPrivilegesRequiredOverrideText2=%1 可以仅为您安装，或为所有用户安装(需要管理员权限)。\nPrivilegesRequiredOverrideAllUsers=为所有用户安装(&A)\nPrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项)\nPrivilegesRequiredOverrideCurrentUser=仅为我安装(&M)\nPrivilegesRequiredOverrideCurrentUserRecommended=仅为我安装(&M) (建议选项)\n\n; *** 其他错误\nErrorCreatingDir=安装程序无法创建目录“%1”\nErrorTooManyFilesInDir=无法在目录“%1”中创建文件，因为里面包含太多文件\n\n; *** 安装程序公共消息\nExitSetupTitle=退出安装程序\nExitSetupMessage=安装程序尚未完成。如果现在退出，将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗？\nAboutSetupMenuItem=关于安装程序(&A)...\nAboutSetupTitle=关于安装程序\nAboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页：%n%4\nAboutSetupNote=\nTranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址：https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation\n\n; *** 按钮\nButtonBack=< 上一步(&B)\nButtonNext=下一步(&N) >\nButtonInstall=安装(&I)\nButtonOK=确定\nButtonCancel=取消\nButtonYes=是(&Y)\nButtonYesToAll=全是(&A)\nButtonNo=否(&N)\nButtonNoToAll=全否(&O)\nButtonFinish=完成(&F)\nButtonBrowse=浏览(&B)...\nButtonWizardBrowse=浏览(&R)...\nButtonNewFolder=新建文件夹(&M)\n\n; *** “选择语言”对话框消息\nSelectLanguageTitle=选择安装语言\nSelectLanguageLabel=选择安装时使用的语言。\n\n; *** 公共向导文字\nClickNext=点击“下一步”继续，或点击“取消”退出安装程序。\nBeveledLabel=\nBrowseDialogTitle=浏览文件夹\nBrowseDialogLabel=在下面的列表中选择一个文件夹，然后点击“确定”。\nNewFolderName=新建文件夹\n\n; *** “欢迎”向导页\nWelcomeLabel1=欢迎使用 [name] 安装向导\nWelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。\n\n; *** “密码”向导页\nWizardPassword=密码\nPasswordLabel1=这个安装程序有密码保护。\nPasswordLabel3=请输入密码，然后点击“下一步”继续。密码区分大小写。\nPasswordEditLabel=密码(&P)：\nIncorrectPassword=您输入的密码不正确，请重新输入。\n\n; *** “许可协议”向导页\nWizardLicense=许可协议\nLicenseLabel=请在继续安装前阅读以下重要信息。\nLicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。\nLicenseAccepted=我同意此协议(&A)\nLicenseNotAccepted=我不同意此协议(&D)\n\n; *** “信息”向导页\nWizardInfoBefore=信息\nInfoBeforeLabel=请在继续安装前阅读以下重要信息。\nInfoBeforeClickLabel=准备好继续安装后，点击“下一步”。\nWizardInfoAfter=信息\nInfoAfterLabel=请在继续安装前阅读以下重要信息。\nInfoAfterClickLabel=准备好继续安装后，点击“下一步”。\n\n; *** “用户信息”向导页\nWizardUserInfo=用户信息\nUserInfoDesc=请输入您的信息。\nUserInfoName=用户名(&U)：\nUserInfoOrg=组织(&O)：\nUserInfoSerial=序列号(&S)：\nUserInfoNameRequired=您必须输入用户名。\n\n; *** “选择目标目录”向导页\nWizardSelectDir=选择目标位置\nSelectDirDesc=您想将 [name] 安装在哪里？\nSelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。\nSelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹，点击“浏览”。\nDiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。\nDiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。\nCannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。\nCannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。\nInvalidPath=您必须输入一个带驱动器卷标的完整路径，例如：%n%nC:\\APP%n%n或UNC路径：%n%n\\\\server\\share\nInvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。\nDiskSpaceWarningTitle=磁盘空间不足\nDiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装，但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗？\nDirNameTooLong=文件夹名称或路径太长。\nInvalidDirName=文件夹名称无效。\nBadDirName32=文件夹名称不能包含下列任何字符：%n%n%1\nDirExistsTitle=文件夹已存在\nDirExists=文件夹：%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗？\nDirDoesntExistTitle=文件夹不存在\nDirDoesntExist=文件夹：%n%n%1%n%n不存在。您想要创建此文件夹吗？\n\n; *** “选择组件”向导页\nWizardSelectComponents=选择组件\nSelectComponentsDesc=您想安装哪些程序组件？\nSelectComponentsLabel2=选中您想安装的组件；取消您不想安装的组件。然后点击“下一步”继续。\nFullInstallation=完全安装\n; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)\nCompactInstallation=简洁安装\nCustomInstallation=自定义安装\nNoUninstallWarningTitle=组件已存在\nNoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中：%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗？\nComponentSize1=%1 KB\nComponentSize2=%1 MB\nComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。\nComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。\n\n; *** “选择附加任务”向导页\nWizardSelectTasks=选择附加任务\nSelectTasksDesc=您想要安装程序执行哪些附加任务？\nSelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务，然后点击“下一步”。\n\n; *** “选择开始菜单文件夹”向导页\nWizardSelectProgramGroup=选择开始菜单文件夹\nSelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式？\nSelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。\nSelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹，点击“浏览”。\nMustEnterGroupName=您必须输入一个文件夹名。\nGroupNameTooLong=文件夹名或路径太长。\nInvalidGroupName=无效的文件夹名字。\nBadGroupName=文件夹名不能包含下列任何字符：%n%n%1\nNoProgramGroupCheck2=不创建开始菜单文件夹(&D)\n\n; *** “准备安装”向导页\nWizardReady=准备安装\nReadyLabel1=安装程序准备就绪，现在可以开始安装 [name] 到您的电脑。\nReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置，点击“上一步”。\nReadyLabel2b=点击“安装”继续此安装程序。\nReadyMemoUserInfo=用户信息：\nReadyMemoDir=目标位置：\nReadyMemoType=安装类型：\nReadyMemoComponents=已选择组件：\nReadyMemoGroup=开始菜单文件夹：\nReadyMemoTasks=附加任务：\n\n; *** TExtractionWizardPage 向导页面与 ExtractArchive\nExtractingLabel=正在解压文件...\nButtonStopExtraction=停止解压(&S)\nStopExtraction=您确定要停止解压吗？\nErrorExtractionAborted=解压已中止\nErrorExtractionFailed=解压失败：%1\n\n; *** 压缩文件解压失败详情\nArchiveIncorrectPassword=压缩文件密码不正确\nArchiveIsCorrupted=压缩文件已损坏\nArchiveUnsupportedFormat=不支持的压缩文件格式\n\n; *** TDownloadWizardPage 向导页面和 DownloadTemporaryFile\nDownloadingLabel2=正在下载文件...\nButtonStopDownload=停止下载(&S)\nStopDownload=您确定要停止下载吗？\nErrorDownloadAborted=下载已中止\nErrorDownloadFailed=下载失败：%1 %2\nErrorDownloadSizeFailed=获取下载大小失败：%1 %2\nErrorProgress=无效的进度：%1 / %2\nErrorFileSize=文件大小错误：预期 %1，实际 %2\n\n; *** “正在准备安装”向导页\nWizardPreparing=正在准备安装\nPreparingDesc=安装程序正在准备安装 [name] 到您的电脑。\nPreviousInstallNotCompleted=先前的程序安装或卸载未完成，您需要重启您的电脑以完成。%n%n在重启电脑后，再次运行安装程序以完成 [name] 的安装。\nCannotContinue=安装程序不能继续。请点击“取消”退出。\nApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。\nApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后，安装程序将尝试重新启动这些应用程序。\nCloseApplications=自动关闭应用程序(&A)\nDontCloseApplications=不要关闭应用程序(&D)\nErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前，关闭所有在使用需要由安装程序更新的文件的应用程序。\nPrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后，请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动？\n\n; *** “正在安装”向导页\nWizardInstalling=正在安装\nInstallingLabel=安装程序正在安装 [name] 到您的电脑，请稍候。\n\n; *** “安装完成”向导页\nFinishedHeadingLabel=[name] 安装完成\nFinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。\nFinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。\nClickFinish=点击“完成”退出安装程序。\nFinishedRestartLabel=为完成 [name] 的安装，安装程序必须重新启动您的电脑。要立即重启吗？\nFinishedRestartMessage=为完成 [name] 的安装，安装程序必须重新启动您的电脑。%n%n要立即重启吗？\nShowReadmeCheck=是，我想查阅自述文件\nYesRadio=是，立即重启电脑(&Y)\nNoRadio=否，稍后重启电脑(&N)\n; used for example as 'Run MyProg.exe'\nRunEntryExec=运行 %1\n; used for example as 'View Readme.txt'\nRunEntryShellExec=查阅 %1\n\n; *** “安装程序需要下一张磁盘”提示\nChangeDiskTitle=安装程序需要下一张磁盘\nSelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到，请输入正确的路径或点击“浏览”。\nPathLabel=路径(&P)：\nFileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。\nSelectDirectoryLabel=请指定下一张磁盘的位置。\n\n; *** 安装阶段消息\nSetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。\nAbortRetryIgnoreSelectAction=选择操作\nAbortRetryIgnoreRetry=重试(&T)\nAbortRetryIgnoreIgnore=忽略错误并继续(&I)\nAbortRetryIgnoreCancel=关闭安装程序\nRetryCancelSelectAction=选择操作\nRetryCancelRetry=重试(&T)\nRetryCancelCancel=取消(&C)\n\n; *** 安装状态消息\nStatusClosingApplications=正在关闭应用程序...\nStatusCreateDirs=正在创建目录...\nStatusExtractFiles=正在提取文件...\nStatusDownloadFiles=正在下载文件...\nStatusCreateIcons=正在创建快捷方式...\nStatusCreateIniEntries=正在创建 INI 条目...\nStatusCreateRegistryEntries=正在创建注册表条目...\nStatusRegisterFiles=正在注册文件...\nStatusSavingUninstall=正在保存卸载信息...\nStatusRunProgram=正在完成安装...\nStatusRestartingApplications=正在重启应用程序...\nStatusRollback=正在撤销更改...\n\n; *** 其他错误\nErrorInternal2=内部错误：%1\nErrorFunctionFailedNoCode=%1 失败\nErrorFunctionFailed=%1 失败；错误代码 %2\nErrorFunctionFailedWithMessage=%1 失败；错误代码 %2.%n%3\nErrorExecutingProgram=无法执行文件：%n%1\n\n; *** 注册表错误\nErrorRegOpenKey=打开注册表项时出错：%n%1\\%2\nErrorRegCreateKey=创建注册表项时出错：%n%1\\%2\nErrorRegWriteKey=写入注册表项时出错：%n%1\\%2\n\n; *** INI 错误\nErrorIniEntry=在文件“%1”中创建 INI 条目时出错。\n\n; *** 文件复制错误\nFileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐)\nFileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐)\nSourceIsCorrupted=源文件已损坏\nSourceDoesntExist=源文件“%1”不存在\nSourceVerificationFailed=源文件验证失败: %1\nVerificationSignatureDoesntExist=签名文件“%1”不存在\nVerificationSignatureInvalid=签名文件“%1”无效\nVerificationKeyNotFound=签名文件“%1”使用了未知密钥\nVerificationFileNameIncorrect=文件名不正确\nVerificationFileTagIncorrect=文件标签不正确\nVerificationFileSizeIncorrect=文件大小不正确\nVerificationFileHashIncorrect=文件哈希值不正确\nExistingFileReadOnly2=无法替换现有文件，它是只读的。\nExistingFileReadOnlyRetry=移除只读属性并重试(&R)\nExistingFileReadOnlyKeepExisting=保留现有文件(&K)\nErrorReadingExistingDest=尝试读取现有文件时出错：\nFileExistsSelectAction=选择操作\nFileExists2=文件已经存在。\nFileExistsOverwriteExisting=覆盖已存在的文件(&O)\nFileExistsKeepExisting=保留现有的文件(&K)\nFileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)\nExistingFileNewerSelectAction=选择操作\nExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。\nExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O)\nExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐)\nExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)\nErrorChangingAttr=尝试更改下列现有文件的属性时出错：\nErrorCreatingTemp=尝试在目标目录创建文件时出错：\nErrorReadingSource=尝试读取下列源文件时出错：\nErrorCopying=尝试复制下列文件时出错：\nErrorDownloading=下载文件时出错：\nErrorExtracting=解压压缩文件时出错：\nErrorReplacingExistingFile=尝试替换现有文件时出错：\nErrorRestartReplace=重启并替换失败：\nErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错：\nErrorRegisterServer=无法注册 DLL/OCX：%1\nErrorRegSvr32Failed=RegSvr32 失败；退出代码 %1\nErrorRegisterTypeLib=无法注册类库：%1\n\n; *** 卸载显示名字标记\n; used for example as 'My Program (32-bit)'\nUninstallDisplayNameMark=%1 (%2)\n; used for example as 'My Program (32-bit, All users)'\nUninstallDisplayNameMarks=%1 (%2, %3)\nUninstallDisplayNameMark32Bit=32 位\nUninstallDisplayNameMark64Bit=64 位\nUninstallDisplayNameMarkAllUsers=所有用户\nUninstallDisplayNameMarkCurrentUser=当前用户\n\n; *** 安装后错误\nErrorOpeningReadme=尝试打开自述文件时出错。\nErrorRestartingComputer=安装程序无法重启电脑，请手动重启。\n\n; *** 卸载消息\nUninstallNotFound=文件“%1”不存在。无法卸载。\nUninstallOpenError=文件“%1”不能被打开。无法卸载。\nUninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载\nUninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1)\nConfirmUninstall=您确认要完全移除 %1 及其所有组件吗？\nUninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。\nOnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。\nUninstallStatusLabel=正在从您的电脑中移除 %1，请稍候。\nUninstalledAll=已顺利从您的电脑中移除 %1。\nUninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除，但您可以手动删除它们。\nUninstalledAndNeedsRestart=为完成 %1 的卸载，需要重启您的电脑。%n%n立即重启电脑吗？\nUninstallDataCorrupted=文件“%1”已损坏。无法卸载\n\n; *** 卸载状态消息\nConfirmDeleteSharedFileTitle=删除共享的文件吗？\nConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗？%n%n如果删除这些文件，但仍有程序在使用这些文件，则这些程序可能出现异常。如果您不能确定，请选择“否”，在系统中保留这些文件以免引发问题。\nSharedFileNameLabel=文件名：\nSharedFileLocationLabel=位置：\nWizardUninstalling=卸载状态\nStatusUninstalling=正在卸载 %1...\n\n; *** Shutdown block reasons\nShutdownBlockReasonInstallingApp=正在安装 %1。\nShutdownBlockReasonUninstallingApp=正在卸载 %1。\n\n; The custom messages below aren't used by Setup itself, but if you make\n; use of them in your scripts, you'll want to translate them.\n\n[CustomMessages]\n\nNameAndVersion=%1 版本 %2\nAdditionalIcons=附加快捷方式：\nCreateDesktopIcon=创建桌面快捷方式(&D)\nCreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q)\nProgramOnTheWeb=%1 网站\nUninstallProgram=卸载 %1\nLaunchProgram=运行 %1\nAssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A)\nAssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联...\nAutoStartProgramGroupDescription=启动：\nAutoStartProgram=自动启动 %1\nAddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗？\n"
  },
  {
    "path": "cmake/packaging/FetchDriverDeps.cmake",
    "content": "# FetchDriverDeps.cmake — Download driver dependencies from GitHub Releases\n#\n# Downloads pre-built signed driver binaries at configure time.\n# All downloads are cached in ${CMAKE_BINARY_DIR}/_driver_deps/.\n#\n# Configuration (CMake cache variables):\n#   FETCH_DRIVER_DEPS       — Enable/disable downloads (default: ON)\n#   VMOUSE_DRIVER_VERSION   — ZakoVirtualMouse release tag (e.g. v1.1.0)\n#   VDD_DRIVER_VERSION      — ZakoVDD release tag (e.g. v0.1.4)\n#   NEFCON_VERSION          — nefcon release tag (e.g. v1.10.0)\n#   GITHUB_TOKEN            — Token for private repos (or set env GITHUB_TOKEN)\n#\n# Output variables (CACHE FORCE, available to parent):\n#   VMOUSE_DRIVER_DIR       — Directory containing vmouse driver files\n#   VDD_DRIVER_DIR          — Directory containing VDD driver files\n#   NEFCON_DRIVER_DIR       — Directory containing nefconw.exe\n\ninclude_guard(GLOBAL)\n\nif(NOT WIN32)\n  return()\nendif()\n\noption(FETCH_DRIVER_DEPS \"Download driver dependencies from GitHub Releases\" ON)\n\n# Version pins\nset(VMOUSE_DRIVER_VERSION \"v1.2.0\" CACHE STRING \"ZakoVirtualMouse driver version tag\")\nset(VDD_DRIVER_VERSION \"v0.14\" CACHE STRING \"ZakoVDD driver version tag\")\nset(NEFCON_VERSION \"v1.10.0\" CACHE STRING \"nefcon version tag\")\n\n# Repositories\nset(_VMOUSE_REPO \"AlkaidLab/ZakoVirtualMouse\")\nset(_VDD_REPO \"qiin2333/zako-vdd\")\nset(_NEFCON_REPO \"nefarius/nefcon\")\n\n# Output directories\nset(DRIVER_DEPS_CACHE \"${CMAKE_BINARY_DIR}/_driver_deps\" CACHE PATH \"Driver dependencies cache\")\nset(VMOUSE_DRIVER_DIR \"${DRIVER_DEPS_CACHE}/vmouse\" CACHE PATH \"\" FORCE)\nset(VDD_DRIVER_DIR \"${DRIVER_DEPS_CACHE}/vdd\" CACHE PATH \"\" FORCE)\nset(NEFCON_DRIVER_DIR \"${DRIVER_DEPS_CACHE}/nefcon\" CACHE PATH \"\" FORCE)\n\nif(NOT FETCH_DRIVER_DEPS)\n  message(STATUS \"Driver dependency downloads disabled (FETCH_DRIVER_DEPS=OFF)\")\n  return()\nendif()\n\n# GitHub token for private repos\nif(NOT GITHUB_TOKEN AND DEFINED ENV{GITHUB_TOKEN})\n  set(GITHUB_TOKEN \"$ENV{GITHUB_TOKEN}\")\nendif()\n\n# ---------------------------------------------------------------------------\n# Helper: download a single file (skip if already cached)\n# Uses curl for authenticated requests to handle GitHub's 302 redirects\n# properly (CMake file(DOWNLOAD) doesn't forward auth headers on redirect).\n# ---------------------------------------------------------------------------\nfunction(_driver_download url output_path)\n  if(EXISTS \"${output_path}\")\n    return()\n  endif()\n\n  get_filename_component(_dir \"${output_path}\" DIRECTORY)\n  file(MAKE_DIRECTORY \"${_dir}\")\n\n  message(STATUS \"  Downloading: ${url}\")\n\n  if(GITHUB_TOKEN)\n    # Use curl to handle GitHub's 302 redirects for private repo assets.\n    # CMake's file(DOWNLOAD) won't send auth headers after redirect to S3.\n    find_program(_CURL curl REQUIRED)\n    execute_process(\n      COMMAND \"${_CURL}\" -fsSL\n        -H \"Authorization: token ${GITHUB_TOKEN}\"\n        -H \"Accept: application/octet-stream\"\n        -o \"${output_path}\"\n        \"${url}\"\n      RESULT_VARIABLE _code\n      ERROR_VARIABLE _err)\n    if(NOT _code EQUAL 0)\n      message(WARNING \"  curl download failed (${_code}): ${_err}\")\n      file(REMOVE \"${output_path}\")\n      return()\n    endif()\n  else()\n    file(DOWNLOAD \"${url}\" \"${output_path}\"\n      STATUS _status\n      TLS_VERIFY ON)\n    list(GET _status 0 _code)\n    if(NOT _code EQUAL 0)\n      list(GET _status 1 _msg)\n      message(WARNING \"  Download failed (${_code}): ${_msg}\")\n      file(REMOVE \"${output_path}\")\n      return()\n    endif()\n  endif()\n\n  if(EXISTS \"${output_path}\")\n    file(SIZE \"${output_path}\" _size)\n    if(_size EQUAL 0)\n      message(WARNING \"  Downloaded file is empty: ${output_path}\")\n      file(REMOVE \"${output_path}\")\n    endif()\n  endif()\nendfunction()\n\n# ---------------------------------------------------------------------------\n# ZakoVirtualMouse  (private repo — use GitHub API for authenticated downloads)\n# For private repos, browser_download_url returns 302→S3 which rejects\n# forwarded auth headers. We must use the GitHub REST API asset endpoint\n# with Accept: application/octet-stream.\n# ---------------------------------------------------------------------------\nfunction(_fetch_vmouse)\n  message(STATUS \"Fetching ZakoVirtualMouse ${VMOUSE_DRIVER_VERSION} ...\")\n\n  set(_files ZakoVirtualMouse.dll ZakoVirtualMouse.inf ZakoVirtualMouse.cat ZakoVirtualMouse.cer)\n\n  # Check if all files already cached\n  set(_all_cached TRUE)\n  foreach(_f ${_files})\n    if(NOT EXISTS \"${VMOUSE_DRIVER_DIR}/${_f}\")\n      set(_all_cached FALSE)\n      break()\n    endif()\n  endforeach()\n  if(_all_cached)\n    message(STATUS \"  All vmouse files already cached\")\n    return()\n  endif()\n\n  if(NOT GITHUB_TOKEN)\n    message(WARNING \"  GITHUB_TOKEN required for private repo ${_VMOUSE_REPO}\")\n    return()\n  endif()\n\n  find_program(_CURL curl REQUIRED)\n  file(MAKE_DIRECTORY \"${VMOUSE_DRIVER_DIR}\")\n\n  # Query release assets via GitHub API\n  set(_api_url \"https://api.github.com/repos/${_VMOUSE_REPO}/releases/tags/${VMOUSE_DRIVER_VERSION}\")\n  set(_json \"${DRIVER_DEPS_CACHE}/_vmouse_release.json\")\n  execute_process(\n    COMMAND \"${_CURL}\" -fsSL\n      -H \"Authorization: token ${GITHUB_TOKEN}\"\n      -H \"Accept: application/vnd.github+json\"\n      -o \"${_json}\"\n      \"${_api_url}\"\n    RESULT_VARIABLE _rc\n    ERROR_VARIABLE _err)\n  if(NOT _rc EQUAL 0)\n    message(WARNING \"  Failed to query vmouse release API (${_rc}): ${_err}\")\n    return()\n  endif()\n\n  # For each required file, find its asset id and download via API\n  foreach(_f ${_files})\n    if(EXISTS \"${VMOUSE_DRIVER_DIR}/${_f}\")\n      continue()\n    endif()\n\n    # Extract asset download URL from JSON using regex\n    # The API JSON contains entries like:\n    #   \"name\": \"ZakoVirtualMouse.dll\", ... \"url\": \"https://api.github.com/repos/.../assets/12345\"\n    file(READ \"${_json}\" _json_content)\n\n    # Find block for this asset: locate \"name\": \"<filename>\" then extract nearest \"url\"\n    # We use string(REGEX) to find the asset API url\n    string(REGEX MATCH \"\\\"url\\\"[^}]*\\\"name\\\":[ ]*\\\"${_f}\\\"\" _match_after \"${_json_content}\")\n    string(REGEX MATCH \"\\\"name\\\":[ ]*\\\"${_f}\\\"[^}]*\\\"url\\\"\" _match_before \"${_json_content}\")\n\n    set(_asset_api_url \"\")\n    # Try to extract the url from the assets array\n    # GitHub API returns assets like: { \"url\": \"https://api.github.com/repos/.../assets/ID\", ... \"name\": \"file\" }\n    string(REGEX MATCH \"\\\"url\\\":[ ]*\\\"(https://api\\\\.github\\\\.com/repos/[^\\\"]+/assets/[0-9]+)\\\"[^}]*\\\"name\\\":[ ]*\\\"${_f}\\\"\" _m \"${_json_content}\")\n    if(_m)\n      set(_asset_api_url \"${CMAKE_MATCH_1}\")\n    endif()\n\n    if(NOT _asset_api_url)\n      message(WARNING \"  Could not find asset URL for ${_f} in release JSON\")\n      continue()\n    endif()\n\n    message(STATUS \"  Downloading ${_f} via API: ${_asset_api_url}\")\n    execute_process(\n      COMMAND \"${_CURL}\" -fsSL\n        -H \"Authorization: token ${GITHUB_TOKEN}\"\n        -H \"Accept: application/octet-stream\"\n        -o \"${VMOUSE_DRIVER_DIR}/${_f}\"\n        \"${_asset_api_url}\"\n      RESULT_VARIABLE _rc\n      ERROR_VARIABLE _err)\n    if(NOT _rc EQUAL 0)\n      message(WARNING \"  Download failed for ${_f} (${_rc}): ${_err}\")\n      file(REMOVE \"${VMOUSE_DRIVER_DIR}/${_f}\")\n    endif()\n  endforeach()\n\n  file(REMOVE \"${_json}\")\nendfunction()\n\n# ---------------------------------------------------------------------------\n# ZakoVDD  (single zip release asset)\n# ---------------------------------------------------------------------------\nfunction(_fetch_vdd)\n  message(STATUS \"Fetching ZakoVDD ${VDD_DRIVER_VERSION} ...\")\n  set(_zip_url \"https://github.com/${_VDD_REPO}/releases/download/${VDD_DRIVER_VERSION}/zakovdd.zip\")\n  set(_zip \"${DRIVER_DEPS_CACHE}/zakovdd-${VDD_DRIVER_VERSION}.zip\")\n\n  _driver_download(\"${_zip_url}\" \"${_zip}\")\n\n  if(EXISTS \"${_zip}\" AND NOT EXISTS \"${VDD_DRIVER_DIR}/ZakoVDD.dll\")\n    file(MAKE_DIRECTORY \"${VDD_DRIVER_DIR}\")\n    file(ARCHIVE_EXTRACT INPUT \"${_zip}\" DESTINATION \"${VDD_DRIVER_DIR}\")\n    message(STATUS \"  Extracted VDD driver to ${VDD_DRIVER_DIR}\")\n  endif()\nendfunction()\n\n# ---------------------------------------------------------------------------\n# nefcon  (zip with architecture subdirectories)\n# ---------------------------------------------------------------------------\nfunction(_fetch_nefcon)\n  message(STATUS \"Fetching nefcon ${NEFCON_VERSION} ...\")\n  set(_zip_url \"https://github.com/${_NEFCON_REPO}/releases/download/${NEFCON_VERSION}/nefcon_${NEFCON_VERSION}.zip\")\n  set(_zip \"${DRIVER_DEPS_CACHE}/nefcon-${NEFCON_VERSION}.zip\")\n\n  _driver_download(\"${_zip_url}\" \"${_zip}\")\n\n  if(EXISTS \"${_zip}\" AND NOT EXISTS \"${NEFCON_DRIVER_DIR}/nefconw.exe\")\n    set(_tmp \"${DRIVER_DEPS_CACHE}/_nefcon_extract\")\n    file(ARCHIVE_EXTRACT INPUT \"${_zip}\" DESTINATION \"${_tmp}\")\n    file(MAKE_DIRECTORY \"${NEFCON_DRIVER_DIR}\")\n    file(COPY_FILE \"${_tmp}/x64/nefconw.exe\" \"${NEFCON_DRIVER_DIR}/nefconw.exe\")\n    file(REMOVE_RECURSE \"${_tmp}\")\n    message(STATUS \"  Extracted nefconw.exe (x64) to ${NEFCON_DRIVER_DIR}\")\n  endif()\nendfunction()\n\n# ---------------------------------------------------------------------------\n# Execute all fetches\n# ---------------------------------------------------------------------------\n_fetch_vmouse()\n_fetch_vdd()\n_fetch_nefcon()\n\n# ---------------------------------------------------------------------------\n# Verify critical files\n# ---------------------------------------------------------------------------\nset(_missing)\nforeach(_f\n    \"${VMOUSE_DRIVER_DIR}/ZakoVirtualMouse.dll\"\n    \"${VDD_DRIVER_DIR}/ZakoVDD.dll\"\n    \"${NEFCON_DRIVER_DIR}/nefconw.exe\")\n  if(NOT EXISTS \"${_f}\")\n    list(APPEND _missing \"${_f}\")\n  endif()\nendforeach()\n\nif(_missing)\n  string(REPLACE \";\" \"\\n  \" _list \"${_missing}\")\n  message(FATAL_ERROR\n    \"Missing driver dependencies:\\n  ${_list}\\n\"\n    \"For private repos, set -DGITHUB_TOKEN=<token> or env GITHUB_TOKEN.\\n\"\n    \"To skip downloads: -DFETCH_DRIVER_DEPS=OFF (provide files manually in ${DRIVER_DEPS_CACHE}).\")\nendif()\n"
  },
  {
    "path": "cmake/packaging/FetchGUI.cmake",
    "content": "# FetchGUI.cmake — Download pre-built Sunshine GUI from GitHub Releases\n#\n# Downloads the Tauri-based GUI binary at configure time instead of building\n# it from source. This removes the Rust/Cargo/Node.js dependency from the\n# main build.\n#\n# Configuration (CMake cache variables):\n#   FETCH_GUI             — Enable/disable GUI download (default: ON)\n#   GUI_VERSION           — Release tag to download (e.g. v0.2.9)\n#   GUI_REPO              — GitHub repo (default: qiin2333/sunshine-control-panel)\n#\n# Output variables (CACHE FORCE):\n#   GUI_DIR               — Directory containing sunshine-gui.exe\n\ninclude_guard(GLOBAL)\n\nif(NOT WIN32)\n  return()\nendif()\n\noption(FETCH_GUI \"Download pre-built GUI from GitHub Releases\" ON)\n\nset(GUI_VERSION \"latest\" CACHE STRING \"Sunshine GUI release tag (or 'latest')\")\nset(GUI_REPO \"qiin2333/sunshine-control-panel\" CACHE STRING \"GUI GitHub repository\")\n\nset(GUI_DIR \"${CMAKE_BINARY_DIR}/_gui\" CACHE PATH \"GUI binary directory\" FORCE)\n\nif(NOT FETCH_GUI)\n  message(STATUS \"GUI download disabled (FETCH_GUI=OFF)\")\n  return()\nendif()\n\n# Skip if already downloaded\nif(EXISTS \"${GUI_DIR}/sunshine-gui.exe\")\n  message(STATUS \"GUI binary already cached at ${GUI_DIR}\")\n  return()\nendif()\n\nfile(MAKE_DIRECTORY \"${GUI_DIR}\")\n\nfind_program(_CURL curl REQUIRED)\n\n# GitHub token (optional, for rate limits)\nif(NOT GITHUB_TOKEN AND DEFINED ENV{GITHUB_TOKEN})\n  set(GITHUB_TOKEN \"$ENV{GITHUB_TOKEN}\")\nendif()\n\n# Resolve release URL\nif(GUI_VERSION STREQUAL \"latest\")\n  set(_api_url \"https://api.github.com/repos/${GUI_REPO}/releases/latest\")\nelse()\n  set(_api_url \"https://api.github.com/repos/${GUI_REPO}/releases/tags/${GUI_VERSION}\")\nendif()\n\nmessage(STATUS \"Fetching Sunshine GUI ${GUI_VERSION} from ${GUI_REPO} ...\")\n\n# Build auth header args\nset(_auth_args)\nif(GITHUB_TOKEN)\n  set(_auth_args -H \"Authorization: token ${GITHUB_TOKEN}\")\nendif()\n\n# Query release to get sunshine-gui.exe asset URL\nset(_json \"${CMAKE_BINARY_DIR}/_gui_release.json\")\nexecute_process(\n  COMMAND \"${_CURL}\" -fsSL\n    ${_auth_args}\n    -H \"Accept: application/vnd.github+json\"\n    -o \"${_json}\"\n    \"${_api_url}\"\n  RESULT_VARIABLE _rc\n  ERROR_VARIABLE _err)\n\nif(NOT _rc EQUAL 0)\n  message(WARNING \"Failed to query GUI release API (${_rc}): ${_err}\")\n  message(WARNING \"GUI will not be available. Build it manually or set FETCH_GUI=OFF.\")\n  return()\nendif()\n\n# Parse asset download URLs from JSON\nfile(READ \"${_json}\" _json_content)\n\n# Extract sunshine-gui.exe browser_download_url\nstring(REGEX MATCH \"\\\"browser_download_url\\\"[^\\\"]*\\\"(https://[^\\\"]*sunshine-gui\\\\.exe)\\\"\" _m \"${_json_content}\")\nif(_m)\n  set(_gui_url \"${CMAKE_MATCH_1}\")\nelse()\n  # Try API asset URL for private repos\n  string(REGEX MATCH \"\\\"url\\\":[ ]*\\\"(https://api\\\\.github\\\\.com/repos/[^\\\"]+/assets/[0-9]+)\\\"[^}]*\\\"name\\\":[ ]*\\\"sunshine-gui\\\\.exe\\\"\" _m2 \"${_json_content}\")\n  if(_m2)\n    set(_gui_api_url \"${CMAKE_MATCH_1}\")\n  else()\n    message(WARNING \"Could not find sunshine-gui.exe in release assets\")\n    file(REMOVE \"${_json}\")\n    return()\n  endif()\nendif()\n\n# Download sunshine-gui.exe\nif(_gui_url)\n  message(STATUS \"  Downloading sunshine-gui.exe ...\")\n  execute_process(\n    COMMAND \"${_CURL}\" -fsSL\n      ${_auth_args}\n      -o \"${GUI_DIR}/sunshine-gui.exe\"\n      -L \"${_gui_url}\"\n    RESULT_VARIABLE _rc\n    ERROR_VARIABLE _err)\nelseif(_gui_api_url)\n  message(STATUS \"  Downloading sunshine-gui.exe via API ...\")\n  execute_process(\n    COMMAND \"${_CURL}\" -fsSL\n      ${_auth_args}\n      -H \"Accept: application/octet-stream\"\n      -o \"${GUI_DIR}/sunshine-gui.exe\"\n      \"${_gui_api_url}\"\n    RESULT_VARIABLE _rc\n    ERROR_VARIABLE _err)\nendif()\n\nif(NOT _rc EQUAL 0)\n  message(WARNING \"  Download failed (${_rc}): ${_err}\")\n  file(REMOVE \"${GUI_DIR}/sunshine-gui.exe\")\nendif()\n\n# Try downloading WebView2Loader.dll (optional, Tauri 2 may embed it)\nstring(REGEX MATCH \"\\\"browser_download_url\\\"[^\\\"]*\\\"(https://[^\\\"]*WebView2Loader\\\\.dll)\\\"\" _wv \"${_json_content}\")\nif(_wv)\n  set(_wv_url \"${CMAKE_MATCH_1}\")\n  message(STATUS \"  Downloading WebView2Loader.dll ...\")\n  execute_process(\n    COMMAND \"${_CURL}\" -fsSL\n      ${_auth_args}\n      -o \"${GUI_DIR}/WebView2Loader.dll\"\n      -L \"${_wv_url}\"\n    RESULT_VARIABLE _rc)\n  if(NOT _rc EQUAL 0)\n    file(REMOVE \"${GUI_DIR}/WebView2Loader.dll\")\n  endif()\nendif()\n\nfile(REMOVE \"${_json}\")\n\n# Verify\nif(NOT EXISTS \"${GUI_DIR}/sunshine-gui.exe\")\n  message(WARNING \"GUI download failed. sunshine-gui.exe will not be available in the install.\")\nelse()\n  file(SIZE \"${GUI_DIR}/sunshine-gui.exe\" _size)\n  math(EXPR _size_mb \"${_size} / 1048576\")\n  message(STATUS \"  GUI downloaded successfully (${_size_mb} MB)\")\nendif()\n"
  },
  {
    "path": "cmake/packaging/common.cmake",
    "content": "# common packaging\n\n# common cpack options\nset(CPACK_PACKAGE_NAME ${CMAKE_PROJECT_NAME})\nset(CPACK_PACKAGE_VENDOR \"qiin2333\")\nstring(REGEX REPLACE \"^v\" \"\" CPACK_PACKAGE_VERSION ${PROJECT_VERSION})  # remove the v prefix if it exists\nset(CPACK_PACKAGE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/cpack_artifacts)\nset(CPACK_PACKAGE_CONTACT \"https://alkaidlab.com\")\nset(CPACK_PACKAGE_DESCRIPTION ${CMAKE_PROJECT_DESCRIPTION})\nset(CPACK_PACKAGE_HOMEPAGE_URL ${CMAKE_PROJECT_HOMEPAGE_URL})\nset(CPACK_RESOURCE_FILE_LICENSE ${PROJECT_SOURCE_DIR}/LICENSE)\nset(CPACK_PACKAGE_ICON ${PROJECT_SOURCE_DIR}/sunshine.png)\nset(CPACK_PACKAGE_FILE_NAME \"${CMAKE_PROJECT_NAME}\")\nset(CPACK_STRIP_FILES YES)\n\n# install common assets\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/\"\n        DESTINATION \"${SUNSHINE_ASSETS_DIR}\"\n        PATTERN \"web\" EXCLUDE)\n\n# install ABR prompt template\ninstall(FILES \"${PROJECT_SOURCE_DIR}/src/assets/abr_prompt.md\"\n        DESTINATION \"${SUNSHINE_ASSETS_DIR}\")\nfile(MAKE_DIRECTORY \"${CMAKE_CURRENT_BINARY_DIR}/assets\")\nconfigure_file(\"${PROJECT_SOURCE_DIR}/src/assets/abr_prompt.md\"\n               \"${CMAKE_CURRENT_BINARY_DIR}/assets/abr_prompt.md\"\n               COPYONLY)\n# copy assets to build directory, for running without install\nfile(GLOB_RECURSE ALL_ASSETS\n        RELATIVE \"${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/\" \"${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/*\")\nlist(FILTER ALL_ASSETS EXCLUDE REGEX \"^web/.*$\")  # Filter out the web directory\nforeach(asset ${ALL_ASSETS})  # Copy assets to build directory, excluding the web directory\n    file(COPY \"${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/${asset}\"\n            DESTINATION \"${CMAKE_CURRENT_BINARY_DIR}/assets\")\nendforeach()\n\n# install built vite assets\ninstall(DIRECTORY \"${CMAKE_CURRENT_BINARY_DIR}/assets/web\"\n        DESTINATION \"${SUNSHINE_ASSETS_DIR}\")\n\n# install sunshine control panel (Tauri GUI) — from pre-built download\nif(WIN32)\n    include(${CMAKE_MODULE_PATH}/packaging/FetchGUI.cmake)\n\n    if(EXISTS \"${GUI_DIR}/sunshine-gui.exe\")\n        install(PROGRAMS \"${GUI_DIR}/sunshine-gui.exe\"\n            DESTINATION \"${SUNSHINE_ASSETS_DIR}/gui\"\n            COMPONENT assets)\n        # WebView2Loader.dll (optional — Tauri 2 may embed it)\n        if(EXISTS \"${GUI_DIR}/WebView2Loader.dll\")\n            install(FILES \"${GUI_DIR}/WebView2Loader.dll\"\n                DESTINATION \"${SUNSHINE_ASSETS_DIR}/gui\"\n                COMPONENT assets)\n        endif()\n    else()\n        # Fallback: try local Tauri build output (for developers building GUI locally)\n        set(TAURI_TARGET_DIR \"${SUNSHINE_SOURCE_ASSETS_DIR}/common/sunshine-control-panel/src-tauri/target\")\n\n        install(PROGRAMS\n            \"${TAURI_TARGET_DIR}/x86_64-pc-windows-gnu/release/sunshine-gui.exe\"\n            DESTINATION \"${SUNSHINE_ASSETS_DIR}/gui\"\n            RENAME \"sunshine-gui.exe\"\n            OPTIONAL)\n        install(PROGRAMS\n            \"${TAURI_TARGET_DIR}/x86_64-pc-windows-msvc/release/sunshine-gui.exe\"\n            DESTINATION \"${SUNSHINE_ASSETS_DIR}/gui\"\n            RENAME \"sunshine-gui.exe\"\n            OPTIONAL)\n        install(PROGRAMS\n            \"${TAURI_TARGET_DIR}/release/sunshine-gui.exe\"\n            DESTINATION \"${SUNSHINE_ASSETS_DIR}/gui\"\n            RENAME \"sunshine-gui.exe\"\n            OPTIONAL)\n\n        install(FILES\n            \"${TAURI_TARGET_DIR}/x86_64-pc-windows-gnu/release/WebView2Loader.dll\"\n            \"${TAURI_TARGET_DIR}/x86_64-pc-windows-msvc/release/WebView2Loader.dll\"\n            \"${TAURI_TARGET_DIR}/release/WebView2Loader.dll\"\n            DESTINATION \"${SUNSHINE_ASSETS_DIR}/gui\"\n            OPTIONAL)\n    endif()\nendif()\n\n# platform specific packaging\nif(WIN32)\n    include(${CMAKE_MODULE_PATH}/packaging/windows.cmake)\nelseif(UNIX)\n    include(${CMAKE_MODULE_PATH}/packaging/unix.cmake)\n\n    if(APPLE)\n        include(${CMAKE_MODULE_PATH}/packaging/macos.cmake)\n    else()\n        include(${CMAKE_MODULE_PATH}/packaging/linux.cmake)\n    endif()\nendif()\n\ninclude(CPack)\n"
  },
  {
    "path": "cmake/packaging/linux.cmake",
    "content": "# linux specific packaging\n\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/\"\n        DESTINATION \"${SUNSHINE_ASSETS_DIR}\")\n\n# copy assets (excluding shaders) to build directory, for running without install\nfile(COPY \"${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/\"\n        DESTINATION \"${CMAKE_BINARY_DIR}/assets\"\n        PATTERN \"shaders\" EXCLUDE)\n# use symbolic link for shaders directory\nfile(CREATE_LINK \"${SUNSHINE_SOURCE_ASSETS_DIR}/linux/assets/shaders\"\n        \"${CMAKE_BINARY_DIR}/assets/shaders\" COPY_ON_ERROR SYMBOLIC)\n\nif(${SUNSHINE_BUILD_APPIMAGE} OR ${SUNSHINE_BUILD_FLATPAK})\n    install(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules\"\n            DESTINATION \"${SUNSHINE_ASSETS_DIR}/udev/rules.d\")\n    install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/sunshine.service\"\n            DESTINATION \"${SUNSHINE_ASSETS_DIR}/systemd/user\")\nelse()\n    find_package(Systemd)\n    find_package(Udev)\n\n    if(UDEV_FOUND)\n        install(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/60-sunshine.rules\"\n                DESTINATION \"${UDEV_RULES_INSTALL_DIR}\")\n    endif()\n    if(SYSTEMD_FOUND)\n        install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/sunshine.service\"\n                DESTINATION \"${SYSTEMD_USER_UNIT_INSTALL_DIR}\")\n    endif()\nendif()\n\n# Post install\nset(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA \"${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/postinst\")\nset(CPACK_RPM_POST_INSTALL_SCRIPT_FILE \"${SUNSHINE_SOURCE_ASSETS_DIR}/linux/misc/postinst\")\n\n# Apply setcap for RPM\n# https://github.com/coreos/rpm-ostree/discussions/5036#discussioncomment-10291071\nset(CPACK_RPM_USER_FILELIST \"%caps(cap_sys_admin+p) ${SUNSHINE_EXECUTABLE_PATH}\")\n\n# Dependencies\nset(CPACK_DEB_COMPONENT_INSTALL ON)\nset(CPACK_DEBIAN_PACKAGE_DEPENDS \"\\\n            ${CPACK_DEB_PLATFORM_PACKAGE_DEPENDS} \\\n            libcap2, \\\n            libcurl4, \\\n            libdrm2, \\\n            libevdev2, \\\n            libnuma1, \\\n            libopus0, \\\n            libpulse0, \\\n            libva2, \\\n            libva-drm2, \\\n            libwayland-client0, \\\n            libx11-6, \\\n            miniupnpc, \\\n            openssl | libssl3\")\nset(CPACK_RPM_PACKAGE_REQUIRES \"\\\n            ${CPACK_RPM_PLATFORM_PACKAGE_REQUIRES} \\\n            libcap >= 2.22, \\\n            libcurl >= 7.0, \\\n            libdrm >= 2.4.97, \\\n            libevdev >= 1.5.6, \\\n            libopusenc >= 0.2.1, \\\n            libva >= 2.14.0, \\\n            libwayland-client >= 1.20.0, \\\n            libX11 >= 1.7.3.1, \\\n            miniupnpc >= 2.2.4, \\\n            numactl-libs >= 2.0.14, \\\n            openssl >= 3.0.2, \\\n            pulseaudio-libs >= 10.0\")\n\nif(NOT BOOST_USE_STATIC)\n    set(CPACK_DEBIAN_PACKAGE_DEPENDS \"\\\n                ${CPACK_DEBIAN_PACKAGE_DEPENDS}, \\\n                libboost-filesystem${Boost_VERSION}, \\\n                libboost-locale${Boost_VERSION}, \\\n                libboost-log${Boost_VERSION}, \\\n                libboost-program-options${Boost_VERSION}\")\n    set(CPACK_RPM_PACKAGE_REQUIRES \"\\\n                ${CPACK_RPM_PACKAGE_REQUIRES}, \\\n                boost-filesystem >= ${Boost_VERSION}, \\\n                boost-locale >= ${Boost_VERSION}, \\\n                boost-log >= ${Boost_VERSION}, \\\n                boost-program-options >= ${Boost_VERSION}\")\nendif()\n\n# This should automatically figure out dependencies, doesn't work with the current config\nset(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF)\n\n# application icon\nif(NOT ${SUNSHINE_BUILD_FLATPAK})\n    install(FILES \"${CMAKE_SOURCE_DIR}/sunshine.svg\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps\")\nelse()\n    install(FILES \"${CMAKE_SOURCE_DIR}/sunshine.svg\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps\"\n            RENAME \"${PROJECT_FQDN}.svg\")\nendif()\n\n# tray icon\nif(${SUNSHINE_TRAY} STREQUAL 1)\n    install(FILES \"${CMAKE_SOURCE_DIR}/sunshine.svg\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status\"\n            RENAME \"sunshine-tray.svg\")\n    install(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-playing.svg\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status\")\n    install(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-pausing.svg\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status\")\n    install(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/common/assets/web/public/images/sunshine-locked.svg\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/status\")\n\n    set(CPACK_DEBIAN_PACKAGE_DEPENDS \"\\\n                    ${CPACK_DEBIAN_PACKAGE_DEPENDS}, \\\n                    libayatana-appindicator3-1, \\\n                    libnotify4\")\n    set(CPACK_RPM_PACKAGE_REQUIRES \"\\\n                    ${CPACK_RPM_PACKAGE_REQUIRES}, \\\n                    libappindicator-gtk3 >= 12.10.0\")\nendif()\n\n# desktop file\n# todo - validate desktop files with `desktop-file-validate`\nif(NOT ${SUNSHINE_BUILD_FLATPAK})\n    install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/sunshine.desktop\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/applications\")\nelse()\n    install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/sunshine.desktop\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/applications\"\n            RENAME \"${PROJECT_FQDN}.desktop\")\n    install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/sunshine_kms.desktop\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/applications\"\n            RENAME \"${PROJECT_FQDN}_kms.desktop\")\nendif()\nif(${SUNSHINE_BUILD_FLATPAK})\n    install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/sunshine_terminal.desktop\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/applications\"\n            RENAME \"${PROJECT_FQDN}_terminal.desktop\")\nelseif(NOT ${SUNSHINE_BUILD_APPIMAGE})\n    install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/sunshine_terminal.desktop\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/applications\")\nendif()\n\n# metadata file\n# todo - validate file with `appstream-util validate-relax`\nif(NOT ${SUNSHINE_BUILD_FLATPAK})\n    install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/sunshine.appdata.xml\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/metainfo\")\nelse()\n    install(FILES \"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_FQDN}.metainfo.xml\"\n            DESTINATION \"${CMAKE_INSTALL_DATAROOTDIR}/metainfo\")\nendif()\n"
  },
  {
    "path": "cmake/packaging/macos.cmake",
    "content": "# macos specific packaging\n\n# todo - bundle doesn't produce a valid .app use cpack -G DragNDrop\nset(CPACK_BUNDLE_NAME \"${CMAKE_PROJECT_NAME}\")\nset(CPACK_BUNDLE_PLIST \"${APPLE_PLIST_FILE}\")\nset(CPACK_BUNDLE_ICON \"${PROJECT_SOURCE_DIR}/sunshine.icns\")\n# set(CPACK_BUNDLE_STARTUP_COMMAND \"${INSTALL_RUNTIME_DIR}/sunshine\")\n\nif(SUNSHINE_PACKAGE_MACOS)  # todo\n    set(MAC_PREFIX \"${CMAKE_PROJECT_NAME}.app/Contents\")\n    set(INSTALL_RUNTIME_DIR \"${MAC_PREFIX}/MacOS\")\n\n    install(TARGETS sunshine\n            BUNDLE DESTINATION . COMPONENT Runtime\n            RUNTIME DESTINATION ${INSTALL_RUNTIME_DIR} COMPONENT Runtime)\nelse()\n    install(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/macos/misc/uninstall_pkg.sh\"\n            DESTINATION \"${SUNSHINE_ASSETS_DIR}\")\nendif()\n\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/\"\n        DESTINATION \"${SUNSHINE_ASSETS_DIR}\")\n# copy assets to build directory, for running without install\nfile(COPY \"${SUNSHINE_SOURCE_ASSETS_DIR}/macos/assets/\"\n        DESTINATION \"${CMAKE_BINARY_DIR}/assets\")\n"
  },
  {
    "path": "cmake/packaging/sunshine.iss.in",
    "content": "; Sunshine Inno Setup Script\n; Generated from CMake template - DO NOT EDIT DIRECTLY\n; Edit cmake/packaging/sunshine.iss.in instead\n\n#define MyAppName \"@CMAKE_PROJECT_NAME@\"\n#define MyAppVersion \"@CPACK_PACKAGE_VERSION@\"\n#define MyAppPublisher \"@CPACK_PACKAGE_VENDOR@\"\n#define MyAppURL \"@CMAKE_PROJECT_HOMEPAGE_URL@\"\n#define MyAppExeName \"@CMAKE_PROJECT_NAME@.exe\"\n#define MyAppGUIExeName \"sunshine-gui.exe\"\n#define MySourceDir \"@INNO_STAGING_DIR@\"\n#define MyIconFile \"@CMAKE_SOURCE_DIR@\\sunshine.ico\"\n#define MyWelcomeBmp \"@CMAKE_SOURCE_DIR@\\welcome.bmp\"\n#define MySmallBmp \"@CMAKE_SOURCE_DIR@\\wizard_small.bmp\"\n#define MyLicenseFile \"@CMAKE_SOURCE_DIR@\\LICENSE\"\n\n[Setup]\n; 基本设置\nAppId={{B16E5C0D-7B8A-4E3B-9F1A-2D4C6E8F0A1B}\nAppName={#MyAppName}\nAppVersion={#MyAppVersion}\nAppVerName=Sunshine Foundation Game Streaming Server v{#MyAppVersion}\nAppPublisher={#MyAppPublisher}\nAppPublisherURL={#MyAppURL}\nAppSupportURL={#MyAppURL}/support\nAppUpdatesURL={#MyAppURL}\nDefaultDirName={code:GetPreviousInstallDir|{autopf}\\{#MyAppName}}\nDefaultGroupName={#MyAppName}\n; 管理员权限\nPrivilegesRequired=admin\nPrivilegesRequiredOverridesAllowed=dialog\n; 输出设置\nOutputDir=@CPACK_PACKAGE_DIRECTORY@\nOutputBaseFilename=Sunshine\n; 压缩\nCompression=lzma2/ultra64\nSolidCompression=yes\n; UI 设置\nSetupIconFile={#MyIconFile}\nUninstallDisplayIcon={app}\\{#MyAppExeName}\nWizardStyle=modern hidebevels\nWizardImageFile={#MyWelcomeBmp}\nWizardSmallImageFile={#MySmallBmp}\nWizardImageAlphaFormat=defined\nWizardSizePercent=120\n; 版本信息 (VersionInfoVersion requires X.X.X.X format, use AppVersion for display)\nVersionInfoCompany={#MyAppPublisher}\nVersionInfoDescription=Sunshine Foundation Game Streaming Server\nVersionInfoProductName={#MyAppName}\n; 其他\nLicenseFile={#MyLicenseFile}\n; 允许覆盖安装\nUsePreviousAppDir=yes\n; 最小 Windows 版本 (Windows 10)\nMinVersion=10.0\n; 64 位安装\nArchitecturesAllowed=x64compatible\nArchitecturesInstallIn64BitMode=x64compatible\n; 重启管理器\nCloseApplications=yes\nRestartApplications=yes\n\n[Languages]\nName: \"english\"; MessagesFile: \"compiler:Default.isl\"\nName: \"chinesesimplified\"; MessagesFile: \"@CMAKE_SOURCE_DIR@\\cmake\\packaging\\ChineseSimplified.isl\"\n\n[CustomMessages]\n; English custom messages\nenglish.ComponentApplication=Sunshine main application\nenglish.ComponentAssets=Required assets (shaders, web UI)\nenglish.ComponentVDD=Zako Virtual Display Driver (HDR)\nenglish.ComponentAutostart=Launch on system startup\nenglish.ComponentFirewall=Add firewall exclusions\nenglish.ComponentGamepad=Virtual Gamepad (ViGEmBus)\nenglish.ComponentVmouse=Virtual Mouse Driver (UMDF)\nenglish.ComponentTools=Diagnostic tools (dxgi-info, audio-info)\nenglish.TypeRecommended=Recommended (virtual display + firewall + autostart)\nenglish.TypeFull=Full installation (all components)\nenglish.TypeCompact=Compact installation (core only)\nenglish.TypeCustom=Custom installation\nenglish.ComponentVDDDesc=Required for headless/remote streaming without a physical monitor. Enables HDR passthrough.\nenglish.ComponentFirewallDesc=Allows Moonlight clients to discover and connect to this PC over the network.\nenglish.ComponentAutostartDesc=Sunshine will start automatically when Windows boots. Recommended for always-on streaming.\nenglish.ComponentGamepadDesc=Emulates Xbox controller for games. Install only if you need gamepad input from the client.\nenglish.ComponentVmouseDesc=Provides smooth absolute mouse positioning. Install if cursor feels laggy.\nenglish.ComponentToolsDesc=dxgi-info, audio-info for troubleshooting display and audio issues.\nenglish.StatusResetPermissions=Resetting file permissions...\nenglish.StatusUpdatePath=Updating system PATH...\nenglish.StatusMigrateConfig=Migrating configuration files...\nenglish.StatusFirewall=Configuring firewall rules...\nenglish.StatusInstallVDD=Installing virtual display driver...\nenglish.StatusInstallGamepad=Installing virtual gamepad...\nenglish.StatusInstallVmouse=Installing virtual mouse driver...\nenglish.StatusInstallService=Installing system service...\nenglish.StatusAutostart=Configuring autostart...\nenglish.FinishOpenDocs=Open documentation\nenglish.FinishLaunchGUI=Launch Sunshine GUI\nenglish.MsgOldNSISDetected=Detected a previous NSIS installation of Sunshine.%nIt must be uninstalled before continuing.%n%nUninstall the previous version now?\nenglish.MsgOldNSISFailed=The previous installation could not be fully removed.%nPlease uninstall it manually and try again.\nenglish.MsgUninstallGamepad=Do you want to remove Virtual Gamepad?\nenglish.MsgUninstallData=Do you want to remove all data in %1?%n(This includes configuration, cover images, and settings)\nenglish.StatusStoppingService=Stopping Sunshine service...\nenglish.StatusStoppingProcesses=Stopping running Sunshine processes...\n\n; Chinese Simplified custom messages\nchinesesimplified.ComponentApplication=Sunshine 主程序\nchinesesimplified.ComponentAssets=必要资源（着色器、Web UI）\nchinesesimplified.ComponentVDD=Zako 虚拟显示驱动（HDR）\nchinesesimplified.ComponentAutostart=开机自动启动\nchinesesimplified.ComponentFirewall=添加防火墙规则\nchinesesimplified.ComponentGamepad=虚拟手柄（ViGEmBus）\nchinesesimplified.ComponentVmouse=虚拟鼠标驱动（UMDF）\nchinesesimplified.ComponentTools=诊断工具（dxgi-info、audio-info）\nchinesesimplified.TypeRecommended=推荐安装（虚拟显示器 + 防火墙 + 开机自启）\nchinesesimplified.TypeFull=完全安装（所有组件）\nchinesesimplified.TypeCompact=简洁安装（仅核心）\nchinesesimplified.TypeCustom=自定义安装\nchinesesimplified.ComponentVDDDesc=无头/远程串流必装。无需外接显示器即可串流，支持 HDR 直通。\nchinesesimplified.ComponentFirewallDesc=允许 Moonlight 客户端通过网络发现并连接本机。\nchinesesimplified.ComponentAutostartDesc=Windows 启动时自动运行 Sunshine。推荐常驻串流使用。\nchinesesimplified.ComponentGamepadDesc=模拟 Xbox 手柄。仅需要从客户端传入手柄输入时安装。\nchinesesimplified.ComponentVmouseDesc=提供平滑的绝对定位鼠标。如鼠标延迟可尝试安装。\nchinesesimplified.ComponentToolsDesc=dxgi-info、audio-info 等，用于排查显示和音频问题。\nchinesesimplified.StatusResetPermissions=正在重置文件权限...\nchinesesimplified.StatusUpdatePath=正在更新系统 PATH...\nchinesesimplified.StatusMigrateConfig=正在迁移配置文件...\nchinesesimplified.StatusFirewall=正在配置防火墙规则...\nchinesesimplified.StatusInstallVDD=正在安装虚拟显示驱动...\nchinesesimplified.StatusInstallGamepad=正在安装虚拟手柄...\nchinesesimplified.StatusInstallVmouse=正在安装虚拟鼠标驱动...\nchinesesimplified.StatusInstallService=正在安装系统服务...\nchinesesimplified.StatusAutostart=正在配置自动启动...\nchinesesimplified.FinishOpenDocs=打开使用文档\nchinesesimplified.FinishLaunchGUI=启动 Sunshine 管理界面\nchinesesimplified.MsgOldNSISDetected=检测到旧版 NSIS 安装的 Sunshine。%n需要先卸载才能继续。%n%n是否现在卸载旧版本？\nchinesesimplified.MsgOldNSISFailed=无法完全移除旧版安装。%n请手动卸载后重试。\nchinesesimplified.MsgUninstallGamepad=是否移除虚拟手柄（ViGEmBus）？\nchinesesimplified.MsgUninstallData=是否删除 %1 中的所有数据？%n（包括配置文件、封面图片和设置）\nchinesesimplified.StatusStoppingService=正在停止 Sunshine 服务...\nchinesesimplified.StatusStoppingProcesses=正在停止 Sunshine 进程...\n\n[Types]\nName: \"recommended\"; Description: \"{cm:TypeRecommended}\"\nName: \"full\"; Description: \"{cm:TypeFull}\"\nName: \"compact\"; Description: \"{cm:TypeCompact}\"\nName: \"custom\"; Description: \"{cm:TypeCustom}\"; Flags: iscustom\n\n[Components]\nName: \"application\"; Description: \"{cm:ComponentApplication}\"; Types: recommended full compact custom; Flags: fixed\nName: \"assets\"; Description: \"{cm:ComponentAssets}\"; Types: recommended full compact custom; Flags: fixed\nName: \"vdd\"; Description: \"{cm:ComponentVDD} — {cm:ComponentVDDDesc}\"; Types: recommended full; ExtraDiskSpaceRequired: 2097152\nName: \"firewall\"; Description: \"{cm:ComponentFirewall} — {cm:ComponentFirewallDesc}\"; Types: recommended full compact\nName: \"autostart\"; Description: \"{cm:ComponentAutostart} — {cm:ComponentAutostartDesc}\"; Types: recommended full\nName: \"gamepad\"; Description: \"{cm:ComponentGamepad} — {cm:ComponentGamepadDesc}\"; Types: full\nName: \"vmouse\"; Description: \"{cm:ComponentVmouse} — {cm:ComponentVmouseDesc}\"; Types: full\nName: \"tools\"; Description: \"{cm:ComponentTools} — {cm:ComponentToolsDesc}\"; Types: full\n\n[Tasks]\nName: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"\n\n[Files]\n; Main application\nSource: \"{#MySourceDir}\\{#MyAppExeName}\"; DestDir: \"{app}\"; Flags: ignoreversion; Components: application\nSource: \"{#MySourceDir}\\zlib1.dll\"; DestDir: \"{app}\"; Flags: ignoreversion; Components: application\n\n; Mandatory tools\nSource: \"{#MySourceDir}\\tools\\sunshinesvc.exe\"; DestDir: \"{app}\\tools\"; Flags: ignoreversion; Components: application\nSource: \"{#MySourceDir}\\tools\\qiin-tabtip.exe\"; DestDir: \"{app}\\tools\"; Flags: ignoreversion; Components: application\nSource: \"{#MySourceDir}\\tools\\nefconw.exe\"; DestDir: \"{app}\\tools\"; Flags: ignoreversion; Components: application\n\n; Portable scripts (will be deleted during install, but needed for layout)\nSource: \"{#MySourceDir}\\install_portable.bat\"; DestDir: \"{app}\"; Flags: ignoreversion; Components: application\nSource: \"{#MySourceDir}\\uninstall_portable.bat\"; DestDir: \"{app}\"; Flags: ignoreversion; Components: application\n\n; Language files\nSource: \"{#MySourceDir}\\scripts\\languages\\*\"; DestDir: \"{app}\\scripts\\languages\"; Flags: ignoreversion recursesubdirs; Components: application\n\n; Service scripts\nSource: \"{#MySourceDir}\\scripts\\install-service.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: application\nSource: \"{#MySourceDir}\\scripts\\uninstall-service.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: application\nSource: \"{#MySourceDir}\\scripts\\sleep.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: application\n\n; Migration scripts\nSource: \"{#MySourceDir}\\scripts\\migrate-config.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: application\nSource: \"{#MySourceDir}\\scripts\\migrate-images.ps1\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: application\nSource: \"{#MySourceDir}\\scripts\\IMAGE_MIGRATION.md\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: application\n\n; PATH management\nSource: \"{#MySourceDir}\\scripts\\update-path.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: application\n\n; Assets (includes GUI, shaders, web UI, etc.)\nSource: \"{#MySourceDir}\\assets\\*\"; DestDir: \"{app}\\assets\"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: assets\n\n; Helper tools\nSource: \"{#MySourceDir}\\tools\\device-toggler.exe\"; DestDir: \"{app}\\tools\"; Flags: ignoreversion; Components: assets\nSource: \"{#MySourceDir}\\tools\\DevManView.exe\"; DestDir: \"{app}\\tools\"; Flags: ignoreversion; Components: assets\nSource: \"{#MySourceDir}\\tools\\restart64.exe\"; DestDir: \"{app}\\tools\"; Flags: ignoreversion; Components: assets\nSource: \"{#MySourceDir}\\tools\\SetDpi.exe\"; DestDir: \"{app}\\tools\"; Flags: ignoreversion; Components: assets\nSource: \"{#MySourceDir}\\tools\\setreg.exe\"; DestDir: \"{app}\\tools\"; Flags: ignoreversion; Components: assets\n\n; Autostart script\nSource: \"{#MySourceDir}\\scripts\\autostart-service.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: autostart\n\n; Firewall scripts\nSource: \"{#MySourceDir}\\scripts\\add-firewall-rule.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: firewall\nSource: \"{#MySourceDir}\\scripts\\delete-firewall-rule.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: firewall\n\n; Virtual Display Driver scripts & files\nSource: \"{#MySourceDir}\\scripts\\install-vdd.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: vdd\nSource: \"{#MySourceDir}\\scripts\\uninstall-vdd.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: vdd\nSource: \"{#MySourceDir}\\scripts\\driver\\*\"; DestDir: \"{app}\\scripts\\driver\"; Flags: ignoreversion recursesubdirs; Components: vdd\n\n; Gamepad scripts\nSource: \"{#MySourceDir}\\scripts\\install-gamepad.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: gamepad\nSource: \"{#MySourceDir}\\scripts\\uninstall-gamepad.bat\"; DestDir: \"{app}\\scripts\"; Flags: ignoreversion; Components: gamepad\n\n; Virtual Mouse Driver scripts & files\nSource: \"{#MySourceDir}\\scripts\\vmouse\\install-vmouse.bat\"; DestDir: \"{app}\\scripts\\vmouse\"; Flags: ignoreversion; Components: vmouse\nSource: \"{#MySourceDir}\\scripts\\vmouse\\uninstall-vmouse.bat\"; DestDir: \"{app}\\scripts\\vmouse\"; Flags: ignoreversion; Components: vmouse\nSource: \"{#MySourceDir}\\scripts\\vmouse\\driver\\*\"; DestDir: \"{app}\\scripts\\vmouse\\driver\"; Flags: ignoreversion recursesubdirs; Components: vmouse\n\n; Optional diagnostic tools\nSource: \"{#MySourceDir}\\tools\\dxgi-info.exe\"; DestDir: \"{app}\\tools\"; Flags: ignoreversion; Components: tools\nSource: \"{#MySourceDir}\\tools\\audio-info.exe\"; DestDir: \"{app}\\tools\"; Flags: ignoreversion; Components: tools\n\n[Icons]\n; Start Menu shortcuts\nName: \"{group}\\Sunshine\"; Filename: \"{app}\\{#MyAppExeName}\"; Parameters: \"--shortcut\"; IconFilename: \"{app}\\{#MyAppExeName}\"\nName: \"{group}\\Sunshine GUI\"; Filename: \"{app}\\assets\\gui\\{#MyAppGUIExeName}\"; IconFilename: \"{app}\\assets\\gui\\{#MyAppGUIExeName}\"\nName: \"{group}\\Sunshine Tools\"; Filename: \"{app}\\tools\"; IconFilename: \"{app}\\{#MyAppExeName}\"\nName: \"{group}\\{cm:UninstallProgram,{#MyAppName}}\"; Filename: \"{uninstallexe}\"\n\n; Desktop shortcuts (optional, controlled by Tasks)\nName: \"{autodesktop}\\Sunshine\"; Filename: \"{app}\\{#MyAppExeName}\"; Parameters: \"--shortcut\"; IconFilename: \"{app}\\{#MyAppExeName}\"; Tasks: desktopicon\nName: \"{autodesktop}\\Sunshine GUI\"; Filename: \"{app}\\assets\\gui\\{#MyAppGUIExeName}\"; IconFilename: \"{app}\\assets\\gui\\{#MyAppGUIExeName}\"; Tasks: desktopicon\n\n; Install dir shortcut\nName: \"{app}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"; Parameters: \"--shortcut\"; IconFilename: \"{app}\\{#MyAppExeName}\"\n\n[Registry]\n; Store installation directory for future upgrades\nRoot: HKLM64; Subkey: \"SOFTWARE\\AlkaidLab\\Sunshine\"; ValueType: string; ValueName: \"InstallDir\"; ValueData: \"{app}\"; Flags: uninsdeletevalue\n\n[Run]\n; Post-install actions (shown on progress page)\nFilename: \"{cmd}\"; Parameters: \"/c icacls \"\"{app}\"\" /reset /T /C /Q >nul 2>&1\"; StatusMsg: \"{cm:StatusResetPermissions}\"; Flags: runhidden\nFilename: \"{app}\\scripts\\update-path.bat\"; Parameters: \"add\"; StatusMsg: \"{cm:StatusUpdatePath}\"; Flags: runhidden waituntilterminated\nFilename: \"{app}\\scripts\\migrate-config.bat\"; StatusMsg: \"{cm:StatusMigrateConfig}\"; Flags: runhidden waituntilterminated\nFilename: \"{app}\\scripts\\add-firewall-rule.bat\"; StatusMsg: \"{cm:StatusFirewall}\"; Flags: runhidden waituntilterminated; Components: firewall\nFilename: \"{app}\\scripts\\install-vdd.bat\"; StatusMsg: \"{cm:StatusInstallVDD}\"; Flags: runhidden waituntilterminated; Components: vdd\nFilename: \"{app}\\scripts\\install-gamepad.bat\"; StatusMsg: \"{cm:StatusInstallGamepad}\"; Flags: runhidden waituntilterminated; Components: gamepad\nFilename: \"{app}\\scripts\\vmouse\\install-vmouse.bat\"; StatusMsg: \"{cm:StatusInstallVmouse}\"; Flags: runhidden waituntilterminated; Components: vmouse\nFilename: \"{app}\\scripts\\install-service.bat\"; StatusMsg: \"{cm:StatusInstallService}\"; Flags: runhidden waituntilterminated\nFilename: \"{app}\\scripts\\autostart-service.bat\"; StatusMsg: \"{cm:StatusAutostart}\"; Flags: runhidden waituntilterminated; Components: autostart\n\n; Finish page actions - user selectable\n; Use explorer.exe to launch GUI so it always runs as the non-elevated current user,\n; even though the installer itself is running with admin privileges.\nFilename: \"https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=DXpTjzl2kZwBjN7jlRMkRJ\"; Description: \"{cm:FinishOpenDocs}\"; Flags: postinstall shellexec runascurrentuser unchecked nowait skipifsilent\nFilename: \"{win}\\explorer.exe\"; Parameters: \"\"\"{app}\\assets\\gui\\{#MyAppGUIExeName}\"\"\"; Description: \"{cm:FinishLaunchGUI}\"; Flags: postinstall nowait skipifsilent\n\n[UninstallRun]\n; Stop running processes first\nFilename: \"taskkill\"; Parameters: \"/f /im sunshine-gui.exe\"; Flags: runhidden; RunOnceId: \"KillGUI\"\nFilename: \"taskkill\"; Parameters: \"/f /im sunshine.exe\"; Flags: runhidden; RunOnceId: \"KillSunshine\"\n; Remove system components\nFilename: \"{app}\\scripts\\delete-firewall-rule.bat\"; Flags: runhidden waituntilterminated; RunOnceId: \"DelFirewall\"\nFilename: \"{app}\\scripts\\uninstall-service.bat\"; Flags: runhidden waituntilterminated; RunOnceId: \"UninstallService\"\nFilename: \"{app}\\scripts\\uninstall-vdd.bat\"; Flags: runhidden waituntilterminated; RunOnceId: \"UninstallVDD\"\nFilename: \"{app}\\scripts\\vmouse\\uninstall-vmouse.bat\"; Flags: runhidden waituntilterminated; RunOnceId: \"UninstallVmouse\"\nFilename: \"{app}\\{#MyAppExeName}\"; Parameters: \"--restore-nvprefs-undo\"; Flags: runhidden waituntilterminated; RunOnceId: \"RestoreNV\"\nFilename: \"{app}\\scripts\\update-path.bat\"; Parameters: \"remove\"; Flags: runhidden waituntilterminated; RunOnceId: \"RemovePath\"\n\n[UninstallDelete]\n; Clean up portable scripts that shouldn't be in installed version\nType: files; Name: \"{app}\\install_portable.bat\"\nType: files; Name: \"{app}\\uninstall_portable.bat\"\n; Clean up temporary files\nType: files; Name: \"{app}\\*.tmp\"\nType: files; Name: \"{app}\\*.old\"\n\n[Code]\n// =============================================================================\n// Pascal Script for custom behavior\n// =============================================================================\n\n// =============================================================================\n// Deep UI Customization — Sunshine Branded Installer Theme\n// =============================================================================\n// Color palette (BGR format for Delphi):\n//   Brand Gold:     $0098E8  (RGB #E89800)  — primary accent\n//   Brand Orange:   $0060C0  (RGB #C06000)  — headings\n//   Dark Text:      $1A1A1A  (RGB #1A1A1A)  — body text\n//   Mid Text:       $444444  (RGB #444444)  — secondary text\n//   Light BG:       $FAF6F2  (RGB #F2F6FA)  — warm tint background\n//   Inner BG:       $F5F2EF  (RGB #EFF2F5)  — inner panel bg\n//   Header BG:      $A86420  (RGB #2064A8)  — header band color\n//   Accent Line:    $0088D8  (RGB #D88800)  — accent bar\n\nvar\n  BrandPanel: TPanel;\n  BrandLogo: TBitmapImage;\n  BrandTitle: TNewStaticText;\n  BrandSubtitle: TNewStaticText;\n  AccentBar: TBevel;\n\nprocedure InitializeWizard();\nvar\n  WelcomeLeft: Integer;\nbegin\n  // ── Main form tweaks ──\n  WizardForm.Color := $FAF6F2;\n\n  // ── Header panel (top bar on inner pages) — brand color ──\n  WizardForm.MainPanel.Color := $C07020;\n  WizardForm.PageNameLabel.Font.Color := $FFFFFF;\n  WizardForm.PageNameLabel.Font.Size := 11;\n  WizardForm.PageNameLabel.Font.Style := [fsBold];\n  WizardForm.PageDescriptionLabel.Font.Color := $F0E0D0;\n  WizardForm.PageDescriptionLabel.Font.Size := 9;\n\n  // ── Welcome page — full brand makeover ──\n  WelcomeLeft := WizardForm.WelcomeLabel1.Left;\n\n  // Welcome heading\n  WizardForm.WelcomeLabel1.Font.Name := 'Segoe UI';\n  WizardForm.WelcomeLabel1.Font.Color := $0060C0;\n  WizardForm.WelcomeLabel1.Font.Size := 16;\n  WizardForm.WelcomeLabel1.Font.Style := [fsBold];\n\n  // Welcome body text\n  WizardForm.WelcomeLabel2.Font.Name := 'Segoe UI';\n  WizardForm.WelcomeLabel2.Font.Color := $333333;\n  WizardForm.WelcomeLabel2.Font.Size := 9;\n\n  // ── License page ──\n  WizardForm.LicenseMemo.Font.Name := 'Consolas';\n  WizardForm.LicenseMemo.Font.Size := 9;\n  WizardForm.LicenseMemo.Color := $FFFFFF;\n\n  // ── Components page ──\n  WizardForm.ComponentsList.Color := $FFFFFF;\n  WizardForm.TypesCombo.Color := $FFFFFF;\n\n  // ── Directory page ──\n  WizardForm.DirEdit.Color := $FFFFFF;\n\n  // ── Ready page memo ──\n  WizardForm.ReadyMemo.Font.Name := 'Segoe UI';\n  WizardForm.ReadyMemo.Font.Size := 9;\n  WizardForm.ReadyMemo.Color := $FFFFFF;\n\n  // ── Progress page ──\n  WizardForm.StatusLabel.Font.Name := 'Segoe UI';\n  WizardForm.StatusLabel.Font.Color := $444444;\n  WizardForm.ProgressGauge.Top := WizardForm.ProgressGauge.Top;\n\n  // ── Finish page — brand style ──\n  WizardForm.FinishedHeadingLabel.Font.Name := 'Segoe UI';\n  WizardForm.FinishedHeadingLabel.Font.Color := $0060C0;\n  WizardForm.FinishedHeadingLabel.Font.Size := 16;\n  WizardForm.FinishedHeadingLabel.Font.Style := [fsBold];\n  WizardForm.FinishedLabel.Font.Name := 'Segoe UI';\n  WizardForm.FinishedLabel.Font.Color := $333333;\n  WizardForm.FinishedLabel.Font.Size := 9;\n\n  // ── Inner page background ──\n  WizardForm.InnerPage.Color := $F5F2EF;\n\n  // ── Bottom button panel styling ──\n  WizardForm.NextButton.Font.Size := 9;\n  WizardForm.BackButton.Font.Size := 9;\n  WizardForm.CancelButton.Font.Size := 9;\n\n  // ── Create accent bar below header ──\n  AccentBar := TBevel.Create(WizardForm);\n  AccentBar.Parent := WizardForm.InnerPage;\n  AccentBar.Shape := bsTopLine;\n  AccentBar.Left := 0;\n  AccentBar.Top := 0;\n  AccentBar.Width := WizardForm.InnerPage.Width;\n  AccentBar.Height := 3;\n\n  // ── Create brand panel on Welcome page (right side overlay) ──\n  BrandPanel := TPanel.Create(WizardForm);\n  BrandPanel.Parent := WizardForm.WelcomeLabel1.Parent;\n  BrandPanel.Left := WelcomeLeft;\n  BrandPanel.Top := WizardForm.WelcomeLabel2.Top + WizardForm.WelcomeLabel2.Height + 20;\n  BrandPanel.Width := WizardForm.WelcomeLabel1.Width;\n  BrandPanel.Height := 90;\n  BrandPanel.Color := $F0E8E0;\n  BrandPanel.BevelOuter := bvNone;\n  BrandPanel.ParentBackground := False;\n\n  // Version info in brand panel  \n  BrandTitle := TNewStaticText.Create(WizardForm);\n  BrandTitle.Parent := BrandPanel;\n  BrandTitle.Left := 16;\n  BrandTitle.Top := 12;\n  BrandTitle.Caption := 'Foundation Game Streaming Server';\n  BrandTitle.Font.Name := 'Segoe UI';\n  BrandTitle.Font.Size := 10;\n  BrandTitle.Font.Color := $0060C0;\n  BrandTitle.Font.Style := [fsBold];\n\n  BrandSubtitle := TNewStaticText.Create(WizardForm);\n  BrandSubtitle.Parent := BrandPanel;\n  BrandSubtitle.Left := 16;\n  BrandSubtitle.Top := 38;\n  BrandSubtitle.Caption := 'v' + '{#MyAppVersion}' + '  |  Open Source  |  Low-latency';\n  BrandSubtitle.Font.Name := 'Segoe UI';\n  BrandSubtitle.Font.Size := 9;\n  BrandSubtitle.Font.Color := $666666;\n\n  // URL link in brand panel\n  BrandLogo := TBitmapImage.Create(WizardForm);\n  BrandLogo.Parent := BrandPanel;\n  BrandLogo.Left := 16;\n  BrandLogo.Top := 60;\n  // Use as a visual separator line effect (reuse the bitmap component for placement)\nend;\n\n// Check if old NSIS installation exists and offer to uninstall\nfunction DetectOldNSISInstall(): Boolean;\nvar\n  UninstallString: String;\nbegin\n  Result := False;\n  // Check for NSIS uninstall registry entry\n  if RegQueryStringValue(HKLM, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Sunshine',\n    'UninstallString', UninstallString) then\n  begin\n    Result := True;\n  end;\n  // Also check WOW6432Node\n  if not Result then\n  begin\n    if RegQueryStringValue(HKLM, 'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Sunshine',\n      'UninstallString', UninstallString) then\n    begin\n      Result := True;\n    end;\n  end;\nend;\n\nfunction GetOldNSISUninstallString(): String;\nvar\n  UninstallString: String;\nbegin\n  Result := '';\n  if RegQueryStringValue(HKLM, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Sunshine',\n    'UninstallString', UninstallString) then\n  begin\n    Result := UninstallString;\n  end\n  else if RegQueryStringValue(HKLM, 'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Sunshine',\n    'UninstallString', UninstallString) then\n  begin\n    Result := UninstallString;\n  end;\nend;\n\n// Read previous install directory from registry\nfunction GetPreviousInstallDir(Default: String): String;\nvar\n  InstallDir: String;\nbegin\n  Result := Default;\n  if RegQueryStringValue(HKLM64, 'SOFTWARE\\AlkaidLab\\Sunshine', 'InstallDir', InstallDir) then\n  begin\n    if DirExists(InstallDir) then\n      Result := InstallDir;\n  end;\nend;\n\n// Initialization: handle old NSIS upgrade\nfunction InitializeSetup(): Boolean;\nvar\n  UninstallString: String;\n  ResultCode: Integer;\nbegin\n  Result := True;\n\n  if DetectOldNSISInstall() then\n  begin\n    UninstallString := GetOldNSISUninstallString();\n    if UninstallString <> '' then\n    begin\n      if MsgBox(CustomMessage('MsgOldNSISDetected'),\n        mbConfirmation, MB_YESNO) = IDYES then\n      begin\n        // Run the NSIS uninstaller silently\n        Exec(RemoveQuotes(UninstallString), '/S', '', SW_SHOW, ewWaitUntilTerminated, ResultCode);\n        // Give it a moment to finish\n        Sleep(2000);\n        // Check if uninstall was successful\n        if DetectOldNSISInstall() then\n        begin\n          MsgBox(CustomMessage('MsgOldNSISFailed'), mbError, MB_OK);\n          Result := False;\n        end;\n      end\n      else\n      begin\n        Result := False;\n      end;\n    end;\n  end;\nend;\n\n// Stop services and processes before installation to avoid locked files\nfunction PrepareToInstall(var NeedsRestart: Boolean): String;\nvar\n  ResultCode: Integer;\nbegin\n  Result := '';\n  // Stop the Sunshine service\n  Exec('net', 'stop SunshineService', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);\n  // Kill any running processes\n  Exec('taskkill', '/f /im sunshine.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);\n  Exec('taskkill', '/f /im sunshine-gui.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);\n  Exec('taskkill', '/f /im sunshinesvc.exe', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);\n  // Small delay to allow processes to release files\n  Sleep(1000);\nend;\n\n// Clean up portable scripts during install\nprocedure CurStepChanged(CurStep: TSetupStep);\nbegin\n  if CurStep = ssPostInstall then\n  begin\n    // Delete portable scripts in installed version\n    DeleteFile(ExpandConstant('{app}\\install_portable.bat'));\n    DeleteFile(ExpandConstant('{app}\\uninstall_portable.bat'));\n  end;\nend;\n\n// Custom uninstall: ask about gamepad and data removal\nprocedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);\nvar\n  ResultCode: Integer;\nbegin\n  if CurUninstallStep = usUninstall then\n  begin\n    // Ask about virtual gamepad\n    if MsgBox(CustomMessage('MsgUninstallGamepad'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then\n    begin\n      Exec(ExpandConstant('{app}\\scripts\\uninstall-gamepad.bat'), '', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);\n    end;\n  end;\n\n  if CurUninstallStep = usPostUninstall then\n  begin\n    // Ask about data removal\n    if MsgBox(FmtMessage(CustomMessage('MsgUninstallData'), [ExpandConstant('{app}')]),\n      mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDNO then\n    begin\n      // User chose to keep data, do nothing\n    end\n    else\n    begin\n      // Remove the installation directory\n      DelTree(ExpandConstant('{app}'), True, True, True);\n      // Clean up registry\n      RegDeleteValue(HKLM64, 'SOFTWARE\\AlkaidLab\\Sunshine', 'InstallDir');\n      RegDeleteKeyIfEmpty(HKLM64, 'SOFTWARE\\AlkaidLab\\Sunshine');\n      RegDeleteKeyIfEmpty(HKLM64, 'SOFTWARE\\AlkaidLab');\n    end;\n  end;\nend;\n\n// Use previous installation directory as default\nfunction NextButtonClick(CurPageID: Integer): Boolean;\nbegin\n  Result := True;\nend;\n"
  },
  {
    "path": "cmake/packaging/unix.cmake",
    "content": "# unix specific packaging\n# put anything here that applies to both linux and macos\n\n# return here if building a macos package\nif(SUNSHINE_PACKAGE_MACOS)\n    return()\nendif()\n\n# Installation destination dir\nset(CPACK_SET_DESTDIR true)\nif(NOT CMAKE_INSTALL_PREFIX)\n    set(CMAKE_INSTALL_PREFIX \"/usr/share/sunshine\")\nendif()\n\ninstall(TARGETS sunshine RUNTIME DESTINATION \"${CMAKE_INSTALL_BINDIR}\")\n"
  },
  {
    "path": "cmake/packaging/windows.cmake",
    "content": "# windows specific packaging\n\n# Fetch driver dependencies (downloads at configure time)\ninclude(${CMAKE_MODULE_PATH}/packaging/FetchDriverDeps.cmake)\n\ninstall(TARGETS sunshine RUNTIME DESTINATION \".\" COMPONENT application)\n\n# Hardening: include zlib1.dll (loaded via LoadLibrary() in openssl's libcrypto.a)\ninstall(FILES \"${ZLIB}\" DESTINATION \".\" COMPONENT application)\n\n# Adding tools\ninstall(TARGETS dxgi-info RUNTIME DESTINATION \"tools\" COMPONENT dxgi)\ninstall(TARGETS audio-info RUNTIME DESTINATION \"tools\" COMPONENT audio)\n\n# Mandatory tools\ninstall(TARGETS sunshinesvc RUNTIME DESTINATION \"tools\" COMPONENT application)\ninstall(TARGETS qiin-tabtip RUNTIME DESTINATION \"tools\" COMPONENT application)\n\n# Shared tool: nefconw.exe (used by VDD and vmouse install scripts)\ninstall(FILES \"${NEFCON_DRIVER_DIR}/nefconw.exe\"\n        DESTINATION \"tools\"\n        COMPONENT application)\n\n# Mandatory scripts\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/service/\"\n        DESTINATION \"scripts\"\n        COMPONENT assets)\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/migration/\"\n        DESTINATION \"scripts\"\n        COMPONENT assets)\n# Portable initialization and uninstall scripts and language files\ninstall(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/install_portable.bat\"\n        DESTINATION \".\"\n        COMPONENT application)\ninstall(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/uninstall_portable.bat\"\n        DESTINATION \".\"\n        COMPONENT application)\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/languages/\"\n        DESTINATION \"scripts/languages\"\n        COMPONENT application)\n# add sunshine environment to PATH\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/path/\"\n        DESTINATION \"scripts\"\n        COMPONENT assets)\n# Configurable options for the service\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/autostart/\"\n        DESTINATION \"scripts\"\n        COMPONENT autostart)\n\n# scripts\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/firewall/\"\n        DESTINATION \"scripts\"\n        COMPONENT firewall)\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/gamepad/\"\n        DESTINATION \"scripts\"\n        COMPONENT gamepad)\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/vsink/\"\n        DESTINATION \"scripts\"\n        COMPONENT vsink)\n# VDD: scripts & config from source tree, driver binaries from download cache\ninstall(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/vdd/install-vdd.bat\"\n              \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/vdd/uninstall-vdd.bat\"\n        DESTINATION \"scripts\"\n        COMPONENT vdd)\ninstall(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/vdd/driver/vdd_settings.xml\"\n        DESTINATION \"scripts/driver\"\n        COMPONENT vdd)\ninstall(FILES \"${VDD_DRIVER_DIR}/ZakoVDD.dll\"\n              \"${VDD_DRIVER_DIR}/ZakoVDD.inf\"\n              \"${VDD_DRIVER_DIR}/zakovdd.cat\"\n              \"${VDD_DRIVER_DIR}/ZakoVDD.cer\"\n        DESTINATION \"scripts/driver\"\n        COMPONENT vdd)\n\n# vmouse: scripts from source tree, driver binaries + cert from download cache\ninstall(FILES \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/vmouse/install-vmouse.bat\"\n              \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/vmouse/uninstall-vmouse.bat\"\n        DESTINATION \"scripts/vmouse\"\n        COMPONENT assets)\ninstall(FILES \"${VMOUSE_DRIVER_DIR}/ZakoVirtualMouse.dll\"\n              \"${VMOUSE_DRIVER_DIR}/ZakoVirtualMouse.inf\"\n              \"${VMOUSE_DRIVER_DIR}/ZakoVirtualMouse.cat\"\n              \"${VMOUSE_DRIVER_DIR}/ZakoVirtualMouse.cer\"\n        DESTINATION \"scripts/vmouse/driver\"\n        COMPONENT assets)\n\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/misc/helper/\"\n        DESTINATION \"tools\"\n        COMPONENT assets)\n\n# Sunshine assets\ninstall(DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/\"\n        DESTINATION \"${SUNSHINE_ASSETS_DIR}\"\n        COMPONENT assets)\n\n# copy assets (excluding shaders) to build directory, for running without install\nfile(COPY \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/\"\n        DESTINATION \"${CMAKE_BINARY_DIR}/assets\"\n        PATTERN \"shaders\" EXCLUDE)\n# use junction for shaders directory\ncmake_path(CONVERT \"${SUNSHINE_SOURCE_ASSETS_DIR}/windows/assets/shaders\"\n        TO_NATIVE_PATH_LIST shaders_in_build_src_native)\ncmake_path(CONVERT \"${CMAKE_BINARY_DIR}/assets/shaders\" TO_NATIVE_PATH_LIST shaders_in_build_dest_native)\n\nset(CPACK_PACKAGE_ICON \"${CMAKE_SOURCE_DIR}\\\\\\\\sunshine.ico\")\n\n# The name of the directory that will be created in C:/Program files/\nset(CPACK_PACKAGE_INSTALL_DIRECTORY \"${CPACK_PACKAGE_NAME}\")\n\n# Setting components groups and dependencies\nset(CPACK_COMPONENT_GROUP_CORE_EXPANDED true)\nset(CPACK_COMPONENT_GROUP_SCRIPTS_EXPANDED true)\n\n# sunshine binary\nset(CPACK_COMPONENT_APPLICATION_DISPLAY_NAME \"${CMAKE_PROJECT_NAME}\")\nset(CPACK_COMPONENT_APPLICATION_DESCRIPTION \"${CMAKE_PROJECT_NAME} main application and required components.\")\nset(CPACK_COMPONENT_APPLICATION_GROUP \"Core\")\nset(CPACK_COMPONENT_APPLICATION_REQUIRED true)\nset(CPACK_COMPONENT_APPLICATION_DEPENDS assets)\n\n# Virtual Display Driver\nset(CPACK_COMPONENT_VDD_DISPLAY_NAME \"Zako Display Driver\")\nset(CPACK_COMPONENT_VDD_DESCRIPTION \"支持HDR的虚拟显示器驱动安装\")\nset(CPACK_COMPONENT_VDD_GROUP \"Core\")\n\n\n\n# service auto-start script\nset(CPACK_COMPONENT_AUTOSTART_DISPLAY_NAME \"Launch on Startup\")\nset(CPACK_COMPONENT_AUTOSTART_DESCRIPTION \"If enabled, launches Sunshine automatically on system startup.\")\nset(CPACK_COMPONENT_AUTOSTART_GROUP \"Core\")\n\n# assets\nset(CPACK_COMPONENT_ASSETS_DISPLAY_NAME \"Required Assets\")\nset(CPACK_COMPONENT_ASSETS_DESCRIPTION \"Shaders, default box art, and web UI.\")\nset(CPACK_COMPONENT_ASSETS_GROUP \"Core\")\nset(CPACK_COMPONENT_ASSETS_REQUIRED true)\n\n# audio tool\nset(CPACK_COMPONENT_AUDIO_DISPLAY_NAME \"audio-info\")\nset(CPACK_COMPONENT_AUDIO_DESCRIPTION \"CLI tool providing information about sound devices.\")\nset(CPACK_COMPONENT_AUDIO_GROUP \"Tools\")\n\n# display tool\nset(CPACK_COMPONENT_DXGI_DISPLAY_NAME \"dxgi-info\")\nset(CPACK_COMPONENT_DXGI_DESCRIPTION \"CLI tool providing information about graphics cards and displays.\")\nset(CPACK_COMPONENT_DXGI_GROUP \"Tools\")\n\n# superCmds\nset(CPACK_COMPONENT_SUPERCMD_DISPLAY_NAME \"super-cmd\")\nset(CPACK_COMPONENT_SUPERCMD_DESCRIPTION \"Commands that can running on host.\")\nset(CPACK_COMPONENT_SUPERCMD_GROUP \"Tools\")\n\n# firewall scripts\nset(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME \"Add Firewall Exclusions\")\nset(CPACK_COMPONENT_FIREWALL_DESCRIPTION \"Scripts to enable or disable firewall rules.\")\nset(CPACK_COMPONENT_FIREWALL_GROUP \"Scripts\")\n\n# gamepad scripts\nset(CPACK_COMPONENT_GAMEPAD_DISPLAY_NAME \"Virtual Gamepad\")\nset(CPACK_COMPONENT_GAMEPAD_DESCRIPTION \"Scripts to install and uninstall Virtual Gamepad.\")\nset(CPACK_COMPONENT_GAMEPAD_GROUP \"Scripts\")\n\n# include specific packaging\ninclude(${CMAKE_MODULE_PATH}/packaging/windows_innosetup.cmake)\n# Legacy NSIS/WiX (kept for reference, no longer used)\n# include(${CMAKE_MODULE_PATH}/packaging/windows_nsis.cmake)\n# include(${CMAKE_MODULE_PATH}/packaging/windows_wix.cmake)\n"
  },
  {
    "path": "cmake/packaging/windows_innosetup.cmake",
    "content": "# Inno Setup Packaging\n# Replaces NSIS packaging with Inno Setup for Windows installer generation\n# \n# Usage:\n#   cmake --build build --target innosetup\n#   或直接:\n#   iscc build/sunshine_installer.iss\n#\n# 依赖: Inno Setup 6.x (https://jrsoftware.org/isinfo.php)\n\n# Find Inno Setup compiler\nfind_program(ISCC_EXECUTABLE iscc\n    PATHS\n        \"$ENV{ProgramFiles\\(x86\\)}/Inno Setup 6\"\n        \"$ENV{ProgramFiles}/Inno Setup 6\"\n        \"$ENV{LOCALAPPDATA}/Programs/Inno Setup 6\"\n    DOC \"Inno Setup Compiler (iscc.exe)\"\n)\n\nif(NOT ISCC_EXECUTABLE)\n    message(STATUS \"Inno Setup not found - 'innosetup' target will not be available\")\n    message(STATUS \"Install from: https://jrsoftware.org/isdl.php\")\n    return()\nendif()\n\nmessage(STATUS \"Found Inno Setup: ${ISCC_EXECUTABLE}\")\n\n# Configure the .iss template with CMake variables\nset(INNO_STAGING_DIR \"${CMAKE_BINARY_DIR}/inno_staging\")\n# Use a separate variable so we don't overwrite CMAKE_INSTALL_PREFIX\nset(CMAKE_INSTALL_PREFIX_SAVED \"${CMAKE_INSTALL_PREFIX}\")\nset(CMAKE_INSTALL_PREFIX \"${INNO_STAGING_DIR}\")\n\nconfigure_file(\n    \"${CMAKE_MODULE_PATH}/packaging/sunshine.iss.in\"\n    \"${CMAKE_BINARY_DIR}/sunshine_installer.iss\"\n    @ONLY\n)\n\n# Restore CMAKE_INSTALL_PREFIX\nset(CMAKE_INSTALL_PREFIX \"${CMAKE_INSTALL_PREFIX_SAVED}\")\n\n# Create a custom target for building the Inno Setup installer\n# Step 1: Install files to staging directory\n# Step 2: Strip debug symbols from executables to reduce installer size\n# Step 3: Run Inno Setup compiler\nadd_custom_target(innosetup\n    COMMENT \"Building Inno Setup installer...\"\n    \n    # Install to staging directory\n    COMMAND ${CMAKE_COMMAND} --install \"${CMAKE_BINARY_DIR}\" --prefix \"${CMAKE_BINARY_DIR}/inno_staging\"\n    \n    # Strip debug symbols from executables to reduce installer size\n    COMMAND ${CMAKE_COMMAND} -E echo \"Stripping debug symbols from executables...\"\n    COMMAND strip --strip-debug \"${CMAKE_BINARY_DIR}/inno_staging/sunshine.exe\"\n    COMMAND strip --strip-debug \"${CMAKE_BINARY_DIR}/inno_staging/tools/sunshinesvc.exe\"\n    COMMAND strip --strip-debug \"${CMAKE_BINARY_DIR}/inno_staging/tools/dxgi-info.exe\"\n    COMMAND strip --strip-debug \"${CMAKE_BINARY_DIR}/inno_staging/tools/audio-info.exe\"\n    \n    # Run Inno Setup compiler\n    COMMAND \"${ISCC_EXECUTABLE}\" \"${CMAKE_BINARY_DIR}/sunshine_installer.iss\"\n    \n    WORKING_DIRECTORY \"${CMAKE_BINARY_DIR}\"\n    VERBATIM\n)\n\n# Make sure sunshine is built before creating the installer\nadd_dependencies(innosetup sunshine)\n\n# Also provide a convenience target to just generate the staging directory\nadd_custom_target(innosetup-staging\n    COMMENT \"Generating Inno Setup staging directory...\"\n    COMMAND ${CMAKE_COMMAND} --install \"${CMAKE_BINARY_DIR}\" --prefix \"${CMAKE_BINARY_DIR}/inno_staging\"\n    WORKING_DIRECTORY \"${CMAKE_BINARY_DIR}\"\n    VERBATIM\n)\n"
  },
  {
    "path": "cmake/packaging/windows_nsis.cmake",
    "content": "# NSIS Packaging\n# see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.html\n\nset(CPACK_NSIS_INSTALLED_ICON_NAME \"${PROJECT__DIR}\\\\\\\\${PROJECT_EXE}\")\n\n# 由于 CPack 的 NSIS 模板限制，我们无法直接修改 .onInit 函数\n# 但可以通过以下方式实现：\n# 通过 MUI_PAGE_CUSTOMFUNCTION_PRE 在目录页面显示前读取注册表\n#\n# 注意：CPACK_NSIS_INSTALLER_MUI_ICON_CODE 是在页面定义之前的钩子\n# 我们用它来定义自定义函数，并设置 MUI_PAGE_CUSTOMFUNCTION_PRE\n\nset(CPACK_NSIS_INSTALLER_MUI_ICON_CODE \"\n; 定义安装程序图标\n!define MUI_ICON \\\\\\\"${CMAKE_SOURCE_DIR}/sunshine.ico\\\\\\\"\n!define MUI_UNICON \\\\\\\"${CMAKE_SOURCE_DIR}/sunshine.ico\\\\\\\"\n\n; 定义在目录页面显示前执行的函数\n!define MUI_PAGE_CUSTOMFUNCTION_PRE PreDirectoryPage\n\n; 从注册表读取之前的安装路径\n; 使用自定义注册表键，避免覆盖安装触发卸载时被清除\n\nFunction PreDirectoryPage\n    ; 只在默认安装目录时才尝试读取注册表\n    StrCmp $IS_DEFAULT_INSTALLDIR '1' 0 SkipRegRead\n\n    Push $0\n    SetRegView 64\n\n    ; 从自定义注册表读取上次安装目录\n    ReadRegStr $0 HKLM 'SOFTWARE\\\\\\\\AlkaidLab\\\\\\\\Sunshine' 'InstallDir'\n    StrCmp $0 '' DoneRegRead 0\n    IfFileExists '$0\\\\\\\\*.*' SetPath DoneRegRead\n\n    SetPath:\n    StrCpy $INSTDIR $0\n    StrCpy $IS_DEFAULT_INSTALLDIR '0'\n\n    DoneRegRead:\n    Pop $0\n\n    SkipRegRead:\nFunctionEnd\n\n; 辅助函数：获取路径的父目录\nFunction GetParent\n    Exch $0\n    Push $1\n    Push $2\n\n    StrLen $1 $0\n    IntOp $1 $1 - 1\n\n    loop:\n        IntOp $1 $1 - 1\n        IntCmp $1 0 done done\n        StrCpy $2 $0 1 $1\n        StrCmp $2 '\\\\\\\\' found\n        Goto loop\n\n    found:\n        StrCpy $0 $0 $1\n\n    done:\n        Pop $2\n        Pop $1\n        Exch $0\nFunctionEnd\n\n; Finish Page 自定义选项\n; 复选框1: 打开使用教程（默认勾选）\n!define MUI_FINISHPAGE_RUN\n!define MUI_FINISHPAGE_RUN_TEXT '打开使用教程'\n!define MUI_FINISHPAGE_RUN_FUNCTION OpenDocumentation\n\n; 复选框2: 启动 Sunshine GUI（默认勾选）\n!define MUI_FINISHPAGE_SHOWREADME\n!define MUI_FINISHPAGE_SHOWREADME_TEXT '启动 Sunshine GUI'\n!define MUI_FINISHPAGE_SHOWREADME_FUNCTION LaunchGUI\n\nFunction OpenDocumentation\n    ExecShell 'open' 'https://docs.qq.com/aio/DSGdQc3htbFJjSFdO?p=DXpTjzl2kZwBjN7jlRMkRJ'\nFunctionEnd\n\nFunction LaunchGUI\n    Exec '\\$INSTDIR\\\\\\\\assets\\\\\\\\gui\\\\\\\\sunshine-gui.exe'\nFunctionEnd\n\")\n\n# ==============================================================================\n# File Conflict Prevention - 在文件解压前停止进程\n# ==============================================================================\n\n# 策略：直接禁用 ENABLE_UNINSTALL_BEFORE_INSTALL，手动在安装过程中处理\n# 这样可以避免在选择目录阶段就检查文件导致冲突\n\n# 自动卸载功能\nset(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL \"ON\")\n\n# Windows Restart Manager 支持和高DPI位图优化\nset(CPACK_NSIS_EXTRA_DEFINES \"\n\\${CPACK_NSIS_EXTRA_DEFINES}\n!define MUI_FINISHPAGE_REBOOTLATER_DEFAULT\nManifestDPIAware true\n\")\n\n# Basic installer configuration\nset(CPACK_NSIS_MUI_ICON \"${CMAKE_SOURCE_DIR}\\\\\\\\sunshine.ico\")\nset(CPACK_NSIS_MUI_UNIICON \"${CMAKE_SOURCE_DIR}\\\\\\\\sunshine.ico\")\n\n# 设置DPI感知\nset(CPACK_NSIS_MANIFEST_DPI_AWARE ON)\nset(CPACK_NSIS_MUI_WELCOMEFINISHPAGE_BITMAP \"${CMAKE_SOURCE_DIR}\\\\\\\\welcome.bmp\")\nset(CPACK_NSIS_MUI_UNWELCOMEFINISHPAGE_BITMAP \"${CMAKE_SOURCE_DIR}\\\\\\\\welcome.bmp\")\n\n# 头部图像（需要150x57像素）\n# set(CPACK_NSIS_MUI_HEADERIMAGE_BITMAP \"${CMAKE_SOURCE_DIR}\\\\\\\\cmake\\\\\\\\packaging\\\\\\\\welcome.bmp\")\n\n# Custom branding\nset(CPACK_NSIS_BRANDING_TEXT \"Sunshine Foundation Game Streaming Server v${CPACK_PACKAGE_VERSION}\")\nset(CPACK_NSIS_BRANDING_TEXT_TRIM_POSITION \"LEFT\")\n\n# ==============================================================================\n# Page Customization and Enhanced User Experience\n# ==============================================================================\n\n# Custom welcome page text\nset(CPACK_NSIS_WELCOME_TITLE \"Welcome to Sunshine Foundation Game Streaming Server Install Wizard\")\nset(CPACK_NSIS_WELCOME_TITLE_3LINES \"ON\")\n\n# Custom finish page configuration\nset(CPACK_NSIS_FINISH_TITLE \"安装完成！\")\nset(CPACK_NSIS_FINISH_TEXT \"Sunshine Foundation Game Streaming Server 已成功安装到您的系统中。\\\\r\\\\n\\\\r\\\\n点击 '完成' 开始使用这个强大的游戏流媒体服务器。\")\n\n# ==============================================================================\n# Installation Progress and User Feedback\n# ==============================================================================\n\n# Enhanced installation commands with progress feedback\nSET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS\n        \"${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}        \n        ; 确保覆盖模式仍然生效\n        SetOverwrite try\n\n        ; ----------------------------------------------------------------------\n        ; 清理便携版脚本：安装版不需要这两个文件\n        ; 需求：如果目录下有 install_portable.bat / uninstall_portable.bat，就删除\n        ; 安全防护：防止符号链接攻击 - 使用 IfFileExists 检查文件是否存在\n        ;           限制在 $INSTDIR 目录内，避免路径遍历攻击\n        ; ----------------------------------------------------------------------\n        DetailPrint '🧹 清理便携版脚本...'\n        ; 安全删除：先检查文件是否存在，避免符号链接攻击\n        IfFileExists '\\$INSTDIR\\\\\\\\install_portable.bat' 0 +2\n        Delete '\\$INSTDIR\\\\\\\\install_portable.bat'\n        IfFileExists '\\$INSTDIR\\\\\\\\uninstall_portable.bat' 0 +2\n        Delete '\\$INSTDIR\\\\\\\\uninstall_portable.bat'\n        \n        ; 重置文件权限\n        DetailPrint '🔓 重置文件权限...'\n        nsExec::ExecToLog 'icacls \\\\\\\"$INSTDIR\\\\\\\" /reset /T /C /Q >nul 2>&1'\n        \n        ; ----------------------------------------------------------------------\n        ; 清理临时文件\n        ; 安全防护：防止符号链接攻击\n        ;           注意：通配符删除（*.tmp, *.old）在遇到符号链接时可能有风险\n        ;           但限制在 $INSTDIR 目录内，且 NSIS 的 Delete 命令会处理符号链接\n        ;           为了更安全，可以考虑逐个检查文件，但通配符删除在安装目录内风险较低\n        ; ----------------------------------------------------------------------\n        DetailPrint '🧹 清理临时文件...'\n        ; 使用通配符删除，限制在 $INSTDIR 目录内\n        ; NSIS 的 Delete 命令在处理符号链接时会删除链接本身，不会跟随到目标\n        Delete '\\$INSTDIR\\\\\\\\*.tmp'\n        Delete '\\$INSTDIR\\\\\\\\*.old'\n        \n        ; 显示安装进度信息\n        DetailPrint '🎯 正在配置 Sunshine Foundation Game Streaming Server...'\n                \n        ; 系统配置\n        DetailPrint '🔧 配置系统权限...'\n        nsExec::ExecToLog 'icacls \\\\\\\"$INSTDIR\\\\\\\" /reset'\n        \n        DetailPrint '🛣️ 更新系统PATH环境变量...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\update-path.bat\\\\\\\" add'\n        \n        DetailPrint '📦 迁移配置文件...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\migrate-config.bat\\\\\\\"'\n        \n        DetailPrint '🔥 配置防火墙规则...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\add-firewall-rule.bat\\\\\\\"'\n        \n        DetailPrint '📺 安装虚拟显示器驱动...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\install-vdd.bat\\\\\\\"'\n        \n\n        DetailPrint '🎯 安装虚拟游戏手柄...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\install-gamepad.bat\\\\\\\"'\n        \n        DetailPrint '⚙️ 安装并启动系统服务...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\install-service.bat\\\\\\\"'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\autostart-service.bat\\\\\\\"'\n\n        ; 写入安装目录，供后续覆盖安装读取\n        SetRegView 64\n        WriteRegStr HKLM 'SOFTWARE\\\\\\\\AlkaidLab\\\\\\\\Sunshine' 'InstallDir' '$INSTDIR'\n        \n        DetailPrint '✅ 安装完成！'\n        \n        NoController:\n        \")\n\n# 卸载命令配置\nset(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS\n        \"${CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS}\n        ; 显示卸载进度信息\n        DetailPrint '正在卸载 Sunshine Foundation Game Streaming Server...'\n        \n        ; 停止运行的程序\n        DetailPrint '停止运行的程序...'\n        nsExec::ExecToLog 'taskkill /f /im sunshine-gui.exe'\n        nsExec::ExecToLog 'taskkill /f /im sunshine.exe'\n        \n        ; 卸载系统组件\n        DetailPrint '删除防火墙规则...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\delete-firewall-rule.bat\\\\\\\"'\n        \n        DetailPrint '卸载系统服务...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\uninstall-service.bat\\\\\\\"'\n        \n        DetailPrint '卸载虚拟显示器驱动...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\uninstall-vdd.bat\\\\\\\"'\n        \n\n        DetailPrint '恢复NVIDIA设置...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\${CMAKE_PROJECT_NAME}.exe\\\\\\\" --restore-nvprefs-undo'\n        \n        MessageBox MB_YESNO|MB_ICONQUESTION \\\n            'Do you want to remove Virtual Gamepad?' \\\n            /SD IDNO IDNO NoGamepad\n            nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\uninstall-gamepad.bat\\\\\\\"'; skipped if no\n        NoGamepad:\n        MessageBox MB_YESNO|MB_ICONQUESTION \\\n            'Do you want to remove $INSTDIR (this includes the configuration, cover images, and settings)?' \\\n            /SD IDNO IDNO NoDelete\n            RMDir /r \\\\\\\"$INSTDIR\\\\\\\"; skipped if no\n            SetRegView 64\n            DeleteRegValue HKLM 'SOFTWARE\\\\\\\\AlkaidLab\\\\\\\\Sunshine' 'InstallDir'\n            DeleteRegKey /ifempty HKLM 'SOFTWARE\\\\\\\\AlkaidLab\\\\\\\\Sunshine'\n        \n        DetailPrint '清理环境变量...'\n        nsExec::ExecToLog '\\\\\\\"$INSTDIR\\\\\\\\scripts\\\\\\\\update-path.bat\\\\\\\" remove'\n        \n        NoDelete:\n        DetailPrint 'Uninstall complete!'\n        \")\n\n# ==============================================================================\n# Start Menu and Shortcuts Configuration\n# ==============================================================================\n\nset(CPACK_NSIS_MODIFY_PATH OFF)\nset(CPACK_NSIS_EXECUTABLES_DIRECTORY \".\")\nset(CPACK_NSIS_INSTALLED_ICON_NAME \"${CMAKE_PROJECT_NAME}.exe\")\n\n# Enhanced Start Menu shortcuts with better icons and descriptions\nset(CPACK_NSIS_CREATE_ICONS_EXTRA\n        \"${CPACK_NSIS_CREATE_ICONS_EXTRA}\n        SetOutPath '\\$INSTDIR'\n\n        ; 主程序快捷方式 - 使用可执行文件的内嵌图标\n        CreateShortCut '\\$SMPROGRAMS\\\\\\\\$STARTMENU_FOLDER\\\\\\\\Sunshine.lnk' \\\n            '\\$INSTDIR\\\\\\\\${CMAKE_PROJECT_NAME}.exe' '--shortcut' '\\$INSTDIR\\\\\\\\${CMAKE_PROJECT_NAME}.exe' 0\n\n        ; 安装目录主程序快捷方式 - 使用可执行文件的内嵌图标\n        CreateShortCut '\\$INSTDIR\\\\\\\\${CMAKE_PROJECT_NAME}.lnk' \\\n            '\\$INSTDIR\\\\\\\\${CMAKE_PROJECT_NAME}.exe' '--shortcut' '\\$INSTDIR\\\\\\\\${CMAKE_PROJECT_NAME}.exe' 0\n\n        ; GUI管理工具快捷方式 - 使用GUI程序的内嵌图标\n        CreateShortCut '\\$SMPROGRAMS\\\\\\\\$STARTMENU_FOLDER\\\\\\\\Sunshine GUI.lnk' \\\n            '\\$INSTDIR\\\\\\\\assets\\\\\\\\gui\\\\\\\\sunshine-gui.exe' '' '\\$INSTDIR\\\\\\\\assets\\\\\\\\gui\\\\\\\\sunshine-gui.exe' 0\n\n        ; 工具文件夹快捷方式 - 使用主程序图标\n        CreateShortCut '\\$SMPROGRAMS\\\\\\\\$STARTMENU_FOLDER\\\\\\\\Sunshine Tools.lnk' \\\n            '\\$INSTDIR\\\\\\\\tools' '' '\\$INSTDIR\\\\\\\\${CMAKE_PROJECT_NAME}.exe' 0\n\n        ; 创建桌面快捷方式 - 使用可执行文件的内嵌图标\n        CreateShortCut '\\$DESKTOP\\\\\\\\Sunshine.lnk' \\\n            '\\$INSTDIR\\\\\\\\${CMAKE_PROJECT_NAME}.exe' '--shortcut' '\\$INSTDIR\\\\\\\\${CMAKE_PROJECT_NAME}.exe' 0\n\n        ; 创建桌面快捷方式 - GUI管理工具\n        CreateShortCut '\\$DESKTOP\\\\\\\\Sunshine GUI.lnk' \\\n            '\\$INSTDIR\\\\\\\\assets\\\\\\\\gui\\\\\\\\sunshine-gui.exe' '' '\\$INSTDIR\\\\\\\\assets\\\\\\\\gui\\\\\\\\sunshine-gui.exe' 0\n        \")\n\nset(CPACK_NSIS_DELETE_ICONS_EXTRA\n        \"${CPACK_NSIS_DELETE_ICONS_EXTRA}\n        ; ----------------------------------------------------------------------\n        ; 安全删除快捷方式：防止符号链接攻击和路径遍历\n        ; \n        ; 安全分析：\n        ; 1. 符号链接攻击风险：如果攻击者在桌面/开始菜单创建符号链接，使用我们的快捷方式名称，\n        ;    删除时可能误删其他文件。但 NSIS 的 Delete 命令对于符号链接会删除链接本身，不会跟随。\n        ; 2. 路径遍历风险：我们使用固定的系统变量（$DESKTOP, $SMPROGRAMS），不接受外部输入，\n        ;    路径是硬编码的，降低了路径遍历风险。\n        ; 3. 文件类型验证：我们只删除预期的 .lnk 文件，文件名是固定的，降低了误删风险。\n        ; \n        ; 防护措施：\n        ; - 使用 IfFileExists 检查文件是否存在，避免删除不存在的文件\n        ; - 使用固定的系统路径变量，不接受外部输入\n        ; - 只删除预期的 .lnk 文件，文件名硬编码\n        ; - NSIS 的 Delete 命令会自动处理符号链接，只删除链接本身\n        ; ----------------------------------------------------------------------\n        \n        ; 删除开始菜单快捷方式（安全删除）\n        ; 注意：$MUI_TEMP 是 NSIS 内部变量，指向开始菜单文件夹，由安装程序控制\n        IfFileExists '\\$SMPROGRAMS\\\\\\\\$MUI_TEMP\\\\\\\\Sunshine.lnk' 0 +2\n        Delete '\\$SMPROGRAMS\\\\\\\\$MUI_TEMP\\\\\\\\Sunshine.lnk'\n        IfFileExists '\\$SMPROGRAMS\\\\\\\\$MUI_TEMP\\\\\\\\Sunshine GUI.lnk' 0 +2\n        Delete '\\$SMPROGRAMS\\\\\\\\$MUI_TEMP\\\\\\\\Sunshine GUI.lnk'\n        IfFileExists '\\$SMPROGRAMS\\\\\\\\$MUI_TEMP\\\\\\\\Sunshine Tools.lnk' 0 +2\n        Delete '\\$SMPROGRAMS\\\\\\\\$MUI_TEMP\\\\\\\\Sunshine Tools.lnk'\n        IfFileExists '\\$SMPROGRAMS\\\\\\\\$MUI_TEMP\\\\\\\\Sunshine Service.lnk' 0 +2\n        Delete '\\$SMPROGRAMS\\\\\\\\$MUI_TEMP\\\\\\\\Sunshine Service.lnk'\n        IfFileExists '\\$SMPROGRAMS\\\\\\\\$MUI_TEMP\\\\\\\\${CMAKE_PROJECT_NAME}.lnk' 0 +2\n        Delete '\\$SMPROGRAMS\\\\\\\\$MUI_TEMP\\\\\\\\${CMAKE_PROJECT_NAME}.lnk'\n        \n        ; 删除桌面快捷方式（安全删除）\n        ; 注意：$DESKTOP 是 NSIS 系统变量，指向当前用户的桌面目录\n        ;       如果攻击者创建符号链接，NSIS 的 Delete 会删除链接本身，不会跟随到目标\n        IfFileExists '\\$DESKTOP\\\\\\\\Sunshine.lnk' 0 +2\n        Delete '\\$DESKTOP\\\\\\\\Sunshine.lnk'\n        IfFileExists '\\$DESKTOP\\\\\\\\Sunshine GUI.lnk' 0 +2\n        Delete '\\$DESKTOP\\\\\\\\Sunshine GUI.lnk'\n        \")\n\n# ==============================================================================\n# Advanced Installation Features\n# ==============================================================================\n\n# Custom installation options\nset(CPACK_NSIS_COMPRESSOR \"lzma\")  # Better compression\nset(CPACK_NSIS_COMPRESSOR_OPTIONS \"/SOLID\")  # Solid compression for smaller file size\n\n# ==============================================================================\n# Support Links and Documentation\n# ==============================================================================\n\nset(CPACK_NSIS_HELP_LINK \"https://alkaidlab.com/sunshine/docs\")\nset(CPACK_NSIS_URL_INFO_ABOUT \"${CMAKE_PROJECT_HOMEPAGE_URL}\")\nset(CPACK_NSIS_CONTACT \"${CMAKE_PROJECT_HOMEPAGE_URL}/support\")\n\n# ==============================================================================\n# System Integration and Compatibility\n# ==============================================================================\n\n# Enable high DPI awareness for modern displays\nset(CPACK_NSIS_MANIFEST_DPI_AWARE true)\n\n# Request administrator privileges for proper installation\nset(CPACK_NSIS_REQUEST_EXECUTION_LEVEL \"admin\")\n\n# Custom installer appearance\nset(CPACK_NSIS_DISPLAY_NAME \"Sunshine Foundation Game Streaming Server v${CPACK_PACKAGE_VERSION}\")\nset(CPACK_NSIS_PACKAGE_NAME \"Sunshine\")\n"
  },
  {
    "path": "cmake/packaging/windows_wix.cmake",
    "content": "# WIX Packaging\n# see options at: https://cmake.org/cmake/help/latest/cpack_gen/wix.html\n\n# TODO: Replace nsis with wix\n"
  },
  {
    "path": "cmake/prep/build_version.cmake",
    "content": "# Check if env vars are defined before attempting to access them, variables will be defined even if blank\nif((DEFINED ENV{BRANCH}) AND (DEFINED ENV{BUILD_VERSION}) AND (DEFINED ENV{COMMIT}))  # cmake-lint: disable=W0106\n    if(($ENV{BRANCH} STREQUAL \"master\") AND (NOT $ENV{BUILD_VERSION} STREQUAL \"\"))\n        # If BRANCH is \"master\" and BUILD_VERSION is not empty, then we are building a master branch\n        MESSAGE(\"Got from CI master branch and version $ENV{BUILD_VERSION}\")\n        set(PROJECT_VERSION $ENV{BUILD_VERSION})\n        set(CMAKE_PROJECT_VERSION ${PROJECT_VERSION})  # cpack will use this to set the binary versions\n    elseif((DEFINED ENV{BRANCH}) AND (DEFINED ENV{COMMIT}))\n        # If BRANCH is set but not BUILD_VERSION we are building a PR, we gather only the commit hash\n        MESSAGE(\"Got from CI $ENV{BRANCH} branch and commit $ENV{COMMIT}\")\n        set(PROJECT_VERSION ${PROJECT_VERSION}.$ENV{COMMIT})\n    endif()\n    # Generate Sunshine Version based of the git tag\n    # https://github.com/nocnokneo/cmake-git-versioning-example/blob/master/LICENSE\nelse()\n    find_package(Git)\n    string(TIMESTAMP COMPILE_TIME \"%Y.%m%d.%H%M%S\")\n    set(PROJECT_VERSION ${COMPILE_TIME})\n    if(GIT_EXECUTABLE)\n        MESSAGE(\"${CMAKE_SOURCE_DIR}\")\n        get_filename_component(SRC_DIR \"${CMAKE_SOURCE_DIR}\" DIRECTORY)\n        #Get current Branch\n        execute_process(\n                COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD\n                OUTPUT_VARIABLE GIT_DESCRIBE_BRANCH\n                RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE\n                OUTPUT_STRIP_TRAILING_WHITESPACE\n        )\n        # Gather current commit\n        execute_process(\n                COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD\n                OUTPUT_VARIABLE GIT_DESCRIBE_VERSION\n                RESULT_VARIABLE GIT_DESCRIBE_ERROR_CODE\n                OUTPUT_STRIP_TRAILING_WHITESPACE\n        )\n        # Check if Dirty\n        execute_process(\n                COMMAND ${GIT_EXECUTABLE} diff --quiet --exit-code\n                RESULT_VARIABLE GIT_IS_DIRTY\n                OUTPUT_STRIP_TRAILING_WHITESPACE\n        )\n        if(NOT GIT_DESCRIBE_ERROR_CODE)\n            MESSAGE(\"Sunshine Branch: ${GIT_DESCRIBE_BRANCH}\")\n            if(NOT GIT_DESCRIBE_BRANCH STREQUAL \"master\")\n                set(PROJECT_VERSION ${PROJECT_VERSION}.${GIT_DESCRIBE_VERSION})\n                MESSAGE(\"Sunshine Version: ${GIT_DESCRIBE_VERSION}\")\n            endif()\n            if(GIT_IS_DIRTY)\n                set(PROJECT_VERSION ${PROJECT_VERSION}.杂鱼)\n                MESSAGE(\"Git tree is dirty!\")\n            endif()\n        else()\n            MESSAGE(ERROR \": Got git error while fetching tags: ${GIT_DESCRIBE_ERROR_CODE}\")\n        endif()\n    else()\n        MESSAGE(WARNING \": Git not found, cannot find git version\")\n    endif()\nendif()\n"
  },
  {
    "path": "cmake/prep/constants.cmake",
    "content": "# source assets will be installed from this directory\nset(SUNSHINE_SOURCE_ASSETS_DIR \"${CMAKE_SOURCE_DIR}/src_assets\")\n\n# enable system tray, we will disable this later if we cannot find the required package config on linux\nset(SUNSHINE_TRAY 1)\n"
  },
  {
    "path": "cmake/prep/init.cmake",
    "content": "if (WIN32)\nelseif (APPLE)\nelseif (UNIX)\n    include(GNUInstallDirs)\n\n    if(NOT DEFINED SUNSHINE_EXECUTABLE_PATH)\n        set(SUNSHINE_EXECUTABLE_PATH \"sunshine\")\n    endif()\n\n    if(SUNSHINE_BUILD_FLATPAK)\n        set(SUNSHINE_SERVICE_START_COMMAND \"ExecStart=flatpak run --command=sunshine ${PROJECT_FQDN}\")\n        set(SUNSHINE_SERVICE_STOP_COMMAND \"ExecStop=flatpak kill ${PROJECT_FQDN}\")\n    else()\n        set(SUNSHINE_SERVICE_START_COMMAND \"ExecStart=${SUNSHINE_EXECUTABLE_PATH}\")\n        set(SUNSHINE_SERVICE_STOP_COMMAND \"\")\n    endif()\nendif ()\n"
  },
  {
    "path": "cmake/prep/options.cmake",
    "content": "# Publisher Metadata\nset(SUNSHINE_PUBLISHER_NAME \"qiin2333\"\n        CACHE STRING \"The name of the publisher (not developer) of the application.\")\nset(SUNSHINE_PUBLISHER_WEBSITE \"https://alkaidlab.com\"\n        CACHE STRING \"The URL of the publisher's website.\")\nset(SUNSHINE_PUBLISHER_ISSUE_URL \"https://github.com/qiin2333/Sunshine/issues\"\n        CACHE STRING \"The URL of the publisher's support site or issue tracker.\n        If you provide a modified version of Sunshine, we kindly request that you use your own url.\")\n\noption(BUILD_DOCS \"Build documentation\" OFF)\noption(BUILD_TESTS \"Build tests\" OFF)\noption(NPM_OFFLINE \"Use offline npm packages. You must ensure packages are in your npm cache.\" OFF)\n\noption(BUILD_WERROR \"Enable -Werror flag.\" OFF)\n\n# if this option is set, the build will exit after configuring special package configuration files\noption(SUNSHINE_CONFIGURE_ONLY \"Configure special files only, then exit.\" OFF)\n\noption(SUNSHINE_ENABLE_TRAY \"Enable system tray icon. This option will be ignored on macOS.\" ON)\noption(SUNSHINE_REQUIRE_TRAY \"Require system tray icon. Fail the build if tray requirements are not met.\" ON)\n\noption(SUNSHINE_SYSTEM_NLOHMANN_JSON \"Use system installation of nlohmann_json rather than the submodule.\" OFF)\noption(SUNSHINE_SYSTEM_WAYLAND_PROTOCOLS \"Use system installation of wayland-protocols rather than the submodule.\" OFF)\n\nif(APPLE)\n    option(BOOST_USE_STATIC \"Use static boost libraries.\" OFF)\nelse()\n    option(BOOST_USE_STATIC \"Use static boost libraries.\" ON)\nendif()\n\noption(CUDA_FAIL_ON_MISSING \"Fail the build if CUDA is not found.\" ON)\noption(CUDA_INHERIT_COMPILE_OPTIONS\n        \"When building CUDA code, inherit compile options from the the main project. You may want to disable this if\n        your IDE throws errors about unknown flags after running cmake.\" ON)\n\nif(UNIX)\n    option(SUNSHINE_BUILD_HOMEBREW\n            \"Enable a Homebrew build.\" OFF)\n    option(SUNSHINE_CONFIGURE_HOMEBREW\n            \"Configure Homebrew formula. Recommended to use with SUNSHINE_CONFIGURE_ONLY\" OFF)\nendif()\n\nif(APPLE)\n    option(SUNSHINE_CONFIGURE_PORTFILE\n            \"Configure macOS Portfile. Recommended to use with SUNSHINE_CONFIGURE_ONLY\" OFF)\n    option(SUNSHINE_PACKAGE_MACOS\n            \"Should only be used when creating a macOS package/dmg.\" OFF)\nelseif(UNIX)  # Linux\n    option(SUNSHINE_BUILD_APPIMAGE\n            \"Enable an AppImage build.\" OFF)\n    option(SUNSHINE_BUILD_FLATPAK\n            \"Enable a Flatpak build.\" OFF)\n    option(SUNSHINE_CONFIGURE_PKGBUILD\n            \"Configure files required for AUR. Recommended to use with SUNSHINE_CONFIGURE_ONLY\" OFF)\n    option(SUNSHINE_CONFIGURE_FLATPAK_MAN\n            \"Configure manifest file required for Flatpak build. Recommended to use with SUNSHINE_CONFIGURE_ONLY\" OFF)\n\n    # Linux capture methods\n    option(SUNSHINE_ENABLE_CUDA\n            \"Enable cuda specific code.\" ON)\n    option(SUNSHINE_ENABLE_DRM\n            \"Enable KMS grab if available.\" ON)\n    option(SUNSHINE_ENABLE_VAAPI\n            \"Enable building vaapi specific code.\" ON)\n    option(SUNSHINE_ENABLE_WAYLAND\n            \"Enable building wayland specific code.\" ON)\n    option(SUNSHINE_ENABLE_X11\n            \"Enable X11 grab if available.\" ON)\nendif()\n"
  },
  {
    "path": "cmake/prep/special_package_configuration.cmake",
    "content": "if(UNIX)\n    if(${SUNSHINE_CONFIGURE_HOMEBREW})\n        configure_file(packaging/sunshine.rb sunshine.rb @ONLY)\n    endif()\nendif()\n\nif(APPLE)\n    if(${SUNSHINE_CONFIGURE_PORTFILE})\n        configure_file(packaging/macos/Portfile Portfile @ONLY)\n    endif()\nelseif(UNIX)\n    # configure the .desktop file\n    set(SUNSHINE_DESKTOP_ICON \"sunshine.svg\")\n    if(${SUNSHINE_BUILD_APPIMAGE})\n        configure_file(packaging/linux/AppImage/sunshine.desktop sunshine.desktop @ONLY)\n    elseif(${SUNSHINE_BUILD_FLATPAK})\n        set(SUNSHINE_DESKTOP_ICON \"${PROJECT_FQDN}.svg\")\n        configure_file(packaging/linux/flatpak/sunshine.desktop sunshine.desktop @ONLY)\n        configure_file(packaging/linux/flatpak/sunshine_kms.desktop sunshine_kms.desktop @ONLY)\n        configure_file(packaging/linux/sunshine_terminal.desktop sunshine_terminal.desktop @ONLY)\n        configure_file(packaging/linux/flatpak/${PROJECT_FQDN}.metainfo.xml\n                ${PROJECT_FQDN}.metainfo.xml @ONLY)\n    else()\n        configure_file(packaging/linux/sunshine.desktop sunshine.desktop @ONLY)\n        configure_file(packaging/linux/sunshine_terminal.desktop sunshine_terminal.desktop @ONLY)\n    endif()\n\n    # configure metadata file\n    configure_file(packaging/linux/sunshine.appdata.xml sunshine.appdata.xml @ONLY)\n\n    # configure service\n    configure_file(packaging/linux/sunshine.service.in sunshine.service @ONLY)\n\n    # configure the arch linux pkgbuild\n    if(${SUNSHINE_CONFIGURE_PKGBUILD})\n        configure_file(packaging/linux/Arch/PKGBUILD PKGBUILD @ONLY)\n        configure_file(packaging/linux/Arch/sunshine.install sunshine.install @ONLY)\n    endif()\n\n    # configure the flatpak manifest\n    if(${SUNSHINE_CONFIGURE_FLATPAK_MAN})\n        configure_file(packaging/linux/flatpak/${PROJECT_FQDN}.yml ${PROJECT_FQDN}.yml @ONLY)\n        file(COPY packaging/linux/flatpak/deps/ DESTINATION ${CMAKE_BINARY_DIR})\n        file(COPY packaging/linux/flatpak/modules DESTINATION ${CMAKE_BINARY_DIR})\n    endif()\nendif()\n\n# return if configure only is set\nif(${SUNSHINE_CONFIGURE_ONLY})\n    # message\n    message(STATUS \"SUNSHINE_CONFIGURE_ONLY: ON, exiting...\")\n    set(END_BUILD ON)\nelse()\n    set(END_BUILD OFF)\nendif()\n"
  },
  {
    "path": "cmake/targets/common.cmake",
    "content": "# common target definitions\n# this file will also load platform specific macros\n\nadd_executable(sunshine ${SUNSHINE_TARGET_FILES})\nforeach(dep ${SUNSHINE_TARGET_DEPENDENCIES})\n    add_dependencies(sunshine ${dep})  # compile these before sunshine\nendforeach()\n\n# platform specific target definitions\nif(WIN32)\n    include(${CMAKE_MODULE_PATH}/targets/windows.cmake)\nelseif(UNIX)\n    include(${CMAKE_MODULE_PATH}/targets/unix.cmake)\n\n    if(APPLE)\n        include(${CMAKE_MODULE_PATH}/targets/macos.cmake)\n    else()\n        include(${CMAKE_MODULE_PATH}/targets/linux.cmake)\n    endif()\nendif()\n\n# todo - is this necessary? ... for anything except linux?\nif(NOT DEFINED CMAKE_CUDA_STANDARD)\n    set(CMAKE_CUDA_STANDARD 17)\n    set(CMAKE_CUDA_STANDARD_REQUIRED ON)\nendif()\n\ntarget_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS})\ntarget_compile_definitions(sunshine PUBLIC ${SUNSHINE_DEFINITIONS})\nset_target_properties(sunshine PROPERTIES CXX_STANDARD 23\n        VERSION ${PROJECT_VERSION}\n        SOVERSION ${PROJECT_VERSION_MAJOR})\n\n# CLion complains about unknown flags after running cmake, and cannot add symbols to the index for cuda files\nif(CUDA_INHERIT_COMPILE_OPTIONS)\n    foreach(flag IN LISTS SUNSHINE_COMPILE_OPTIONS)\n        list(APPEND SUNSHINE_COMPILE_OPTIONS_CUDA \"$<$<COMPILE_LANGUAGE:CUDA>:--compiler-options=${flag}>\")\n    endforeach()\nendif()\n\ntarget_compile_options(sunshine PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${SUNSHINE_COMPILE_OPTIONS}>;$<$<COMPILE_LANGUAGE:CUDA>:${SUNSHINE_COMPILE_OPTIONS_CUDA};-std=c++17>)  # cmake-lint: disable=C0301\n\n# Homebrew build fails the vite build if we set these environment variables\nif(${SUNSHINE_BUILD_HOMEBREW})\n    set(NPM_SOURCE_ASSETS_DIR \"\")\n    set(NPM_ASSETS_DIR \"\")\n    set(NPM_BUILD_HOMEBREW \"true\")\nelse()\n    set(NPM_SOURCE_ASSETS_DIR ${SUNSHINE_SOURCE_ASSETS_DIR})\n    set(NPM_ASSETS_DIR ${CMAKE_BINARY_DIR})\n    set(NPM_BUILD_HOMEBREW \"\")\nendif()\n\n#WebUI build\noption(BUILD_WEB_UI \"Build the web UI via npm\" ON)\n\nif(BUILD_WEB_UI)\n    find_program(NPM npm REQUIRED)\n\n    if (NPM_OFFLINE)\n        set(NPM_INSTALL_FLAGS \"--offline\")\n    else()\n        set(NPM_INSTALL_FLAGS \"\")\n    endif()\n\n    add_custom_target(web-ui ALL\n            WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n            COMMENT \"Installing NPM Dependencies and Building the Web UI\"\n            COMMAND \"$<$<BOOL:${WIN32}>:cmd;/C>\" \"${NPM}\" install ${NPM_INSTALL_FLAGS}\n            COMMAND \"${CMAKE_COMMAND}\" -E env \"SUNSHINE_BUILD_HOMEBREW=${NPM_BUILD_HOMEBREW}\" \"SUNSHINE_SOURCE_ASSETS_DIR=${NPM_SOURCE_ASSETS_DIR}\" \"SUNSHINE_ASSETS_DIR=${NPM_ASSETS_DIR}\" \"$<$<BOOL:${WIN32}>:cmd;/C>\" \"${NPM}\" run build  # cmake-lint: disable=C0301\n            COMMAND_EXPAND_LISTS\n            VERBATIM)\nendif()\n\n# docs\nif(BUILD_DOCS)\n    add_subdirectory(third-party/doxyconfig docs)\nendif()\n\n# tests\nif(BUILD_TESTS)\n    add_subdirectory(tests)\nendif()\n\n# custom compile flags, must be after adding tests\n\nif (NOT BUILD_TESTS)\n    set(TEST_DIR \"\")\nelse()\n    set(TEST_DIR \"${CMAKE_SOURCE_DIR}/tests\")\nendif()\n\n# src/upnp\nset_source_files_properties(\"${CMAKE_SOURCE_DIR}/src/upnp.cpp\"\n        DIRECTORY \"${CMAKE_SOURCE_DIR}\" \"${TEST_DIR}\"\n        PROPERTIES COMPILE_FLAGS -Wno-pedantic)\n\n# third-party/nanors\nset_source_files_properties(\"${CMAKE_SOURCE_DIR}/src/rswrapper.c\"\n        DIRECTORY \"${CMAKE_SOURCE_DIR}\" \"${TEST_DIR}\"\n        PROPERTIES COMPILE_FLAGS \"-ftree-vectorize -funroll-loops\")\n\n# third-party/ViGEmClient\nset(VIGEM_COMPILE_FLAGS \"\")\nstring(APPEND VIGEM_COMPILE_FLAGS \"-Wno-unknown-pragmas \")\nstring(APPEND VIGEM_COMPILE_FLAGS \"-Wno-misleading-indentation \")\nstring(APPEND VIGEM_COMPILE_FLAGS \"-Wno-class-memaccess \")\nstring(APPEND VIGEM_COMPILE_FLAGS \"-Wno-unused-function \")\nstring(APPEND VIGEM_COMPILE_FLAGS \"-Wno-unused-variable \")\nset_source_files_properties(\"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp\"\n        DIRECTORY \"${CMAKE_SOURCE_DIR}\" \"${TEST_DIR}\"\n        PROPERTIES\n        COMPILE_DEFINITIONS \"UNICODE=1;ERROR_INVALID_DEVICE_OBJECT_PARAMETER=650\"\n        COMPILE_FLAGS ${VIGEM_COMPILE_FLAGS})\n\n# src/nvhttp\nstring(TOUPPER \"x${CMAKE_BUILD_TYPE}\" BUILD_TYPE)\nif(\"${BUILD_TYPE}\" STREQUAL \"XDEBUG\")\n    if(WIN32)\n        if (NOT BUILD_TESTS)\n            set_source_files_properties(\"${CMAKE_SOURCE_DIR}/src/nvhttp.cpp\"\n                    DIRECTORY \"${CMAKE_SOURCE_DIR}\"\n                    PROPERTIES COMPILE_FLAGS -O2)\n        else()\n            set_source_files_properties(\"${CMAKE_SOURCE_DIR}/src/nvhttp.cpp\"\n                    DIRECTORY \"${CMAKE_SOURCE_DIR}\" \"${CMAKE_SOURCE_DIR}/tests\"\n                    PROPERTIES COMPILE_FLAGS -O2)\n        endif()\n    endif()\nelse()\n    add_definitions(-DNDEBUG)\nendif()\n"
  },
  {
    "path": "cmake/targets/linux.cmake",
    "content": "# linux specific target definitions\n"
  },
  {
    "path": "cmake/targets/macos.cmake",
    "content": "# macos specific target definitions\ntarget_link_options(sunshine PRIVATE LINKER:-sectcreate,__TEXT,__info_plist,${APPLE_PLIST_FILE})\n# Tell linker to dynamically load these symbols at runtime, in case they're unavailable:\ntarget_link_options(sunshine PRIVATE -Wl,-U,_CGPreflightScreenCaptureAccess -Wl,-U,_CGRequestScreenCaptureAccess)\n"
  },
  {
    "path": "cmake/targets/unix.cmake",
    "content": "# unix specific target definitions\n# put anything here that applies to both linux and macos\n"
  },
  {
    "path": "cmake/targets/windows.cmake",
    "content": "# windows specific target definitions\nset_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1)\nset(CMAKE_FIND_LIBRARY_SUFFIXES \".dll\")\nfind_library(ZLIB ZLIB1)\nlist(APPEND SUNSHINE_EXTERNAL_LIBRARIES\n        Windowsapp.lib\n        Wtsapi32.lib)\n\n# GUI build (optional — CI uses pre-built binary from GUI repo releases)\n# For local development: ninja -C build sunshine-control-panel\nfind_program(NPM npm)\nfind_program(CARGO cargo)\n\nif(NPM AND CARGO)\n  add_custom_target(sunshine-control-panel\n          WORKING_DIRECTORY \"${SUNSHINE_SOURCE_ASSETS_DIR}/common/sunshine-control-panel\"\n          COMMENT \"Building Sunshine Control Panel (Tauri GUI)\"\n          COMMAND ${CMAKE_COMMAND} -E echo \"Installing npm dependencies...\"\n          COMMAND ${NPM} install\n          COMMAND ${CMAKE_COMMAND} -E echo \"Building frontend with Vite...\"\n          COMMAND ${NPM} run build:renderer\n          COMMAND ${CMAKE_COMMAND} -E echo \"Building Tauri backend with Cargo...\"\n          COMMAND ${CARGO} build --manifest-path src-tauri/Cargo.toml --release\n          USES_TERMINAL)\nelse()\n  message(STATUS \"npm/cargo not found — sunshine-control-panel target disabled (GUI will be fetched from release)\")\nendif()\n"
  },
  {
    "path": "codecov.yml",
    "content": "---\ncodecov:\n  branch: master\n\ncoverage:\n  status:\n    project:\n      default:\n        target: auto\n        threshold: 10%\n\ncomment:\n  layout: \"diff, flags, files\"\n  behavior: default\n  require_changes: false  # if true: only post the comment if coverage changes\n\nignore:\n  - \"tests\"\n  - \"third-party\"\n"
  },
  {
    "path": "crowdin.yml",
    "content": "---\n\"base_path\": \".\"\n\"base_url\": \"https://api.crowdin.com\"  # optional (for Crowdin Enterprise only)\n\"preserve_hierarchy\": true  # false will flatten tree on crowdin, but doesn't work with dest option\n\"pull_request_title\": \"chore(l10n): update translations\"\n\"pull_request_labels\": [\n  \"crowdin\",\n  \"l10n\"\n]\n\n\"files\": [\n  {\n    \"source\": \"/locale/*.po\",\n    \"dest\": \"/%original_file_name%\",\n    \"translation\": \"/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%\",\n    \"languages_mapping\": {\n      \"two_letters_code\": {\n        # map non-two letter codes here, left side is crowdin designation, right side is babel designation\n        \"en-GB\": \"en_GB\",\n        \"en-US\": \"en_US\",\n        \"pt-BR\": \"pt_BR\",\n        \"zh-TW\": \"zh_TW\"\n      }\n    },\n    \"update_option\": \"update_as_unapproved\"\n  },\n  {\n    \"source\": \"/src_assets/common/assets/web/public/assets/locale/en.json\",\n    \"dest\": \"/sunshine.json\",\n    \"translation\": \"/src_assets/common/assets/web/public/assets/locale/%two_letters_code%.%file_extension%\",\n    \"update_option\": \"update_as_unapproved\"\n  }\n]\n"
  },
  {
    "path": "docker/archlinux.dockerfile",
    "content": "# syntax=docker/dockerfile:1\n# artifacts: true\n# platforms: linux/amd64\n# archlinux does not have an arm64 base image\n# no-cache-filters: artifacts,sunshine\nARG BASE=archlinux/archlinux\nARG TAG=base-devel\nFROM ${BASE}:${TAG} AS sunshine-base\n\n# Update keyring to avoid signature errors, and update system\nRUN <<_DEPS\n#!/bin/bash\nset -e\npacman -Syy --disable-download-timeout --needed --noconfirm \\\n  archlinux-keyring\npacman -Syu --disable-download-timeout --noconfirm\npacman -Scc --noconfirm\n_DEPS\n\nFROM sunshine-base AS sunshine-build\n\nARG BRANCH\nARG BUILD_VERSION\nARG COMMIT\nARG CLONE_URL\n# note: BUILD_VERSION may be blank\n\nENV BRANCH=${BRANCH}\nENV BUILD_VERSION=${BUILD_VERSION}\nENV COMMIT=${COMMIT}\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# hadolint ignore=SC2016\nRUN <<_SETUP\n#!/bin/bash\nset -e\n\n# Setup builder user, arch prevents running makepkg as root\nuseradd -m builder\necho 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers\n\n# patch the build flags\nsed -i 's,#MAKEFLAGS=\"-j2\",MAKEFLAGS=\"-j$(nproc)\",g' /etc/makepkg.conf\n\n# install dependencies\npacman -Syu --disable-download-timeout --needed --noconfirm \\\n  base-devel \\\n  cmake \\\n  cuda \\\n  git \\\n  namcap \\\n  xorg-server-xvfb\npacman -Scc --noconfirm\n_SETUP\n\n# Setup builder user\nUSER builder\n\n# copy repository\nWORKDIR /build/sunshine/\nCOPY --link .. .\n\n# setup build directory\nWORKDIR /build/sunshine/build\n\n# configure PKGBUILD file\nRUN <<_MAKE\n#!/bin/bash\nset -e\nif [[ \"${BUILD_VERSION}\" == '' ]]; then\n  sub_version=\".r${COMMIT}\"\nelse\n  sub_version=\"\"\nfi\ncmake \\\n  -DSUNSHINE_CONFIGURE_PKGBUILD=ON \\\n  -DSUNSHINE_SUB_VERSION=\"${sub_version}\" \\\n  -DGITHUB_CLONE_URL=\"${CLONE_URL}\" \\\n  -DGITHUB_BRANCH=${BRANCH} \\\n  -DGITHUB_BUILD_VERSION=${BUILD_VERSION} \\\n  -DGITHUB_COMMIT=\"${COMMIT}\" \\\n  -DSUNSHINE_CONFIGURE_ONLY=ON \\\n  /build/sunshine\n_MAKE\n\nWORKDIR /build/sunshine/pkg\nRUN <<_PACKAGE\nmv /build/sunshine/build/PKGBUILD .\nmv /build/sunshine/build/sunshine.install .\nmakepkg --printsrcinfo > .SRCINFO\n_PACKAGE\n\n# create a PKGBUILD archive\nUSER root\nRUN <<_REPO\n#!/bin/bash\nset -e\ntar -czf /build/sunshine/sunshine.pkg.tar.gz .\n_REPO\n\n# namcap and build PKGBUILD file\nUSER builder\nRUN <<_PKGBUILD\n#!/bin/bash\nset -e\n# shellcheck source=/dev/null\nsource /etc/profile  # ensure cuda is in the PATH\nexport DISPLAY=:1\nXvfb ${DISPLAY} -screen 0 1024x768x24 &\nnamcap -i PKGBUILD\nmakepkg -si --noconfirm\nrm -f /build/sunshine/pkg/sunshine-debug*.pkg.tar.zst\nls -a\n_PKGBUILD\n\nFROM scratch AS artifacts\n\nCOPY --link --from=sunshine-build /build/sunshine/pkg/sunshine*.pkg.tar.zst /sunshine.pkg.tar.zst\nCOPY --link --from=sunshine-build /build/sunshine/sunshine.pkg.tar.gz /sunshine.pkg.tar.gz\n\nFROM sunshine-base AS sunshine\n\nCOPY --link --from=artifacts /sunshine.pkg.tar.zst /\n\n# install sunshine\nRUN <<_INSTALL_SUNSHINE\n#!/bin/bash\nset -e\npacman -U --disable-download-timeout --needed --noconfirm \\\n  /sunshine.pkg.tar.zst\npacman -Scc --noconfirm\n_INSTALL_SUNSHINE\n\n# network setup\nEXPOSE 47984-47990/tcp\nEXPOSE 48010\nEXPOSE 47998-48000/udp\n\n# setup user\nARG PGID=1000\nENV PGID=${PGID}\nARG PUID=1000\nENV PUID=${PUID}\nENV TZ=\"UTC\"\nARG UNAME=lizard\nENV UNAME=${UNAME}\n\nENV HOME=/home/$UNAME\n\n# setup user\nRUN <<_SETUP_USER\n#!/bin/bash\nset -e\ngroupadd -f -g \"${PGID}\" \"${UNAME}\"\nuseradd -lm -d ${HOME} -s /bin/bash -g \"${PGID}\" -u \"${PUID}\" \"${UNAME}\"\nmkdir -p ${HOME}/.config/sunshine\nln -s ${HOME}/.config/sunshine /config\nchown -R ${UNAME} ${HOME}\n_SETUP_USER\n\nUSER ${UNAME}\nWORKDIR ${HOME}\n\n# entrypoint\nENTRYPOINT [\"/usr/bin/sunshine\"]\n"
  },
  {
    "path": "docker/clion-toolchain.dockerfile",
    "content": "# syntax=docker/dockerfile:1\n# artifacts: false\n# platforms: linux/amd64\n# platforms_pr: linux/amd64\n# no-cache-filters: toolchain-base,toolchain\nARG BASE=ubuntu\nARG TAG=22.04\nFROM ${BASE}:${TAG} AS toolchain-base\n\nENV DEBIAN_FRONTEND=noninteractive\n\nFROM toolchain-base AS toolchain\n\nARG TARGETPLATFORM\nRUN echo \"target_platform: ${TARGETPLATFORM}\"\n\nENV DISPLAY=:0\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# install dependencies\n# hadolint ignore=SC1091\nRUN <<_DEPS\n#!/bin/bash\nset -e\napt-get update -y\napt-get install -y --no-install-recommends \\\n  build-essential \\\n  cmake=3.22.* \\\n  ca-certificates \\\n  doxygen \\\n  gcc=4:11.2.* \\\n  g++=4:11.2.* \\\n  gdb \\\n  git \\\n  graphviz \\\n  libayatana-appindicator3-dev \\\n  libcap-dev \\\n  libcurl4-openssl-dev \\\n  libdrm-dev \\\n  libevdev-dev \\\n  libminiupnpc-dev \\\n  libnotify-dev \\\n  libnuma-dev \\\n  libopus-dev \\\n  libpulse-dev \\\n  libssl-dev \\\n  libva-dev \\\n  libwayland-dev \\\n  libx11-dev \\\n  libxcb-shm0-dev \\\n  libxcb-xfixes0-dev \\\n  libxcb1-dev \\\n  libxfixes-dev \\\n  libxrandr-dev \\\n  libxtst-dev \\\n  udev \\\n  wget \\\n  x11-xserver-utils \\\n  xvfb\napt-get clean\nrm -rf /var/lib/apt/lists/*\n\n# Install Node\nwget --max-redirect=0 -qO- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash\nsource \"$HOME/.nvm/nvm.sh\"\nnvm install node\nnvm use node\nnvm alias default node\n_DEPS\n\n# install cuda\nWORKDIR /build/cuda\n# versions: https://developer.nvidia.com/cuda-toolkit-archive\nENV CUDA_VERSION=\"11.8.0\"\nENV CUDA_BUILD=\"520.61.05\"\n# hadolint ignore=SC3010\nRUN <<_INSTALL_CUDA\n#!/bin/bash\nset -e\ncuda_prefix=\"https://developer.download.nvidia.com/compute/cuda/\"\ncuda_suffix=\"\"\nif [[ \"${TARGETPLATFORM}\" == 'linux/arm64' ]]; then\n  cuda_suffix=\"_sbsa\"\nfi\nurl=\"${cuda_prefix}${CUDA_VERSION}/local_installers/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux${cuda_suffix}.run\"\necho \"cuda url: ${url}\"\nwget \"$url\" --progress=bar:force:noscroll -q --show-progress -O ./cuda.run\nchmod a+x ./cuda.run\n./cuda.run --silent --toolkit --toolkitpath=/usr/local --no-opengl-libs --no-man-page --no-drm\nrm ./cuda.run\n_INSTALL_CUDA\n\nWORKDIR /\n# Write a shell script that starts Xvfb and then runs a shell\nRUN <<_ENTRYPOINT\n#!/bin/bash\nset -e\ncat <<EOF > /entrypoint.sh\n#!/bin/bash\nXvfb ${DISPLAY} -screen 0 1024x768x24 &\nif [ \"\\$#\" -eq 0 ]; then\n  exec \"/bin/bash\"\nelse\n  exec \"\\$@\"\nfi\nEOF\n\n# Make the script executable\nchmod +x /entrypoint.sh\n\n# Note about CLion\necho \"ATTENTION: CLion will override the entrypoint, you can disable this in the toolchain settings\"\n_ENTRYPOINT\n\n# Use the shell script as the entrypoint\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "docker/debian-bookworm.dockerfile",
    "content": "# syntax=docker/dockerfile:1\n# artifacts: true\n# platforms: linux/amd64,linux/arm64/v8\n# platforms_pr: linux/amd64\n# no-cache-filters: sunshine-base,artifacts,sunshine\nARG BASE=debian\nARG TAG=bookworm\nFROM ${BASE}:${TAG} AS sunshine-base\n\nENV DEBIAN_FRONTEND=noninteractive\n\nFROM sunshine-base AS sunshine-build\n\nARG BRANCH\nARG BUILD_VERSION\nARG COMMIT\n# note: BUILD_VERSION may be blank\n\nENV BRANCH=${BRANCH}\nENV BUILD_VERSION=${BUILD_VERSION}\nENV COMMIT=${COMMIT}\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# copy repository\nWORKDIR /build/sunshine/\nCOPY --link .. .\n\n# cmake and cpack\nRUN <<_BUILD\n#!/bin/bash\nset -e\nchmod +x ./scripts/linux_build.sh\n./scripts/linux_build.sh \\\n  --publisher-name='LizardByte' \\\n  --publisher-website='https://app.lizardbyte.dev' \\\n  --publisher-issue-url='https://app.lizardbyte.dev/support' \\\n  --sudo-off\napt-get clean\nrm -rf /var/lib/apt/lists/*\n_BUILD\n\n# run tests\nWORKDIR /build/sunshine/build/tests\n# hadolint ignore=SC1091\nRUN <<_TEST\n#!/bin/bash\nset -e\nexport DISPLAY=:1\nXvfb ${DISPLAY} -screen 0 1024x768x24 &\n./test_sunshine --gtest_color=yes\n_TEST\n\nFROM scratch AS artifacts\nARG BASE\nARG TAG\nARG TARGETARCH\nCOPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /sunshine-${BASE}-${TAG}-${TARGETARCH}.deb\n\nFROM sunshine-base AS sunshine\n\n# copy deb from builder\nCOPY --link --from=artifacts /sunshine*.deb /sunshine.deb\n\n# install sunshine\nRUN <<_INSTALL_SUNSHINE\n#!/bin/bash\nset -e\napt-get update -y\napt-get install -y --no-install-recommends /sunshine.deb\napt-get clean\nrm -rf /var/lib/apt/lists/*\n_INSTALL_SUNSHINE\n\n# network setup\nEXPOSE 47984-47990/tcp\nEXPOSE 48010\nEXPOSE 47998-48000/udp\n\n# setup user\nARG PGID=1000\nENV PGID=${PGID}\nARG PUID=1000\nENV PUID=${PUID}\nENV TZ=\"UTC\"\nARG UNAME=lizard\nENV UNAME=${UNAME}\n\nENV HOME=/home/$UNAME\n\n# setup user\nRUN <<_SETUP_USER\n#!/bin/bash\nset -e\ngroupadd -f -g \"${PGID}\" \"${UNAME}\"\nuseradd -lm -d ${HOME} -s /bin/bash -g \"${PGID}\" -u \"${PUID}\" \"${UNAME}\"\nmkdir -p ${HOME}/.config/sunshine\nln -s ${HOME}/.config/sunshine /config\nchown -R ${UNAME} ${HOME}\n_SETUP_USER\n\nUSER ${UNAME}\nWORKDIR ${HOME}\n\n# entrypoint\nENTRYPOINT [\"/usr/bin/sunshine\"]\n"
  },
  {
    "path": "docker/fedora-39.dockerfile",
    "content": "# syntax=docker/dockerfile:1\n# artifacts: true\n# platforms: linux/amd64\n# platforms_pr: linux/amd64\n# no-cache-filters: sunshine-base,artifacts,sunshine\nARG BASE=fedora\nARG TAG=39\nFROM ${BASE}:${TAG} AS sunshine-base\n\nFROM sunshine-base AS sunshine-build\n\nARG BRANCH\nARG BUILD_VERSION\nARG COMMIT\n# note: BUILD_VERSION may be blank\n\nENV BRANCH=${BRANCH}\nENV BUILD_VERSION=${BUILD_VERSION}\nENV COMMIT=${COMMIT}\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# copy repository\nWORKDIR /build/sunshine/\nCOPY --link .. .\n\n# cmake and cpack\nRUN <<_BUILD\n#!/bin/bash\nset -e\nchmod +x ./scripts/linux_build.sh\n./scripts/linux_build.sh \\\n  --publisher-name='LizardByte' \\\n  --publisher-website='https://app.lizardbyte.dev' \\\n  --publisher-issue-url='https://app.lizardbyte.dev/support' \\\n  --sudo-off\ndnf clean all\nrm -rf /var/cache/yum\n_BUILD\n\n# run tests\nWORKDIR /build/sunshine/build/tests\n# hadolint ignore=SC1091\nRUN <<_TEST\n#!/bin/bash\nset -e\nexport DISPLAY=:1\nXvfb ${DISPLAY} -screen 0 1024x768x24 &\n./test_sunshine --gtest_color=yes\n_TEST\n\nFROM scratch AS artifacts\nARG BASE\nARG TAG\nARG TARGETARCH\nCOPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.rpm /sunshine-${BASE}-${TAG}-${TARGETARCH}.rpm\n\nFROM sunshine-base AS sunshine\n\n# copy deb from builder\nCOPY --link --from=artifacts /sunshine*.rpm /sunshine.rpm\n\n# install sunshine\nRUN <<_INSTALL_SUNSHINE\n#!/bin/bash\nset -e\ndnf -y update\ndnf -y install /sunshine.rpm\ndnf clean all\nrm -rf /var/cache/yum\n_INSTALL_SUNSHINE\n\n# network setup\nEXPOSE 47984-47990/tcp\nEXPOSE 48010\nEXPOSE 47998-48000/udp\n\n# setup user\nARG PGID=1000\nENV PGID=${PGID}\nARG PUID=1000\nENV PUID=${PUID}\nENV TZ=\"UTC\"\nARG UNAME=lizard\nENV UNAME=${UNAME}\n\nENV HOME=/home/$UNAME\n\n# setup user\nRUN <<_SETUP_USER\n#!/bin/bash\nset -e\ngroupadd -f -g \"${PGID}\" \"${UNAME}\"\nuseradd -lm -d ${HOME} -s /bin/bash -g \"${PGID}\" -u \"${PUID}\" \"${UNAME}\"\nmkdir -p ${HOME}/.config/sunshine\nln -s ${HOME}/.config/sunshine /config\nchown -R ${UNAME} ${HOME}\n_SETUP_USER\n\nUSER ${UNAME}\nWORKDIR ${HOME}\n\n# entrypoint\nENTRYPOINT [\"/usr/bin/sunshine\"]\n"
  },
  {
    "path": "docker/fedora-40.dockerfile",
    "content": "# syntax=docker/dockerfile:1\n# artifacts: true\n# platforms: linux/amd64\n# platforms_pr: linux/amd64\n# no-cache-filters: sunshine-base,artifacts,sunshine\nARG BASE=fedora\nARG TAG=40\nFROM ${BASE}:${TAG} AS sunshine-base\n\nFROM sunshine-base AS sunshine-build\n\nARG BRANCH\nARG BUILD_VERSION\nARG COMMIT\n# note: BUILD_VERSION may be blank\n\nENV BRANCH=${BRANCH}\nENV BUILD_VERSION=${BUILD_VERSION}\nENV COMMIT=${COMMIT}\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# copy repository\nWORKDIR /build/sunshine/\nCOPY --link .. .\n\n# cmake and cpack\nRUN <<_BUILD\n#!/bin/bash\nset -e\nchmod +x ./scripts/linux_build.sh\n./scripts/linux_build.sh \\\n  --publisher-name='LizardByte' \\\n  --publisher-website='https://app.lizardbyte.dev' \\\n  --publisher-issue-url='https://app.lizardbyte.dev/support' \\\n  --sudo-off\ndnf clean all\nrm -rf /var/cache/yum\n_BUILD\n\n# run tests\nWORKDIR /build/sunshine/build/tests\n# hadolint ignore=SC1091\nRUN <<_TEST\n#!/bin/bash\nset -e\nexport DISPLAY=:1\nXvfb ${DISPLAY} -screen 0 1024x768x24 &\n./test_sunshine --gtest_color=yes\n_TEST\n\nFROM scratch AS artifacts\nARG BASE\nARG TAG\nARG TARGETARCH\nCOPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.rpm /sunshine-${BASE}-${TAG}-${TARGETARCH}.rpm\n\nFROM sunshine-base AS sunshine\n\n# copy deb from builder\nCOPY --link --from=artifacts /sunshine*.rpm /sunshine.rpm\n\n# install sunshine\nRUN <<_INSTALL_SUNSHINE\n#!/bin/bash\nset -e\ndnf -y update\ndnf -y install /sunshine.rpm\ndnf clean all\nrm -rf /var/cache/yum\n_INSTALL_SUNSHINE\n\n# network setup\nEXPOSE 47984-47990/tcp\nEXPOSE 48010\nEXPOSE 47998-48000/udp\n\n# setup user\nARG PGID=1000\nENV PGID=${PGID}\nARG PUID=1000\nENV PUID=${PUID}\nENV TZ=\"UTC\"\nARG UNAME=lizard\nENV UNAME=${UNAME}\n\nENV HOME=/home/$UNAME\n\n# setup user\nRUN <<_SETUP_USER\n#!/bin/bash\nset -e\ngroupadd -f -g \"${PGID}\" \"${UNAME}\"\nuseradd -lm -d ${HOME} -s /bin/bash -g \"${PGID}\" -u \"${PUID}\" \"${UNAME}\"\nmkdir -p ${HOME}/.config/sunshine\nln -s ${HOME}/.config/sunshine /config\nchown -R ${UNAME} ${HOME}\n_SETUP_USER\n\nUSER ${UNAME}\nWORKDIR ${HOME}\n\n# entrypoint\nENTRYPOINT [\"/usr/bin/sunshine\"]\n"
  },
  {
    "path": "docker/ubuntu-22.04.dockerfile",
    "content": "# syntax=docker/dockerfile:1\n# artifacts: true\n# platforms: linux/amd64,linux/arm64/v8\n# platforms_pr: linux/amd64\n# no-cache-filters: sunshine-base,artifacts,sunshine\nARG BASE=ubuntu\nARG TAG=22.04\nFROM ${BASE}:${TAG} AS sunshine-base\n\nENV DEBIAN_FRONTEND=noninteractive\n\nFROM sunshine-base AS sunshine-build\n\nARG BRANCH\nARG BUILD_VERSION\nARG COMMIT\n# note: BUILD_VERSION may be blank\n\nENV BRANCH=${BRANCH}\nENV BUILD_VERSION=${BUILD_VERSION}\nENV COMMIT=${COMMIT}\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# copy repository\nWORKDIR /build/sunshine/\nCOPY --link .. .\n\n# cmake and cpack\nRUN <<_BUILD\n#!/bin/bash\nset -e\nchmod +x ./scripts/linux_build.sh\n./scripts/linux_build.sh \\\n  --publisher-name='LizardByte' \\\n  --publisher-website='https://app.lizardbyte.dev' \\\n  --publisher-issue-url='https://app.lizardbyte.dev/support' \\\n  --sudo-off\napt-get clean\nrm -rf /var/lib/apt/lists/*\n_BUILD\n\n# run tests\nWORKDIR /build/sunshine/build/tests\n# hadolint ignore=SC1091\nRUN <<_TEST\n#!/bin/bash\nset -e\nexport DISPLAY=:1\nXvfb ${DISPLAY} -screen 0 1024x768x24 &\n./test_sunshine --gtest_color=yes\n_TEST\n\nFROM scratch AS artifacts\nARG BASE\nARG TAG\nARG TARGETARCH\nCOPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /sunshine-${BASE}-${TAG}-${TARGETARCH}.deb\n\nFROM sunshine-base AS sunshine\n\n# copy deb from builder\nCOPY --link --from=artifacts /sunshine*.deb /sunshine.deb\n\n# install sunshine\nRUN <<_INSTALL_SUNSHINE\n#!/bin/bash\nset -e\napt-get update -y\napt-get install -y --no-install-recommends /sunshine.deb\napt-get clean\nrm -rf /var/lib/apt/lists/*\n_INSTALL_SUNSHINE\n\n# network setup\nEXPOSE 47984-47990/tcp\nEXPOSE 48010\nEXPOSE 47998-48000/udp\n\n# setup user\nARG PGID=1000\nENV PGID=${PGID}\nARG PUID=1000\nENV PUID=${PUID}\nENV TZ=\"UTC\"\nARG UNAME=lizard\nENV UNAME=${UNAME}\n\nENV HOME=/home/$UNAME\n\n# setup user\nRUN <<_SETUP_USER\n#!/bin/bash\nset -e\ngroupadd -f -g \"${PGID}\" \"${UNAME}\"\nuseradd -lm -d ${HOME} -s /bin/bash -g \"${PGID}\" -u \"${PUID}\" \"${UNAME}\"\nmkdir -p ${HOME}/.config/sunshine\nln -s ${HOME}/.config/sunshine /config\nchown -R ${UNAME} ${HOME}\n_SETUP_USER\n\nUSER ${UNAME}\nWORKDIR ${HOME}\n\n# entrypoint\nENTRYPOINT [\"/usr/bin/sunshine\"]\n"
  },
  {
    "path": "docker/ubuntu-24.04.dockerfile",
    "content": "# syntax=docker/dockerfile:1\n# artifacts: true\n# platforms: linux/amd64,linux/arm64/v8\n# platforms_pr: linux/amd64\n# no-cache-filters: sunshine-base,artifacts,sunshine\nARG BASE=ubuntu\nARG TAG=24.04\nFROM ${BASE}:${TAG} AS sunshine-base\n\nENV DEBIAN_FRONTEND=noninteractive\n\nFROM sunshine-base AS sunshine-build\n\nARG BRANCH\nARG BUILD_VERSION\nARG COMMIT\n# note: BUILD_VERSION may be blank\n\nENV BRANCH=${BRANCH}\nENV BUILD_VERSION=${BUILD_VERSION}\nENV COMMIT=${COMMIT}\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# copy repository\nWORKDIR /build/sunshine/\nCOPY --link .. .\n\n# cmake and cpack\nRUN <<_BUILD\n#!/bin/bash\nset -e\nchmod +x ./scripts/linux_build.sh\n./scripts/linux_build.sh \\\n  --publisher-name='LizardByte' \\\n  --publisher-website='https://app.lizardbyte.dev' \\\n  --publisher-issue-url='https://app.lizardbyte.dev/support' \\\n  --sudo-off\napt-get clean\nrm -rf /var/lib/apt/lists/*\n_BUILD\n\n# run tests\nWORKDIR /build/sunshine/build/tests\n# hadolint ignore=SC1091\nRUN <<_TEST\n#!/bin/bash\nset -e\nexport DISPLAY=:1\nXvfb ${DISPLAY} -screen 0 1024x768x24 &\n./test_sunshine --gtest_color=yes\n_TEST\n\nFROM scratch AS artifacts\nARG BASE\nARG TAG\nARG TARGETARCH\nCOPY --link --from=sunshine-build /build/sunshine/build/cpack_artifacts/Sunshine.deb /sunshine-${BASE}-${TAG}-${TARGETARCH}.deb\n\nFROM sunshine-base AS sunshine\n\n# copy deb from builder\nCOPY --link --from=artifacts /sunshine*.deb /sunshine.deb\n\n# install sunshine\nRUN <<_INSTALL_SUNSHINE\n#!/bin/bash\nset -e\napt-get update -y\napt-get install -y --no-install-recommends /sunshine.deb\napt-get clean\nrm -rf /var/lib/apt/lists/*\n_INSTALL_SUNSHINE\n\n# network setup\nEXPOSE 47984-47990/tcp\nEXPOSE 48010\nEXPOSE 47998-48000/udp\n\n# setup user\nARG PGID=1001\nENV PGID=${PGID}\nARG PUID=1001\nENV PUID=${PUID}\nENV TZ=\"UTC\"\nARG UNAME=lizard\nENV UNAME=${UNAME}\n\nENV HOME=/home/$UNAME\n\n# setup user\nRUN <<_SETUP_USER\n#!/bin/bash\nset -e\ngroupadd -f -g \"${PGID}\" \"${UNAME}\"\nuseradd -lm -d ${HOME} -s /bin/bash -g \"${PGID}\" -u \"${PUID}\" \"${UNAME}\"\nmkdir -p ${HOME}/.config/sunshine\nln -s ${HOME}/.config/sunshine /config\nchown -R ${UNAME} ${HOME}\n_SETUP_USER\n\nUSER ${UNAME}\nWORKDIR ${HOME}\n\n# entrypoint\nENTRYPOINT [\"/usr/bin/sunshine\"]\n"
  },
  {
    "path": "docs/Doxyfile",
    "content": "# This file describes the settings to be used by the documentation system\n# doxygen (www.doxygen.org) for a project.\n#\n# All text after a double hash (##) is considered a comment and is placed in\n# front of the TAG it is preceding.\n#\n# All text after a single hash (#) is considered a comment and will be ignored.\n# The format is:\n# TAG = value [value, ...]\n# For lists, items can also be appended using:\n# TAG += value [value, ...]\n# Values that contain spaces should be placed between quotes (\\\" \\\").\n#\n# Note:\n#\n# Use doxygen to compare the used configuration file with the template\n# configuration file:\n# doxygen -x [configFile]\n# Use doxygen to compare the used configuration file with the template\n# configuration file without replacing the environment variables or CMake type\n# replacement variables:\n# doxygen -x_noenv [configFile]\n\n# project metadata\nDOCSET_BUNDLE_ID = dev.lizardbyte.Sunshine\nDOCSET_PUBLISHER_ID = dev.lizardbyte.Sunshine.documentation\nPROJECT_BRIEF = \"Self-hosted game stream host for Moonlight.\"\nPROJECT_ICON = ../sunshine.ico\nPROJECT_LOGO = ../sunshine.png\nPROJECT_NAME = Sunshine\n\n# project specific settings\nDOT_GRAPH_MAX_NODES = 60\nIMAGE_PATH = ../docs/images\nINCLUDE_PATH = ../third-party/build-deps/ffmpeg/Linux-x86_64/include/\nPREDEFINED += SUNSHINE_BUILD_WAYLAND\nPREDEFINED += SUNSHINE_TRAY=1\n\n# TODO: Enable this when we have complete documentation\nWARN_IF_UNDOCUMENTED = NO\n\n# files and directories to process\nUSE_MDFILE_AS_MAINPAGE = ../README.md\nINPUT = ../README.md \\\n        getting_started.md \\\n        changelog.md \\\n        ../DOCKER_README.md \\\n        third_party_packages.md \\\n        gamestream_migration.md \\\n        legal.md \\\n        configuration.md \\\n        app_examples.md \\\n        guides.md \\\n        performance_tuning.md \\\n        troubleshooting.md \\\n        building.md \\\n        contributing.md \\\n        ../third-party/doxyconfig/docs/source_code.md \\\n        ../src\n"
  },
  {
    "path": "docs/Doxyfile-1.10.0-default",
    "content": "# Doxyfile 1.10.0\n\n# This file describes the settings to be used by the documentation system\n# doxygen (www.doxygen.org) for a project.\n#\n# All text after a double hash (##) is considered a comment and is placed in\n# front of the TAG it is preceding.\n#\n# All text after a single hash (#) is considered a comment and will be ignored.\n# The format is:\n# TAG = value [value, ...]\n# For lists, items can also be appended using:\n# TAG += value [value, ...]\n# Values that contain spaces should be placed between quotes (\\\" \\\").\n#\n# Note:\n#\n# Use doxygen to compare the used configuration file with the template\n# configuration file:\n# doxygen -x [configFile]\n# Use doxygen to compare the used configuration file with the template\n# configuration file without replacing the environment variables or CMake type\n# replacement variables:\n# doxygen -x_noenv [configFile]\n\n#---------------------------------------------------------------------------\n# Project related configuration options\n#---------------------------------------------------------------------------\n\n# This tag specifies the encoding used for all characters in the configuration\n# file that follow. The default is UTF-8 which is also the encoding used for all\n# text before the first occurrence of this tag. Doxygen uses libiconv (or the\n# iconv built into libc) for the transcoding. See\n# https://www.gnu.org/software/libiconv/ for the list of possible encodings.\n# The default value is: UTF-8.\n\nDOXYFILE_ENCODING      = UTF-8\n\n# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by\n# double-quotes, unless you are using Doxywizard) that should identify the\n# project for which the documentation is generated. This name is used in the\n# title of most generated pages and in a few other places.\n# The default value is: My Project.\n\nPROJECT_NAME           = \"My Project\"\n\n# The PROJECT_NUMBER tag can be used to enter a project or revision number. This\n# could be handy for archiving the generated documentation or if some version\n# control system is used.\n\nPROJECT_NUMBER         =\n\n# Using the PROJECT_BRIEF tag one can provide an optional one line description\n# for a project that appears at the top of each page and should give viewer a\n# quick idea about the purpose of the project. Keep the description short.\n\nPROJECT_BRIEF          =\n\n# With the PROJECT_LOGO tag one can specify a logo or an icon that is included\n# in the documentation. The maximum height of the logo should not exceed 55\n# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy\n# the logo to the output directory.\n\nPROJECT_LOGO           =\n\n# With the PROJECT_ICON tag one can specify an icon that is included in the tabs\n# when the HTML document is shown. Doxygen will copy the logo to the output\n# directory.\n\nPROJECT_ICON           =\n\n# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path\n# into which the generated documentation will be written. If a relative path is\n# entered, it will be relative to the location where doxygen was started. If\n# left blank the current directory will be used.\n\nOUTPUT_DIRECTORY       =\n\n# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096\n# sub-directories (in 2 levels) under the output directory of each output format\n# and will distribute the generated files over these directories. Enabling this\n# option can be useful when feeding doxygen a huge amount of source files, where\n# putting all generated files in the same directory would otherwise causes\n# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to\n# control the number of sub-directories.\n# The default value is: NO.\n\nCREATE_SUBDIRS         = NO\n\n# Controls the number of sub-directories that will be created when\n# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every\n# level increment doubles the number of directories, resulting in 4096\n# directories at level 8 which is the default and also the maximum value. The\n# sub-directories are organized in 2 levels, the first level always has a fixed\n# number of 16 directories.\n# Minimum value: 0, maximum value: 8, default value: 8.\n# This tag requires that the tag CREATE_SUBDIRS is set to YES.\n\nCREATE_SUBDIRS_LEVEL   = 8\n\n# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII\n# characters to appear in the names of generated files. If set to NO, non-ASCII\n# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode\n# U+3044.\n# The default value is: NO.\n\nALLOW_UNICODE_NAMES    = NO\n\n# The OUTPUT_LANGUAGE tag is used to specify the language in which all\n# documentation generated by doxygen is written. Doxygen will use this\n# information to generate all constant output in the proper language.\n# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian,\n# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English\n# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek,\n# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with\n# English messages), Korean, Korean-en (Korean with English messages), Latvian,\n# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese,\n# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish,\n# Swedish, Turkish, Ukrainian and Vietnamese.\n# The default value is: English.\n\nOUTPUT_LANGUAGE        = English\n\n# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member\n# descriptions after the members that are listed in the file and class\n# documentation (similar to Javadoc). Set to NO to disable this.\n# The default value is: YES.\n\nBRIEF_MEMBER_DESC      = YES\n\n# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief\n# description of a member or function before the detailed description\n#\n# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the\n# brief descriptions will be completely suppressed.\n# The default value is: YES.\n\nREPEAT_BRIEF           = YES\n\n# This tag implements a quasi-intelligent brief description abbreviator that is\n# used to form the text in various listings. Each string in this list, if found\n# as the leading text of the brief description, will be stripped from the text\n# and the result, after processing the whole list, is used as the annotated\n# text. Otherwise, the brief description is used as-is. If left blank, the\n# following values are used ($name is automatically replaced with the name of\n# the entity):The $name class, The $name widget, The $name file, is, provides,\n# specifies, contains, represents, a, an and the.\n\nABBREVIATE_BRIEF       = \"The $name class\" \\\n                         \"The $name widget\" \\\n                         \"The $name file\" \\\n                         is \\\n                         provides \\\n                         specifies \\\n                         contains \\\n                         represents \\\n                         a \\\n                         an \\\n                         the\n\n# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then\n# doxygen will generate a detailed section even if there is only a brief\n# description.\n# The default value is: NO.\n\nALWAYS_DETAILED_SEC    = NO\n\n# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all\n# inherited members of a class in the documentation of that class as if those\n# members were ordinary class members. Constructors, destructors and assignment\n# operators of the base classes will not be shown.\n# The default value is: NO.\n\nINLINE_INHERITED_MEMB  = NO\n\n# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path\n# before files name in the file list and in the header files. If set to NO the\n# shortest path that makes the file name unique will be used\n# The default value is: YES.\n\nFULL_PATH_NAMES        = YES\n\n# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.\n# Stripping is only done if one of the specified strings matches the left-hand\n# part of the path. The tag can be used to show relative paths in the file list.\n# If left blank the directory from which doxygen is run is used as the path to\n# strip.\n#\n# Note that you can specify absolute paths here, but also relative paths, which\n# will be relative from the directory where doxygen is started.\n# This tag requires that the tag FULL_PATH_NAMES is set to YES.\n\nSTRIP_FROM_PATH        =\n\n# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the\n# path mentioned in the documentation of a class, which tells the reader which\n# header file to include in order to use a class. If left blank only the name of\n# the header file containing the class definition is used. Otherwise one should\n# specify the list of include paths that are normally passed to the compiler\n# using the -I flag.\n\nSTRIP_FROM_INC_PATH    =\n\n# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but\n# less readable) file names. This can be useful is your file systems doesn't\n# support long names like on DOS, Mac, or CD-ROM.\n# The default value is: NO.\n\nSHORT_NAMES            = NO\n\n# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the\n# first line (until the first dot) of a Javadoc-style comment as the brief\n# description. If set to NO, the Javadoc-style will behave just like regular Qt-\n# style comments (thus requiring an explicit @brief command for a brief\n# description.)\n# The default value is: NO.\n\nJAVADOC_AUTOBRIEF      = NO\n\n# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line\n# such as\n# /***************\n# as being the beginning of a Javadoc-style comment \"banner\". If set to NO, the\n# Javadoc-style will behave just like regular comments and it will not be\n# interpreted by doxygen.\n# The default value is: NO.\n\nJAVADOC_BANNER         = NO\n\n# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first\n# line (until the first dot) of a Qt-style comment as the brief description. If\n# set to NO, the Qt-style will behave just like regular Qt-style comments (thus\n# requiring an explicit \\brief command for a brief description.)\n# The default value is: NO.\n\nQT_AUTOBRIEF           = NO\n\n# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a\n# multi-line C++ special comment block (i.e. a block of //! or /// comments) as\n# a brief description. This used to be the default behavior. The new default is\n# to treat a multi-line C++ comment block as a detailed description. Set this\n# tag to YES if you prefer the old behavior instead.\n#\n# Note that setting this tag to YES also means that rational rose comments are\n# not recognized any more.\n# The default value is: NO.\n\nMULTILINE_CPP_IS_BRIEF = NO\n\n# By default Python docstrings are displayed as preformatted text and doxygen's\n# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the\n# doxygen's special commands can be used and the contents of the docstring\n# documentation blocks is shown as doxygen documentation.\n# The default value is: YES.\n\nPYTHON_DOCSTRING       = YES\n\n# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the\n# documentation from any documented member that it re-implements.\n# The default value is: YES.\n\nINHERIT_DOCS           = YES\n\n# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new\n# page for each member. If set to NO, the documentation of a member will be part\n# of the file/class/namespace that contains it.\n# The default value is: NO.\n\nSEPARATE_MEMBER_PAGES  = NO\n\n# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen\n# uses this value to replace tabs by spaces in code fragments.\n# Minimum value: 1, maximum value: 16, default value: 4.\n\nTAB_SIZE               = 4\n\n# This tag can be used to specify a number of aliases that act as commands in\n# the documentation. An alias has the form:\n# name=value\n# For example adding\n# \"sideeffect=@par Side Effects:^^\"\n# will allow you to put the command \\sideeffect (or @sideeffect) in the\n# documentation, which will result in a user-defined paragraph with heading\n# \"Side Effects:\". Note that you cannot put \\n's in the value part of an alias\n# to insert newlines (in the resulting output). You can put ^^ in the value part\n# of an alias to insert a newline as if a physical newline was in the original\n# file. When you need a literal { or } or , in the value part of an alias you\n# have to escape them by means of a backslash (\\), this can lead to conflicts\n# with the commands \\{ and \\} for these it is advised to use the version @{ and\n# @} or use a double escape (\\\\{ and \\\\})\n\nALIASES                =\n\n# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources\n# only. Doxygen will then generate output that is more tailored for C. For\n# instance, some of the names that are used will be different. The list of all\n# members will be omitted, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_FOR_C  = NO\n\n# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or\n# Python sources only. Doxygen will then generate output that is more tailored\n# for that language. For instance, namespaces will be presented as packages,\n# qualified scopes will look different, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_JAVA   = NO\n\n# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran\n# sources. Doxygen will then generate output that is tailored for Fortran.\n# The default value is: NO.\n\nOPTIMIZE_FOR_FORTRAN   = NO\n\n# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL\n# sources. Doxygen will then generate output that is tailored for VHDL.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_VHDL   = NO\n\n# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice\n# sources only. Doxygen will then generate output that is more tailored for that\n# language. For instance, namespaces will be presented as modules, types will be\n# separated into more groups, etc.\n# The default value is: NO.\n\nOPTIMIZE_OUTPUT_SLICE  = NO\n\n# Doxygen selects the parser to use depending on the extension of the files it\n# parses. With this tag you can assign which parser to use for a given\n# extension. Doxygen has a built-in mapping, but you can override or extend it\n# using this tag. The format is ext=language, where ext is a file extension, and\n# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,\n# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice,\n# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:\n# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser\n# tries to guess whether the code is fixed or free formatted code, this is the\n# default for Fortran type files). For instance to make doxygen treat .inc files\n# as Fortran files (default is PHP), and .f files as C (default is Fortran),\n# use: inc=Fortran f=C.\n#\n# Note: For files without extension you can use no_extension as a placeholder.\n#\n# Note that for custom extensions you also need to set FILE_PATTERNS otherwise\n# the files are not read by doxygen. When specifying no_extension you should add\n# * to the FILE_PATTERNS.\n#\n# Note see also the list of default file extension mappings.\n\nEXTENSION_MAPPING      =\n\n# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments\n# according to the Markdown format, which allows for more readable\n# documentation. See https://daringfireball.net/projects/markdown/ for details.\n# The output of markdown processing is further processed by doxygen, so you can\n# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in\n# case of backward compatibilities issues.\n# The default value is: YES.\n\nMARKDOWN_SUPPORT       = YES\n\n# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up\n# to that level are automatically included in the table of contents, even if\n# they do not have an id attribute.\n# Note: This feature currently applies only to Markdown headings.\n# Minimum value: 0, maximum value: 99, default value: 5.\n# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.\n\nTOC_INCLUDE_HEADINGS   = 5\n\n# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to\n# generate identifiers for the Markdown headings. Note: Every identifier is\n# unique.\n# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a\n# sequence number starting at 0 and GITHUB use the lower case version of title\n# with any whitespace replaced by '-' and punctuation characters removed.\n# The default value is: DOXYGEN.\n# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.\n\nMARKDOWN_ID_STYLE      = DOXYGEN\n\n# When enabled doxygen tries to link words that correspond to documented\n# classes, or namespaces to their corresponding documentation. Such a link can\n# be prevented in individual cases by putting a % sign in front of the word or\n# globally by setting AUTOLINK_SUPPORT to NO.\n# The default value is: YES.\n\nAUTOLINK_SUPPORT       = YES\n\n# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want\n# to include (a tag file for) the STL sources as input, then you should set this\n# tag to YES in order to let doxygen match functions declarations and\n# definitions whose arguments contain STL classes (e.g. func(std::string);\n# versus func(std::string) {}). This also make the inheritance and collaboration\n# diagrams that involve STL classes more complete and accurate.\n# The default value is: NO.\n\nBUILTIN_STL_SUPPORT    = NO\n\n# If you use Microsoft's C++/CLI language, you should set this option to YES to\n# enable parsing support.\n# The default value is: NO.\n\nCPP_CLI_SUPPORT        = NO\n\n# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:\n# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen\n# will parse them like normal C++ but will assume all classes use public instead\n# of private inheritance when no explicit protection keyword is present.\n# The default value is: NO.\n\nSIP_SUPPORT            = NO\n\n# For Microsoft's IDL there are propget and propput attributes to indicate\n# getter and setter methods for a property. Setting this option to YES will make\n# doxygen to replace the get and set methods by a property in the documentation.\n# This will only work if the methods are indeed getting or setting a simple\n# type. If this is not the case, or you want to show the methods anyway, you\n# should set this option to NO.\n# The default value is: YES.\n\nIDL_PROPERTY_SUPPORT   = YES\n\n# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC\n# tag is set to YES then doxygen will reuse the documentation of the first\n# member in the group (if any) for the other members of the group. By default\n# all members of a group must be documented explicitly.\n# The default value is: NO.\n\nDISTRIBUTE_GROUP_DOC   = NO\n\n# If one adds a struct or class to a group and this option is enabled, then also\n# any nested class or struct is added to the same group. By default this option\n# is disabled and one has to add nested compounds explicitly via \\ingroup.\n# The default value is: NO.\n\nGROUP_NESTED_COMPOUNDS = NO\n\n# Set the SUBGROUPING tag to YES to allow class member groups of the same type\n# (for instance a group of public functions) to be put as a subgroup of that\n# type (e.g. under the Public Functions section). Set it to NO to prevent\n# subgrouping. Alternatively, this can be done per class using the\n# \\nosubgrouping command.\n# The default value is: YES.\n\nSUBGROUPING            = YES\n\n# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions\n# are shown inside the group in which they are included (e.g. using \\ingroup)\n# instead of on a separate page (for HTML and Man pages) or section (for LaTeX\n# and RTF).\n#\n# Note that this feature does not work in combination with\n# SEPARATE_MEMBER_PAGES.\n# The default value is: NO.\n\nINLINE_GROUPED_CLASSES = NO\n\n# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions\n# with only public data fields or simple typedef fields will be shown inline in\n# the documentation of the scope in which they are defined (i.e. file,\n# namespace, or group documentation), provided this scope is documented. If set\n# to NO, structs, classes, and unions are shown on a separate page (for HTML and\n# Man pages) or section (for LaTeX and RTF).\n# The default value is: NO.\n\nINLINE_SIMPLE_STRUCTS  = NO\n\n# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or\n# enum is documented as struct, union, or enum with the name of the typedef. So\n# typedef struct TypeS {} TypeT, will appear in the documentation as a struct\n# with name TypeT. When disabled the typedef will appear as a member of a file,\n# namespace, or class. And the struct will be named TypeS. This can typically be\n# useful for C code in case the coding convention dictates that all compound\n# types are typedef'ed and only the typedef is referenced, never the tag name.\n# The default value is: NO.\n\nTYPEDEF_HIDES_STRUCT   = NO\n\n# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This\n# cache is used to resolve symbols given their name and scope. Since this can be\n# an expensive process and often the same symbol appears multiple times in the\n# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small\n# doxygen will become slower. If the cache is too large, memory is wasted. The\n# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range\n# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536\n# symbols. At the end of a run doxygen will report the cache usage and suggest\n# the optimal cache size from a speed point of view.\n# Minimum value: 0, maximum value: 9, default value: 0.\n\nLOOKUP_CACHE_SIZE      = 0\n\n# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use\n# during processing. When set to 0 doxygen will based this on the number of\n# cores available in the system. You can set it explicitly to a value larger\n# than 0 to get more control over the balance between CPU load and processing\n# speed. At this moment only the input processing can be done using multiple\n# threads. Since this is still an experimental feature the default is set to 1,\n# which effectively disables parallel processing. Please report any issues you\n# encounter. Generating dot graphs in parallel is controlled by the\n# DOT_NUM_THREADS setting.\n# Minimum value: 0, maximum value: 32, default value: 1.\n\nNUM_PROC_THREADS       = 1\n\n# If the TIMESTAMP tag is set different from NO then each generated page will\n# contain the date or date and time when the page was generated. Setting this to\n# NO can help when comparing the output of multiple runs.\n# Possible values are: YES, NO, DATETIME and DATE.\n# The default value is: NO.\n\nTIMESTAMP              = NO\n\n#---------------------------------------------------------------------------\n# Build related configuration options\n#---------------------------------------------------------------------------\n\n# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in\n# documentation are documented, even if no documentation was available. Private\n# class members and static file members will be hidden unless the\n# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.\n# Note: This will also disable the warnings about undocumented members that are\n# normally produced when WARNINGS is set to YES.\n# The default value is: NO.\n\nEXTRACT_ALL            = NO\n\n# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will\n# be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PRIVATE        = NO\n\n# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual\n# methods of a class will be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PRIV_VIRTUAL   = NO\n\n# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal\n# scope will be included in the documentation.\n# The default value is: NO.\n\nEXTRACT_PACKAGE        = NO\n\n# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be\n# included in the documentation.\n# The default value is: NO.\n\nEXTRACT_STATIC         = NO\n\n# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined\n# locally in source files will be included in the documentation. If set to NO,\n# only classes defined in header files are included. Does not have any effect\n# for Java sources.\n# The default value is: YES.\n\nEXTRACT_LOCAL_CLASSES  = YES\n\n# This flag is only useful for Objective-C code. If set to YES, local methods,\n# which are defined in the implementation section but not in the interface are\n# included in the documentation. If set to NO, only methods in the interface are\n# included.\n# The default value is: NO.\n\nEXTRACT_LOCAL_METHODS  = NO\n\n# If this flag is set to YES, the members of anonymous namespaces will be\n# extracted and appear in the documentation as a namespace called\n# 'anonymous_namespace{file}', where file will be replaced with the base name of\n# the file that contains the anonymous namespace. By default anonymous namespace\n# are hidden.\n# The default value is: NO.\n\nEXTRACT_ANON_NSPACES   = NO\n\n# If this flag is set to YES, the name of an unnamed parameter in a declaration\n# will be determined by the corresponding definition. By default unnamed\n# parameters remain unnamed in the output.\n# The default value is: YES.\n\nRESOLVE_UNNAMED_PARAMS = YES\n\n# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all\n# undocumented members inside documented classes or files. If set to NO these\n# members will be included in the various overviews, but no documentation\n# section is generated. This option has no effect if EXTRACT_ALL is enabled.\n# The default value is: NO.\n\nHIDE_UNDOC_MEMBERS     = NO\n\n# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all\n# undocumented classes that are normally visible in the class hierarchy. If set\n# to NO, these classes will be included in the various overviews. This option\n# will also hide undocumented C++ concepts if enabled. This option has no effect\n# if EXTRACT_ALL is enabled.\n# The default value is: NO.\n\nHIDE_UNDOC_CLASSES     = NO\n\n# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend\n# declarations. If set to NO, these declarations will be included in the\n# documentation.\n# The default value is: NO.\n\nHIDE_FRIEND_COMPOUNDS  = NO\n\n# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any\n# documentation blocks found inside the body of a function. If set to NO, these\n# blocks will be appended to the function's detailed documentation block.\n# The default value is: NO.\n\nHIDE_IN_BODY_DOCS      = NO\n\n# The INTERNAL_DOCS tag determines if documentation that is typed after a\n# \\internal command is included. If the tag is set to NO then the documentation\n# will be excluded. Set it to YES to include the internal documentation.\n# The default value is: NO.\n\nINTERNAL_DOCS          = NO\n\n# With the correct setting of option CASE_SENSE_NAMES doxygen will better be\n# able to match the capabilities of the underlying filesystem. In case the\n# filesystem is case sensitive (i.e. it supports files in the same directory\n# whose names only differ in casing), the option must be set to YES to properly\n# deal with such files in case they appear in the input. For filesystems that\n# are not case sensitive the option should be set to NO to properly deal with\n# output files written for symbols that only differ in casing, such as for two\n# classes, one named CLASS and the other named Class, and to also support\n# references to files without having to specify the exact matching casing. On\n# Windows (including Cygwin) and MacOS, users should typically set this option\n# to NO, whereas on Linux or other Unix flavors it should typically be set to\n# YES.\n# Possible values are: SYSTEM, NO and YES.\n# The default value is: SYSTEM.\n\nCASE_SENSE_NAMES       = SYSTEM\n\n# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with\n# their full class and namespace scopes in the documentation. If set to YES, the\n# scope will be hidden.\n# The default value is: NO.\n\nHIDE_SCOPE_NAMES       = NO\n\n# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will\n# append additional text to a page's title, such as Class Reference. If set to\n# YES the compound reference will be hidden.\n# The default value is: NO.\n\nHIDE_COMPOUND_REFERENCE= NO\n\n# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class\n# will show which file needs to be included to use the class.\n# The default value is: YES.\n\nSHOW_HEADERFILE        = YES\n\n# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of\n# the files that are included by a file in the documentation of that file.\n# The default value is: YES.\n\nSHOW_INCLUDE_FILES     = YES\n\n# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each\n# grouped member an include statement to the documentation, telling the reader\n# which file to include in order to use the member.\n# The default value is: NO.\n\nSHOW_GROUPED_MEMB_INC  = NO\n\n# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include\n# files with double quotes in the documentation rather than with sharp brackets.\n# The default value is: NO.\n\nFORCE_LOCAL_INCLUDES   = NO\n\n# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the\n# documentation for inline members.\n# The default value is: YES.\n\nINLINE_INFO            = YES\n\n# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the\n# (detailed) documentation of file and class members alphabetically by member\n# name. If set to NO, the members will appear in declaration order.\n# The default value is: YES.\n\nSORT_MEMBER_DOCS       = YES\n\n# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief\n# descriptions of file, namespace and class members alphabetically by member\n# name. If set to NO, the members will appear in declaration order. Note that\n# this will also influence the order of the classes in the class list.\n# The default value is: NO.\n\nSORT_BRIEF_DOCS        = NO\n\n# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the\n# (brief and detailed) documentation of class members so that constructors and\n# destructors are listed first. If set to NO the constructors will appear in the\n# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.\n# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief\n# member documentation.\n# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting\n# detailed member documentation.\n# The default value is: NO.\n\nSORT_MEMBERS_CTORS_1ST = NO\n\n# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy\n# of group names into alphabetical order. If set to NO the group names will\n# appear in their defined order.\n# The default value is: NO.\n\nSORT_GROUP_NAMES       = NO\n\n# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by\n# fully-qualified names, including namespaces. If set to NO, the class list will\n# be sorted only by class name, not including the namespace part.\n# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.\n# Note: This option applies only to the class list, not to the alphabetical\n# list.\n# The default value is: NO.\n\nSORT_BY_SCOPE_NAME     = NO\n\n# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper\n# type resolution of all parameters of a function it will reject a match between\n# the prototype and the implementation of a member function even if there is\n# only one candidate or it is obvious which candidate to choose by doing a\n# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still\n# accept a match between prototype and implementation in such cases.\n# The default value is: NO.\n\nSTRICT_PROTO_MATCHING  = NO\n\n# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo\n# list. This list is created by putting \\todo commands in the documentation.\n# The default value is: YES.\n\nGENERATE_TODOLIST      = YES\n\n# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test\n# list. This list is created by putting \\test commands in the documentation.\n# The default value is: YES.\n\nGENERATE_TESTLIST      = YES\n\n# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug\n# list. This list is created by putting \\bug commands in the documentation.\n# The default value is: YES.\n\nGENERATE_BUGLIST       = YES\n\n# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)\n# the deprecated list. This list is created by putting \\deprecated commands in\n# the documentation.\n# The default value is: YES.\n\nGENERATE_DEPRECATEDLIST= YES\n\n# The ENABLED_SECTIONS tag can be used to enable conditional documentation\n# sections, marked by \\if <section_label> ... \\endif and \\cond <section_label>\n# ... \\endcond blocks.\n\nENABLED_SECTIONS       =\n\n# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the\n# initial value of a variable or macro / define can have for it to appear in the\n# documentation. If the initializer consists of more lines than specified here\n# it will be hidden. Use a value of 0 to hide initializers completely. The\n# appearance of the value of individual variables and macros / defines can be\n# controlled using \\showinitializer or \\hideinitializer command in the\n# documentation regardless of this setting.\n# Minimum value: 0, maximum value: 10000, default value: 30.\n\nMAX_INITIALIZER_LINES  = 30\n\n# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at\n# the bottom of the documentation of classes and structs. If set to YES, the\n# list will mention the files that were used to generate the documentation.\n# The default value is: YES.\n\nSHOW_USED_FILES        = YES\n\n# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This\n# will remove the Files entry from the Quick Index and from the Folder Tree View\n# (if specified).\n# The default value is: YES.\n\nSHOW_FILES             = YES\n\n# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces\n# page. This will remove the Namespaces entry from the Quick Index and from the\n# Folder Tree View (if specified).\n# The default value is: YES.\n\nSHOW_NAMESPACES        = YES\n\n# The FILE_VERSION_FILTER tag can be used to specify a program or script that\n# doxygen should invoke to get the current version for each file (typically from\n# the version control system). Doxygen will invoke the program by executing (via\n# popen()) the command command input-file, where command is the value of the\n# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided\n# by doxygen. Whatever the program writes to standard output is used as the file\n# version. For an example see the documentation.\n\nFILE_VERSION_FILTER    =\n\n# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed\n# by doxygen. The layout file controls the global structure of the generated\n# output files in an output format independent way. To create the layout file\n# that represents doxygen's defaults, run doxygen with the -l option. You can\n# optionally specify a file name after the option, if omitted DoxygenLayout.xml\n# will be used as the name of the layout file. See also section \"Changing the\n# layout of pages\" for information.\n#\n# Note that if you run doxygen from a directory containing a file called\n# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE\n# tag is left empty.\n\nLAYOUT_FILE            =\n\n# The CITE_BIB_FILES tag can be used to specify one or more bib files containing\n# the reference definitions. This must be a list of .bib files. The .bib\n# extension is automatically appended if omitted. This requires the bibtex tool\n# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.\n# For LaTeX the style of the bibliography can be controlled using\n# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the\n# search path. See also \\cite for info how to create references.\n\nCITE_BIB_FILES         =\n\n#---------------------------------------------------------------------------\n# Configuration options related to warning and progress messages\n#---------------------------------------------------------------------------\n\n# The QUIET tag can be used to turn on/off the messages that are generated to\n# standard output by doxygen. If QUIET is set to YES this implies that the\n# messages are off.\n# The default value is: NO.\n\nQUIET                  = NO\n\n# The WARNINGS tag can be used to turn on/off the warning messages that are\n# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES\n# this implies that the warnings are on.\n#\n# Tip: Turn warnings on while writing the documentation.\n# The default value is: YES.\n\nWARNINGS               = YES\n\n# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate\n# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag\n# will automatically be disabled.\n# The default value is: YES.\n\nWARN_IF_UNDOCUMENTED   = YES\n\n# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for\n# potential errors in the documentation, such as documenting some parameters in\n# a documented function twice, or documenting parameters that don't exist or\n# using markup commands wrongly.\n# The default value is: YES.\n\nWARN_IF_DOC_ERROR      = YES\n\n# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete\n# function parameter documentation. If set to NO, doxygen will accept that some\n# parameters have no documentation without warning.\n# The default value is: YES.\n\nWARN_IF_INCOMPLETE_DOC = YES\n\n# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that\n# are documented, but have no documentation for their parameters or return\n# value. If set to NO, doxygen will only warn about wrong parameter\n# documentation, but not about the absence of documentation. If EXTRACT_ALL is\n# set to YES then this flag will automatically be disabled. See also\n# WARN_IF_INCOMPLETE_DOC\n# The default value is: NO.\n\nWARN_NO_PARAMDOC       = NO\n\n# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about\n# undocumented enumeration values. If set to NO, doxygen will accept\n# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag\n# will automatically be disabled.\n# The default value is: NO.\n\nWARN_IF_UNDOC_ENUM_VAL = NO\n\n# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when\n# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS\n# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but\n# at the end of the doxygen process doxygen will return with a non-zero status.\n# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves\n# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not\n# write the warning messages in between other messages but write them at the end\n# of a run, in case a WARN_LOGFILE is defined the warning messages will be\n# besides being in the defined file also be shown at the end of a run, unless\n# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case\n# the behavior will remain as with the setting FAIL_ON_WARNINGS.\n# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.\n# The default value is: NO.\n\nWARN_AS_ERROR          = NO\n\n# The WARN_FORMAT tag determines the format of the warning messages that doxygen\n# can produce. The string should contain the $file, $line, and $text tags, which\n# will be replaced by the file and line number from which the warning originated\n# and the warning text. Optionally the format may contain $version, which will\n# be replaced by the version of the file (if it could be obtained via\n# FILE_VERSION_FILTER)\n# See also: WARN_LINE_FORMAT\n# The default value is: $file:$line: $text.\n\nWARN_FORMAT            = \"$file:$line: $text\"\n\n# In the $text part of the WARN_FORMAT command it is possible that a reference\n# to a more specific place is given. To make it easier to jump to this place\n# (outside of doxygen) the user can define a custom \"cut\" / \"paste\" string.\n# Example:\n# WARN_LINE_FORMAT = \"'vi $file +$line'\"\n# See also: WARN_FORMAT\n# The default value is: at line $line of file $file.\n\nWARN_LINE_FORMAT       = \"at line $line of file $file\"\n\n# The WARN_LOGFILE tag can be used to specify a file to which warning and error\n# messages should be written. If left blank the output is written to standard\n# error (stderr). In case the file specified cannot be opened for writing the\n# warning and error messages are written to standard error. When as file - is\n# specified the warning and error messages are written to standard output\n# (stdout).\n\nWARN_LOGFILE           =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the input files\n#---------------------------------------------------------------------------\n\n# The INPUT tag is used to specify the files and/or directories that contain\n# documented source files. You may enter file names like myfile.cpp or\n# directories like /usr/src/myproject. Separate the files or directories with\n# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING\n# Note: If this tag is empty the current directory is searched.\n\nINPUT                  =\n\n# This tag can be used to specify the character encoding of the source files\n# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses\n# libiconv (or the iconv built into libc) for the transcoding. See the libiconv\n# documentation (see:\n# https://www.gnu.org/software/libiconv/) for the list of possible encodings.\n# See also: INPUT_FILE_ENCODING\n# The default value is: UTF-8.\n\nINPUT_ENCODING         = UTF-8\n\n# This tag can be used to specify the character encoding of the source files\n# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify\n# character encoding on a per file pattern basis. Doxygen will compare the file\n# name with each pattern and apply the encoding instead of the default\n# INPUT_ENCODING) if there is a match. The character encodings are a list of the\n# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding\n# \"INPUT_ENCODING\" for further information on supported encodings.\n\nINPUT_FILE_ENCODING    =\n\n# If the value of the INPUT tag contains directories, you can use the\n# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and\n# *.h) to filter out the source-files in the directories.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# read by doxygen.\n#\n# Note the list of default checked file patterns might differ from the list of\n# default file extension mappings.\n#\n# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm,\n# *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl,\n# *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d,\n# *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to\n# be provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,\n# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.\n\nFILE_PATTERNS          = *.c \\\n                         *.cc \\\n                         *.cxx \\\n                         *.cxxm \\\n                         *.cpp \\\n                         *.cppm \\\n                         *.ccm \\\n                         *.c++ \\\n                         *.c++m \\\n                         *.java \\\n                         *.ii \\\n                         *.ixx \\\n                         *.ipp \\\n                         *.i++ \\\n                         *.inl \\\n                         *.idl \\\n                         *.ddl \\\n                         *.odl \\\n                         *.h \\\n                         *.hh \\\n                         *.hxx \\\n                         *.hpp \\\n                         *.h++ \\\n                         *.ixx \\\n                         *.l \\\n                         *.cs \\\n                         *.d \\\n                         *.php \\\n                         *.php4 \\\n                         *.php5 \\\n                         *.phtml \\\n                         *.inc \\\n                         *.m \\\n                         *.markdown \\\n                         *.md \\\n                         *.mm \\\n                         *.dox \\\n                         *.py \\\n                         *.pyw \\\n                         *.f90 \\\n                         *.f95 \\\n                         *.f03 \\\n                         *.f08 \\\n                         *.f18 \\\n                         *.f \\\n                         *.for \\\n                         *.vhd \\\n                         *.vhdl \\\n                         *.ucf \\\n                         *.qsf \\\n                         *.ice\n\n# The RECURSIVE tag can be used to specify whether or not subdirectories should\n# be searched for input files as well.\n# The default value is: NO.\n\nRECURSIVE              = NO\n\n# The EXCLUDE tag can be used to specify files and/or directories that should be\n# excluded from the INPUT source files. This way you can easily exclude a\n# subdirectory from a directory tree whose root is specified with the INPUT tag.\n#\n# Note that relative paths are relative to the directory from which doxygen is\n# run.\n\nEXCLUDE                =\n\n# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or\n# directories that are symbolic links (a Unix file system feature) are excluded\n# from the input.\n# The default value is: NO.\n\nEXCLUDE_SYMLINKS       = NO\n\n# If the value of the INPUT tag contains directories, you can use the\n# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude\n# certain files from those directories.\n#\n# Note that the wildcards are matched against the file with absolute path, so to\n# exclude all test directories for example use the pattern */test/*\n\nEXCLUDE_PATTERNS       =\n\n# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names\n# (namespaces, classes, functions, etc.) that should be excluded from the\n# output. The symbol name can be a fully qualified name, a word, or if the\n# wildcard * is used, a substring. Examples: ANamespace, AClass,\n# ANamespace::AClass, ANamespace::*Test\n\nEXCLUDE_SYMBOLS        =\n\n# The EXAMPLE_PATH tag can be used to specify one or more files or directories\n# that contain example code fragments that are included (see the \\include\n# command).\n\nEXAMPLE_PATH           =\n\n# If the value of the EXAMPLE_PATH tag contains directories, you can use the\n# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and\n# *.h) to filter out the source-files in the directories. If left blank all\n# files are included.\n\nEXAMPLE_PATTERNS       = *\n\n# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be\n# searched for input files to be used with the \\include or \\dontinclude commands\n# irrespective of the value of the RECURSIVE tag.\n# The default value is: NO.\n\nEXAMPLE_RECURSIVE      = NO\n\n# The IMAGE_PATH tag can be used to specify one or more files or directories\n# that contain images that are to be included in the documentation (see the\n# \\image command).\n\nIMAGE_PATH             =\n\n# The INPUT_FILTER tag can be used to specify a program that doxygen should\n# invoke to filter for each input file. Doxygen will invoke the filter program\n# by executing (via popen()) the command:\n#\n# <filter> <input-file>\n#\n# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the\n# name of an input file. Doxygen will then use the output that the filter\n# program writes to standard output. If FILTER_PATTERNS is specified, this tag\n# will be ignored.\n#\n# Note that the filter must not add or remove lines; it is applied before the\n# code is scanned, but not when the output code is generated. If lines are added\n# or removed, the anchors will not be placed correctly.\n#\n# Note that doxygen will use the data processed and written to standard output\n# for further processing, therefore nothing else, like debug statements or used\n# commands (so in case of a Windows batch file always use @echo OFF), should be\n# written to standard output.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# properly processed by doxygen.\n\nINPUT_FILTER           =\n\n# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern\n# basis. Doxygen will compare the file name with each pattern and apply the\n# filter if there is a match. The filters are a list of the form: pattern=filter\n# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how\n# filters are used. If the FILTER_PATTERNS tag is empty or if none of the\n# patterns match the file name, INPUT_FILTER is applied.\n#\n# Note that for custom extensions or not directly supported extensions you also\n# need to set EXTENSION_MAPPING for the extension otherwise the files are not\n# properly processed by doxygen.\n\nFILTER_PATTERNS        =\n\n# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using\n# INPUT_FILTER) will also be used to filter the input files that are used for\n# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).\n# The default value is: NO.\n\nFILTER_SOURCE_FILES    = NO\n\n# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file\n# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and\n# it is also possible to disable source filtering for a specific pattern using\n# *.ext= (so without naming a filter).\n# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.\n\nFILTER_SOURCE_PATTERNS =\n\n# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that\n# is part of the input, its contents will be placed on the main page\n# (index.html). This can be useful if you have a project on for instance GitHub\n# and want to reuse the introduction page also for the doxygen output.\n\nUSE_MDFILE_AS_MAINPAGE =\n\n# The Fortran standard specifies that for fixed formatted Fortran code all\n# characters from position 72 are to be considered as comment. A common\n# extension is to allow longer lines before the automatic comment starts. The\n# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can\n# be processed before the automatic comment starts.\n# Minimum value: 7, maximum value: 10000, default value: 72.\n\nFORTRAN_COMMENT_AFTER  = 72\n\n#---------------------------------------------------------------------------\n# Configuration options related to source browsing\n#---------------------------------------------------------------------------\n\n# If the SOURCE_BROWSER tag is set to YES then a list of source files will be\n# generated. Documented entities will be cross-referenced with these sources.\n#\n# Note: To get rid of all source code in the generated output, make sure that\n# also VERBATIM_HEADERS is set to NO.\n# The default value is: NO.\n\nSOURCE_BROWSER         = NO\n\n# Setting the INLINE_SOURCES tag to YES will include the body of functions,\n# multi-line macros, enums or list initialized variables directly into the\n# documentation.\n# The default value is: NO.\n\nINLINE_SOURCES         = NO\n\n# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any\n# special comment blocks from generated source code fragments. Normal C, C++ and\n# Fortran comments will always remain visible.\n# The default value is: YES.\n\nSTRIP_CODE_COMMENTS    = YES\n\n# If the REFERENCED_BY_RELATION tag is set to YES then for each documented\n# entity all documented functions referencing it will be listed.\n# The default value is: NO.\n\nREFERENCED_BY_RELATION = NO\n\n# If the REFERENCES_RELATION tag is set to YES then for each documented function\n# all documented entities called/used by that function will be listed.\n# The default value is: NO.\n\nREFERENCES_RELATION    = NO\n\n# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set\n# to YES then the hyperlinks from functions in REFERENCES_RELATION and\n# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will\n# link to the documentation.\n# The default value is: YES.\n\nREFERENCES_LINK_SOURCE = YES\n\n# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the\n# source code will show a tooltip with additional information such as prototype,\n# brief description and links to the definition and documentation. Since this\n# will make the HTML file larger and loading of large files a bit slower, you\n# can opt to disable this feature.\n# The default value is: YES.\n# This tag requires that the tag SOURCE_BROWSER is set to YES.\n\nSOURCE_TOOLTIPS        = YES\n\n# If the USE_HTAGS tag is set to YES then the references to source code will\n# point to the HTML generated by the htags(1) tool instead of doxygen built-in\n# source browser. The htags tool is part of GNU's global source tagging system\n# (see https://www.gnu.org/software/global/global.html). You will need version\n# 4.8.6 or higher.\n#\n# To use it do the following:\n# - Install the latest version of global\n# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file\n# - Make sure the INPUT points to the root of the source tree\n# - Run doxygen as normal\n#\n# Doxygen will invoke htags (and that will in turn invoke gtags), so these\n# tools must be available from the command line (i.e. in the search path).\n#\n# The result: instead of the source browser generated by doxygen, the links to\n# source code will now point to the output of htags.\n# The default value is: NO.\n# This tag requires that the tag SOURCE_BROWSER is set to YES.\n\nUSE_HTAGS              = NO\n\n# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a\n# verbatim copy of the header file for each class for which an include is\n# specified. Set to NO to disable this.\n# See also: Section \\class.\n# The default value is: YES.\n\nVERBATIM_HEADERS       = YES\n\n# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the\n# clang parser (see:\n# http://clang.llvm.org/) for more accurate parsing at the cost of reduced\n# performance. This can be particularly helpful with template rich C++ code for\n# which doxygen's built-in parser lacks the necessary type information.\n# Note: The availability of this option depends on whether or not doxygen was\n# generated with the -Duse_libclang=ON option for CMake.\n# The default value is: NO.\n\nCLANG_ASSISTED_PARSING = NO\n\n# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS\n# tag is set to YES then doxygen will add the directory of each input to the\n# include path.\n# The default value is: YES.\n# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.\n\nCLANG_ADD_INC_PATHS    = YES\n\n# If clang assisted parsing is enabled you can provide the compiler with command\n# line options that you would normally use when invoking the compiler. Note that\n# the include paths will already be set by doxygen for the files and directories\n# specified with INPUT and INCLUDE_PATH.\n# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.\n\nCLANG_OPTIONS          =\n\n# If clang assisted parsing is enabled you can provide the clang parser with the\n# path to the directory containing a file called compile_commands.json. This\n# file is the compilation database (see:\n# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the\n# options used when the source files were built. This is equivalent to\n# specifying the -p option to a clang tool, such as clang-check. These options\n# will then be passed to the parser. Any options specified with CLANG_OPTIONS\n# will be added as well.\n# Note: The availability of this option depends on whether or not doxygen was\n# generated with the -Duse_libclang=ON option for CMake.\n\nCLANG_DATABASE_PATH    =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the alphabetical class index\n#---------------------------------------------------------------------------\n\n# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all\n# compounds will be generated. Enable this if the project contains a lot of\n# classes, structs, unions or interfaces.\n# The default value is: YES.\n\nALPHABETICAL_INDEX     = YES\n\n# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes)\n# that should be ignored while generating the index headers. The IGNORE_PREFIX\n# tag works for classes, function and member names. The entity will be placed in\n# the alphabetical list under the first letter of the entity name that remains\n# after removing the prefix.\n# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.\n\nIGNORE_PREFIX          =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the HTML output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output\n# The default value is: YES.\n\nGENERATE_HTML          = YES\n\n# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: html.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_OUTPUT            = html\n\n# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each\n# generated HTML page (for example: .htm, .php, .asp).\n# The default value is: .html.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FILE_EXTENSION    = .html\n\n# The HTML_HEADER tag can be used to specify a user-defined HTML header file for\n# each generated HTML page. If the tag is left blank doxygen will generate a\n# standard header.\n#\n# To get valid HTML the header file that includes any scripts and style sheets\n# that doxygen needs, which is dependent on the configuration options used (e.g.\n# the setting GENERATE_TREEVIEW). It is highly recommended to start with a\n# default header using\n# doxygen -w html new_header.html new_footer.html new_stylesheet.css\n# YourConfigFile\n# and then modify the file new_header.html. See also section \"Doxygen usage\"\n# for information on how to generate the default header that doxygen normally\n# uses.\n# Note: The header is subject to change so you typically have to regenerate the\n# default header when upgrading to a newer version of doxygen. For a description\n# of the possible markers and block names see the documentation.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_HEADER            =\n\n# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each\n# generated HTML page. If the tag is left blank doxygen will generate a standard\n# footer. See HTML_HEADER for more information on how to generate a default\n# footer and what special commands can be used inside the footer. See also\n# section \"Doxygen usage\" for information on how to generate the default footer\n# that doxygen normally uses.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FOOTER            =\n\n# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style\n# sheet that is used by each HTML page. It can be used to fine-tune the look of\n# the HTML output. If left blank doxygen will generate a default style sheet.\n# See also section \"Doxygen usage\" for information on how to generate the style\n# sheet that doxygen normally uses.\n# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as\n# it is more robust and this tag (HTML_STYLESHEET) will in the future become\n# obsolete.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_STYLESHEET        =\n\n# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined\n# cascading style sheets that are included after the standard style sheets\n# created by doxygen. Using this option one can overrule certain style aspects.\n# This is preferred over using HTML_STYLESHEET since it does not replace the\n# standard style sheet and is therefore more robust against future updates.\n# Doxygen will copy the style sheet files to the output directory.\n# Note: The order of the extra style sheet files is of importance (e.g. the last\n# style sheet in the list overrules the setting of the previous ones in the\n# list).\n# Note: Since the styling of scrollbars can currently not be overruled in\n# Webkit/Chromium, the styling will be left out of the default doxygen.css if\n# one or more extra stylesheets have been specified. So if scrollbar\n# customization is desired it has to be added explicitly. For an example see the\n# documentation.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_EXTRA_STYLESHEET  =\n\n# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or\n# other source files which should be copied to the HTML output directory. Note\n# that these files will be copied to the base HTML output directory. Use the\n# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these\n# files. In the HTML_STYLESHEET file, use the file name only. Also note that the\n# files will be copied as-is; there are no commands or markers available.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_EXTRA_FILES       =\n\n# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output\n# should be rendered with a dark or light theme.\n# Possible values are: LIGHT always generate light mode output, DARK always\n# generate dark mode output, AUTO_LIGHT automatically set the mode according to\n# the user preference, use light mode if no preference is set (the default),\n# AUTO_DARK automatically set the mode according to the user preference, use\n# dark mode if no preference is set and TOGGLE allow to user to switch between\n# light and dark mode via a button.\n# The default value is: AUTO_LIGHT.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE        = AUTO_LIGHT\n\n# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen\n# will adjust the colors in the style sheet and background images according to\n# this color. Hue is specified as an angle on a color-wheel, see\n# https://en.wikipedia.org/wiki/Hue for more information. For instance the value\n# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300\n# purple, and 360 is red again.\n# Minimum value: 0, maximum value: 359, default value: 220.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_HUE    = 220\n\n# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors\n# in the HTML output. For a value of 0 the output will use gray-scales only. A\n# value of 255 will produce the most vivid colors.\n# Minimum value: 0, maximum value: 255, default value: 100.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_SAT    = 100\n\n# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the\n# luminance component of the colors in the HTML output. Values below 100\n# gradually make the output lighter, whereas values above 100 make the output\n# darker. The value divided by 100 is the actual gamma applied, so 80 represents\n# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not\n# change the gamma.\n# Minimum value: 40, maximum value: 240, default value: 80.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COLORSTYLE_GAMMA  = 80\n\n# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML\n# documentation will contain a main index with vertical navigation menus that\n# are dynamically created via JavaScript. If disabled, the navigation index will\n# consists of multiple levels of tabs that are statically embedded in every HTML\n# page. Disable this option to support browsers that do not have JavaScript,\n# like the Qt help browser.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_DYNAMIC_MENUS     = YES\n\n# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML\n# documentation will contain sections that can be hidden and shown after the\n# page has loaded.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_DYNAMIC_SECTIONS  = NO\n\n# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be\n# dynamically folded and expanded in the generated HTML source code.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_CODE_FOLDING      = YES\n\n# If the HTML_COPY_CLIPBOARD tag is set to YES then doxygen will show an icon in\n# the top right corner of code and text fragments that allows the user to copy\n# its content to the clipboard. Note this only works if supported by the browser\n# and the web page is served via a secure context (see:\n# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file:\n# protocol.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_COPY_CLIPBOARD    = YES\n\n# Doxygen stores a couple of settings persistently in the browser (via e.g.\n# cookies). By default these settings apply to all HTML pages generated by\n# doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store\n# the settings under a project specific key, such that the user preferences will\n# be stored separately.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_PROJECT_COOKIE    =\n\n# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries\n# shown in the various tree structured indices initially; the user can expand\n# and collapse entries dynamically later on. Doxygen will expand the tree to\n# such a level that at most the specified number of entries are visible (unless\n# a fully collapsed tree already exceeds this amount). So setting the number of\n# entries 1 will produce a full collapsed tree by default. 0 is a special value\n# representing an infinite number of entries and will result in a full expanded\n# tree by default.\n# Minimum value: 0, maximum value: 9999, default value: 100.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_INDEX_NUM_ENTRIES = 100\n\n# If the GENERATE_DOCSET tag is set to YES, additional index files will be\n# generated that can be used as input for Apple's Xcode 3 integrated development\n# environment (see:\n# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To\n# create a documentation set, doxygen will generate a Makefile in the HTML\n# output directory. Running make will produce the docset in that directory and\n# running make install will install the docset in\n# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at\n# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy\n# genXcode/_index.html for more information.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_DOCSET        = NO\n\n# This tag determines the name of the docset feed. A documentation feed provides\n# an umbrella under which multiple documentation sets from a single provider\n# (such as a company or product suite) can be grouped.\n# The default value is: Doxygen generated docs.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_FEEDNAME        = \"Doxygen generated docs\"\n\n# This tag determines the URL of the docset feed. A documentation feed provides\n# an umbrella under which multiple documentation sets from a single provider\n# (such as a company or product suite) can be grouped.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_FEEDURL         =\n\n# This tag specifies a string that should uniquely identify the documentation\n# set bundle. This should be a reverse domain-name style string, e.g.\n# com.mycompany.MyDocSet. Doxygen will append .docset to the name.\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_BUNDLE_ID       = org.doxygen.Project\n\n# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify\n# the documentation publisher. This should be a reverse domain-name style\n# string, e.g. com.mycompany.MyDocSet.documentation.\n# The default value is: org.doxygen.Publisher.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_PUBLISHER_ID    = org.doxygen.Publisher\n\n# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.\n# The default value is: Publisher.\n# This tag requires that the tag GENERATE_DOCSET is set to YES.\n\nDOCSET_PUBLISHER_NAME  = Publisher\n\n# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three\n# additional HTML index files: index.hhp, index.hhc, and index.hhk. The\n# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop\n# on Windows. In the beginning of 2021 Microsoft took the original page, with\n# a.o. the download links, offline the HTML help workshop was already many years\n# in maintenance mode). You can download the HTML help workshop from the web\n# archives at Installation executable (see:\n# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo\n# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe).\n#\n# The HTML Help Workshop contains a compiler that can convert all HTML output\n# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML\n# files are now used as the Windows 98 help format, and will replace the old\n# Windows help format (.hlp) on all Windows platforms in the future. Compressed\n# HTML files also contain an index, a table of contents, and you can search for\n# words in the documentation. The HTML workshop also contains a viewer for\n# compressed HTML files.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_HTMLHELP      = NO\n\n# The CHM_FILE tag can be used to specify the file name of the resulting .chm\n# file. You can add a path in front of the file if the result should not be\n# written to the html output directory.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nCHM_FILE               =\n\n# The HHC_LOCATION tag can be used to specify the location (absolute path\n# including file name) of the HTML help compiler (hhc.exe). If non-empty,\n# doxygen will try to run the HTML help compiler on the generated index.hhp.\n# The file has to be specified with full path.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nHHC_LOCATION           =\n\n# The GENERATE_CHI flag controls if a separate .chi index file is generated\n# (YES) or that it should be included in the main .chm file (NO).\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nGENERATE_CHI           = NO\n\n# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)\n# and project file content.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nCHM_INDEX_ENCODING     =\n\n# The BINARY_TOC flag controls whether a binary table of contents is generated\n# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it\n# enables the Previous and Next buttons.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nBINARY_TOC             = NO\n\n# The TOC_EXPAND flag can be set to YES to add extra items for group members to\n# the table of contents of the HTML help documentation and to the tree view.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTMLHELP is set to YES.\n\nTOC_EXPAND             = NO\n\n# The SITEMAP_URL tag is used to specify the full URL of the place where the\n# generated documentation will be placed on the server by the user during the\n# deployment of the documentation. The generated sitemap is called sitemap.xml\n# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL\n# is specified no sitemap is generated. For information about the sitemap\n# protocol see https://www.sitemaps.org\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nSITEMAP_URL            =\n\n# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and\n# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that\n# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help\n# (.qch) of the generated HTML documentation.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_QHP           = NO\n\n# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify\n# the file name of the resulting .qch file. The path specified is relative to\n# the HTML output folder.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQCH_FILE               =\n\n# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help\n# Project output. For more information please see Qt Help Project / Namespace\n# (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_NAMESPACE          = org.doxygen.Project\n\n# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt\n# Help Project output. For more information please see Qt Help Project / Virtual\n# Folders (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders).\n# The default value is: doc.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_VIRTUAL_FOLDER     = doc\n\n# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom\n# filter to add. For more information please see Qt Help Project / Custom\n# Filters (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_CUST_FILTER_NAME   =\n\n# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the\n# custom filter to add. For more information please see Qt Help Project / Custom\n# Filters (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_CUST_FILTER_ATTRS  =\n\n# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this\n# project's filter section matches. Qt Help Project / Filter Attributes (see:\n# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHP_SECT_FILTER_ATTRS  =\n\n# The QHG_LOCATION tag can be used to specify the location (absolute path\n# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to\n# run qhelpgenerator on the generated .qhp file.\n# This tag requires that the tag GENERATE_QHP is set to YES.\n\nQHG_LOCATION           =\n\n# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be\n# generated, together with the HTML files, they form an Eclipse help plugin. To\n# install this plugin and make it available under the help contents menu in\n# Eclipse, the contents of the directory containing the HTML and XML files needs\n# to be copied into the plugins directory of eclipse. The name of the directory\n# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.\n# After copying Eclipse needs to be restarted before the help appears.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_ECLIPSEHELP   = NO\n\n# A unique identifier for the Eclipse help plugin. When installing the plugin\n# the directory name containing the HTML and XML files should also have this\n# name. Each documentation set should have its own identifier.\n# The default value is: org.doxygen.Project.\n# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.\n\nECLIPSE_DOC_ID         = org.doxygen.Project\n\n# If you want full control over the layout of the generated HTML pages it might\n# be necessary to disable the index and replace it with your own. The\n# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top\n# of each HTML page. A value of NO enables the index and the value YES disables\n# it. Since the tabs in the index contain the same information as the navigation\n# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nDISABLE_INDEX          = NO\n\n# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index\n# structure should be generated to display hierarchical information. If the tag\n# value is set to YES, a side panel will be generated containing a tree-like\n# index structure (just like the one that is generated for HTML Help). For this\n# to work a browser that supports JavaScript, DHTML, CSS and frames is required\n# (i.e. any modern browser). Windows users are probably better off using the\n# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can\n# further fine tune the look of the index (see \"Fine-tuning the output\"). As an\n# example, the default style sheet generated by doxygen has an example that\n# shows how to put an image at the root of the tree instead of the PROJECT_NAME.\n# Since the tree basically has the same information as the tab index, you could\n# consider setting DISABLE_INDEX to YES when enabling this option.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nGENERATE_TREEVIEW      = NO\n\n# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the\n# FULL_SIDEBAR option determines if the side bar is limited to only the treeview\n# area (value NO) or if it should extend to the full height of the window (value\n# YES). Setting this to YES gives a layout similar to\n# https://docs.readthedocs.io with more room for contents, but less room for the\n# project logo, title, and description. If either GENERATE_TREEVIEW or\n# DISABLE_INDEX is set to NO, this option has no effect.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nFULL_SIDEBAR           = NO\n\n# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that\n# doxygen will group on one line in the generated HTML documentation.\n#\n# Note that a value of 0 will completely suppress the enum values from appearing\n# in the overview section.\n# Minimum value: 0, maximum value: 20, default value: 4.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nENUM_VALUES_PER_LINE   = 4\n\n# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used\n# to set the initial width (in pixels) of the frame in which the tree is shown.\n# Minimum value: 0, maximum value: 1500, default value: 250.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nTREEVIEW_WIDTH         = 250\n\n# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to\n# external symbols imported via tag files in a separate window.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nEXT_LINKS_IN_WINDOW    = NO\n\n# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email\n# addresses.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nOBFUSCATE_EMAILS       = YES\n\n# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg\n# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see\n# https://inkscape.org) to generate formulas as SVG images instead of PNGs for\n# the HTML output. These images will generally look nicer at scaled resolutions.\n# Possible values are: png (the default) and svg (looks nicer but requires the\n# pdf2svg or inkscape tool).\n# The default value is: png.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nHTML_FORMULA_FORMAT    = png\n\n# Use this tag to change the font size of LaTeX formulas included as images in\n# the HTML documentation. When you change the font size after a successful\n# doxygen run you need to manually remove any form_*.png images from the HTML\n# output directory to force them to be regenerated.\n# Minimum value: 8, maximum value: 50, default value: 10.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nFORMULA_FONTSIZE       = 10\n\n# The FORMULA_MACROFILE can contain LaTeX \\newcommand and \\renewcommand commands\n# to create new LaTeX commands to be used in formulas as building blocks. See\n# the section \"Including formulas\" for details.\n\nFORMULA_MACROFILE      =\n\n# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see\n# https://www.mathjax.org) which uses client side JavaScript for the rendering\n# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX\n# installed or if you want to formulas look prettier in the HTML output. When\n# enabled you may also need to install MathJax separately and configure the path\n# to it using the MATHJAX_RELPATH option.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nUSE_MATHJAX            = NO\n\n# With MATHJAX_VERSION it is possible to specify the MathJax version to be used.\n# Note that the different versions of MathJax have different requirements with\n# regards to the different settings, so it is possible that also other MathJax\n# settings have to be changed when switching between the different MathJax\n# versions.\n# Possible values are: MathJax_2 and MathJax_3.\n# The default value is: MathJax_2.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_VERSION        = MathJax_2\n\n# When MathJax is enabled you can set the default output format to be used for\n# the MathJax output. For more details about the output format see MathJax\n# version 2 (see:\n# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3\n# (see:\n# http://docs.mathjax.org/en/latest/web/components/output.html).\n# Possible values are: HTML-CSS (which is slower, but has the best\n# compatibility. This is the name for Mathjax version 2, for MathJax version 3\n# this will be translated into chtml), NativeMML (i.e. MathML. Only supported\n# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This\n# is the name for Mathjax version 3, for MathJax version 2 this will be\n# translated into HTML-CSS) and SVG.\n# The default value is: HTML-CSS.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_FORMAT         = HTML-CSS\n\n# When MathJax is enabled you need to specify the location relative to the HTML\n# output directory using the MATHJAX_RELPATH option. The destination directory\n# should contain the MathJax.js script. For instance, if the mathjax directory\n# is located at the same level as the HTML output directory, then\n# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax\n# Content Delivery Network so you can quickly see the result without installing\n# MathJax. However, it is strongly recommended to install a local copy of\n# MathJax from https://www.mathjax.org before deployment. The default value is:\n# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2\n# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_RELPATH        =\n\n# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax\n# extension names that should be enabled during MathJax rendering. For example\n# for MathJax version 2 (see\n# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions):\n# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols\n# For example for MathJax version 3 (see\n# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html):\n# MATHJAX_EXTENSIONS = ams\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_EXTENSIONS     =\n\n# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces\n# of code that will be used on startup of the MathJax code. See the MathJax site\n# (see:\n# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an\n# example see the documentation.\n# This tag requires that the tag USE_MATHJAX is set to YES.\n\nMATHJAX_CODEFILE       =\n\n# When the SEARCHENGINE tag is enabled doxygen will generate a search box for\n# the HTML output. The underlying search engine uses javascript and DHTML and\n# should work on any modern browser. Note that when using HTML help\n# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)\n# there is already a search function so this one should typically be disabled.\n# For large projects the javascript based search engine can be slow, then\n# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to\n# search using the keyboard; to jump to the search box use <access key> + S\n# (what the <access key> is depends on the OS and browser, but it is typically\n# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down\n# key> to jump into the search results window, the results can be navigated\n# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel\n# the search. The filter options can be selected when the cursor is inside the\n# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>\n# to select a filter and <Enter> or <escape> to activate or cancel the filter\n# option.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_HTML is set to YES.\n\nSEARCHENGINE           = YES\n\n# When the SERVER_BASED_SEARCH tag is enabled the search engine will be\n# implemented using a web server instead of a web client using JavaScript. There\n# are two flavors of web server based searching depending on the EXTERNAL_SEARCH\n# setting. When disabled, doxygen will generate a PHP script for searching and\n# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing\n# and searching needs to be provided by external tools. See the section\n# \"External Indexing and Searching\" for details.\n# The default value is: NO.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSERVER_BASED_SEARCH    = NO\n\n# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP\n# script for searching. Instead the search results are written to an XML file\n# which needs to be processed by an external indexer. Doxygen will invoke an\n# external search engine pointed to by the SEARCHENGINE_URL option to obtain the\n# search results.\n#\n# Doxygen ships with an example indexer (doxyindexer) and search engine\n# (doxysearch.cgi) which are based on the open source search engine library\n# Xapian (see:\n# https://xapian.org/).\n#\n# See the section \"External Indexing and Searching\" for details.\n# The default value is: NO.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTERNAL_SEARCH        = NO\n\n# The SEARCHENGINE_URL should point to a search engine hosted by a web server\n# which will return the search results when EXTERNAL_SEARCH is enabled.\n#\n# Doxygen ships with an example indexer (doxyindexer) and search engine\n# (doxysearch.cgi) which are based on the open source search engine library\n# Xapian (see:\n# https://xapian.org/). See the section \"External Indexing and Searching\" for\n# details.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSEARCHENGINE_URL       =\n\n# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed\n# search data is written to a file for indexing by an external tool. With the\n# SEARCHDATA_FILE tag the name of this file can be specified.\n# The default file is: searchdata.xml.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nSEARCHDATA_FILE        = searchdata.xml\n\n# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the\n# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is\n# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple\n# projects and redirect the results back to the right project.\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTERNAL_SEARCH_ID     =\n\n# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen\n# projects other than the one defined by this configuration file, but that are\n# all added to the same external search index. Each project needs to have a\n# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of\n# to a relative location where the documentation can be found. The format is:\n# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...\n# This tag requires that the tag SEARCHENGINE is set to YES.\n\nEXTRA_SEARCH_MAPPINGS  =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the LaTeX output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.\n# The default value is: YES.\n\nGENERATE_LATEX         = YES\n\n# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: latex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_OUTPUT           = latex\n\n# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be\n# invoked.\n#\n# Note that when not enabling USE_PDFLATEX the default is latex when enabling\n# USE_PDFLATEX the default is pdflatex and when in the later case latex is\n# chosen this is overwritten by pdflatex. For specific output languages the\n# default can have been set differently, this depends on the implementation of\n# the output language.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_CMD_NAME         =\n\n# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate\n# index for LaTeX.\n# Note: This tag is used in the Makefile / make.bat.\n# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file\n# (.tex).\n# The default file is: makeindex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nMAKEINDEX_CMD_NAME     = makeindex\n\n# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to\n# generate index for LaTeX. In case there is no backslash (\\) as first character\n# it will be automatically added in the LaTeX code.\n# Note: This tag is used in the generated output file (.tex).\n# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat.\n# The default value is: makeindex.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_MAKEINDEX_CMD    = makeindex\n\n# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX\n# documents. This may be useful for small projects and may help to save some\n# trees in general.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nCOMPACT_LATEX          = NO\n\n# The PAPER_TYPE tag can be used to set the paper type that is used by the\n# printer.\n# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x\n# 14 inches) and executive (7.25 x 10.5 inches).\n# The default value is: a4.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nPAPER_TYPE             = a4\n\n# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names\n# that should be included in the LaTeX output. The package can be specified just\n# by its name or with the correct syntax as to be used with the LaTeX\n# \\usepackage command. To get the times font for instance you can specify :\n# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}\n# To use the option intlimits with the amsmath package you can specify:\n# EXTRA_PACKAGES=[intlimits]{amsmath}\n# If left blank no extra packages will be included.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nEXTRA_PACKAGES         =\n\n# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for\n# the generated LaTeX document. The header should contain everything until the\n# first chapter. If it is left blank doxygen will generate a standard header. It\n# is highly recommended to start with a default header using\n# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty\n# and then modify the file new_header.tex. See also section \"Doxygen usage\" for\n# information on how to generate the default header that doxygen normally uses.\n#\n# Note: Only use a user-defined header if you know what you are doing!\n# Note: The header is subject to change so you typically have to regenerate the\n# default header when upgrading to a newer version of doxygen. The following\n# commands have a special meaning inside the header (and footer): For a\n# description of the possible markers and block names see the documentation.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_HEADER           =\n\n# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for\n# the generated LaTeX document. The footer should contain everything after the\n# last chapter. If it is left blank doxygen will generate a standard footer. See\n# LATEX_HEADER for more information on how to generate a default footer and what\n# special commands can be used inside the footer. See also section \"Doxygen\n# usage\" for information on how to generate the default footer that doxygen\n# normally uses. Note: Only use a user-defined footer if you know what you are\n# doing!\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_FOOTER           =\n\n# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined\n# LaTeX style sheets that are included after the standard style sheets created\n# by doxygen. Using this option one can overrule certain style aspects. Doxygen\n# will copy the style sheet files to the output directory.\n# Note: The order of the extra style sheet files is of importance (e.g. the last\n# style sheet in the list overrules the setting of the previous ones in the\n# list).\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EXTRA_STYLESHEET =\n\n# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or\n# other source files which should be copied to the LATEX_OUTPUT output\n# directory. Note that the files will be copied as-is; there are no commands or\n# markers available.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EXTRA_FILES      =\n\n# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is\n# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will\n# contain links (just like the HTML output) instead of page references. This\n# makes the output suitable for online browsing using a PDF viewer.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nPDF_HYPERLINKS         = YES\n\n# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as\n# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX\n# files. Set this option to YES, to get a higher quality PDF documentation.\n#\n# See also section LATEX_CMD_NAME for selecting the engine.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nUSE_PDFLATEX           = YES\n\n# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error.\n# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch\n# mode nothing is printed on the terminal, errors are scrolled as if <return> is\n# hit at every error; missing files that TeX tries to input or request from\n# keyboard input (\\read on a not open input stream) cause the job to abort,\n# NON_STOP In nonstop mode the diagnostic message will appear on the terminal,\n# but there is no possibility of user interaction just like in batch mode,\n# SCROLL In scroll mode, TeX will stop only for missing files to input or if\n# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at\n# each error, asking for user intervention.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_BATCHMODE        = NO\n\n# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the\n# index chapters (such as File Index, Compound Index, etc.) in the output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_HIDE_INDICES     = NO\n\n# The LATEX_BIB_STYLE tag can be used to specify the style to use for the\n# bibliography, e.g. plainnat, or ieeetr. See\n# https://en.wikipedia.org/wiki/BibTeX and \\cite for more info.\n# The default value is: plain.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_BIB_STYLE        = plain\n\n# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)\n# path from which the emoji images will be read. If a relative path is entered,\n# it will be relative to the LATEX_OUTPUT directory. If left blank the\n# LATEX_OUTPUT directory will be used.\n# This tag requires that the tag GENERATE_LATEX is set to YES.\n\nLATEX_EMOJI_DIRECTORY  =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the RTF output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The\n# RTF output is optimized for Word 97 and may not look too pretty with other RTF\n# readers/editors.\n# The default value is: NO.\n\nGENERATE_RTF           = NO\n\n# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: rtf.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_OUTPUT             = rtf\n\n# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF\n# documents. This may be useful for small projects and may help to save some\n# trees in general.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nCOMPACT_RTF            = NO\n\n# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will\n# contain hyperlink fields. The RTF file will contain links (just like the HTML\n# output) instead of page references. This makes the output suitable for online\n# browsing using Word or some other Word compatible readers that support those\n# fields.\n#\n# Note: WordPad (write) and others do not support links.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_HYPERLINKS         = NO\n\n# Load stylesheet definitions from file. Syntax is similar to doxygen's\n# configuration file, i.e. a series of assignments. You only have to provide\n# replacements, missing definitions are set to their default value.\n#\n# See also section \"Doxygen usage\" for information on how to generate the\n# default style sheet that doxygen normally uses.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_STYLESHEET_FILE    =\n\n# Set optional variables used in the generation of an RTF document. Syntax is\n# similar to doxygen's configuration file. A template extensions file can be\n# generated using doxygen -e rtf extensionFile.\n# This tag requires that the tag GENERATE_RTF is set to YES.\n\nRTF_EXTENSIONS_FILE    =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the man page output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for\n# classes and files.\n# The default value is: NO.\n\nGENERATE_MAN           = NO\n\n# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it. A directory man3 will be created inside the directory specified by\n# MAN_OUTPUT.\n# The default directory is: man.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_OUTPUT             = man\n\n# The MAN_EXTENSION tag determines the extension that is added to the generated\n# man pages. In case the manual section does not start with a number, the number\n# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is\n# optional.\n# The default value is: .3.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_EXTENSION          = .3\n\n# The MAN_SUBDIR tag determines the name of the directory created within\n# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by\n# MAN_EXTENSION with the initial . removed.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_SUBDIR             =\n\n# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it\n# will generate one additional man file for each entity documented in the real\n# man page(s). These additional files only source the real man page, but without\n# them the man command would be unable to find the correct page.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_MAN is set to YES.\n\nMAN_LINKS              = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the XML output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that\n# captures the structure of the code including all documentation.\n# The default value is: NO.\n\nGENERATE_XML           = NO\n\n# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a\n# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of\n# it.\n# The default directory is: xml.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_OUTPUT             = xml\n\n# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program\n# listings (including syntax highlighting and cross-referencing information) to\n# the XML output. Note that enabling this will significantly increase the size\n# of the XML output.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_PROGRAMLISTING     = YES\n\n# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include\n# namespace members in file scope as well, matching the HTML output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_XML is set to YES.\n\nXML_NS_MEMB_FILE_SCOPE = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to the DOCBOOK output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files\n# that can be used to generate PDF.\n# The default value is: NO.\n\nGENERATE_DOCBOOK       = NO\n\n# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.\n# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in\n# front of it.\n# The default directory is: docbook.\n# This tag requires that the tag GENERATE_DOCBOOK is set to YES.\n\nDOCBOOK_OUTPUT         = docbook\n\n#---------------------------------------------------------------------------\n# Configuration options for the AutoGen Definitions output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an\n# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures\n# the structure of the code including all documentation. Note that this feature\n# is still experimental and incomplete at the moment.\n# The default value is: NO.\n\nGENERATE_AUTOGEN_DEF   = NO\n\n#---------------------------------------------------------------------------\n# Configuration options related to Sqlite3 output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3\n# database with symbols found by doxygen stored in tables.\n# The default value is: NO.\n\nGENERATE_SQLITE3       = NO\n\n# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be\n# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put\n# in front of it.\n# The default directory is: sqlite3.\n# This tag requires that the tag GENERATE_SQLITE3 is set to YES.\n\nSQLITE3_OUTPUT         = sqlite3\n\n# The SQLITE3_RECREATE_DB tag is set to YES, the existing doxygen_sqlite3.db\n# database file will be recreated with each doxygen run. If set to NO, doxygen\n# will warn if a database file is already found and not modify it.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_SQLITE3 is set to YES.\n\nSQLITE3_RECREATE_DB    = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to the Perl module output\n#---------------------------------------------------------------------------\n\n# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module\n# file that captures the structure of the code including all documentation.\n#\n# Note that this feature is still experimental and incomplete at the moment.\n# The default value is: NO.\n\nGENERATE_PERLMOD       = NO\n\n# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary\n# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI\n# output from the Perl module output.\n# The default value is: NO.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_LATEX          = NO\n\n# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely\n# formatted so it can be parsed by a human reader. This is useful if you want to\n# understand what is going on. On the other hand, if this tag is set to NO, the\n# size of the Perl module output will be much smaller and Perl will parse it\n# just the same.\n# The default value is: YES.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_PRETTY         = YES\n\n# The names of the make variables in the generated doxyrules.make file are\n# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful\n# so different doxyrules.make files included by the same Makefile don't\n# overwrite each other's variables.\n# This tag requires that the tag GENERATE_PERLMOD is set to YES.\n\nPERLMOD_MAKEVAR_PREFIX =\n\n#---------------------------------------------------------------------------\n# Configuration options related to the preprocessor\n#---------------------------------------------------------------------------\n\n# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all\n# C-preprocessor directives found in the sources and include files.\n# The default value is: YES.\n\nENABLE_PREPROCESSING   = YES\n\n# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names\n# in the source code. If set to NO, only conditional compilation will be\n# performed. Macro expansion can be done in a controlled way by setting\n# EXPAND_ONLY_PREDEF to YES.\n# The default value is: NO.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nMACRO_EXPANSION        = NO\n\n# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then\n# the macro expansion is limited to the macros specified with the PREDEFINED and\n# EXPAND_AS_DEFINED tags.\n# The default value is: NO.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nEXPAND_ONLY_PREDEF     = NO\n\n# If the SEARCH_INCLUDES tag is set to YES, the include files in the\n# INCLUDE_PATH will be searched if a #include is found.\n# The default value is: YES.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nSEARCH_INCLUDES        = YES\n\n# The INCLUDE_PATH tag can be used to specify one or more directories that\n# contain include files that are not input files but should be processed by the\n# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of\n# RECURSIVE has no effect here.\n# This tag requires that the tag SEARCH_INCLUDES is set to YES.\n\nINCLUDE_PATH           =\n\n# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard\n# patterns (like *.h and *.hpp) to filter out the header-files in the\n# directories. If left blank, the patterns specified with FILE_PATTERNS will be\n# used.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nINCLUDE_FILE_PATTERNS  =\n\n# The PREDEFINED tag can be used to specify one or more macro names that are\n# defined before the preprocessor is started (similar to the -D option of e.g.\n# gcc). The argument of the tag is a list of macros of the form: name or\n# name=definition (no spaces). If the definition and the \"=\" are omitted, \"=1\"\n# is assumed. To prevent a macro definition from being undefined via #undef or\n# recursively expanded use the := operator instead of the = operator.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nPREDEFINED             =\n\n# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this\n# tag can be used to specify a list of macro names that should be expanded. The\n# macro definition that is found in the sources will be used. Use the PREDEFINED\n# tag if you want to use a different macro definition that overrules the\n# definition found in the source code.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nEXPAND_AS_DEFINED      =\n\n# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will\n# remove all references to function-like macros that are alone on a line, have\n# an all uppercase name, and do not end with a semicolon. Such function macros\n# are typically used for boiler-plate code, and will confuse the parser if not\n# removed.\n# The default value is: YES.\n# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.\n\nSKIP_FUNCTION_MACROS   = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to external references\n#---------------------------------------------------------------------------\n\n# The TAGFILES tag can be used to specify one or more tag files. For each tag\n# file the location of the external documentation should be added. The format of\n# a tag file without this location is as follows:\n# TAGFILES = file1 file2 ...\n# Adding location for the tag files is done as follows:\n# TAGFILES = file1=loc1 \"file2 = loc2\" ...\n# where loc1 and loc2 can be relative or absolute paths or URLs. See the\n# section \"Linking to external documentation\" for more information about the use\n# of tag files.\n# Note: Each tag file must have a unique name (where the name does NOT include\n# the path). If a tag file is not located in the directory in which doxygen is\n# run, you must also specify the path to the tagfile here.\n\nTAGFILES               =\n\n# When a file name is specified after GENERATE_TAGFILE, doxygen will create a\n# tag file that is based on the input files it reads. See section \"Linking to\n# external documentation\" for more information about the usage of tag files.\n\nGENERATE_TAGFILE       =\n\n# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces\n# will be listed in the class and namespace index. If set to NO, only the\n# inherited external classes will be listed.\n# The default value is: NO.\n\nALLEXTERNALS           = NO\n\n# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed\n# in the topic index. If set to NO, only the current project's groups will be\n# listed.\n# The default value is: YES.\n\nEXTERNAL_GROUPS        = YES\n\n# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in\n# the related pages index. If set to NO, only the current project's pages will\n# be listed.\n# The default value is: YES.\n\nEXTERNAL_PAGES         = YES\n\n#---------------------------------------------------------------------------\n# Configuration options related to diagram generator tools\n#---------------------------------------------------------------------------\n\n# If set to YES the inheritance and collaboration graphs will hide inheritance\n# and usage relations if the target is undocumented or is not a class.\n# The default value is: YES.\n\nHIDE_UNDOC_RELATIONS   = YES\n\n# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is\n# available from the path. This tool is part of Graphviz (see:\n# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent\n# Bell Labs. The other options in this section have no effect if this option is\n# set to NO\n# The default value is: NO.\n\nHAVE_DOT               = NO\n\n# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed\n# to run in parallel. When set to 0 doxygen will base this on the number of\n# processors available in the system. You can set it explicitly to a value\n# larger than 0 to get control over the balance between CPU load and processing\n# speed.\n# Minimum value: 0, maximum value: 32, default value: 0.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_NUM_THREADS        = 0\n\n# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of\n# subgraphs. When you want a differently looking font in the dot files that\n# doxygen generates you can specify fontname, fontcolor and fontsize attributes.\n# For details please see <a href=https://graphviz.org/doc/info/attrs.html>Node,\n# Edge and Graph Attributes specification</a> You need to make sure dot is able\n# to find the font, which can be done by putting it in a standard location or by\n# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the\n# directory containing the font. Default graphviz fontsize is 14.\n# The default value is: fontname=Helvetica,fontsize=10.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_COMMON_ATTR        = \"fontname=Helvetica,fontsize=10\"\n\n# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can\n# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. <a\n# href=https://graphviz.org/doc/info/arrows.html>Complete documentation about\n# arrows shapes.</a>\n# The default value is: labelfontname=Helvetica,labelfontsize=10.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_EDGE_ATTR          = \"labelfontname=Helvetica,labelfontsize=10\"\n\n# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes\n# around nodes set 'shape=plain' or 'shape=plaintext' <a\n# href=https://www.graphviz.org/doc/info/shapes.html>Shapes specification</a>\n# The default value is: shape=box,height=0.2,width=0.4.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_NODE_ATTR          = \"shape=box,height=0.2,width=0.4\"\n\n# You can set the path where dot can find font specified with fontname in\n# DOT_COMMON_ATTR and others dot attributes.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_FONTPATH           =\n\n# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will\n# generate a graph for each documented class showing the direct and indirect\n# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and\n# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case\n# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the\n# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used.\n# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance\n# relations will be shown as texts / links. Explicit enabling an inheritance\n# graph or choosing a different representation for an inheritance graph of a\n# specific class, can be accomplished by means of the command \\inheritancegraph.\n# Disabling an inheritance graph can be accomplished by means of the command\n# \\hideinheritancegraph.\n# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN.\n# The default value is: YES.\n\nCLASS_GRAPH            = YES\n\n# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a\n# graph for each documented class showing the direct and indirect implementation\n# dependencies (inheritance, containment, and class references variables) of the\n# class with other documented classes. Explicit enabling a collaboration graph,\n# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the\n# command \\collaborationgraph. Disabling a collaboration graph can be\n# accomplished by means of the command \\hidecollaborationgraph.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCOLLABORATION_GRAPH    = YES\n\n# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for\n# groups, showing the direct groups dependencies. Explicit enabling a group\n# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means\n# of the command \\groupgraph. Disabling a directory graph can be accomplished by\n# means of the command \\hidegroupgraph. See also the chapter Grouping in the\n# manual.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGROUP_GRAPHS           = YES\n\n# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and\n# collaboration diagrams in a style similar to the OMG's Unified Modeling\n# Language.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nUML_LOOK               = NO\n\n# If the UML_LOOK tag is enabled, the fields and methods are shown inside the\n# class node. If there are many fields or methods and many nodes the graph may\n# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the\n# number of items for each type to make the size more manageable. Set this to 0\n# for no limit. Note that the threshold may be exceeded by 50% before the limit\n# is enforced. So when you set the threshold to 10, up to 15 fields may appear,\n# but if the number exceeds 15, the total amount of fields shown is limited to\n# 10.\n# Minimum value: 0, maximum value: 100, default value: 10.\n# This tag requires that the tag UML_LOOK is set to YES.\n\nUML_LIMIT_NUM_FIELDS   = 10\n\n# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and\n# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS\n# tag is set to YES, doxygen will add type and arguments for attributes and\n# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen\n# will not generate fields with class member information in the UML graphs. The\n# class diagrams will look similar to the default class diagrams but using UML\n# notation for the relationships.\n# Possible values are: NO, YES and NONE.\n# The default value is: NO.\n# This tag requires that the tag UML_LOOK is set to YES.\n\nDOT_UML_DETAILS        = NO\n\n# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters\n# to display on a single line. If the actual line length exceeds this threshold\n# significantly it will be wrapped across multiple lines. Some heuristics are\n# applied to avoid ugly line breaks.\n# Minimum value: 0, maximum value: 1000, default value: 17.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_WRAP_THRESHOLD     = 17\n\n# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and\n# collaboration graphs will show the relations between templates and their\n# instances.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nTEMPLATE_RELATIONS     = NO\n\n# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to\n# YES then doxygen will generate a graph for each documented file showing the\n# direct and indirect include dependencies of the file with other documented\n# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO,\n# can be accomplished by means of the command \\includegraph. Disabling an\n# include graph can be accomplished by means of the command \\hideincludegraph.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINCLUDE_GRAPH          = YES\n\n# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are\n# set to YES then doxygen will generate a graph for each documented file showing\n# the direct and indirect include dependencies of the file with other documented\n# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set\n# to NO, can be accomplished by means of the command \\includedbygraph. Disabling\n# an included by graph can be accomplished by means of the command\n# \\hideincludedbygraph.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINCLUDED_BY_GRAPH      = YES\n\n# If the CALL_GRAPH tag is set to YES then doxygen will generate a call\n# dependency graph for every global function or class method.\n#\n# Note that enabling this option will significantly increase the time of a run.\n# So in most cases it will be better to enable call graphs for selected\n# functions only using the \\callgraph command. Disabling a call graph can be\n# accomplished by means of the command \\hidecallgraph.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCALL_GRAPH             = NO\n\n# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller\n# dependency graph for every global function or class method.\n#\n# Note that enabling this option will significantly increase the time of a run.\n# So in most cases it will be better to enable caller graphs for selected\n# functions only using the \\callergraph command. Disabling a caller graph can be\n# accomplished by means of the command \\hidecallergraph.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nCALLER_GRAPH           = NO\n\n# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical\n# hierarchy of all classes instead of a textual one.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGRAPHICAL_HIERARCHY    = YES\n\n# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the\n# dependencies a directory has on other directories in a graphical way. The\n# dependency relations are determined by the #include relations between the\n# files in the directories. Explicit enabling a directory graph, when\n# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command\n# \\directorygraph. Disabling a directory graph can be accomplished by means of\n# the command \\hidedirectorygraph.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDIRECTORY_GRAPH        = YES\n\n# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels\n# of child directories generated in directory dependency graphs by dot.\n# Minimum value: 1, maximum value: 25, default value: 1.\n# This tag requires that the tag DIRECTORY_GRAPH is set to YES.\n\nDIR_GRAPH_MAX_DEPTH    = 1\n\n# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images\n# generated by dot. For an explanation of the image formats see the section\n# output formats in the documentation of the dot tool (Graphviz (see:\n# https://www.graphviz.org/)).\n# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order\n# to make the SVG files visible in IE 9+ (other browsers do not have this\n# requirement).\n# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,\n# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and\n# png:gdiplus:gdiplus.\n# The default value is: png.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_IMAGE_FORMAT       = png\n\n# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to\n# enable generation of interactive SVG images that allow zooming and panning.\n#\n# Note that this requires a modern browser other than Internet Explorer. Tested\n# and working are Firefox, Chrome, Safari, and Opera.\n# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make\n# the SVG files visible. Older versions of IE do not have SVG support.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nINTERACTIVE_SVG        = NO\n\n# The DOT_PATH tag can be used to specify the path where the dot tool can be\n# found. If left blank, it is assumed the dot tool can be found in the path.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_PATH               =\n\n# The DOTFILE_DIRS tag can be used to specify one or more directories that\n# contain dot files that are included in the documentation (see the \\dotfile\n# command).\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOTFILE_DIRS           =\n\n# You can include diagrams made with dia in doxygen documentation. Doxygen will\n# then run dia to produce the diagram and insert it in the documentation. The\n# DIA_PATH tag allows you to specify the directory where the dia binary resides.\n# If left empty dia is assumed to be found in the default search path.\n\nDIA_PATH               =\n\n# The DIAFILE_DIRS tag can be used to specify one or more directories that\n# contain dia files that are included in the documentation (see the \\diafile\n# command).\n\nDIAFILE_DIRS           =\n\n# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the\n# path where java can find the plantuml.jar file or to the filename of jar file\n# to be used. If left blank, it is assumed PlantUML is not used or called during\n# a preprocessing step. Doxygen will generate a warning when it encounters a\n# \\startuml command in this case and will not generate output for the diagram.\n\nPLANTUML_JAR_PATH      =\n\n# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a\n# configuration file for plantuml.\n\nPLANTUML_CFG_FILE      =\n\n# When using plantuml, the specified paths are searched for files specified by\n# the !include statement in a plantuml block.\n\nPLANTUML_INCLUDE_PATH  =\n\n# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes\n# that will be shown in the graph. If the number of nodes in a graph becomes\n# larger than this value, doxygen will truncate the graph, which is visualized\n# by representing a node as a red box. Note that doxygen if the number of direct\n# children of the root node in a graph is already larger than\n# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that\n# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.\n# Minimum value: 0, maximum value: 10000, default value: 50.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_GRAPH_MAX_NODES    = 50\n\n# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs\n# generated by dot. A depth value of 3 means that only nodes reachable from the\n# root by following a path via at most 3 edges will be shown. Nodes that lay\n# further from the root node will be omitted. Note that setting this option to 1\n# or 2 may greatly reduce the computation time needed for large code bases. Also\n# note that the size of a graph can be further restricted by\n# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.\n# Minimum value: 0, maximum value: 1000, default value: 0.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nMAX_DOT_GRAPH_DEPTH    = 0\n\n# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output\n# files in one run (i.e. multiple -o and -T options on the command line). This\n# makes dot run faster, but since only newer versions of dot (>1.8.10) support\n# this, this feature is disabled by default.\n# The default value is: NO.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nDOT_MULTI_TARGETS      = NO\n\n# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page\n# explaining the meaning of the various boxes and arrows in the dot generated\n# graphs.\n# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal\n# graphical representation for inheritance and collaboration diagrams is used.\n# The default value is: YES.\n# This tag requires that the tag HAVE_DOT is set to YES.\n\nGENERATE_LEGEND        = YES\n\n# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate\n# files that are used to generate the various graphs.\n#\n# Note: This setting is not only used for dot files but also for msc temporary\n# files.\n# The default value is: YES.\n\nDOT_CLEANUP            = YES\n\n# You can define message sequence charts within doxygen comments using the \\msc\n# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will\n# use a built-in version of mscgen tool to produce the charts. Alternatively,\n# the MSCGEN_TOOL tag can also specify the name an external tool. For instance,\n# specifying prog as the value, doxygen will call the tool as prog -T\n# <outfile_format> -o <outputfile> <inputfile>. The external tool should support\n# output file formats \"png\", \"eps\", \"svg\", and \"ismap\".\n\nMSCGEN_TOOL            =\n\n# The MSCFILE_DIRS tag can be used to specify one or more directories that\n# contain msc files that are included in the documentation (see the \\mscfile\n# command).\n\nMSCFILE_DIRS           =\n"
  },
  {
    "path": "docs/WEBUI_DEVELOPMENT.md",
    "content": "# WebUI 开发指南\n\nSunshine 包含一个现代化的 Web 控制界面，基于 Vue 3 和 Composition API 构建，遵循 Vue 最佳实践。\n\n> **注意**: 本文档已更新以反映最新的项目结构优化。所有页面已重构为使用 Composition API 和模块化架构。\n\n## 🛠️ 技术栈\n\n- **前端框架**: Vue 3 + Composition API\n- **构建工具**: Vite 5.4+ (支持 Rolldown)\n- **打包器**: Rolldown (实验性，更快)\n- **UI 组件**: Bootstrap 5\n- **图标库**: FontAwesome 6\n- **国际化**: Vue-i18n 11 (Composition API 模式)\n- **拖拽功能**: Vuedraggable 4\n- **模块系统**: ES Modules (`\"type\": \"module\"`)\n\n> **注意**: 本文档已更新以反映最新的项目结构优化。所有页面已重构为使用 Composition API 和模块化架构。\n\n## 🚀 开发环境设置\n\n### 1. 安装依赖\n\n```bash\nnpm install\n```\n\n### 2. 开发命令\n\n```bash\n# 开发模式 - 实时构建和监听文件变化\nnpm run dev\n\n# 开发服务器 - 启动HTTPS开发服务器 (推荐)\nnpm run dev-server\n\n# 完整开发环境 - 包含模拟API服务\nnpm run dev-full\n\n# 构建生产版本\nnpm run build\n\n# 清理构建目录并重新构建\nnpm run build-clean\n\n# 预览生产构建\nnpm run preview\n\n# 自动构建并预览生产版本（推荐）\nnpm run preview:build\n```\n\n> **注意**: 项目已配置为使用 Rolldown（Vite 5.1+ 的实验性打包器）以获得更快的构建速度。所有构建命令默认启用 Rolldown。\n\n### 3. 开发服务器特性\n\n- **HTTPS支持**: 自动生成本地SSL证书\n- **热重载**: 实时更新代码变更\n- **代理配置**: 自动代理API请求到Sunshine服务\n- **模拟数据**: 开发模式下提供模拟API响应\n- **端口**: 默认运行在 `https://localhost:3000`\n\n## 📁 项目结构\n\n```\nsrc_assets/common/assets/web/\n├── views/                    # 页面组件（路由级组件）\n│   ├── Home.vue             # 首页\n│   ├── Apps.vue             # 应用管理页面\n│   ├── Config.vue            # 配置管理页面\n│   ├── Troubleshooting.vue  # 故障排除页面\n│   ├── Pin.vue              # PIN 配对页面\n│   ├── Password.vue         # 密码修改页面\n│   └── Welcome.vue          # 欢迎页面\n│\n├── components/              # Vue 组件\n│   ├── layout/              # 布局组件\n│   │   ├── Navbar.vue       # 导航栏\n│   │   └── PlatformLayout.vue # 平台布局组件\n│   ├── common/              # 通用组件\n│   │   ├── ThemeToggle.vue  # 主题切换\n│   │   ├── ResourceCard.vue  # 资源卡片\n│   │   ├── VersionCard.vue  # 版本信息卡片\n│   │   ├── ErrorLogs.vue    # 错误日志组件\n│   │   └── Locale.vue        # 语言组件\n│   ├── SetupWizard.vue       # 设置向导\n│   └── ...                  # 其他功能组件\n│\n├── composables/             # 组合式函数（可复用逻辑）\n│   ├── useVersion.js        # 版本管理\n│   ├── useLogs.js           # 日志管理\n│   ├── useSetupWizard.js    # 设置向导逻辑\n│   ├── useApps.js           # 应用管理\n│   ├── useConfig.js         # 配置管理\n│   ├── useTroubleshooting.js # 故障排除\n│   ├── usePin.js            # PIN 配对\n│   ├── useWelcome.js        # 欢迎页面\n│   └── useTheme.js          # 主题管理\n│\n├── config/                  # 配置文件\n│   ├── firebase.js          # Firebase 配置\n│   └── i18n.js              # 国际化配置\n│\n├── services/                # API 服务\n│   └── appService.js         # 应用服务\n│\n├── utils/                   # 工具函数\n│   ├── constants.js         # 常量定义\n│   ├── helpers.js           # 辅助函数\n│   ├── validation.js        # 表单验证\n│   ├── theme.js             # 主题工具\n│   └── ...\n│\n├── styles/                  # 样式文件\n│   ├── apps.css             # 应用页面样式\n│   ├── welcome.css          # 欢迎页面样式\n│   └── ...\n│\n├── public/                  # 静态资源\n│   ├── assets/\n│   │   ├── css/             # 全局样式\n│   │   └── locale/          # 国际化文件\n│   └── images/              # 图片资源\n│\n├── configs/                  # 配置页面子组件\n│   └── tabs/                # 配置标签页组件\n│\n├── *.html                   # 页面入口文件（已简化）\n└── init.js                  # 应用初始化\n```\n\n## 🎯 架构设计原则\n\n### 1. 目录组织\n\n- **views/**: 页面级组件，对应路由\n- **components/layout/**: 布局相关组件（Navbar, PlatformLayout）\n- **components/common/**: 通用可复用组件\n- **components/**: 功能特定组件\n- **composables/**: 可复用的业务逻辑\n- **config/**: 配置文件\n- **services/**: API 服务层\n- **utils/**: 纯函数工具\n\n### 2. 组件分类\n\n#### 页面组件 (views/)\n- 对应一个完整的页面\n- 使用 Composition API (`<script setup>`)\n- 组合多个子组件和 composables\n- 处理页面级状态和生命周期\n\n#### 布局组件 (components/layout/)\n- 页面布局相关（如导航栏）\n- 可跨页面复用\n\n#### 通用组件 (components/common/)\n- 高度可复用的 UI 组件\n- 无业务逻辑或逻辑简单\n\n#### 功能组件 (components/)\n- 特定功能的组件\n- 包含一定业务逻辑\n\n### 3. Composables 设计\n\nComposables 用于提取可复用的业务逻辑：\n\n```javascript\n// composables/useExample.js\nimport { ref, computed } from 'vue'\n\nexport function useExample() {\n  const data = ref(null)\n  const loading = ref(false)\n  \n  const computedValue = computed(() => {\n    // 计算逻辑\n  })\n  \n  const fetchData = async () => {\n    // 数据获取逻辑\n  }\n  \n  return {\n    data,\n    loading,\n    computedValue,\n    fetchData,\n  }\n}\n```\n\n## 📝 开发规范\n\n### 1. 创建新页面\n\n#### 步骤 1: 创建页面组件\n\n```vue\n<!-- views/NewPage.vue -->\n<template>\n  <div>\n    <Navbar />\n    <div class=\"container\">\n      <h1>{{ $t('newpage.title') }}</h1>\n      <!-- 页面内容 -->\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport Navbar from '../components/layout/Navbar.vue'\n// 导入需要的 composables\nimport { useNewPage } from '../composables/useNewPage.js'\n\nconst {\n  // 解构需要的状态和方法\n} = useNewPage()\n</script>\n\n<style scoped>\n/* 页面特定样式 */\n</style>\n```\n\n#### 步骤 2: 创建 Composable（如需要）\n\n```javascript\n// composables/useNewPage.js\nimport { ref, computed } from 'vue'\n\nexport function useNewPage() {\n  const data = ref(null)\n  \n  const fetchData = async () => {\n    // 数据获取逻辑\n  }\n  \n  return {\n    data,\n    fetchData,\n  }\n}\n```\n\n#### 步骤 3: 创建 HTML 入口文件\n\n```html\n<!-- newpage.html -->\n<!DOCTYPE html>\n<html lang=\"en\" data-bs-theme=\"auto\">\n  <head>\n    <%- header %>\n  </head>\n\n  <body id=\"app\" v-cloak>\n    <!-- Vue 应用挂载点 -->\n  </body>\n\n  <script type=\"module\">\n    import { createApp } from 'vue'\n    import { initApp } from './init'\n    import NewPage from './views/NewPage.vue'\n\n    const app = createApp(NewPage)\n    initApp(app)\n  </script>\n</html>\n```\n\n### 2. 使用 Composition API\n\n**推荐使用 `<script setup>` 语法：**\n\n```vue\n<script setup>\nimport { ref, computed, onMounted } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nconst { t } = useI18n()\nconst count = ref(0)\n\nconst doubleCount = computed(() => count.value * 2)\n\nonMounted(() => {\n  // 初始化逻辑\n})\n</script>\n```\n\n### 3. 国际化使用\n\n#### 在模板中\n\n```vue\n<template>\n  <div>\n    <!-- 在模板中使用 $t (通过 globalInjection) -->\n    <h1>{{ $t('common.title') }}</h1>\n    <p>{{ $t('common.description') }}</p>\n    \n    <!-- 在属性中使用 -->\n    <input :placeholder=\"$t('common.placeholder')\" />\n    <button :title=\"$t('common.tooltip')\">{{ $t('common.button') }}</button>\n  </div>\n</template>\n\n<script setup>\nimport { useI18n } from 'vue-i18n'\n// 在 script 中使用 useI18n() 获取 t 函数\nconst { t } = useI18n()\n</script>\n```\n\n#### 在 `<script setup>` 中使用\n\n当需要在 JavaScript 代码中使用翻译（如 `alert()`, `confirm()` 等），必须使用 `useI18n()`：\n\n```vue\n<script setup>\nimport { useI18n } from 'vue-i18n'\n\nconst { t } = useI18n()\n\n// 在函数中使用\nconst handleConfirm = () => {\n  if (confirm(t('common.confirm_message'))) {\n    // 处理确认\n  }\n}\n\nconst showError = () => {\n  alert(t('common.error_message'))\n}\n</script>\n```\n\n#### 在 Composables 中\n\n```javascript\nimport { useI18n } from 'vue-i18n'\n\nexport function useExample() {\n  const { t } = useI18n()\n  \n  const showMessage = (key) => {\n    alert(t(key))\n  }\n  \n  return { showMessage }\n}\n```\n\n### 4. 样式组织\n\n- **全局样式**: `public/assets/css/` 或 `styles/`\n- **组件样式**: 使用 `<style scoped>` 在组件内\n- **页面特定样式**: 在对应的页面组件中\n\n### 5. API 调用\n\n使用 `services/` 目录组织 API 调用：\n\n```javascript\n// services/exampleService.js\nexport class ExampleService {\n  static async getData() {\n    const response = await fetch('/api/example')\n    return response.json()\n  }\n  \n  static async saveData(data) {\n    const response = await fetch('/api/example', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify(data),\n    })\n    return response.json()\n  }\n}\n```\n\n## 🚀 开发流程\n\n### 1. 开发新功能\n\n1. **分析需求**：确定是页面、组件还是功能增强\n2. **创建 Composables**：提取可复用的业务逻辑\n3. **创建组件**：实现 UI 和交互\n4. **创建页面**：组合组件和 composables\n5. **添加路由**：创建 HTML 入口文件\n6. **测试验证**：确保功能正常\n\n### 2. 代码审查要点\n\n- ✅ 是否遵循目录结构规范\n- ✅ 是否使用 Composition API\n- ✅ 业务逻辑是否提取到 composables\n- ✅ 组件是否可复用\n- ✅ 样式是否合理组织\n- ✅ 是否添加了必要的错误处理\n\n## 📚 示例代码\n\n### 完整页面示例\n\n```vue\n<!-- views/Example.vue -->\n<template>\n  <div>\n    <Navbar />\n    <div class=\"container\">\n      <h1>{{ $t('example.title') }}</h1>\n      \n      <ExampleCard \n        v-for=\"item in items\" \n        :key=\"item.id\"\n        :item=\"item\"\n        @action=\"handleAction\"\n      />\n      \n      <div v-if=\"loading\" class=\"text-center\">\n        <div class=\"spinner-border\" role=\"status\">\n          <span class=\"visually-hidden\">Loading...</span>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { onMounted } from 'vue'\nimport Navbar from '../components/layout/Navbar.vue'\nimport ExampleCard from '../components/ExampleCard.vue'\nimport { useExample } from '../composables/useExample.js'\nimport { trackEvents } from '../config/firebase.js'\n\nconst {\n  items,\n  loading,\n  fetchItems,\n  handleAction,\n} = useExample()\n\nonMounted(async () => {\n  trackEvents.pageView('example')\n  await fetchItems()\n})\n</script>\n\n<style scoped>\n.container {\n  padding: 1rem;\n}\n</style>\n```\n\n### Composables 示例\n\n```javascript\n// composables/useExample.js\nimport { ref, computed } from 'vue'\nimport { ExampleService } from '../services/exampleService.js'\nimport { trackEvents } from '../config/firebase.js'\n\nexport function useExample() {\n  const items = ref([])\n  const loading = ref(false)\n  const error = ref(null)\n  \n  const itemCount = computed(() => items.value.length)\n  \n  const fetchItems = async () => {\n    loading.value = true\n    error.value = null\n    try {\n      items.value = await ExampleService.getItems()\n      trackEvents.userAction('items_loaded', { count: items.value.length })\n    } catch (err) {\n      error.value = err.message\n      trackEvents.errorOccurred('fetch_items', err.message)\n    } finally {\n      loading.value = false\n    }\n  }\n  \n  const handleAction = async (itemId) => {\n    try {\n      await ExampleService.performAction(itemId)\n      await fetchItems() // 刷新列表\n    } catch (err) {\n      console.error('Action failed:', err)\n    }\n  }\n  \n  return {\n    items,\n    loading,\n    error,\n    itemCount,\n    fetchItems,\n    handleAction,\n  }\n}\n```\n\n## 🔧 配置说明\n\n### i18n 配置\n\n```javascript\n// config/i18n.js\nconst i18n = createI18n({\n  legacy: false,           // 使用 Composition API 模式\n  locale: locale,\n  fallbackLocale: 'en',\n  messages: messages,\n  globalInjection: true,   // 允许在模板中使用 $t\n})\n```\n\n### Firebase 配置\n\n```javascript\n// config/firebase.js\nimport { initFirebase, trackEvents } from './config/firebase.js'\n\n// 初始化\ninitFirebase()\n\n// 使用\ntrackEvents.pageView('page_name')\ntrackEvents.userAction('action_name', { data })\ntrackEvents.gpuReported({ platform: 'windows', adapters: [...] })\n```\n\n**可用事件**:\n- `pageView(pageName)` - 页面访问\n- `userAction(actionName, data)` - 用户操作\n- `errorOccurred(errorType, message)` - 错误发生\n- `gpuReported(gpuInfo)` - 显卡信息上报（24小时内仅上报一次）\n\n## 🎨 样式指南\n\n### 使用 Bootstrap 5\n\n项目使用 Bootstrap 5 作为 UI 框架，优先使用 Bootstrap 组件和工具类。\n\n### 自定义样式\n\n- 组件特定样式使用 `<style scoped>`\n- 全局样式放在 `styles/` 目录\n- 使用 CSS 变量进行主题定制\n\n## 📦 依赖管理\n\n主要依赖：\n- `vue` - Vue 3 框架\n- `vue-i18n` - 国际化（Composition API 模式）\n- `bootstrap` - UI 框架\n- `vuedraggable` - 拖拽功能\n- `marked` - Markdown 解析\n\n## 🐛 调试技巧\n\n1. **使用 Vue DevTools**：安装 Vue DevTools 浏览器扩展\n2. **控制台日志**：使用 `console.log` 进行调试\n3. **网络请求**：使用浏览器开发者工具查看 API 请求\n4. **组件检查**：在 Vue DevTools 中检查组件状态\n\n## 🔧 开发配置\n\n### Vite 配置\n\n- **开发配置**: `vite.dev.config.js` - 开发环境专用配置\n- **生产配置**: `vite.config.js` - 生产构建配置\n- **EJS模板**: 支持HTML模板预处理\n- **路径别名**: 配置了Vue和Bootstrap的路径别名\n- **Rolldown支持**: 使用 Rolldown 作为实验性打包器（更快）\n- **ESM模式**: 项目使用 ES 模块（`\"type\": \"module\"`）\n\n### 代理配置\n\n开发服务器包含以下代理设置：\n- `/api/*` → `https://localhost:47990` (Sunshine API)\n- `/steam-api/*` → Steam API服务\n- `/steam-store/*` → Steam商店服务\n\n### 预览模式\n\n预览模式用于测试生产构建，但需要注意：\n\n1. **API 不可用**: 预览模式下没有后端 API 服务器\n2. **错误处理**: 代码已优化，在预览模式下会优雅降级\n3. **使用场景**: 主要用于验证构建产物和静态资源\n\n```bash\n# 构建并预览\nnpm run preview:build\n\n# 或分步执行\nnpm run build\nnpm run preview\n```\n\n访问地址：`http://localhost:3000`\n\n### 代码分包策略\n\n> **注意**: 手动分包 (`manualChunks`) 当前已禁用，因为可能导致 Bootstrap 和 Popper.js 的依赖关系问题，影响下拉菜单等功能的正常工作。Vite 会自动进行代码分割优化。\n\n## 🌍 国际化支持\n\n- 支持多语言切换\n- 基于 Vue-i18n 11 (Composition API 模式)\n- 语言文件位于 `public/assets/locale/` 目录\n- 配置在 `config/i18n.js` 中\n\n### i18n 开发工作流\n\n项目提供了一套完整的国际化（i18n）工具链，用于确保翻译文件的质量和一致性。基准语言文件是 `en.json`，所有其他语言文件需要与其保持同步。\n\n#### 可用命令\n\n```bash\n# 验证所有语言文件的完整性\nnpm run i18n:validate\n\n# 检查并自动同步缺失的翻译键（仅为补充键，缺失键会用英文占位值填充）\n# 注意：sync 只保证“键齐全”，不会做翻译。其他语言文件中的英文占位值仍需人工改为对应语言。\nnpm run i18n:sync\n\n# 格式化并排序所有语言文件（按字母顺序）\nnpm run i18n:format\n\n# 检查文件格式\nnpm run i18n:format:check\n\n# 验证翻译完整性\nnpm run i18n:validate\n```\n\n#### 添加新的翻译键\n\n1. **在基准文件中添加新键**：首先在 `en.json` 中添加新的翻译键和英文值\n   ```json\n   {\n     \"myfeature\": {\n       \"title\": \"My Feature Title\",\n       \"description\": \"My feature description\",\n       \"button_label\": \"Submit\"\n     }\n   }\n   ```\n\n2. **同步到其他语言文件**：\n   ```bash\n   npm run i18n:sync\n   ```\n   这将自动在所有语言文件中添加缺失的键，并用英文值作为占位符。**sync 只负责补全键，不负责翻译**；各语言文件中的英文占位值需要人工改成对应语言。\n\n3. **格式化文件**：\n   ```bash\n   npm run i18n:format\n   ```\n   这将对所有语言文件进行统一排序和格式化，减少 Git 冲突\n\n4. **翻译占位符（必做）**：将各语言文件中由 sync 填入的英文占位值，手动修改为该语言的实际译文。未修改时界面会显示英文。\n\n5. **验证**：\n   ```bash\n   npm run i18n:validate\n   ```\n   确保所有语言文件都包含完整的翻译键\n\n#### 国际化现有组件示例\n\n以下是一个完整的国际化现有组件的示例：\n\n**步骤 1：识别硬编码文本**\n```vue\n<!-- 原始组件 -->\n<template>\n  <div>\n    <h2>客户端列表</h2>\n    <table>\n      <thead>\n        <tr>\n          <th>名称</th>\n          <th>操作</th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr v-for=\"client in clients\" :key=\"client.id\">\n          <td>{{ client.name || '未知客户端' }}</td>\n          <td>\n            <button @click=\"handleDelete\">删除</button>\n          </td>\n        </tr>\n      </tbody>\n    </table>\n  </div>\n</template>\n\n<script setup>\nconst handleDelete = () => {\n  if (confirm('确定要删除吗？')) {\n    // 删除逻辑\n  }\n}\n</script>\n```\n\n**步骤 2：在 `en.json` 中添加翻译键**\n```json\n{\n  \"client\": {\n    \"list_title\": \"Client List\",\n    \"name\": \"Name\",\n    \"actions\": \"Actions\",\n    \"unknown_client\": \"Unknown Client\",\n    \"delete\": \"Delete\",\n    \"confirm_delete\": \"Are you sure you want to delete?\"\n  }\n}\n```\n\n**步骤 3：更新组件使用翻译**\n```vue\n<template>\n  <div>\n    <h2>{{ $t('client.list_title') }}</h2>\n    <table>\n      <thead>\n        <tr>\n          <th>{{ $t('client.name') }}</th>\n          <th>{{ $t('client.actions') }}</th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr v-for=\"client in clients\" :key=\"client.id\">\n          <td>{{ client.name || $t('client.unknown_client') }}</td>\n          <td>\n            <button @click=\"handleDelete\">{{ $t('client.delete') }}</button>\n          </td>\n        </tr>\n      </tbody>\n    </table>\n  </div>\n</template>\n\n<script setup>\nimport { useI18n } from 'vue-i18n'\n\nconst { t } = useI18n()\n\nconst handleDelete = () => {\n  if (confirm(t('client.confirm_delete'))) {\n    // 删除逻辑\n  }\n}\n</script>\n```\n\n**步骤 4：同步和验证**\n```bash\nnpm run i18n:sync\nnpm run i18n:format\nnpm run i18n:validate\n```\n\n#### 最佳实践\n\n- **提交前验证**：在提交代码前运行 `npm run i18n:validate` 确保没有缺失的翻译\n- **保持格式一致**：定期运行 `npm run i18n:format` 保持文件格式统一\n- **避免直接编辑**：不要直接删除或重命名翻译键，应先在 `en.json` 中修改，然后同步\n- **CI 集成**：CI 会自动检查翻译文件的完整性和格式，确保代码质量\n\n#### 脚本说明\n\n- **validate-i18n.js**：验证所有语言文件是否包含 `en.json` 中定义的所有键，并报告缺失或多余的键\n- **format-i18n.js**：对所有语言文件的键进行字母排序，并应用统一的格式化（2 空格缩进）\n\n这些工具确保了：\n- ✅ 所有语言文件具有相同的翻译键\n- ✅ 文件格式统一，减少不必要的 Git 冲突  \n- ✅ 翻译缺失可以快速被发现和修复\n- ✅ 代码审查更加容易\n\n## 🎨 主题系统\n\n- 支持明暗主题切换\n- 基于 CSS 变量实现\n- 主题工具在 `utils/theme.js` 中\n- 使用 `composables/useTheme.js` 在组件中管理主题\n\n## 📱 响应式设计\n\n- 基于 Bootstrap 5 的响应式布局\n- 支持桌面端和移动端\n- 优化的触摸交互体验\n\n## 🧪 测试和调试\n\n- 开发模式下启用源码映射\n- 详细的代理请求日志\n- 模拟 API 数据用于前端开发\n- 使用 Vue DevTools 进行组件调试\n\n## 📦 构建和部署\n\n### 构建命令\n\n```bash\n# 生产构建\nnpm run build\n\n# 构建输出目录: build/assets/web/\n# 包含所有静态资源和HTML文件\n```\n\n## 📖 相关资源\n\n- [Vue 3 文档](https://vuejs.org/)\n- [Vue I18n 文档](https://vue-i18n.intlify.dev/)\n- [Bootstrap 5 文档](https://getbootstrap.com/docs/5.3/)\n- [Composition API 指南](https://vuejs.org/guide/extras/composition-api-faq.html)\n- [Vue I18n Composition API 模式](https://vue-i18n.intlify.dev/guide/advanced/composition.html)\n\n## 🔄 迁移指南\n\n### 从 Options API 迁移到 Composition API\n\n如果遇到旧的 Options API 组件，可以按以下步骤迁移：\n\n1. 将 `data()` 改为 `ref()` 或 `reactive()`\n2. 将 `computed` 改为 `computed()`\n3. 将 `methods` 改为普通函数\n4. 将生命周期钩子改为组合式 API 版本\n5. 使用 `<script setup>` 简化代码\n\n### 示例迁移\n\n**之前 (Options API):**\n```javascript\nexport default {\n  data() {\n    return {\n      count: 0\n    }\n  },\n  computed: {\n    double() {\n      return this.count * 2\n    }\n  },\n  methods: {\n    increment() {\n      this.count++\n    }\n  }\n}\n```\n\n**之后 (Composition API):**\n```javascript\n<script setup>\nimport { ref, computed } from 'vue'\n\nconst count = ref(0)\nconst double = computed(() => count.value * 2)\nconst increment = () => count.value++\n</script>\n```\n\n## ✅ 最佳实践检查清单\n\n- [ ] 使用 Composition API (`<script setup>`)\n- [ ] 业务逻辑提取到 composables\n- [ ] 组件按功能分类到正确目录\n- [ ] 样式使用 scoped 或放在 styles 目录\n- [ ] 使用 TypeScript 类型（如适用）\n- [ ] 添加错误处理\n- [ ] 使用国际化 (`$t` 或 `t`)\n- [ ] 添加必要的用户反馈\n- [ ] 代码格式化统一\n- [ ] 添加必要的注释\n\n## 📋 快速参考\n\n### 文件命名规范\n\n- **页面组件**: `PascalCase.vue` (如 `Home.vue`, `Apps.vue`)\n- **Composables**: `useXxx.js` (如 `useVersion.js`, `useApps.js`)\n- **服务类**: `xxxService.js` (如 `appService.js`)\n- **工具函数**: `camelCase.js` (如 `helpers.js`, `validation.js`)\n\n### 导入路径规范\n\n```javascript\n// 页面组件\nimport Navbar from '../components/layout/Navbar.vue'\n\n// Composables\nimport { useVersion } from '../composables/useVersion.js'\n\n// 服务\nimport { AppService } from '../services/appService.js'\n\n// 工具函数\nimport { debounce } from '../utils/helpers.js'\n\n// 配置\nimport { trackEvents } from '../config/firebase.js'\n```\n\n### 常用 Composables\n\n| Composable | 用途 | 返回内容 |\n|-----------|------|---------|\n| `useVersion` | 版本管理 | version, githubVersion, fetchVersions |\n| `useLogs` | 日志管理 | logs, fatalLogs, fetchLogs |\n| `useApps` | 应用管理 | apps, loadApps, save, editApp |\n| `useConfig` | 配置管理 | config, save, apply |\n| `useTheme` | 主题管理 | - |\n| `usePin` | PIN 配对 | clients, unpairAll, save |\n\n## 🎯 下一步\n\n- 考虑添加 TypeScript 支持\n- 考虑添加单元测试\n- 考虑添加 E2E 测试\n- 优化性能（懒加载、代码分割）\n\n## 🤝 贡献指南\n\n欢迎为 WebUI 贡献代码！请确保：\n\n1. **遵循代码规范**\n   - 使用 Composition API\n   - 业务逻辑提取到 composables\n   - 组件按功能分类\n\n2. **代码质量**\n   - 添加必要的错误处理\n   - 使用国际化\n   - 添加必要的注释\n\n3. **测试验证**\n   - 提交前运行构建命令确保无错误\n   - 测试新功能在不同浏览器中的表现\n\n4. **文档更新**\n   - 更新相关文档\n   - 添加必要的代码注释\n\n## 📝 更新日志\n\n### 最新更新 (2024)\n\n- ✅ 所有页面重构为 Composition API\n- ✅ 业务逻辑提取到 composables\n- ✅ 组件按功能重新组织\n- ✅ 配置文件统一管理\n- ✅ Vue I18n 迁移到 Composition API 模式\n- ✅ 简化所有 HTML 入口文件\n- ✅ 升级 Vite 到 5.4+ 并支持 Rolldown\n- ✅ 修复 CJS Node API 弃用警告（添加 `\"type\": \"module\"`）\n- ✅ 添加生产环境预览功能\n- ✅ 优化预览模式下的 API 错误处理\n- ✅ 添加 GPU 信息上报功能（Firebase Analytics）\n- ✅ 改进国际化配置的错误处理\n- ✅ 跨平台环境变量支持（使用 `cross-env`）\n\n### 技术改进\n\n- **构建系统**: 升级到 Vite 5.4+，支持 Rolldown 实验性打包器\n- **模块系统**: 迁移到 ES 模块（`\"type\": \"module\"`）\n- **错误处理**: 改进预览模式下的 API 错误处理\n- **性能优化**: 使用 Rolldown 加速构建过程\n- **开发体验**: 改进预览功能，支持一键构建并预览\n"
  },
  {
    "path": "docs/WGC_vs_DDAPI_Smoothness_Fun_Guide.md",
    "content": "# 杂鱼！你还在用 DDAPI 吗？WGC vs DDAPI 流畅度大对决！\n\n> 杂鱼杂鱼~连捕获模式都不会选的杂鱼~  \n> 本文用通俗到杂鱼都能懂的方式，解释为什么 WGC 比 DDAPI 更丝滑！\n\n---\n\n## 诶？为什么同样的帧率，画面流畅度却不一样？\n\n杂鱼们在用 Sunshine 串流的时候有没有发现——明明都设成 60fps 了，WGC 就是比 DDAPI 看起来更顺滑？\n\n\"一定是我的错觉吧\"——才不是呢杂鱼！这是有实打实的技术原因的！\n\n---\n\n## 先来搞懂：你的游戏画面是怎么传到串流里的\n\n杂鱼也知道，游戏画面不是凭空出现的吧？整个流程是这样的：\n\n```mermaid\nflowchart LR\n    A[\"游戏渲染好一帧\"] --> B[\"交给 DWM 大人\"]\n    B --> C[\"DWM 合成桌面画面\"]\n    C --> D[\"Sunshine 来抄作业\"]\n    D --> E[\"编码成视频\"]\n    E --> F[\"发给你的客户端\"]\n```\n\n**DWM**（Desktop Window Manager）就是那个负责把所有窗口画面合成到一起的 Windows 大管家。不管你用什么捕获方式，最终都是从 DWM 那里拿画面的。\n\n但是！**怎么从 DWM 拿画面**，这两种方式可太不一样了——\n\n---\n\n## DDAPI：那个笨笨的等待狂\n\nDDAPI 的工作方式，打个比方就是——\n\n> 杂鱼 DDAPI 每隔一会儿就跑去 DWM 门口问：\"有新帧吗？有新帧吗？\"\n> DWM：\"还没好呢！等着！\"\n> DDAPI：\"好……那我就站在这里等……\"\n> **然后它就真的傻站在那里不动了！**\n> 更要命的是——**它等的时候还把 GPU 的钥匙攥在手里不撒手！**\n\n```mermaid\nsequenceDiagram\n    participant DD as DDAPI（笨蛋杂鱼）\n    participant GPU as GPU 钥匙🔑\n    participant ENC as 编码器（干活的）\n\n    DD->>GPU: \"我要等新帧！钥匙给我！\"\n    activate GPU\n    Note over DD: 杂鱼傻等中......\n    ENC--xGPU: \"我要编码啊！钥匙呢？\"\n    Note over ENC: 被锁在门外.jpg\n    Note over DD: 终于等到了！\n    DD->>GPU: \"处理完了，钥匙还你\"\n    deactivate GPU\n    ENC->>GPU: \"终于轮到我了呜呜\"\n    activate GPU\n    Note over ENC: 开始（迟到的）编码\n    deactivate GPU\n```\n\n看到没！编码器明明想干活，结果被 DDAPI 这个杂鱼挡在门外！\n\n---\n\n## WGC：优等生的做事方式\n\nWGC 可聪明多了——\n\n> WGC：\"DWM 大人，有新帧了麻烦叫我一声~\"\n> DWM：\"行。\"\n> WGC 回到座位上安静等通知，**期间不占用任何公共资源**\n> DWM：\"新帧好了！\"\n> WGC：\"收到！我就拿一下图片，马上归还 GPU 钥匙~\"\n\n```mermaid\nsequenceDiagram\n    participant WGC as WGC（优等生）\n    participant GPU as GPU 钥匙🔑\n    participant ENC as 编码器（干活的）\n\n    WGC->>WGC: 安静等通知（不拿钥匙）\n    ENC->>GPU: 我要编码！\n    activate GPU\n    Note over ENC: 编码顺利进行中~\n    deactivate GPU\n    Note over WGC: DWM 通知到了！\n    WGC->>GPU: 借一下钥匙，马上还！\n    activate GPU\n    deactivate GPU\n    Note over WGC: 0.1ms 搞定\n```\n\n编码器全程不受影响！想啥时候用 GPU 就啥时候用！\n\n---\n\n## 所以到底有啥区别？\n\n| | DDAPI（杂鱼） | WGC（优等生） |\n|---|---|---|\n| 取帧方式 | 傻傻地轮询等待 | 聪明地等回调通知 |\n| 拿 GPU 钥匙的时间 | 整个等待期间都捏着 | 只在复制纹理时借一下 |\n| 编码器能不能正常干活 | 经常被挡在门外 | 畅通无阻 |\n| 帧间距稳定性 | 忽大忽小（±3~4ms） | 非常稳定（±0.5ms） |\n| 给人的感觉 | 有点涩涩的（不是） | 丝滑~ |\n\n---\n\n## 等等！我客户端开了帧缓冲和 V-Sync 啊！为什么还是能感觉到！\n\n杂鱼的这个问题问得好（难得）！\n\nV-Sync 确实让每一帧在屏幕上显示的**时间**完全一样——都是 16.67ms。帧缓冲也确实把网络抖动给抹平了。\n\n**但是！** 问题不在\"帧投递的时间\"，而在\"帧里面画的是什么\"！\n\n来看这个例子——假设一个球在屏幕上匀速移动：\n\n```mermaid\nflowchart TB\n    subgraph DD [\"DDAPI 的帧内容\"]\n        direction LR\n        A1[\"帧1: 球移动了 2.5px\"] --> A2[\"帧2: 球移动了 3.6px\"] --> A3[\"帧3: 球移动了 2.7px\"]\n    end\n\n    subgraph W [\"WGC 的帧内容\"]\n        direction LR\n        B1[\"帧1: 球移动了 3.0px\"] --> B2[\"帧2: 球移动了 3.0px\"] --> B3[\"帧3: 球移动了 3.0px\"]\n    end\n```\n\nDDAPI 因为编码器被耽误了，每帧**包含的游戏运动量不一样**——有的帧里球走了 2.5 像素，有的却走了 3.6 像素。\n\nV-Sync 让每帧显示时间相同（16.67ms），但你的眼睛看到的是：**球忽快忽慢地移动**。\n\n这东西有个专业名字叫 **judder**（运动抖动）——不是掉帧，也不是卡顿，就是那种\"说不清哪里不对但就是不够顺\"的感觉。\n\n### 杂鱼都能看懂的比喻\n\n想象你坐在一辆平稳行驶的公交车上看窗外的路灯：\n- **WGC**：路灯间距完全均匀，看着超舒服~\n- **DDAPI**：路灯间距忽大忽小，看久了会晕……\n\nV-Sync 保证了\"你每秒看到相同数量的路灯\"，帧缓冲保证了\"路灯不会突然消失\"，但**路灯之间的距离不均匀**——这是在路灯被\"种下去\"的时候就决定了的，后面怎么也改不了。\n\n```mermaid\nflowchart TB\n    ROOT[\"根因：DDAPI 锁竞争\\n↓\\n帧内容时间切片不等\"]\n    BUF>\"帧缓冲\\n只管传输节奏\\n管不了帧内容\"]\n    VSYNC>\"V-Sync\\n只管显示节奏\\n管不了帧内容\"]\n    JUDDER[\"结果：运动抖动\\n眼睛：总觉得不对劲\"]\n\n    ROOT --> JUDDER\n    JUDDER -.- BUF\n    JUDDER -.- VSYNC\n```\n\n---\n\n## 高帧率时更明显哦~杂鱼~\n\n| 帧率 | 帧间距 | DDAPI 的 ±3ms 抖动占比 | 你的感受 |\n|---|---|---|---|\n| 30fps | 33.3ms | 9% | 有点涩 |\n| 60fps | 16.7ms | 18% | 明显不顺 |\n| 120fps | 8.3ms | **36%** | 很不舒服 |\n| 240fps | 4.2ms | **72%** | 杂鱼你是来搞笑的吧 |\n\n帧率越高，同样的 3ms 抖动占比越大，judder 越明显。所以追求高帧率串流的杂鱼们——**用 WGC 啊！**\n\n---\n\n## 那 DDAPI 就一无是处吗？\n\n也不是啦~（安慰杂鱼）\n\n- **旧系统兼容性**：Windows 10 1903 以下没有 WGC，只能用 DDAPI\n- **某些特殊场景**：个别应用 WGC 抓不到画面但 DDAPI 可以\n- **Sunshine 已经在努力优化了**：用\"短超时 + 间歇释放锁\"的策略缓解锁竞争\n\n但如果你的系统支持 WGC……\n\n> **杂鱼！赶紧去改成 WGC 啊！别在那犹豫了！** \n\n---\n\n## 总结（给看到这里的杂鱼的奖励）\n\n```\n流畅度 = 投递有多均匀 × 帧内容有多均匀\n              ↑                  ↑\n         V-Sync搞定         WGC ✓  DDAPI ✗\n```\n\n| 一句话总结 |\n|---|\n| WGC 事件驱动不抢 GPU 锁 → 编码器不被耽误 → 帧间距均匀 → 丝滑 |\n| DDAPI 傻等时霸占 GPU 锁 → 编码器被饿死 → 帧间距抖动 → judder |\n| 帧缓冲和 V-Sync 管不了帧内容 → judder 从服务端就决定了 → 客户端救不了 |\n\n> 所以杂鱼~还不快去设置里把捕获模式改成 WGC~？\n"
  },
  {
    "path": "docs/app_examples.md",
    "content": "# App Examples\nSince not all applications behave the same, we decided to create some examples to help you get started adding games\nand applications to Sunshine.\n\n@attention{Throughout these examples, any fields not shown are left blank. You can enhance your experience by\nadding an image or a log file (via the `Output` field).}\n\n@note{When a working directory is not specified, it defaults to the folder where the target application resides.}\n\n\n## Common Examples\n\n### Desktop\n\n| Field            | Value                      |\n|------------------|----------------------------|\n| Application Name | @code{}Desktop@endcode     |\n| Image            | @code{}desktop.png@endcode |\n\n### Steam Big Picture\n@note{Steam is launched as a detached command because Steam starts with a process that self updates itself and the original\nprocess is killed.}\n\n@tabs{\n  @tab{Linux | <!-- -->\n    \\| Field             \\| Value                                               \\|\n    \\|-------------------\\|-----------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Steam Big Picture@endcode                    \\|\n    \\| Detached Commands \\| @code{}setsid steam steam://open/bigpicture@endcode \\|\n    \\| Image             \\| @code{}steam.png@endcode                            \\|\n  }\n  @tab{macOS | <!-- -->\n    \\| Field             \\| Value                                             \\|\n    \\|-------------------\\|---------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Steam Big Picture@endcode                  \\|\n    \\| Detached Commands \\| @code{}open steam steam://open/bigpicture@endcode \\|\n    \\| Image             \\| @code{}steam.png@endcode                          \\|\n  }\n  @tab{Windows | <!-- -->\n    \\| Field             \\| Value                                  \\|\n    \\|-------------------\\|----------------------------------------\\|\n    \\| Application Name  \\| @code{}Steam Big Picture@endcode       \\|\n    \\| Detached Commands \\| @code{}steam://open/bigpicture@endcode \\|\n    \\| Image             \\| @code{}steam.png@endcode               \\|\n  }\n}\n\n### Epic Game Store game\n@note{Using URI method will be the most consistent between various games.}\n\n#### URI\n\n@tabs{\n  @tab{Windows | <!-- -->\n    \\| Field            \\| Value                                                                                                                                                 \\|\n    \\|------------------\\|-------------------------------------------------------------------------------------------------------------------------------------------------------\\|\n    \\| Application Name \\| @code{}Surviving Mars@endcode                                                                                                                         \\|\n    \\| Commands         \\| @code{}com.epicgames.launcher://apps/d759128018124dcabb1fbee9bb28e178%3A20729b9176c241f0b617c5723e70ec2d%3AOvenbird?action=launch&silent=true@endcode \\|\n  }\n}\n\n#### Binary (w/ working directory\n@tabs{\n  @tab{Windows | <!-- -->\n    \\| Field             \\| Value                                                      \\|\n    \\|-------------------\\|------------------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode                              \\|\n    \\| Command           \\| @code{}MarsEpic.exe@endcode                                \\|\n    \\| Working Directory \\| @code{}\"C:\\Program Files\\Epic Games\\SurvivingMars\"@endcode \\|\n  }\n}\n\n#### Binary (w/o working directory)\n@tabs{\n  @tab{Windows | <!-- -->\n    \\| Field             \\| Value                                                                   \\|\n    \\|-------------------\\|-------------------------------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode                                           \\|\n    \\| Command           \\| @code{}\"C:\\Program Files\\Epic Games\\SurvivingMars\\MarsEpic.exe\"@endcode \\|\n  }\n}\n\n### Steam game\n@note{Using URI method will be the most consistent between various games.}\n\n#### URI\n\n@tabs{\n  @tab{Linux | <!-- -->\n    \\| Field             \\| Value                                                \\|\n    \\|-------------------\\|------------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode                        \\|\n    \\| Detached Commands \\| @code{}setsid steam steam://rungameid/464920@endcode \\|\n  }\n  @tab{macOS | <!-- -->\n    \\| Field             \\| Value                                        \\|\n    \\|-------------------\\|----------------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode                \\|\n    \\| Detached Commands \\| @code{}open steam://rungameid/464920@endcode \\|\n  }\n  @tab{Windows | <!-- -->\n    \\| Field             \\| Value                                   \\|\n    \\|-------------------\\|-----------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode           \\|\n    \\| Detached Commands \\| @code{}steam://rungameid/464920@endcode \\|\n  }\n}\n\n#### Binary (w/ working directory\n@tabs{\n  @tab{Linux | <!-- -->\n    \\| Field             \\| Value                                                        \\|\n    \\|-------------------\\|--------------------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode                                \\|\n    \\| Command           \\| @code{}MarsSteam@endcode                                     \\|\n    \\| Working Directory \\| @code{}~/.steam/steam/SteamApps/common/Survivng Mars@endcode \\|\n  }\n  @tab{macOS | <!-- -->\n    \\| Field             \\| Value                                                        \\|\n    \\|-------------------\\|--------------------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode                                \\|\n    \\| Command           \\| @code{}MarsSteam@endcode                                     \\|\n    \\| Working Directory \\| @code{}~/.steam/steam/SteamApps/common/Survivng Mars@endcode \\|\n  }\n  @tab{Windows | <!-- -->\n    \\| Field             \\| Value                                                                         \\|\n    \\|-------------------\\|-------------------------------------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode                                                 \\|\n    \\| Command           \\| @code{}MarsSteam.exe@endcode                                                  \\|\n    \\| Working Directory \\| @code{}\"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Surviving Mars\"@endcode \\|\n  }\n}\n\n#### Binary (w/o working directory)\n@tabs{\n  @tab{Linux | <!-- -->\n    \\| Field             \\| Value                                                                  \\|\n    \\|-------------------\\|------------------------------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode                                          \\|\n    \\| Command           \\| @code{}~/.steam/steam/SteamApps/common/Survivng Mars/MarsSteam@endcode \\|\n  }\n  @tab{macOS | <!-- -->\n    \\| Field             \\| Value                                                                  \\|\n    \\|-------------------\\|------------------------------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode                                          \\|\n    \\| Command           \\| @code{}~/.steam/steam/SteamApps/common/Survivng Mars/MarsSteam@endcode \\|\n  }\n  @tab{Windows | <!-- -->\n    \\| Field             \\| Value                                                                                       \\|\n    \\|-------------------\\|---------------------------------------------------------------------------------------------\\|\n    \\| Application Name  \\| @code{}Surviving Mars@endcode                                                               \\|\n    \\| Command           \\| @code{}\"C:\\Program Files (x86)\\Steam\\steamapps\\common\\Surviving Mars\\MarsSteam.exe\"@endcode \\|\n  }\n}\n\n### Prep Commands\n\n#### Changing Resolution and Refresh Rate\n\n##### Linux\n\n###### X11\n\n| Prep Step | Command                                                                                                                               |\n|-----------|---------------------------------------------------------------------------------------------------------------------------------------|\n| Do        | @code{}sh -c \"xrandr --output HDMI-1 --mode ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} --rate ${SUNSHINE_CLIENT_FPS}\"@endcode |\n| Undo      | @code{}xrandr --output HDMI-1 --mode 3840x2160 --rate 120@endcode                                                                     |\n\n@hint{The above only works if the xrandr mode already exists. You will need to create new modes to stream to macOS\nand iOS devices, since they use non standard resolutions.\n\nYou can update the ``Do`` command to this:\n```bash\nbash -c \"${HOME}/scripts/set-custom-res.sh \\\"${SUNSHINE_CLIENT_WIDTH}\\\" \\\"${SUNSHINE_CLIENT_HEIGHT}\\\" \\\"${SUNSHINE_CLIENT_FPS}\\\"\"\n```\n\nThe `set-custom-res.sh` will have this content:\n```bash\n#!/bin/bash\nset -e\n\n# Get params and set any defaults\nwidth=${1:-1920}\nheight=${2:-1080}\nrefresh_rate=${3:-60}\n\n# You may need to adjust the scaling differently so the UI/text isn't too small / big\nscale=${4:-0.55}\n\n# Get the name of the active display\ndisplay_output=$(xrandr | grep \" connected\" | awk '{ print $1 }')\n\n# Get the modeline info from the 2nd row in the cvt output\nmodeline=$(cvt ${width} ${height} ${refresh_rate} | awk 'FNR == 2')\nxrandr_mode_str=${modeline//Modeline \\\"*\\\" /}\nmode_alias=\"${width}x${height}\"\n\necho \"xrandr setting new mode ${mode_alias} ${xrandr_mode_str}\"\nxrandr --newmode ${mode_alias} ${xrandr_mode_str}\nxrandr --addmode ${display_output} ${mode_alias}\n\n# Reset scaling\nxrandr --output ${display_output} --scale 1\n\n# Apply new xrandr mode\nxrandr --output ${display_output} --primary --mode ${mode_alias} --pos 0x0 --rotate normal --scale ${scale}\n\n# Optional reset your wallpaper to fit to new resolution\n# xwallpaper --zoom /path/to/wallpaper.png\n```\n}\n\n###### Wayland\n\n| Prep Step | Command                                                                                                                                  |\n|-----------|------------------------------------------------------------------------------------------------------------------------------------------|\n| Do        | @code{}sh -c \"wlr-xrandr --output HDMI-1 --mode \\\"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}Hz\\\"\"@endcode |\n| Undo      | @code{}wlr-xrandr --output HDMI-1 --mode 3840x2160@120Hz@endcode                                                                         |\n\n@hint{`wlr-xrandr` only works with wlroots-based compositors.}\n\n###### Gnome (Wayland, X11)\n\n| Prep Step | Command                                                                                                                               |\n|-----------|---------------------------------------------------------------------------------------------------------------------------------------|\n| Do        | @code{}sh -c \"xrandr --output HDMI-1 --mode ${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} --rate ${SUNSHINE_CLIENT_FPS}\"@endcode |\n| Undo      | @code{}xrandr --output HDMI-1 --mode 3840x2160 --rate 120@endcode                                                                     |\n\nThe commands above are valid for an X11 session but won't work for\nWayland. In that case `xrandr` must be replaced by [gnome-randr.py](https://gitlab.com/Oschowa/gnome-randr).\nThis script is intended as a drop-in replacement with the same syntax. (It can be saved in\n`/usr/local/bin` and needs to be made executable.)\n\n###### KDE Plasma (Wayland, X11)\n\n| Prep Step | Command                                                                                                                              |\n|-----------|--------------------------------------------------------------------------------------------------------------------------------------|\n| Do        | @code{}sh -c \"kscreen-doctor output.HDMI-A-1.mode.${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}\"@endcode |\n| Undo      | @code{}kscreen-doctor output.HDMI-A-1.mode.3840x2160@120@endcode                                                                     |\n\n###### NVIDIA\n\n| Prep Step | Command                                                                                                     |\n|-----------|-------------------------------------------------------------------------------------------------------------|\n| Do        | @code{}sh -c \"${HOME}/scripts/set-custom-res.sh ${SUNSHINE_CLIENT_WIDTH} ${SUNSHINE_CLIENT_HEIGHT}\"@endcode |\n| Undo      | @code{}sh -c \"${HOME}/scripts/set-custom-res.sh 3840 2160\"@endcode                                          |\n\nThe ``set-custom-res.sh`` will have this content:\n```bash\n#!/bin/bash\nset -e\n\n# Get params and set any defaults\nwidth=${1:-1920}\nheight=${2:-1080}\noutput=${3:-HDMI-1}\nnvidia-settings -a CurrentMetaMode=\"${output}: nvidia-auto-select { ViewPortIn=${width}x${height}, ViewPortOut=${width}x${height}+0+0 }\"\n```\n\n##### macOS\n\n###### displayplacer\n@note{This example uses the `displayplacer` tool to change the resolution.\nThis tool can be installed following instructions in their\n[GitHub repository](https://github.com/jakehilborn/displayplacer)}.\n\n| Prep Step | Command                                                                                            |\n|-----------|----------------------------------------------------------------------------------------------------|\n| Do        | @code{}displayplacer \"id:<screenId> res:1920x1080 hz:60 scaling:on origin:(0,0) degree:0\"@endcode  |\n| Undo      | @code{}displayplacer \"id:<screenId> res:3840x2160 hz:120 scaling:on origin:(0,0) degree:0\"@endcode |\n\n##### Windows\n\n###### QRes\n@note{This example uses the *QRes* tool to change the resolution and refresh rate.\nThis tool can be downloaded from their [SourceForge repository](https://sourceforge.net/projects/qres).}.\n\n| Prep Step | Command                                                                                                                 |\n|-----------|-------------------------------------------------------------------------------------------------------------------------|\n| Do        | @code{}cmd /C FullPath\\qres.exe /x:%SUNSHINE_CLIENT_WIDTH% /y:%SUNSHINE_CLIENT_HEIGHT% /r:%SUNSHINE_CLIENT_FPS%@endcode |\n| Undo      | @code{}cmd /C FullPath\\qres.exe /x:3840 /y:2160 /r:120@endcode                                                          |\n\n### Additional Considerations\n\n#### Linux (Flatpak)\n@attention{Because Flatpak packages run in a sandboxed environment and do not normally have access to the\nhost, the Flatpak of Sunshine requires commands to be prefixed with `flatpak-spawn --host`.}\n\n#### Windows\n**Elevating Commands (Windows)**\n\nIf you've installed Sunshine as a service (default), you can specify if a command should be elevated with\nadministrative privileges. Simply enable the elevated option in the WEB UI, or add it to the JSON configuration.\nThis is an option for both prep-cmd and regular commands and will launch the process with the current user without a\nUAC prompt.\n\n@note{It is important to write the values \"true\" and \"false\" as string values, not as the typical true/false\nvalues in most JSON.}\n\n**Example**\n```json\n{\n  \"name\": \"Game With AntiCheat that Requires Admin\",\n  \"output\": \"\",\n  \"cmd\": \"ping 127.0.0.1\",\n  \"exclude-global-prep-cmd\": \"false\",\n  \"elevated\": \"true\",\n  \"prep-cmd\": [\n    {\n      \"do\": \"powershell.exe -command \\\"Start-Streaming\\\"\",\n      \"undo\": \"powershell.exe -command \\\"Stop-Streaming\\\"\",\n      \"elevated\": \"false\"\n    }\n  ],\n  \"image-path\": \"\"\n}\n```\n\n<div class=\"section_buttons\">\n\n| Previous                          |                Next |\n|:----------------------------------|--------------------:|\n| [Configuration](configuration.md) | [Guides](guides.md) |\n\n</div>\n"
  },
  {
    "path": "docs/building.md",
    "content": "# Building\nSunshine binaries are built using [CMake](https://cmake.org) and requires `cmake` > 3.25.\n\n## Building Locally\n\n### Dependencies\n\n#### Linux\nDependencies vary depending on the distribution. You can reference our\n[linux_build.sh](https://github.com/LizardByte/Sunshine/blob/master/scripts/linux_build.sh) script for a list of\ndependencies we use in Debian-based and Fedora-based distributions. Please submit a PR if you would like to extend the\nscript to support other distributions.\n\n##### CUDA Toolkit\nSunshine requires CUDA Toolkit for NVFBC capture. There are two caveats to CUDA:\n\n1. The version installed depends on the version of GCC.\n2. The version of CUDA you use will determine compatibility with various GPU generations.\n   At the time of writing, the recommended version to use is CUDA ~11.8.\n   See [CUDA compatibility](https://docs.nvidia.com/deploy/cuda-compatibility/index.html) for more info.\n\n@tip{To install older versions, select the appropriate run file based on your desired CUDA version and architecture\naccording to [CUDA Toolkit Archive](https://developer.nvidia.com/cuda-toolkit-archive)}\n\n#### macOS\nYou can either use [Homebrew](https://brew.sh) or [MacPorts](https://www.macports.org) to install dependencies.\n\n##### Homebrew\n```bash\ndependencies=(\n  \"boost\"  # Optional\n  \"cmake\"\n  \"doxygen\"  # Optional, for docs\n  \"graphviz\"  # Optional, for docs\n  \"icu4c\"  # Optional, if boost is not installed\n  \"miniupnpc\"\n  \"node\"\n  \"openssl@3\"\n  \"opus\"\n  \"pkg-config\"\n)\nbrew install ${dependencies[@]}\n```\n\nIf there are issues with an SSL header that is not found:\n\n@tabs{\n  @tab{ Intel | ```bash\n    ln -s /usr/local/opt/openssl/include/openssl /usr/local/include/openssl\n    ```}\n  @tab{ Apple Silicon | ```bash\n    ln -s /opt/homebrew/opt/openssl/include/openssl /opt/homebrew/include/openssl\n    ```\n  }\n}\n\n##### MacPorts\n```bash\ndependencies=(\n  \"cmake\"\n  \"curl\"\n  \"doxygen\"  # Optional, for docs\n  \"graphviz\"  # Optional, for docs\n  \"libopus\"\n  \"miniupnpc\"\n  \"npm9\"\n  \"pkgconfig\"\n)\nsudo port install ${dependencies[@]}\n```\n\n#### Windows\nFirst you need to install [MSYS2](https://www.msys2.org), then startup \"MSYS2 UCRT64\" and execute the following\ncommands.\n\n##### Update all packages\n```bash\npacman -Syu\n```\n\n##### Install dependencies\n```bash\ndependencies=(\n  \"doxygen\"  # Optional, for docs\n  \"git\"\n  \"mingw-w64-ucrt-x86_64-boost\"  # Optional\n  \"mingw-w64-ucrt-x86_64-cmake\"\n  \"mingw-w64-ucrt-x86_64-cppwinrt\"\n  \"mingw-w64-ucrt-x86_64-curl-winssl\"\n  \"mingw-w64-ucrt-x86_64-graphviz\"  # Optional, for docs\n  \"mingw-w64-ucrt-x86_64-MinHook\"\n  \"mingw-w64-ucrt-x86_64-miniupnpc\"\n  \"mingw-w64-ucrt-x86_64-nlohmann-json\"\n  \"mingw-w64-ucrt-x86_64-nodejs\"\n  \"mingw-w64-ucrt-x86_64-nsis\"\n  \"mingw-w64-ucrt-x86_64-onevpl\"\n  \"mingw-w64-ucrt-x86_64-openssl\"\n  \"mingw-w64-ucrt-x86_64-opus\"\n  \"mingw-w64-ucrt-x86_64-toolchain\"\n)\npacman -S ${dependencies[@]}\n```\n\n### Clone\nEnsure [git](https://git-scm.com) is installed on your system, then clone the repository using the following command:\n\n```bash\ngit clone https://github.com/lizardbyte/sunshine.git --recurse-submodules\ncd sunshine\nmkdir build\n```\n\n### Build\n\n```bash\ncmake -B build -G Ninja -S .\nninja -C build\n```\n\n@tip{Available build options can be found in\n[options.cmake](https://github.com/LizardByte/Sunshine/blob/master/cmake/prep/options.cmake).}\n\n### Package\n\n@tabs{\n  @tab{Linux | @tabs{\n    @tab{deb | ```bash\n      cpack -G DEB --config ./build/CPackConfig.cmake\n      ```}\n    @tab{rpm | ```bash\n      cpack -G RPM --config ./build/CPackConfig.cmake\n      ```}\n  }}\n  @tab{macOS | @tabs{\n    @tab{DragNDrop | ```bash\n      cpack -G DragNDrop --config ./build/CPackConfig.cmake\n      ```}\n  }}\n  @tab{Windows | @tabs{\n    @tab{Installer | ```bash\n      cpack -G NSIS --config ./build/CPackConfig.cmake\n      ```}\n    @tab{Portable | ```bash\n      cpack -G ZIP --config ./build/CPackConfig.cmake\n      ```}\n  }}\n}\n\n### Remote Build\nIt may be beneficial to build remotely in some cases. This will enable easier building on different operating systems.\n\n1. Fork the project\n2. Activate workflows\n3. Trigger the *CI* workflow manually\n4. Download the artifacts/binaries from the workflow run summary\n\n<div class=\"section_buttons\">\n\n| Previous                              |                            Next |\n|:--------------------------------------|--------------------------------:|\n| [Troubleshooting](troubleshooting.md) | [Contributing](contributing.md) |\n\n</div>\n"
  },
  {
    "path": "docs/changelog.md",
    "content": "# Changelog\n\n@htmlonly\n<script type=\"module\" src=\"https://md-block.verou.me/md-block.js\"></script>\n<md-block\n  hmin=\"2\"\n  src=\"https://raw.githubusercontent.com/LizardByte/Sunshine/changelog/CHANGELOG.md\">\n</md-block>\n@endhtmlonly\n\n<div class=\"section_buttons\">\n\n| Previous                              |                          Next |\n|:--------------------------------------|------------------------------:|\n| [Getting Started](getting_started.md) | [Docker](../DOCKER_README.md) |\n\n</div>\n"
  },
  {
    "path": "docs/configuration.md",
    "content": "# Configuration\nSunshine will work with the default settings for most users. In some cases you may want to configure Sunshine further.\n\nThe default location for the configuration file is listed below. You can use another location if you\nchoose, by passing in the full configuration file path as the first argument when you start Sunshine.\n\n**Example**\n```bash\nsunshine ~/sunshine_config.conf\n```\n\nThe default location of the `apps.json` is the same as the configuration file. You can use a custom\nlocation by modifying the configuration file.\n\n**Default Config Directory**\n\n| OS      | Location                                        |\n|---------|-------------------------------------------------|\n| Docker  | @code{}/config@endcode                          |\n| Linux   | @code{}~/.config/sunshine@endcode               |\n| macOS   | @code{}~/.config/sunshine@endcode               |\n| Windows | @code{}%ProgramFiles%\\\\Sunshine\\\\config@endcode |\n\nAlthough it is recommended to use the configuration UI, it is possible manually configure Sunshine by\nediting the `conf` file in a text editor. Use the examples as reference.\n\n## [General](https://localhost:47990/config/#general)\n\n### [locale](https://localhost:47990/config/#locale)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The locale used for Sunshine's user interface.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            en\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            locale = en\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"20\">Choices</td>\n        <td>bg</td>\n        <td>Bulgarian</td>\n    </tr>\n    <tr>\n        <td>cs</td>\n        <td>Czech</td>\n    </tr>\n    <tr>\n        <td>de</td>\n        <td>German</td>\n    </tr>\n    <tr>\n        <td>en</td>\n        <td>English</td>\n    </tr>\n    <tr>\n        <td>en_GB</td>\n        <td>English (UK)</td>\n    </tr>\n    <tr>\n        <td>en_US</td>\n        <td>English (United States)</td>\n    </tr>\n    <tr>\n        <td>es</td>\n        <td>Spanish</td>\n    </tr>\n    <tr>\n        <td>fr</td>\n        <td>French</td>\n    </tr>\n    <tr>\n        <td>it</td>\n        <td>Italian</td>\n    </tr>\n    <tr>\n        <td>ja</td>\n        <td>Japanese</td>\n    </tr>\n    <tr>\n        <td>pt</td>\n        <td>Portuguese</td>\n    </tr>\n    <tr>\n        <td>ru</td>\n        <td>Russian</td>\n    </tr>\n    <tr>\n        <td>sv</td>\n        <td>Swedish</td>\n    </tr>\n    <tr>\n        <td>tr</td>\n        <td>Turkish</td>\n    </tr>\n    <tr>\n        <td>zh</td>\n        <td>Chinese (Simplified)</td>\n    </tr>\n    <tr>\n        <td>zh_TW</td>\n        <td>Chinese (Traditional)</td>\n    </tr>\n</table>\n\n### [sunshine_name](https://localhost:47990/config/#sunshine_name)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The name displayed by Moonlight.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">PC hostname</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            sunshine_name = Sunshine\n            @endcode</td>\n    </tr>\n</table>\n\n### [min_log_level](https://localhost:47990/config/#min_log_level)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The minimum log level printed to standard out.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            info\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            min_log_level = info\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"7\">Choices</td>\n        <td>verbose</td>\n        <td>All logging message.\n            @attention{This may negatively affect streaming performance.}</td>\n    </tr>\n    <tr>\n        <td>debug</td>\n        <td>Debug log messages and higher.\n            @attention{This may negatively affect streaming performance.}</td>\n    </tr>\n    <tr>\n        <td>info</td>\n        <td>Informational log messages and higher.</td>\n    </tr>\n    <tr>\n        <td>warning</td>\n        <td>Warning log messages and higher.</td>\n    </tr>\n    <tr>\n        <td>error</td>\n        <td>Error log messages and higher.</td>\n    </tr>\n    <tr>\n        <td>fatal</td>\n        <td>Only fatal log messages.</td>\n    </tr>\n    <tr>\n        <td>none</td>\n        <td>No log messages.</td>\n    </tr>\n</table>\n\n### global_prep_cmd\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            A list of commands to be run before/after all applications.\n            If any of the prep-commands fail, starting the application is aborted.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            []\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            global_prep_cmd = [{\"do\":\"nircmd.exe setdisplay 1280 720 32 144\",\"undo\":\"nircmd.exe setdisplay 2560 1440 32 144\"}]\n            @endcode</td>\n    </tr>\n</table>\n\n### [notify_pre_releases](https://localhost:47990/config/#notify_pre_releases)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Whether to be notified of new pre-release versions of Sunshine.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            notify_pre_releases = disabled\n            @endcode</td>\n    </tr>\n</table>\n\n## [Input](https://localhost:47990/config/#input)\n\n### [controller](https://localhost:47990/config/#controller)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Whether to allow controller input from the client.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            controller = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [gamepad](https://localhost:47990/config/#gamepad)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The type of gamepad to emulate on the host.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            auto\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            gamepad = auto\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"6\">Choices</td>\n        <td>ds4</td>\n        <td>DualShock 4 controller (PS4)\n            @note{This option applies to Windows only.}</td>\n    </tr>\n    <tr>\n        <td>ds5</td>\n        <td>DualShock 5 controller (PS5)\n            @note{This option applies to Linux only.}</td>\n    </tr>\n    <tr>\n        <td>switch</td>\n        <td>Switch Pro controller\n            @note{This option applies to Linux only.}</td>\n    </tr>\n    <tr>\n        <td>x360</td>\n        <td>Xbox 360 controller\n            @note{This option applies to Windows only.}</td>\n    </tr>\n    <tr>\n        <td>xone</td>\n        <td>Xbox One controller\n            @note{This option applies to Linux only.}</td>\n    </tr>\n</table>\n\n### [ds4_back_as_touchpad_click](https://localhost:47990/config/#ds4_back_as_touchpad_click)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Allow Select/Back inputs to also trigger DS4 touchpad click. Useful for clients looking to\n            emulate touchpad click on Xinput devices.\n            @hint{Only applies when gamepad is set to ds4 manually. Unused in other gamepad modes.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            ds4_back_as_touchpad_click = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [motion_as_ds4](https://localhost:47990/config/#motion_as_ds4)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            If a client reports that a connected gamepad has motion sensor support, emulate it on the\n            host as a DS4 controller.\n            <br>\n            <br>\n            When disabled, motion sensors will not be taken into account during gamepad type selection.\n            @hint{Only applies when gamepad is set to auto.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            motion_as_ds4 = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [touchpad_as_ds4](https://localhost:47990/config/#touchpad_as_ds4)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            If a client reports that a connected gamepad has a touchpad, emulate it on the host\n            as a DS4 controller.\n            <br>\n            <br>\n            When disabled, touchpad presence will not be taken into account during gamepad type selection.\n            @hint{Only applies when gamepad is set to auto.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            touchpad_as_ds4 = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [back_button_timeout](https://localhost:47990/config/#back_button_timeout)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            If the Back/Select button is held down for the specified number of milliseconds,\n            a Home/Guide button press is emulated.\n            @tip{If back_button_timeout < 0, then the Home/Guide button will not be emulated.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            -1\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            back_button_timeout = 2000\n            @endcode</td>\n    </tr>\n</table>\n\n### [keyboard](https://localhost:47990/config/#keyboard)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Whether to allow keyboard input from the client.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            keyboard = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [key_repeat_delay](https://localhost:47990/config/#key_repeat_delay)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The initial delay, in milliseconds, before repeating keys. Controls how fast keys will\n            repeat themselves.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            500\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            key_repeat_delay = 500\n            @endcode</td>\n    </tr>\n</table>\n\n### [key_repeat_frequency](https://localhost:47990/config/#key_repeat_frequency)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            How often keys repeat every second.\n            @tip{This configurable option supports decimals.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            24.9\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            key_repeat_frequency = 24.9\n            @endcode</td>\n    </tr>\n</table>\n\n### [always_send_scancodes](https://localhost:47990/config/#always_send_scancodes)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Sending scancodes enhances compatibility with games and apps but may result in incorrect keyboard input\n            from certain clients that aren't using a US English keyboard layout.\n            <br>\n            <br>\n            Enable if keyboard input is not working at all in certain applications.\n            <br>\n            <br>\n            Disable if keys on the client are generating the wrong input on the host.\n            @caution{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            always_send_scancodes = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [key_rightalt_to_key_win](https://localhost:47990/config/#key_rightalt_to_key_win)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">It may be possible that you cannot send the Windows Key from Moonlight directly. In those cases it may be useful to\n            make Sunshine think the Right Alt key is the Windows key.\n            </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            key_rightalt_to_key_win = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [mouse](https://localhost:47990/config/#mouse)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Whether to allow mouse input from the client.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            mouse = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [high_resolution_scrolling](https://localhost:47990/config/#high_resolution_scrolling)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            When enabled, Sunshine will pass through high resolution scroll events from Moonlight clients.\n            <br>\n            This can be useful to disable for older applications that scroll too fast with high resolution scroll\n            events.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            high_resolution_scrolling = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [native_pen_touch](https://localhost:47990/config/#native_pen_touch)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            When enabled, Sunshine will pass through native pen/touch events from Moonlight clients.\n            <br>\n            This can be useful to disable for older applications without native pen/touch support.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            native_pen_touch = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [native_pen_touch](https://localhost:47990/config/#native_pen_touch)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Sometimes it may be useful to map keybindings. Wayland won't allow clients to capture the Win Key\n            for example.\n            @tip{See [virtual key codes](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes)}\n            @hint{keybindings needs to have a multiple of two elements.}\n            @note{This option is not available in the UI. A PR would be welcome.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            [\n              0x10, 0xA0,\n              0x11, 0xA2,\n              0x12, 0xA4\n            ]\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            keybindings = [\n              0x10, 0xA0,\n              0x11, 0xA2,\n              0x12, 0xA4,\n              0x4A, 0x4B\n            ]\n            @endcode</td>\n    </tr>\n</table>\n\n## [Audio/Video](https://localhost:47990/config/#audio-video)\n\n### [audio_sink](https://localhost:47990/config/#audio_sink)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The name of the audio sink used for audio loopback.\n            @tip{To find the name of the audio sink follow these instructions.\n            <br>\n            <br>\n            **Linux + pulseaudio:**\n            <br>\n            @code{}\n            pacmd list-sinks | grep \"name:\"\n            @endcode\n            <br>\n            <br>\n            **Linux + pipewire:**\n            <br>\n            @code{}\n            pactl info | grep Source\n            # in some causes you'd need to use the `Sink` device, if `Source` doesn't work, so try:\n            pactl info | grep Sink\n            @endcode\n            <br>\n            <br>\n            **macOS:**\n            <br>\n            Sunshine can only access microphones on macOS due to system limitations.\n            To stream system audio use\n            [Soundflower](https://github.com/mattingalls/Soundflower) or\n            [BlackHole](https://github.com/ExistentialAudio/BlackHole).\n            <br>\n            <br>\n            **Windows:**\n            <br>\n            Enter the following command in command prompt or PowerShell.\n            @code{}\n            %ProgramFiles%\\Sunshine\\tools\\audio-info.exe\n            @endcode\n            If you have multiple audio devices with identical names, use the Device ID instead.\n            }\n            @attention{If you want to mute the host speakers, use\n            [virtual_sink](#virtual_sinkhttpslocalhost47990configvirtual_sink) instead.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">Sunshine will select the default audio device.</td>\n    </tr>\n    <tr>\n        <td>Example (Linux)</td>\n        <td colspan=\"2\">@code{}\n            audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example (macOS)</td>\n        <td colspan=\"2\">@code{}\n            audio_sink = BlackHole 2ch\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example (Windows)</td>\n        <td colspan=\"2\">@code{}\n            audio_sink = Speakers (High Definition Audio Device)\n            @endcode</td>\n    </tr>\n</table>\n\n### [virtual_sink](https://localhost:47990/config/#virtual_sink)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The audio device that's virtual, like Steam Streaming Speakers. This allows Sunshine to stream audio,\n            while muting the speakers.\n            @tip{See [audio_sink](#audio_sinkhttpslocalhost47990configaudio_sink)!}\n            @tip{These are some options for virtual sound devices.\n            * Stream Streaming Speakers (Linux, macOS, Windows)\n              * Steam must be installed.\n              * Enable [install_steam_audio_drivers](#install_steam_audio_drivershttpslocalhost47990configinstall_steam_audio_drivers)\n                or use Steam Remote Play at least once to install the drivers.\n            * [Virtual Audio Cable](https://vb-audio.com/Cable) (macOS, Windows)\n            }\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">n/a</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            virtual_sink = Steam Streaming Speakers\n            @endcode</td>\n    </tr>\n</table>\n\n### stream_audio\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Whether to stream audio or not. Disabling this can be useful for streaming headless displays as second monitors.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            stream_audio = disabled\n            @endcode</td>\n    </tr>\n</table>\n\n### install_steam_audio_drivers\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Installs the Steam Streaming Speakers driver (if Steam is installed) to support surround sound and muting\n            host audio.\n            @note{This option is only supported on Windows.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            install_steam_audio_drivers = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [adapter_name](https://localhost:47990/config/#adapter_name)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Select the video card you want to stream.\n            @tip{To find the appropriate values follow these instructions.\n            <br>\n            <br>\n            **Linux + VA-API:**\n            <br>\n            Unlike with *amdvce* and *nvenc*, it doesn't matter if video encoding is done on a different GPU.\n            @code{}\n            ls /dev/dri/renderD*  # to find all devices capable of VAAPI\n            # replace ``renderD129`` with the device from above to list the name and capabilities of the device\n            vainfo --display drm --device /dev/dri/renderD129 | \\\n              grep -E \"((VAProfileH264High|VAProfileHEVCMain|VAProfileHEVCMain10).*VAEntrypointEncSlice)|Driver version\"\n            @endcode\n            To be supported by Sunshine, it needs to have at the very minimum:\n            `VAProfileH264High   : VAEntrypointEncSlice`\n            <br>\n            <br>\n            **Windows:**\n            <br>\n            Enter the following command in command prompt or PowerShell.\n            @code{}\n            %ProgramFiles%\\Sunshine\\tools\\dxgi-info.exe\n            @endcode\n            For hybrid graphics systems, DXGI reports the outputs are connected to whichever graphics\n            adapter that the application is configured to use, so it's not a reliable indicator of how the\n            display is physically connected.\n            }\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">Sunshine will select the default video card.</td>\n    </tr>\n    <tr>\n        <td>Example (Linux)</td>\n        <td colspan=\"2\">@code{}\n            adapter_name = /dev/dri/renderD128\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example (Windows)</td>\n        <td colspan=\"2\">@code{}\n            adapter_name = Radeon RX 580 Series\n            @endcode</td>\n    </tr>\n</table>\n\n### output_name\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Select the display number you want to stream.\n            @tip{To find the appropriate values follow these instructions.\n            <br>\n            <br>\n            **Linux:**\n            <br>\n            During Sunshine startup, you should see the list of detected displays:\n            @code{}\n            Info: Detecting displays\n            Info: Detected display: DVI-D-0 (id: 0) connected: false\n            Info: Detected display: HDMI-0 (id: 1) connected: true\n            Info: Detected display: DP-0 (id: 2) connected: true\n            Info: Detected display: DP-1 (id: 3) connected: false\n            Info: Detected display: DVI-D-1 (id: 4) connected: false\n            @endcode\n            You need to use the id value inside the parenthesis, e.g. `1`.\n            <br>\n            <br>\n            **macOS:**\n            <br>\n            During Sunshine startup, you should see the list of detected displays:\n            @code{}\n            Info: Detecting displays\n            Info: Detected display: Monitor-0 (id: 3) connected: true\n            Info: Detected display: Monitor-1 (id: 2) connected: true\n            @endcode\n            You need to use the id value inside the parenthesis, e.g. `3`.\n            <br>\n            <br>\n            **Windows:**\n            <br>\n            Enter the following command in command prompt or PowerShell.\n            @code{}\n            %ProgramFiles%\\Sunshine\\tools\\dxgi-info.exe\n            @endcode\n            }\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">Sunshine will select the default display.</td>\n    </tr>\n    <tr>\n        <td>Example (Linux)</td>\n        <td colspan=\"2\">@code{}\n            output_name = 0\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example (macOS)</td>\n        <td colspan=\"2\">@code{}\n            output_name = 3\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example (Windows)</td>\n        <td colspan=\"2\">@code{}\n            output_name  = \\\\.\\DISPLAY1\n            @endcode</td>\n    </tr>\n</table>\n\n### dd_configuration_option\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Perform mandatory verification and additional configuration for the display device.\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            dd_configuration_option = ensure_only_display\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"5\">Choices</td>\n        <td>disabled</td>\n        <td>Perform no additional configuration (disables all `dd_` configuration options).</td>\n    </tr>\n    <tr>\n        <td>verify_only</td>\n        <td>Verify that display is active only (this is a mandatory step without any extra steps to verify display state).</td>\n    </tr>\n    <tr>\n        <td>ensure_active</td>\n        <td>Activate the display if it's currently inactive.</td>\n    </tr>\n    <tr>\n        <td>ensure_primary</td>\n        <td>Activate the display if it's currently inactive and make it primary.</td>\n    </tr>\n    <tr>\n        <td>ensure_only_display</td>\n        <td>Activate the display if it's currently inactive and disable all others.</td>\n    </tr>\n</table>\n\n### dd_resolution_option\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Perform additional resolution configuration for the display device.\n            @note{\"Optimize game settings\" must be enabled in Moonlight for this option to work.}\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}auto@endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            dd_resolution_option = manual\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>disabled</td>\n        <td>Perform no additional configuration.</td>\n    </tr>\n    <tr>\n        <td>auto</td>\n        <td>Change resolution to the requested resolution from the client.</td>\n    </tr>\n    <tr>\n        <td>manual</td>\n        <td>Change resolution to the user specified one (set via [dd_manual_resolution](#dd_manual_resolution)).</td>\n    </tr>\n</table>\n\n### dd_manual_resolution\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Specify manual resolution to be used.\n            @note{[dd_resolution_option](#dd_resolution_option) must be set to `manual`}\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">n/a</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            dd_manual_resolution = 1920x1080\n            @endcode</td>\n    </tr>\n</table>\n\n### dd_refresh_rate_option\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Perform additional refresh rate configuration for the display device.\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}auto@endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            dd_refresh_rate_option = manual\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>disabled</td>\n        <td>Perform no additional configuration.</td>\n    </tr>\n    <tr>\n        <td>auto</td>\n        <td>Change refresh rate to the requested FPS value from the client.</td>\n    </tr>\n    <tr>\n        <td>manual</td>\n        <td>Change refresh rate to the user specified one (set via [dd_manual_refresh_rate](#dd_manual_refresh_rate)).</td>\n    </tr>\n</table>\n\n### dd_manual_refresh_rate\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Specify manual refresh rate to be used.\n            @note{[dd_refresh_rate_option](#dd_refresh_rate_option) must be set to `manual`}\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">n/a</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            dd_manual_resolution = 120\n            dd_manual_resolution = 59.95\n            @endcode</td>\n    </tr>\n</table>\n\n### dd_hdr_option\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Perform additional HDR configuration for the display device.\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}auto@endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            dd_hdr_option = disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">Choices</td>\n        <td>disabled</td>\n        <td>Perform no additional configuration.</td>\n    </tr>\n    <tr>\n        <td>auto</td>\n        <td>Change HDR to the requested state from the client if the display supports it.</td>\n    </tr>\n</table>\n\n### dd_wa_hdr_toggle_delay\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            When using virtual display device (VDD) for streaming, it might incorrectly display HDR color. Sunshine can try to mitigate this issue, by turning HDR off and then on again.<br>\n            If the value is set to 0, the workaround is disabled (default). If the value is between 0 and 3000 milliseconds, Sunshine will turn off HDR, wait for the specified amount of time and then turn HDR on again. The recommended delay time is around 500 milliseconds in most cases.<br>\n            DO NOT use this workaround unless you actually have issues with HDR as it directly impacts stream start time!\n            @note{This option works independently of [dd_hdr_option](#dd_hdr_option)}\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            0\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            dd_wa_hdr_toggle_delay = 500\n            @endcode</td>\n    </tr>\n</table>\n\n### dd_config_revert_delay\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Additional delay in milliseconds to wait before reverting configuration when the app has been closed or the last session terminated.\n            Main purpose is to provide a smoother transition when quickly switching between apps.\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}3000@endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            dd_config_revert_delay = 1500\n            @endcode</td>\n    </tr>\n</table>\n\n\n### dd_config_revert_on_disconnect\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            When enabled, display configuration is reverted upon disconnect of all clients instead of app close or last session termination.\n            This can be useful for returning to physical usage of the host machine without closing the active app.\n            @warning{Some applications may not function properly when display configuration is changed while active.}\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}disabled@endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            dd_config_revert_on_disconnect = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### dd_mode_remapping\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Remap the requested resolution and FPS to another display mode.<br>\n            Depending on the [dd_resolution_option](#dd_resolution_option) and\n            [dd_refresh_rate_option](#dd_refresh_rate_option) values, the following mapping \n            groups are available:\n            <ul>\n                <li>`mixed` - both options are set to `auto`.</li>\n                <li>\n                  `resolution_only` - only [dd_resolution_option](#dd_resolution_option) is set to `auto`.\n                </li>\n                <li>\n                  `refresh_rate_only` - only [dd_refresh_rate_option](#dd_refresh_rate_option) is set to `auto`.\n                </li>\n            </ul>\n            For each of those groups, a list of fields can be configured to perform remapping:  \n            <ul>\n                <li>\n                  `requested_resolution` - resolution that needs to be matched in order to use this remapping entry.\n                </li>\n                <li>`requested_fps` - FPS that needs to be matched in order to use this remapping entry.</li>\n                <li>`final_resolution` - resolution value to be used if the entry was matched.</li>\n                <li>`final_refresh_rate` - refresh rate value to be used if the entry was matched.</li>\n            </ul>\n            If `requested_*` field is left empty, it will match <b>everything</b>.<br>\n            If `final_*` field is left empty, the original value will not be remapped and either a requested, manual \n            or current value is used. However, at least one `final_*` must be set, otherwise the entry is considered \n            invalid.<br>\n            @note{\"Optimize game settings\" must be enabled on client side for ANY entry with `resolution` \n            field to be considered.}\n            @note{First entry to be matched in the list is the one that will be used.}\n            @tip{`requested_resolution` and `final_resolution` can be omitted for `refresh_rate_only` group.}\n            @tip{`requested_fps` and `final_refresh_rate` can be omitted for `resolution_only` group.}\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            dd_mode_remapping = {\n              \"mixed\": [],\n              \"resolution_only\": [],\n              \"refresh_rate_only\": []\n            }\n            @endcode\n        </td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            dd_mode_remapping = {\n              \"mixed\": [\n                {\n                  \"requested_fps\": \"60\",\n                  \"final_refresh_rate\": \"119.95\",\n                  \"requested_resolution\": \"1920x1080\",\n                  \"final_resolution\": \"2560x1440\"\n                },\n                {\n                  \"requested_fps\": \"60\",\n                  \"final_refresh_rate\": \"120\",\n                  \"requested_resolution\": \"\",\n                  \"final_resolution\": \"\"\n                }\n              ],\n              \"resolution_only\": [\n                {\n                  \"requested_resolution\": \"1920x1080\",\n                  \"final_resolution\": \"2560x1440\"\n                }\n              ],\n              \"refresh_rate_only\": [\n                {\n                  \"requested_fps\": \"60\",\n                  \"final_refresh_rate\": \"119.95\"\n                }\n              ]\n            }@endcode\n        </td>\n    </tr>\n</table>\n\n### max_bitrate\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The maximum bitrate (in Kbps) that Sunshine will encode the stream at. If set to 0, it will always use the bitrate requested by Moonlight.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            0\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            max_bitrate = 5000\n            @endcode</td>\n    </tr>\n</table>\n\n## Network\n\n### [upnp](https://localhost:47990/config/#upnp)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Sunshine will attempt to open ports for streaming over the internet.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            upnp = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [address_family](https://localhost:47990/config/#address_family)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Set the address family that Sunshine will use.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            ipv4\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            address_family = both\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">Choices</td>\n        <td>ipv4</td>\n        <td>IPv4 only</td>\n    </tr>\n    <tr>\n        <td>both</td>\n        <td>IPv4+IPv6</td>\n    </tr>\n</table>\n\n### bind_address\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Set the IP address to bind Sunshine to. This is useful when you have multiple network interfaces\n            and want to restrict Sunshine to a specific one. If not set, Sunshine will bind to all available\n            interfaces (0.0.0.0 for IPv4 or :: for IPv6).\n            <br><br>\n            <strong>Note:</strong> The address must be valid for the system and must match the address family\n            being used. When using IPv6, you can specify an IPv6 address even with address_family set to \"both\".\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            (empty - bind to all interfaces)\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example (IPv4)</td>\n        <td colspan=\"2\">@code{}\n            bind_address = 192.168.1.100\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example (IPv6)</td>\n        <td colspan=\"2\">@code{}\n            bind_address = 2001:db8::1\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example (Loopback)</td>\n        <td colspan=\"2\">@code{}\n            bind_address = 127.0.0.1\n            @endcode</td>\n    </tr>\n</table>\n\n### port\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Set the family of ports used by Sunshine.\n            Changing this value will offset other ports as shown in config UI.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            47989\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Range</td>\n        <td colspan=\"2\">1029-65514</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            port = 47989\n            @endcode</td>\n    </tr>\n</table>\n\n### [origin_web_ui_allowed](https://localhost:47990/config/#origin_web_ui_allowed)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The origin of the remote endpoint address that is not denied for HTTPS Web UI.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            lan\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            origin_web_ui_allowed = lan\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>pc</td>\n        <td>Only localhost may access the web ui</td>\n    </tr>\n    <tr>\n        <td>lan</td>\n        <td>Only LAN devices may access the web ui</td>\n    </tr>\n    <tr>\n        <td>wan</td>\n        <td>Anyone may access the web ui</td>\n    </tr>\n</table>\n\n### [external_ip](https://localhost:47990/config/#external_ip)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            If no external IP address is given, Sunshine will attempt to automatically detect external ip-address.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">Automatic</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            external_ip = 123.456.789.12\n            @endcode</td>\n    </tr>\n</table>\n\n### [lan_encryption_mode](https://localhost:47990/config/#lan_encryption_mode)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            This determines when encryption will be used when streaming over your local network.\n            @warning{Encryption can reduce streaming performance, particularly on less powerful hosts and clients.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            0\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            lan_encryption_mode = 0\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>0</td>\n        <td>encryption will not be used</td>\n    </tr>\n    <tr>\n        <td>1</td>\n        <td>encryption will be used if the client supports it</td>\n    </tr>\n    <tr>\n        <td>2</td>\n        <td>encryption is mandatory and unencrypted connections are rejected</td>\n    </tr>\n</table>\n\n### [wan_encryption_mode](https://localhost:47990/config/#wan_encryption_mode)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            This determines when encryption will be used when streaming over the Internet.\n            @warning{Encryption can reduce streaming performance, particularly on less powerful hosts and clients.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            1\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            wan_encryption_mode = 1\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>0</td>\n        <td>encryption will not be used</td>\n    </tr>\n    <tr>\n        <td>1</td>\n        <td>encryption will be used if the client supports it</td>\n    </tr>\n    <tr>\n        <td>2</td>\n        <td>encryption is mandatory and unencrypted connections are rejected</td>\n    </tr>\n</table>\n\n### [ping_timeout](https://localhost:47990/config/#ping_timeout)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            How long to wait, in milliseconds, for data from Moonlight before shutting down the stream.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            10000\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            ping_timeout = 10000\n            @endcode</td>\n    </tr>\n</table>\n\n## [Config Files](https://localhost:47990/config/#files)\n\n### [file_apps](https://localhost:47990/config/#file_apps)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The application configuration file path. The file contains a JSON formatted list of applications that\n            can be started by Moonlight.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            apps.json\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            file_apps = apps.json\n            @endcode</td>\n    </tr>\n</table>\n\n### [credentials_file](https://localhost:47990/config/#credentials_file)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The file where user credentials for the UI are stored.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            sunshine_state.json\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            credentials_file = sunshine_state.json\n            @endcode</td>\n    </tr>\n</table>\n\n### [log_path](https://localhost:47990/config/#log_path)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The path where the Sunshine log is stored.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            sunshine.log\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            log_path = sunshine.log\n            @endcode</td>\n    </tr>\n</table>\n\n### [pkey](https://localhost:47990/config/#pkey)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The private key used for the web UI and Moonlight client pairing. For best compatibility,\n            this should be an RSA-2048 private key.\n            @warning{Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            credentials/cakey.pem\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            pkey = /dir/pkey.pem\n            @endcode</td>\n    </tr>\n</table>\n\n### [cert](https://localhost:47990/config/#cert)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The certificate used for the web UI and Moonlight client pairing. For best compatibility,\n            this should have an RSA-2048 public key.\n            @warning{Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            credentials/cacert.pem\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            cert = /dir/cert.pem\n            @endcode</td>\n    </tr>\n</table>\n\n### [file_state](https://localhost:47990/config/#file_state)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The file where current state of Sunshine is stored.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            sunshine_state.json\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            file_state = sunshine_state.json\n            @endcode</td>\n    </tr>\n</table>\n\n## [Advanced](https://localhost:47990/config/#advanced)\n\n### [fec_percentage](https://localhost:47990/config/#fec_percentage)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Percentage of error correcting packets per data packet in each video frame.\n            @warning{Higher values can correct for more network packet loss,\n            but at the cost of increasing bandwidth usage.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            20\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Range</td>\n        <td colspan=\"2\">1-255</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            fec_percentage = 20\n            @endcode</td>\n    </tr>\n</table>\n\n### [qp](https://localhost:47990/config/#qp)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Quantization Parameter. Some devices don't support Constant Bit Rate. For those devices, QP is used instead.\n            @warning{Higher value means more compression, but less quality.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            28\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            qp = 28\n            @endcode</td>\n    </tr>\n</table>\n\n### [min_threads](https://localhost:47990/config/#min_threads)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Minimum number of CPU threads used for encoding.\n            @note{Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually worth it to\n            gain the use of more CPU cores for encoding. The ideal value is the lowest value that can reliably encode\n            at your desired streaming settings on your hardware.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            2\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            min_threads = 2\n            @endcode</td>\n    </tr>\n</table>\n\n### [hevc_mode](https://localhost:47990/config/#hevc_mode)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Allows the client to request HEVC Main or HEVC Main10 video streams.\n            @warning{HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software\n            encoding.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            0\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            hevc_mode = 2\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"4\">Choices</td>\n        <td>0</td>\n        <td>advertise support for HEVC based on encoder capabilities (recommended)</td>\n    </tr>\n    <tr>\n        <td>1</td>\n        <td>do not advertise support for HEVC</td>\n    </tr>\n    <tr>\n        <td>2</td>\n        <td>advertise support for HEVC Main profile</td>\n    </tr>\n    <tr>\n        <td>3</td>\n        <td>advertise support for HEVC Main and Main10 (HDR) profiles</td>\n    </tr>\n</table>\n\n### [av1_mode](https://localhost:47990/config/#av1_mode)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Allows the client to request AV1 Main 8-bit or 10-bit video streams.\n            @warning{AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software\n            encoding.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            0\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            av1_mode = 2\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"4\">Choices</td>\n        <td>0</td>\n        <td>advertise support for AV1 based on encoder capabilities (recommended)</td>\n    </tr>\n    <tr>\n        <td>1</td>\n        <td>do not advertise support for AV1</td>\n    </tr>\n    <tr>\n        <td>2</td>\n        <td>advertise support for AV1 Main 8-bit profile</td>\n    </tr>\n    <tr>\n        <td>3</td>\n        <td>advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles</td>\n    </tr>\n</table>\n\n### [capture](https://localhost:47990/config/#capture)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Force specific screen capture method.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">Automatic.\n            Sunshine will use the first capture method available in the order of the table above.</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            capture = kms\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"6\">Choices</td>\n        <td>nvfbc</td>\n        <td>Use NVIDIA Frame Buffer Capture to capture direct to GPU memory. This is usually the fastest method for\n            NVIDIA cards. NvFBC does not have native Wayland support and does not work with XWayland.\n            @note{Applies to Linux only.}</td>\n    </tr>\n    <tr>\n        <td>wlr</td>\n        <td>Capture for wlroots based Wayland compositors via DMA-BUF.\n            @note{Applies to Linux only.}</td>\n    </tr>\n    <tr>\n        <td>kms</td>\n        <td>DRM/KMS screen capture from the kernel. This requires that Sunshine has `cap_sys_admin` capability.\n            @note{Applies to Linux only.}</td>\n    </tr>\n    <tr>\n        <td>x11</td>\n        <td>Uses XCB. This is the slowest and most CPU intensive so should be avoided if possible.\n            @note{Applies to Linux only.}</td>\n    </tr>\n    <tr>\n        <td>ddx</td>\n        <td>Use DirectX Desktop Duplication API to capture the display. This is well-supported on Windows machines.\n            @note{Applies to Windows only.}</td>\n    </tr>\n    <tr>\n        <td>wgc</td>\n        <td>(beta feature) Use Windows.Graphics.Capture to capture the display.\n            @note{Applies to Windows only.}\n            @attention{This capture method is not compatible with the Sunshine service.}</td>\n    </tr>\n</table>\n\n### [encoder](https://localhost:47990/config/#encoder)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Force a specific encoder.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">Sunshine will use the first encoder that is available.</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            encoder = nvenc\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"5\">Choices</td>\n        <td>nvenc</td>\n        <td>For NVIDIA graphics cards</td>\n    </tr>\n    <tr>\n        <td>quicksync</td>\n        <td>For Intel graphics cards</td>\n    </tr>\n    <tr>\n        <td>amdvce</td>\n        <td>For AMD graphics cards</td>\n    </tr>\n    <tr>\n        <td>vaapi</td>\n        <td>Use Linux VA-API (AMD, Intel)</td>\n    </tr>\n    <tr>\n        <td>software</td>\n        <td>Encoding occurs on the CPU</td>\n    </tr>\n</table>\n\n## [NVIDIA NVENC Encoder](https://localhost:47990/config/#nvidia-nvenc-encoder)\n\n### [nvenc_preset](https://localhost:47990/config/#nvenc_preset)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            NVENC encoder performance preset.\n            Higher numbers improve compression (quality at given bitrate) at the cost of increased encoding latency.\n            Recommended to change only when limited by network or decoder, otherwise similar effect can be accomplished\n            by increasing bitrate.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            1\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_preset = 1\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"7\">Choices</td>\n        <td>1</td>\n        <td>P1 (fastest)</td>\n    </tr>\n    <tr>\n        <td>2</td>\n        <td>P2</td>\n    </tr>\n    <tr>\n        <td>3</td>\n        <td>P3</td>\n    </tr>\n    <tr>\n        <td>4</td>\n        <td>P4</td>\n    </tr>\n    <tr>\n        <td>5</td>\n        <td>P5</td>\n    </tr>\n    <tr>\n        <td>6</td>\n        <td>P6</td>\n    </tr>\n    <tr>\n        <td>7</td>\n        <td>P7 (slowest)</td>\n    </tr>\n</table>\n\n### [nvenc_twopass](https://localhost:47990/config/#nvenc_twopass)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Enable two-pass mode in NVENC encoder.\n            This allows to detect more motion vectors, better distribute bitrate across the frame and more strictly\n            adhere to bitrate limits. Disabling it is not recommended since this can lead to occasional bitrate\n            overshoot and subsequent packet loss.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            quarter_res\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_twopass = quarter_res\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>disabled</td>\n        <td>One pass (fastest)</td>\n    </tr>\n    <tr>\n        <td>quarter_res</td>\n        <td>Two passes, first pass at quarter resolution (faster)</td>\n    </tr>\n    <tr>\n        <td>full_res</td>\n        <td>Two passes, first pass at full resolution (slower)</td>\n    </tr>\n</table>\n\n### [nvenc_spatial_aq](https://localhost:47990/config/#nvenc_spatial_aq)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Assign higher QP values to flat regions of the video.\n            Recommended to enable when streaming at lower bitrates.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n            @warning{Enabling this option may reduce performance.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_spatial_aq = disabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [nvenc_vbv_increase](https://localhost:47990/config/#nvenc_vbv_increase)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Single-frame VBV/HRD percentage increase.\n            By default Sunshine uses single-frame VBV/HRD, which means any encoded video frame size is not expected to\n            exceed requested bitrate divided by requested frame rate. Relaxing this restriction can be beneficial and\n            act as low-latency variable bitrate, but may also lead to packet loss if the network doesn't have buffer\n            headroom to handle bitrate spikes. Maximum accepted value is 400, which corresponds to 5x increased\n            encoded video frame upper size limit.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n            @warning{Can lead to network packet loss.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            0\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Range</td>\n        <td colspan=\"2\">0-400</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_vbv_increase = 0\n            @endcode</td>\n    </tr>\n</table>\n\n### [nvenc_realtime_hags](https://localhost:47990/config/#nvenc_realtime_hags)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Use realtime gpu scheduling priority in NVENC when hardware accelerated gpu scheduling (HAGS) is enabled\n            in Windows. Currently, NVIDIA drivers may freeze in encoder when HAGS is enabled, realtime priority is used\n            and VRAM utilization is close to maximum. Disabling this option lowers the priority to high, sidestepping\n            the freeze at the cost of reduced capture performance when the GPU is heavily loaded.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_realtime_hags = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [nvenc_split_encode](https://localhost:47990/config/#nvenc_split_encode)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Split the encoding of each video frame over multiple NVENC hardware units.\n            Significantly reduces encoding latency with a marginal compression efficiency penalty.\n            This option is ignored if your GPU has a singular NVENC unit.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            driver_decides\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_split_encode = driver_decides\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>disabled</td>\n        <td>Disabled</td>\n    </tr>\n    <tr>\n        <td>driver_decides</td>\n        <td>NVIDIA driver makes the decision whether to enable it</td>\n    </tr>\n    <tr>\n        <td>enabled</td>\n        <td>Enabled</td>\n    </tr>\n    <tr>\n        <td>two_strips</td>\n        <td>Force 2-strip split frame encoding (requires 2+ NVENC engines)</td>\n    </tr>\n    <tr>\n        <td>three_strips</td>\n        <td>Force 3-strip split frame encoding (requires 3+ NVENC engines)</td>\n    </tr>\n    <tr>\n        <td>four_strips</td>\n        <td>Force 4-strip split frame encoding (requires 4+ NVENC engines)</td>\n    </tr>\n</table>\n\n### [nvenc_lookahead_depth](https://localhost:47990/config/#nvenc_lookahead_depth)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Number of frames to look ahead during encoding.\n            Lookahead improves encoding quality, especially in complex scenes, by providing better motion estimation\n            and bitrate distribution. Higher values improve quality but increase encoding latency.\n            Set to 0 to disable lookahead.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n            @note{Requires NVENC SDK 13.0 (1202) or newer.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            0\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Range</td>\n        <td colspan=\"2\">0-32</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_lookahead_depth = 16\n            @endcode</td>\n    </tr>\n</table>\n\n### [nvenc_lookahead_level](https://localhost:47990/config/#nvenc_lookahead_level)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Lookahead quality level. Higher levels improve quality at the expense of performance.\n            This option only takes effect when lookahead_depth is greater than 0.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n            @note{Requires NVENC SDK 13.0 (1202) or newer.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_lookahead_level = 2\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"6\">Choices</td>\n        <td>disabled</td>\n        <td>Disabled (same as level 0)</td>\n    </tr>\n    <tr>\n        <td>0</td>\n        <td>Level 0 (lowest quality, fastest)</td>\n    </tr>\n    <tr>\n        <td>1</td>\n        <td>Level 1</td>\n    </tr>\n    <tr>\n        <td>2</td>\n        <td>Level 2</td>\n    </tr>\n    <tr>\n        <td>3</td>\n        <td>Level 3 (highest quality, slowest)</td>\n    </tr>\n    <tr>\n        <td>autoselect</td>\n        <td>Let NVIDIA driver auto-select the optimal level</td>\n    </tr>\n</table>\n\n### [nvenc_rate_control](https://localhost:47990/config/#nvenc_rate_control)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Rate control mode for NVENC encoding. CBR (Constant Bitrate) provides fixed bitrate for low latency streaming. VBR (Variable Bitrate) allows bitrate to vary based on scene complexity, providing better quality for complex scenes at the cost of variable bitrate.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            cbr\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_rate_control = vbr\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">Choices</td>\n        <td>cbr</td>\n        <td>Constant Bitrate - Fixed bitrate, best for low latency streaming</td>\n    </tr>\n    <tr>\n        <td>vbr</td>\n        <td>Variable Bitrate - Variable bitrate, better quality for complex scenes</td>\n    </tr>\n</table>\n\n### [nvenc_target_quality](https://localhost:47990/config/#nvenc_target_quality)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Target quality level for VBR mode. Lower values = higher quality. Set to 0 for automatic quality selection. Only used when rate control mode is VBR. Range: 0-51 for H.264/HEVC, 0-63 for AV1.\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            0\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Range</td>\n        <td colspan=\"2\">0-63</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_target_quality = 28\n            @endcode</td>\n    </tr>\n</table>\n\n### [nvenc_temporal_filter](https://localhost:47990/config/#nvenc_temporal_filter)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Temporal filtering strength applied before encoding.\n            Temporal filter reduces noise and improves compression efficiency, especially for natural content.\n            Higher levels provide better noise reduction but may introduce slight blurring.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n            @note{Requires NVENC SDK 13.0 (1202) or newer.}\n            @warning{Requires frameIntervalP >= 5. Not compatible with zeroReorderDelay or stereo MVC.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_temporal_filter = 4\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>disabled</td>\n        <td>Disabled (no temporal filtering)</td>\n    </tr>\n    <tr>\n        <td>0</td>\n        <td>Disabled (same as disabled)</td>\n    </tr>\n    <tr>\n        <td>4</td>\n        <td>Level 4 (maximum strength)</td>\n    </tr>\n</table>\n\n### [nvenc_temporal_aq](https://localhost:47990/config/#nvenc_temporal_aq)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Enable temporal adaptive quantization.\n            Temporal AQ optimizes quantization across time, providing better bitrate distribution\n            and improved quality in motion scenes. This feature works in conjunction with spatial AQ\n            and requires lookahead to be enabled (lookahead_depth > 0).\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n            @note{Requires NVENC SDK 13.0 (1202) or newer.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_temporal_aq = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [nvenc_latency_over_power](https://localhost:47990/config/#nvenc_latency_over_power)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Adaptive P-State algorithm which NVIDIA drivers employ doesn't work well with low latency streaming,\n            so Sunshine requests high power mode explicitly.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n            @warning{Disabling this is not recommended since this can lead to significantly increased encoding latency.}\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_latency_over_power = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [nvenc_opengl_vulkan_on_dxgi](https://localhost:47990/config/#nvenc_opengl_vulkan_on_dxgi)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Sunshine can't capture fullscreen OpenGL and Vulkan programs at full frame rate unless they present on\n            top of DXGI. With this option enabled Sunshine changes global Vulkan/OpenGL present method to\n            \"Prefer layered on DXGI Swapchain\". This is system-wide setting that is reverted on Sunshine program exit.\n            @note{This option only applies when using NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n            @note{Applies to Windows only.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_opengl_vulkan_on_dxgi = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [nvenc_h264_cavlc](https://localhost:47990/config/#nvenc_h264_cavlc)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Prefer CAVLC entropy coding over CABAC in H.264 when using NVENC.\n            CAVLC is outdated and needs around 10% more bitrate for same quality, but provides slightly faster\n            decoding when using software decoder.\n            @note{This option only applies when using H.264 format with the\n            NVENC [encoder](#encoderhttpslocalhost47990configencoder).}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            nvenc_h264_cavlc = disabled\n            @endcode</td>\n    </tr>\n</table>\n\n## [Intel QuickSync Encoder](https://localhost:47990/config/#intel-quicksync-encoder)\n\n### [qsv_preset](https://localhost:47990/config/#qsv_preset)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The encoder preset to use.\n            @note{This option only applies when using quicksync [encoder](#encoderhttpslocalhost47990configencoder).}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            medium\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            qsv_preset = medium\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"7\">Choices</td>\n        <td>veryfast</td>\n        <td>fastest (lowest quality)</td>\n    </tr>\n    <tr>\n        <td>faster</td>\n        <td>faster (lower quality)</td>\n    </tr>\n    <tr>\n        <td>fast</td>\n        <td>fast (low quality)</td>\n    </tr>\n    <tr>\n        <td>medium</td>\n        <td>medium (default)</td>\n    </tr>\n    <tr>\n        <td>slow</td>\n        <td>slow (good quality)</td>\n    </tr>\n    <tr>\n        <td>slower</td>\n        <td>slower (better quality)</td>\n    </tr>\n    <tr>\n        <td>veryslow</td>\n        <td>slowest (best quality)</td>\n    </tr>\n</table>\n\n### [qsv_coder](https://localhost:47990/config/#qsv_coder)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The entropy encoding to use.\n            @note{This option only applies when using H.264 with the quicksync\n            [encoder](#encoderhttpslocalhost47990configencoder).}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            auto\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            qsv_coder = auto\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>auto</td>\n        <td>let ffmpeg decide</td>\n    </tr>\n    <tr>\n        <td>cabac</td>\n        <td>context adaptive binary arithmetic coding - higher quality</td>\n    </tr>\n    <tr>\n        <td>cavlc</td>\n        <td>context adaptive variable-length coding - faster decode</td>\n    </tr>\n</table>\n\n### [qsv_slow_hevc](https://localhost:47990/config/#qsv_slow_hevc)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            This options enables use of HEVC on older Intel GPUs that only support low power encoding for H.264.\n            @note{This option only applies when using quicksync [encoder](#encoderhttpslocalhost47990configencoder).}\n            @caution{Streaming performance may be significantly reduced when this option is enabled.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            qsv_slow_hevc = disabled\n            @endcode</td>\n    </tr>\n</table>\n\n## [AMD AMF Encoder](https://localhost:47990/config/#amd-amf-encoder)\n\n### [amd_usage](https://localhost:47990/config/#amd_usage)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The encoder usage profile is used to set the base set of encoding parameters.\n            @note{This option only applies when using amdvce [encoder](#encoderhttpslocalhost47990configencoder).}\n            @note{The other AMF options that follow will override a subset of the settings applied by your usage\n            profile, but there are hidden parameters set in usage profiles that cannot be overridden elsewhere.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            ultralowlatency\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            amd_usage = ultralowlatency\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"5\">Choices</td>\n        <td>transcoding</td>\n        <td>transcoding (slowest)</td>\n    </tr>\n    <tr>\n        <td>webcam</td>\n        <td>webcam (slow)</td>\n    </tr>\n    <tr>\n        <td>lowlatency_high_quality</td>\n        <td>low latency, high quality (fast)</td>\n    </tr>\n    <tr>\n        <td>lowlatency</td>\n        <td>low latency (faster)</td>\n    </tr>\n    <tr>\n        <td>ultralowlatency</td>\n        <td>ultra low latency (fastest)</td>\n    </tr>\n</table>\n\n### [amd_rc](https://localhost:47990/config/#amd_rc)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The encoder rate control.\n            @note{This option only applies when using amdvce [encoder](#encoderhttpslocalhost47990configencoder).}\n            @warning{The `vbr_latency` option generally works best, but some bitrate overshoots may still occur.\n            Enabling HRD allows all bitrate based rate controls to better constrain peak bitrate, but may result in\n            encoding artifacts depending on your card.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            vbr_latency\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            amd_rc = vbr_latency\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"4\">Choices</td>\n        <td>cqp</td>\n        <td>constant qp mode</td>\n    </tr>\n    <tr>\n        <td>cbr</td>\n        <td>constant bitrate</td>\n    </tr>\n    <tr>\n        <td>vbr_latency</td>\n        <td>variable bitrate, latency constrained</td>\n    </tr>\n    <tr>\n        <td>vbr_peak</td>\n        <td>variable bitrate, peak constrained</td>\n    </tr>\n</table>\n\n### [amd_enforce_hrd](https://localhost:47990/config/#amd_enforce_hrd)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Enable Hypothetical Reference Decoder (HRD) enforcement to help constrain the target bitrate.\n            @note{This option only applies when using amdvce [encoder](#encoderhttpslocalhost47990configencoder).}\n            @warning{HRD is known to cause encoding artifacts or negatively affect encoding quality on certain cards.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            amd_enforce_hrd = disabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [amd_quality](https://localhost:47990/config/#amd_quality)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The quality profile controls the tradeoff between speed and quality of encoding.\n            @note{This option only applies when using amdvce [encoder](#encoderhttpslocalhost47990configencoder).}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            balanced\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            amd_quality = balanced\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>speed</td>\n        <td>prefer speed</td>\n    </tr>\n    <tr>\n        <td>balanced</td>\n        <td>balanced</td>\n    </tr>\n    <tr>\n        <td>quality</td>\n        <td>prefer quality</td>\n    </tr>\n</table>\n\n### [amd_preanalysis](https://localhost:47990/config/#amd_preanalysis)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Preanalysis can increase encoding quality at the cost of latency.\n            @note{This option only applies when using amdvce [encoder](#encoderhttpslocalhost47990configencoder).}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            disabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            amd_preanalysis = disabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [amd_vbaq](https://localhost:47990/config/#amd_vbaq)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Variance Based Adaptive Quantization (VBAQ) can increase subjective visual quality by prioritizing\n            allocation of more bits to smooth areas compared to more textured areas.\n            @note{This option only applies when using amdvce [encoder](#encoderhttpslocalhost47990configencoder).}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            amd_vbaq = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n### [amd_coder](https://localhost:47990/config/#amd_coder)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The entropy encoding to use.\n            @note{This option only applies when using H.264 with the amdvce\n            [encoder](#encoderhttpslocalhost47990configencoder).}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            auto\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            amd_coder = auto\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>auto</td>\n        <td>let ffmpeg decide</td>\n    </tr>\n    <tr>\n        <td>cabac</td>\n        <td>context adaptive binary arithmetic coding - faster decode</td>\n    </tr>\n    <tr>\n        <td>cavlc</td>\n        <td>context adaptive variable-length coding - higher quality</td>\n    </tr>\n</table>\n\n## [VideoToolbox Encoder](https://localhost:47990/config/#videotoolbox-encoder)\n\n### [vt_coder](https://localhost:47990/config/#vt_coder)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The entropy encoding to use.\n            @note{This option only applies when using macOS.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            auto\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            vt_coder = auto\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">Choices</td>\n        <td>auto</td>\n        <td>let ffmpeg decide</td>\n    </tr>\n    <tr>\n        <td>cabac</td>\n        <td>context adaptive binary arithmetic coding - faster decode</td>\n    </tr>\n    <tr>\n        <td>cavlc</td>\n        <td>context adaptive variable-length coding - higher quality</td>\n    </tr>\n</table>\n\n### [vt_software](https://localhost:47990/config/#vt_software)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Force Video Toolbox to use software encoding.\n            @note{This option only applies when using macOS.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            auto\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            vt_software = auto\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"4\">Choices</td>\n        <td>auto</td>\n        <td>let ffmpeg decide</td>\n    </tr>\n    <tr>\n        <td>disabled</td>\n        <td>disable software encoding</td>\n    </tr>\n    <tr>\n        <td>allowed</td>\n        <td>allow software encoding</td>\n    </tr>\n    <tr>\n        <td>forced</td>\n        <td>force software encoding</td>\n    </tr>\n</table>\n\n### [vt_realtime](https://localhost:47990/config/#vt_realtime)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            Realtime encoding.\n            @note{This option only applies when using macOS.}\n            @warning{Disabling realtime encoding might result in a delayed frame encoding or frame drop.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            enabled\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            vt_realtime = enabled\n            @endcode</td>\n    </tr>\n</table>\n\n## [Software Encoder](https://localhost:47990/config/#software-encoder)\n\n### [sw_preset](https://localhost:47990/config/#sw_preset)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The encoder preset to use.\n            @note{This option only applies when using software [encoder](#encoderhttpslocalhost47990configencoder).}\n            @note{From [FFmpeg](https://trac.ffmpeg.org/wiki/Encode/H.264#preset).\n            <br>\n            <br>\n            A preset is a collection of options that will provide a certain encoding speed to compression ratio. A slower\n            preset will provide better compression (compression is quality per filesize). This means that, for example, if\n            you target a certain file size or constant bit rate, you will achieve better quality with a slower preset.\n            Similarly, for constant quality encoding, you will simply save bitrate by choosing a slower preset.\n            <br>\n            <br>\n            Use the slowest preset that you have patience for.}\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            superfast\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            sw_preset = superfast\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"9\">Choices</td>\n        <td>ultrafast</td>\n        <td>fastest</td>\n    </tr>\n    <tr>\n        <td>superfast</td>\n        <td></td>\n    </tr>\n    <tr>\n        <td>veryfast</td>\n        <td></td>\n    </tr>\n    <tr>\n        <td>faster</td>\n        <td></td>\n    </tr>\n    <tr>\n        <td>fast</td>\n        <td></td>\n    </tr>\n    <tr>\n        <td>medium</td>\n        <td></td>\n    </tr>\n    <tr>\n        <td>slow</td>\n        <td></td>\n    </tr>\n    <tr>\n        <td>slower</td>\n        <td></td>\n    </tr>\n    <tr>\n        <td>veryslow</td>\n        <td>slowest</td>\n    </tr>\n</table>\n\n### [sw_tune](https://localhost:47990/config/#sw_tune)\n\n<table>\n    <tr>\n        <td>Description</td>\n        <td colspan=\"2\">\n            The tuning preset to use.\n            @note{This option only applies when using software [encoder](#encoderhttpslocalhost47990configencoder).}\n            @note{From [FFmpeg](https://trac.ffmpeg.org/wiki/Encode/H.264#preset).\n            <br>\n            <br>\n            You can optionally use -tune to change settings based upon the specifics of your input.\n            }\n        </td>\n    </tr>\n    <tr>\n        <td>Default</td>\n        <td colspan=\"2\">@code{}\n            zerolatency\n            @endcode</td>\n    </tr>\n    <tr>\n        <td>Example</td>\n        <td colspan=\"2\">@code{}\n            sw_tune = zerolatency\n            @endcode</td>\n    </tr>\n    <tr>\n        <td rowspan=\"6\">Choices</td>\n        <td>film</td>\n        <td>use for high quality movie content; lowers deblocking</td>\n    </tr>\n    <tr>\n        <td>animation</td>\n        <td>good for cartoons; uses higher deblocking and more reference frames</td>\n    </tr>\n    <tr>\n        <td>grain</td>\n        <td>preserves the grain structure in old, grainy film material</td>\n    </tr>\n    <tr>\n        <td>stillimage</td>\n        <td>good for slideshow-like content</td>\n    </tr>\n    <tr>\n        <td>fastdecode</td>\n        <td>allows faster decoding by disabling certain filters</td>\n    </tr>\n    <tr>\n        <td>zerolatency</td>\n        <td>good for fast encoding and low-latency streaming</td>\n    </tr>\n</table>\n\n<div class=\"section_buttons\">\n\n| Previous          |                            Next |\n|:------------------|--------------------------------:|\n| [Legal](legal.md) | [App Examples](app_examples.md) |\n\n</div>\n"
  },
  {
    "path": "docs/contributing.md",
    "content": "# Contributing\nRead our contribution guide in our organization level\n[docs](https://lizardbyte.readthedocs.io/en/latest/developers/contributing.html).\n\n## Project Patterns\n\n### Web UI\n* The Web UI uses [Vite](https://vitejs.dev) as its build system.\n* The HTML pages used by the Web UI are found in `./src_assets/common/assets/web`.\n* [EJS](https://www.npmjs.com/package/vite-plugin-ejs) is used as a templating system for the pages\n  (check `template_header.html` and `template_header_main.html`).\n* The Style System is provided by [Bootstrap](https://getbootstrap.com).\n* The JS framework used by the more interactive pages is [Vus.js](https://vuejs.org).\n\n#### Building\n\n@tabs{\n  @tab{CMake | ```bash\n    cmake -B build -G Ninja -S . --target web-ui\n    ninja -C build web-ui\n    ```}\n  @tab{Manual | ```bash\n    npm run dev\n    ```}\n}\n\n### Localization\nSunshine and related LizardByte projects are being localized into various languages.\nThe default language is `en` (English).\n\n![](https://app.lizardbyte.dev/uno/crowdin/LizardByte_graph.svg)\n\n@admonition{Community | We are looking for language coordinators to help approve translations.\nThe goal is to have the bars above filled with green!\nIf you are interesting, please reach out to us on our Discord server.}\n\n#### CrowdIn\nThe translations occur on [CrowdIn][crowdin-url].\nAnyone is free to contribute to the localization there.\n\n##### Translation Basics\n* The brand names *LizardByte* and *Sunshine* should never be translated.\n* Other brand names should never be translated. Examples include *AMD*, *Intel*, and *NVIDIA*.\n\n##### CrowdIn Integration\nHow does it work?\n\nWhen a change is made to Sunshine source code, a workflow generates new translation templates\nthat get pushed to CrowdIn automatically.\n\nWhen translations are updated on CrowdIn, a push gets made to the *l10n_master* branch and a PR is made against the\n*master* branch. Once the PR is merged, all updated translations are part of the project and will be included in the\nnext release.\n\n#### Extraction\n\n##### Web UI\nSunshine uses [Vue I18n](https://vue-i18n.intlify.dev) for localizing the UI.\nThe following is a simple example of how to use it.\n\n* Add the string to the `./src_assets/common/assets/web/public/assets/locale/en.json` file, in English.\n  ```json\n  {\n   \"index\": {\n     \"welcome\": \"Hello, Sunshine!\"\n   }\n  }\n  ```\n\n  @note{The json keys should be sorted alphabetically. You can use the provided i18n tools to automatically\n  format and sort all locale files: `npm run i18n:format`}\n\n  @attention{Due to the integration with Crowdin, it is important to only add strings to the *en.json* file,\n  and to not modify any other language files. After the PR is merged, the translations can take place\n  on [CrowdIn][crowdin-url]. Once the translations are complete, a PR will be made\n  to merge the translations into Sunshine.}\n\n##### i18n Development Tools\n\nThe project provides several npm scripts to help maintain translation quality:\n\n```bash\n# Validate all locale files have the same keys as en.json\nnpm run i18n:validate\n\n# Auto-sync missing keys to all locale files (uses English as placeholder)\nnpm run i18n:sync\n\n# Format and sort all locale JSON files alphabetically\nnpm run i18n:format\n\n# Check if files are properly formatted\nnpm run i18n:format:check\n\n# Validate translations\nnpm run i18n:validate\n```\n\n**Workflow when adding new translation keys:**\n\n1. Add new keys to `en.json` only\n2. Run `npm run i18n:sync` to add missing keys to all locale files\n3. Run `npm run i18n:format` to ensure consistent formatting\n4. Run `npm run i18n:validate` to verify completeness\n5. Commit your changes - CI will automatically validate the translations\n\nThe i18n validation is integrated into the CI pipeline and will prevent merging PRs with\nincomplete or incorrectly formatted translation files.\n\n* Use the string in the Vue component.\n  ```html\n  <template>\n    <div>\n      <!-- In template, use $t (global injection) -->\n      <p>{{ $t('index.welcome') }}</p>\n      \n      <!-- Or use in attributes -->\n      <input :placeholder=\"$t('index.placeholder')\" />\n      <button :title=\"$t('index.tooltip')\">{{ $t('index.button') }}</button>\n    </div>\n  </template>\n  \n  <script setup>\n  import { useI18n } from 'vue-i18n'\n  \n  // In script, use useI18n() to get t function\n  const { t } = useI18n()\n  \n  const handleClick = () => {\n    alert(t('index.success_message'))\n    if (confirm(t('index.confirm_action'))) {\n      // Handle confirmation\n    }\n  }\n  </script>\n  ```\n\n  @tip{More formatting examples can be found in the\n  [Vue I18n guide](https://vue-i18n.intlify.dev/guide/formatting.html).}\n\n##### Internationalizing Existing Components\n\nWhen internationalizing existing components with hardcoded text, follow these steps:\n\n1. **Identify hardcoded strings** in the component (both in template and script)\n\n2. **Add translation keys to `en.json`**:\n   ```json\n   {\n     \"mycomponent\": {\n       \"title\": \"My Title\",\n       \"button_text\": \"Click Me\",\n       \"confirm_message\": \"Are you sure?\"\n     }\n   }\n   ```\n\n3. **Replace hardcoded text in template**:\n   ```vue\n   <!-- Before -->\n   <h1>我的标题</h1>\n   <button>点击我</button>\n   \n   <!-- After -->\n   <h1>{{ $t('mycomponent.title') }}</h1>\n   <button>{{ $t('mycomponent.button_text') }}</button>\n   ```\n\n4. **Replace hardcoded text in script** (must use `useI18n()`):\n   ```vue\n   <script setup>\n   import { useI18n } from 'vue-i18n'\n   const { t } = useI18n()\n   \n   // Before\n   const handleClick = () => {\n     if (confirm('确定吗？')) {\n       // ...\n     }\n   }\n   \n   // After\n   const handleClick = () => {\n     if (confirm(t('mycomponent.confirm_message'))) {\n       // ...\n     }\n   }\n   </script>\n   ```\n\n5. **Sync translation keys**:\n   ```bash\n   npm run i18n:sync\n   npm run i18n:format\n   npm run i18n:validate\n   ```\n\n@note{Always use `useI18n()` in `<script setup>` when you need translations in JavaScript code (like `alert()`, `confirm()`, etc.). The `$t` function is only available in templates through global injection.}\n\n##### C++\n\nThere should be minimal cases where strings need to be extracted from C++ source code; however it may be necessary in\nsome situations. For example the system tray icon could be localized as it is user interfacing.\n\n* Wrap the string to be extracted in a function as shown.\n  ```cpp\n  #include <boost/locale.hpp>\n  #include <string>\n\n  std::string msg = boost::locale::translate(\"Hello world!\");\n  ```\n\n@tip{More examples can be found in the documentation for\n[boost locale](https://www.boost.org/doc/libs/1_70_0/libs/locale/doc/html/messages_formatting.html).}\n\n@warning{The below is for information only. Contributors should never include manually updated template files, or\nmanually compiled language files in Pull Requests.}\n\nStrings are automatically extracted from the code to the `locale/sunshine.po` template file. The generated file is\nused by CrowdIn to generate language specific template files. The file is generated using the\n`.github/workflows/localize.yml` workflow and is run on any push event into the `master` branch. Jobs are only run if\nany of the following paths are modified.\n\n```yaml\n- 'src/**'\n```\n\nWhen testing locally it may be desirable to manually extract, initialize, update, and compile strings. Python is\nrequired for this, along with the python dependencies in the `./scripts/requirements.txt` file. Additionally,\n[xgettext](https://www.gnu.org/software/gettext) must be installed.\n\n* Extract, initialize, and update\n  ```bash\n  python ./scripts/_locale.py --extract --init --update\n  ```\n\n* Compile\n  ```bash\n  python ./scripts/_locale.py --compile\n  ```\n\n@attention{Due to the integration with CrowdIn, it is important to not include any extracted or compiled files in\nPull Requests. The files are automatically generated and updated by the workflow. Once the PR is merged, the\ntranslations can take place on [CrowdIn][crowdin-url]. Once the translations are\ncomplete, a PR will be made to merge the translations into Sunshine.}\n\n### Testing\n\n#### Clang Format\nSource code is tested against the `.clang-format` file for linting errors. The workflow file responsible for clang\nformat testing is `.github/workflows/cpp-clang-format-lint.yml`.\n\nOption 1:\n```bash\nfind ./ -iname *.cpp -o -iname *.h -iname *.m -iname *.mm | xargs clang-format -i\n```\n\nOption 2 (will modify files):\n```bash\npython ./scripts/update_clang_format.py\n```\n\n#### Unit Testing\nSunshine uses [Google Test](https://github.com/google/googletest) for unit testing. Google Test is included in the\nrepo as a submodule. The test sources are located in the `./tests` directory.\n\nThe tests need to be compiled into an executable, and then run. The tests are built using the normal build process, but\ncan be disabled by setting the `BUILD_TESTS` CMake option to `OFF`.\n\nTo run the tests, execute the following command.\n\n```bash\n./build/tests/test_sunshine\n```\n\nTo see all available options, run the tests with the `--help` flag.\n\n```bash\n./build/tests/test_sunshine --help\n```\n\n@tip{See the googletest [FAQ](https://google.github.io/googletest/faq.html) for more information on how to use\nGoogle Test.}\n\nWe use [gcovr](https://www.gcovr.com) to generate code coverage reports,\nand [Codecov](https://about.codecov.io) to analyze the reports for all PRs and commits.\n\nCodecov will fail a PR if the total coverage is reduced too much, or if not enough of the diff is covered by tests.\nIn some cases, the code cannot be covered when running the tests inside of GitHub runners. For example, any test that\nneeds access to the GPU will not be able to run. In these cases, the coverage can be omitted by adding comments to the\ncode. See the [gcovr documentation](https://gcovr.com/en/stable/guide/exclusion-markers.html#exclusion-markers) for\nmore information.\n\nEven if your changes cannot be covered in the CI, we still encourage you to write the tests for them. This will allow\nmaintainers to run the tests locally.\n\n[crowdin-url]: https://translate.lizardbyte.dev\n\n<div class=\"section_buttons\">\n\n| Previous                |                                                         Next |\n|:------------------------|-------------------------------------------------------------:|\n| [Building](building.md) | [Source Code](../third-party/doxyconfig/docs/source_code.md) |\n\n</div>\n"
  },
  {
    "path": "docs/gamestream_migration.md",
    "content": "# GameStream Migration\nNvidia announced that their GameStream service for Nvidia Games clients will be discontinued in February 2023.\nLuckily, Sunshine performance is now equal to or better than Nvidia GameStream.\n\n## Migration\nWe have developed a simple migration tool to help you migrate your GameStream games and apps to Sunshine automatically.\nPlease check out our [GSMS](https://github.com/LizardByte/GSMS) project if you're interested in an automated\nmigration option. GSMS offers the ability to migrate your custom and auto-detected games and apps. The\nworking directory, command, and image are all set in Sunshine's `apps.json` file. The box-art image is also copied\nto a specified directory.\n\n## Internet Streaming\nIf you are using the Moonlight Internet Hosting Tool, you can remove it from your system when you migrate to Sunshine.\nTo stream over the Internet with Sunshine and a UPnP-capable router, enable the UPnP option in the Sunshine Web UI.\n\n@note{Running Sunshine together with versions of the Moonlight Internet Hosting Tool prior to v5.6 will cause UPnP\nport forwarding to become unreliable. Either uninstall the tool entirely or update it to v5.6 or later.}\n\n## Limitations\nSunshine does have some limitations, as compared to Nvidia GameStream.\n\n* Automatic game/application list.\n* Changing game settings automatically, to optimize streaming.\n\n<div class=\"section_buttons\">\n\n| Previous                                        |              Next |\n|:------------------------------------------------|------------------:|\n| [Third-party Packages](third_party_packages.md) | [Legal](legal.md) |\n\n</div>\n"
  },
  {
    "path": "docs/getting_started.md",
    "content": "# Getting Started\n\nThe recommended method for running Sunshine is to use the [binaries](#binaries) included in the\n[latest release][latest-release], unless otherwise specified.\n\n[Pre-releases](https://github.com/LizardByte/Sunshine/releases) are also available. These should be considered beta,\nand release artifacts may be missing when merging changes on a faster cadence.\n\n## Binaries\n\nBinaries of Sunshine are created for each release. They are available for Linux, macOS, and Windows.\nBinaries can be found in the [latest release][latest-release].\n\n@tip{Some third party packages also exist.\nSee [Third Party Packages](third_party_packages.md) for more information.\nNo support will be provided for third party packages!}\n\n## Install\n\n### Docker\n@warning{The Docker images are not recommended for most users.}\n\nDocker images are available on [Dockerhub.io](https://hub.docker.com/repository/docker/lizardbyte/sunshin)\nand [ghcr.io](https://github.com/orgs/LizardByte/packages?repo_name=sunshine).\n\nSee [Docker](../DOCKER_README.md) for more information.\n\n### Linux\n**CUDA Compatibility**\n\nCUDA is used for NVFBC capture.\n\n@tip{See [CUDA GPUS](https://developer.nvidia.com/cuda-gpus) to cross-reference Compute Capability to your GPU.}\n\n<table>\n    <caption>CUDA Compatibility</caption>\n    <tr>\n        <th>CUDA Version</th>\n        <th>Min Driver</th>\n        <th>CUDA Compute Capabilities</th>\n        <th>Package</th>\n    </tr>\n    <tr>\n        <td rowspan=\"3\">11.8.0</td>\n        <td rowspan=\"3\">450.80.02</td>\n        <td rowspan=\"3\">35;50;52;60;61;62;70;72;75;80;86;87;89;90</td>\n        <td>sunshine.AppImage</td>\n    </tr>\n    <tr>\n        <td>sunshine-ubuntu-22.04-{arch}.deb</td>\n    </tr>\n    <tr>\n        <td>sunshine-ubuntu-24.04-{arch}.deb</td>\n    </tr>\n    <tr>\n        <td rowspan=\"2\">12.0.0</td>\n        <td rowspan=\"4\">525.60.13</td>\n        <td rowspan=\"4\">50;52;60;61;62;70;72;75;80;86;87;89;90</td>\n        <td>sunshine_{arch}.flatpak</td>\n    </tr>\n    <tr>\n        <td>sunshine-debian-bookworm-{arch}.deb</td>\n    </tr>\n    <tr>\n        <td>12.4.0</td>\n        <td>sunshine-fedora-39-{arch}.rpm</td>\n    </tr>\n    <tr>\n        <td>12.5.1</td>\n        <td>sunshine.pkg.tar.zst</td>\n    </tr>\n    <tr>\n        <td>n/a</td>\n        <td>n/a</td>\n        <td>n/a</td>\n        <td>sunshine-fedora-40-{arch}.rpm</td>\n    </tr>\n</table>\n\n#### AppImage\n@caution{Use distro-specific packages instead of the AppImage if they are available.}\n\nAccording to AppImageLint the supported distro matrix of the AppImage is below.\n\n- ✖ Debian bullseye\n- ✔ Debian bookworm\n- ✔ Debian trixie\n- ✔ Debian sid\n- ✔ Ubuntu noble\n- ✔ Ubuntu jammy\n- ✖ Ubuntu focal\n- ✖ Ubuntu bionic\n- ✖ Ubuntu xenial\n- ✖ Ubuntu trusty\n- ✖ CentOS 7\n\n##### Install\n1. Download [sunshine.AppImage](https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine.AppImage)\n   into your home directory.\n   ```bash\n   cd ~\n   wget https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine.AppImage\n   ```\n2. Open terminal and run the following command.\n   ```bash\n   ./sunshine.AppImage --install\n   ```\n\n##### Run\n```bash\n./sunshine.AppImage --install && ./sunshine.AppImage\n```\n\n##### Uninstall\n```bash\n./sunshine.AppImage --remove\n```\n\n#### ArchLinux\n@warning{We do not provide support for any AUR packages.}\n\n##### Install Prebuilt Packages\nFollow the instructions at LizardByte's [pacman-repo](https://github.com/LizardByte/pacman-repo) to add\nthe repository. Then run the following command.\n```bash\npacman -S sunshine\n```\n\n##### Install PKGBUILD Archive\nOpen terminal and run the following command.\n```bash\nwget https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine.pkg.tar.gz\ntar -xvf sunshine.pkg.tar.gz\ncd sunshine\n\n# install optional dependencies\npacman -S cuda  # Nvidia GPU encoding support\npacman -S libva-mesa-driver  # AMD GPU encoding support\n\nmakepkg -si\n```\n\n##### Uninstall\n```bash\npacman -R sunshine\n```\n\n#### Debian/Ubuntu\n##### Install\nDownload `sunshine-{distro}-{distro-version}-{arch}.deb` and run the following command.\n```bash\nsudo dpkg -i ./sunshine-{distro}-{distro-version}-{arch}.deb\n```\n\n@note{The `{distro-version}` is the version of the distro we built the package on. The `{arch}` is the\narchitecture of your operating system.}\n\n@tip{You can double-click the deb file to see details about the package and begin installation.}\n\n##### Uninstall\n```bash\nsudo apt remove sunshine\n```\n\n#### Fedora\n##### Install\n1. Add `rpmfusion` repositories.\n   ```bash\n   sudo dnf install \\\n     https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \\\n     https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm\n   ```\n2. Download `sunshine-{distro}-{distro-version}-{arch}.rpm` and run the following command.\n   ```bash\n   sudo dnf install ./sunshine-{distro}-{distro-version}-{arch}.rpm\n   ```\n\n@note{The `{distro-version}` is the version of the distro we built the package on. The `{arch}` is the\narchitecture of your operating system.}\n\n@tip{You can double-click the rpm file to see details about the package and begin installation.}\n\n##### Uninstall\n```bash\nsudo dnf remove sunshine\n```\n\n#### Flatpak\n@caution{Use distro-specific packages instead of the Flatpak if they are available.}\n\n@important{The instructions provided here are for the version supplied in the [latest release][latest-release],\nwhich does not necessarily match the version in the Flathub repository!}\n\nUsing this package requires that you have [Flatpak](https://flatpak.org/setup) installed.\n\n##### Download\n1. Download `sunshine_{arch}.flatpak` and run the following command.\n   @note{Replace `{arch}` with your system architecture.}\n\n##### Install (system level) \n```bash\nflatpak install --system ./sunshine_{arch}.flatpak\n```\n\n##### Install (user level)\n```bash\nflatpak install --user ./sunshine_{arch}.flatpak\n```\n\n##### Additional installation (required)\n```bash\nflatpak run --command=additional-install.sh dev.lizardbyte.app.Sunshine\n```\n\n##### Run with NVFBC capture (X11 Only)\n```bash\nflatpak run dev.lizardbyte.app.Sunshine\n```\n\n##### Run with KMS capture (Wayland & X11)\n```bash\nsudo -i PULSE_SERVER=unix:/run/user/$(id -u $whoami)/pulse/native flatpak run dev.lizardbyte.app.Sunshine\n```\n\n##### Uninstall\n```bash\nflatpak run --command=remove-additional-install.sh dev.lizardbyte.app.Sunshine\nflatpak uninstall --delete-data dev.lizardbyte.app.Sunshine\n```\n\n#### Homebrew\n@important{The Homebrew package is experimental on Linux.}\n\nThis package requires that you have [Homebrew](https://docs.brew.sh/Installation) installed.\n\n##### Install\n```bash\nbrew tap LizardByte/homebrew\nbrew install sunshine\n```\n\n##### Uninstall\n```bash\nbrew uninstall sunshine\n```\n\n### macOS\n\n@important{Sunshine on macOS is experimental. Gamepads do not work.}\n\n#### Homebrew\nThis package requires that you have [Homebrew](https://docs.brew.sh/Installation) installed.\n\n##### Install\n```bash\nbrew tap LizardByte/homebrew\nbrew install sunshine\n```\n\n##### Uninstall\n```bash\nbrew uninstall sunshine\n```\n\n@tip{For beta you can replace `sunshine` with `sunshine-beta` in the above commands.}\n\n#### Portfile\nThis package requires that you have [MacPorts](https://www.macports.org/install.php) installed.\n\n##### Install\n1. Update the Macports sources.\n   ```bash\n   sudo nano /opt/local/etc/macports/sources.conf\n   ```\n\n   Add this line, replacing your username, below the line that starts with `rsync`.\n   ```bash\n   file:///Users/<username>/ports\n   ```\n\n   `Ctrl+x`, then `Y` to exit and save changes.\n\n2. Download and install by running the following commands.\n   ```bash\n   mkdir -p ~/ports/multimedia/sunshine\n   cd ~/ports/multimedia/sunshine\n   curl -OL https://github.com/LizardByte/Sunshine/releases/latest/download/Portfile\n   cd ~/ports\n   portindex\n   sudo port install sunshine\n   ```\n\n##### Install service (optional)\n```bash\nsudo port load sunshine\n```\n\n##### Uninstall\n```bash\nsudo port uninstall sunshine\n```\n\n### Windows\n\n#### Installer (recommended)\n\n1. Download and install\n   [sunshine-windows-installer.exe](https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine-windows-installer.exe)\n\n@attention{You should carefully select or unselect the options you want to install. Do not blindly install or\nenable features.}\n\nTo uninstall, find Sunshine in the list <a href=\"ms-settings:installed-apps\">here</a> and select \"Uninstall\" from the\noverflow menu. Different versions of Windows may provide slightly different steps for uninstall.\n\n#### Standalone (lite version)\n\n@warning{By using this package instead of the installer, performance will be reduced. This package is not\nrecommended for most users. No support will be provided!}\n\n1. Download and extract\n   [sunshine-windows-portable.zip](https://github.com/LizardByte/Sunshine/releases/latest/download/sunshine-windows-portable.zip)\n2. Open command prompt as administrator\n3. Firewall rules\n\n   Install:\n   ```bash\n   cd /d {path to extracted directory}\n   scripts/add-firewall-rule.bat\n   ```\n\n   Uninstall:\n   ```bash\n   cd /d {path to extracted directory}\n   scripts/delete-firewall-rule.bat\n   ```\n\n4. Virtual Gamepad Support\n\n   Install:\n   ```bash\n   cd /d {path to extracted directory}\n   scripts/install-gamepad.bat\n   ```\n\n   Uninstall:\n   ```bash\n   cd /d {path to extracted directory}\n   scripts/uninstall-gamepad.bat\n   ```\n\n5. Windows service\n\n   Install:\n   ```bash\n   cd /d {path to extracted directory}\n   scripts/install-service.bat\n   scripts/autostart-service.bat\n   ```\n\n   Uninstall:\n   ```bash\n   cd /d {path to extracted directory}\n   scripts/uninstall-service.bat\n   ```\n\nTo uninstall, delete the extracted directory which contains the `sunshine.exe` file.\n\n## Initial Setup\nAfter installation, some initial setup is required.\n\n### Linux\n\n#### KMS Capture\n@warning{Capture of most Wayland-based desktop environments will fail unless this step is performed.}\n@note{`cap_sys_admin` may as well be root, except you don't need to be root to run the program. This is necessary to\nallow Sunshine to use KMS capture.}\n\n##### Enable\n```bash\nsudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))\n```\n\n#### X11 Capture\nFor X11 capture to work, you may need to disable the capabilities that were set for KMS capture.\n\n```bash\nsudo setcap -r $(readlink -f $(which sunshine))\n```\n\n#### Service\n\n**Start once**\n```bash\nsystemctl --user start sunshine\n```\n\n**Start on boot**\n```bash\nsystemctl --user enable sunshine\n```\n\n### macOS\nThe first time you start Sunshine, you will be asked to grant access to screen recording and your microphone.\n\nSunshine can only access microphones on macOS due to system limitations. To stream system audio use\n[Soundflower](https://github.com/mattingalls/Soundflower) or\n[BlackHole](https://github.com/ExistentialAudio/BlackHole).\n\n@note{Command Keys are not forwarded by Moonlight. Right Option-Key is mapped to CMD-Key.}\n@caution{Gamepads are not currently supported.}\n\n## Usage\n\n### Basic usage\nIf Sunshine is not installed/running as a service, then start Sunshine with the following command, unless a start\ncommand is listed in the specified package [install](#install) instructions above.\n\n@note{A service is a process that runs in the background. This is the default when installing Sunshine from the\nWindows installer. Running multiple instances of Sunshine is not advised.}\n\n```bash\nsunshine\n```\n\n### Specify config file\n```bash\nsunshine <directory of conf file>/sunshine.conf\n```\n\n@note{You do not need to specify a config file. If no config file is entered the default location will be used.}\n\n@attention{The configuration file specified will be created if it doesn't exist.}\n\n### Start Sunshine over SSH (Linux/X11)\nAssuming you are already logged into the host, you can use this command\n\n```bash\nssh <user>@<ip_address> 'export DISPLAY=:0; sunshine'\n```\n\nIf you are logged into the host with only a tty (teletypewriter), you can use `startx` to start the X server prior to\nexecuting Sunshine. You nay need to add `sleep` between `startx` and `sunshine` to allow more time for the display to\nbe ready.\n\n```bash\nssh <user>@<ip_address> 'startx &; export DISPLAY=:0; sunshine'\n```\n\n@tip{You could also utilize the `~/.bash_profile` or `~/.bashrc` files to set up the `DISPLAY` variable.}\n\n@seealso{ See [Remote SSH Headless Setup](md_docs_2guides.html#remote-ssh-headless-setup)\non how to set up a headless streaming server without autologin and dummy plugs (X11 + NVidia GPUs)}\n\n### Configuration\n\nSunshine is configured via the web ui, which is available on [https://localhost:47990](https://localhost:47990)\nby default. You may replace *localhost* with your internal ip address.\n\n@attention{Ignore any warning given by your browser about \"insecure website\". This is due to the SSL certificate\nbeing self-signed.}\n\n@caution{If running for the first time, make sure to note the username and password that you created.}\n\n1. Add games and applications.\n2. Adjust any configuration settings as needed.\n3. In Moonlight, you may need to add the PC manually.\n4. When Moonlight requests for you insert the pin:\n\n   - Login to the web ui\n   - Go to \"PIN\" in the Navbar\n   - Type in your PIN and press Enter, you should get a Success Message\n   - In Moonlight, select one of the Applications listed\n\n### Arguments\nTo get a list of available arguments, run the following command.\n\n@tabs{\n   @tab{ General | @code{.bash}\n      sunshine --help\n      @endcode }\n   @tab{ AppImage | @code{.bash}\n      ./sunshine.AppImage --help\n      @endcode }\n   @tab{ Flatpak | @code{.bash}\n      flatpak run --command=sunshine dev.lizardbyte.app.Sunshine --help\n      @endcode }\n}\n\n### Shortcuts\nAll shortcuts start with `Ctrl+Alt+Shift`, just like Moonlight.\n\n* `Ctrl+Alt+Shift+N`: Hide/Unhide the cursor (This may be useful for Remote Desktop Mode for Moonlight)\n* `Ctrl+Alt+Shift+F1/F12`: Switch to different monitor for Streaming\n\n### Application List\n* Applications should be configured via the web UI\n* A basic understanding of working directories and commands is required\n* You can use Environment variables in place of values\n* `$(HOME)` will be replaced by the value of `$HOME`\n* `$$` will be replaced by `$`, e.g. `$$(HOME)` will be become `$(HOME)`\n* `env` - Adds or overwrites Environment variables for the commands/applications run by Sunshine.\n  This can only be changed by modifying the `apps.json` file directly.\n\n### Considerations\n* On Windows, Sunshine uses the Desktop Duplication API which only supports capturing from the GPU used for display.\n  If you want to capture and encode on the eGPU, connect a display or HDMI dummy display dongle to it and run the games\n  on that display.\n* When an application is started, if there is an application already running, it will be terminated.\n* If any of the prep-commands fail, starting the application is aborted.\n* When the application has been shutdown, the stream shuts down as well.\n\n  * For example, if you attempt to run `steam` as a `cmd` instead of `detached` the stream will immediately fail.\n    This is due to the method in which the steam process is executed. Other applications may behave similarly.\n  * This does not apply to `detached` applications.\n\n* The \"Desktop\" app works the same as any other application except it has no commands. It does not start an application,\n  instead it simply starts a stream. If you removed it and would like to get it back, just add a new application with\n  the name \"Desktop\" and \"desktop.png\" as the image path.\n* For the Linux flatpak you must prepend commands with `flatpak-spawn --host`.\n\n### HDR Support\nStreaming HDR content is officially supported on Windows hosts and experimentally supported for Linux hosts.\n\n* General HDR support information and requirements:\n\n  * HDR must be activated in the host OS, which may require an HDR-capable display or EDID emulator dongle\n    connected to your host PC.\n  * You must also enable the HDR option in your Moonlight client settings, otherwise the stream will be SDR\n    (and probably overexposed if your host is HDR).\n  * A good HDR experience relies on proper HDR display calibration both in the OS and in game. HDR calibration can\n    differ significantly between client and host displays.\n  * You may also need to tune the brightness slider or HDR calibration options in game to the different HDR brightness\n    capabilities of your client's display.\n  * Some GPUs video encoders can produce lower image quality or encoding performance when streaming in HDR compared\n    to SDR.\n\n* Additional information:\n\n  @tabs{\n    @tab{ Windows |\n    - HDR streaming is supported for Intel, AMD, and NVIDIA GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles.\n    - We recommend calibrating the display by streaming the Windows HDR Calibration app to your client device and saving an HDR calibration profile to use while streaming.\n    - Older games that use NVIDIA-specific NVAPI HDR rather than native Windows HDR support may not display properly in HDR.\n    }\n\n    @tab{ Linux |\n    - HDR streaming is supported for Intel and AMD GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles using VAAPI.\n    - The KMS capture backend is required for HDR capture. Other capture methods, like NvFBC or X11, do not support HDR.\n    - You will need a desktop environment with a compositor that supports HDR rendering, such as Gamescope or KDE Plasma 6.\n\n    @seealso{[Arch wiki on HDR Support for Linux](https://wiki.archlinux.org/title/HDR_monitor_support) and\n    [Reddit Guide for HDR Support for AMD GPUs](https://www.reddit.com/r/linux_gaming/comments/10m2gyx/guide_alpha_test_hdr_on_linux)}\n    }\n  }\n\n### Tutorials and Guides\nTutorial videos are available [here](https://www.youtube.com/playlist?list=PLMYr5_xSeuXAbhxYHz86hA1eCDugoxXY0).\n\nGuides are available [here](guides.md).\n\n@admonition{Community! |\nTutorials and Guides are community generated. Want to contribute? Reach out to us on our discord server.}\n\n<div class=\"section_buttons\">\n\n| Previous                 |                      Next |\n|:-------------------------|--------------------------:|\n| [Overview](../README.md) | [Changelog](changelog.md) |\n\n</div>\n\n[latest-release]: https://github.com/LizardByte/Sunshine/releases/latest\n"
  },
  {
    "path": "docs/guides.md",
    "content": "# Guides\n\n@admonition{Community | This collection of guides is written by the community!\nFeel free to contribute your own tips and trips by making a PR.}\n\n\n## Linux\n\n### Discord call cancellation\n\n| Author     | [RickAndTired](https://github.com/RickAndTired) |\n|------------|-------------------------------------------------|\n| Difficulty | Easy                                            |\n\n* Set your normal *Sound Output* volume to 100%\n\n  ![](images/discord_calls_01.png)\n\n* Start Sunshine\n\n* Set *Sound Output* to *sink-sunshine-stereo* (if it isn't automatic)\n\n  ![](images/discord_calls_02.png)\n\n* In Discord, right click *Deafen* and select your normal *Output Device*.\n  This is also where you will need to adjust output volume for Discord calls\n\n  ![](images/discord_calls_03.png)\n\n* Open *qpwgraph*\n\n  ![](images/discord_calls_04.png)\n\n* Connect `sunshine [sunshine-record]` to your normal *Output Device*\n  * Drag `monitor_FL` to `playback_FL`\n  * Drag `monitor_FR` to `playback_FR`\n\n  ![](images/discord_calls_05.png)\n\n### Remote SSH Headless Setup\n\n| Author     | [Eric Dong](https://github.com/e-dong) |\n|------------|----------------------------------------|\n| Difficulty | Intermediate                           |\n\nThis is a guide to setup remote SSH into host to startup X server and Sunshine without physical login and dummy plug.\nThe virtual display is accelerated by the NVidia GPU using the TwinView configuration.\n\n@attention{This guide is specific for Xorg and NVidia GPUs. I start the X server using the `startx` command.\nI also only tested this on an Artix runit init system on LAN.\nI didn't have to do anything special with pulseaudio (pipewire untested).\n\nKeep your monitors plugged in until the [Checkpoint](#checkpoint) step.}\n\n@tip{Prior to editing any system configurations, you should make a copy of the original file.\nThis will allow you to use it for reference or revert your changes easily.}\n\n#### The Big Picture\nOnce you are done, you will need to perform these 3 steps:\n\n1. Turn on the host machine\n2. Start Sunshine on remote host with a script that:\n\n   * Edits permissions of `/dev/uinput` (added sudo config to execute script with no password prompt)\n   * Starts X server with `startx` on virtual display\n   * Starts Sunshine\n\n3. Startup Moonlight on the client of interest and connect to host\n\n@hint{As an alternative to SSH...\n\n**Step 2** can be replaced with autologin and starting Sunshine as a service or putting\n`sunshine &` in your `.xinitrc` file if you start your X server with `startx`.\nIn this case, the workaround for `/dev/uinput` permissions is not needed because the udev rule would be triggered\nfor \"physical\" login. See [Linux Setup](md_docs_2getting__started.html#linux). I personally think autologin compromises\nthe security of the PC, so I went with the remote SSH route. I use the PC more than for gaming, so I don't need a\nvirtual display everytime I turn on the PC (E.g running updates, config changes, file/media server).}\n\nFirst we will [setup the host](#host-setup) and then the [SSH Client](#ssh-client-setup)\n(Which may not be the same as the machine running the moonlight client).\n\n#### Host Setup\nWe will be setting up:\n\n1. [Static IP Setup](#static-ip-setup)\n2. [SSH Server Setup](#ssh-server-setup)\n3. [Virtual Display Setup](#virtual-display-setup)\n4. [Uinput Permissions Workaround](#uinput-permissions-workaround)\n5. [Stream Launcher Script](#stream-launcher-script)\n\n#### Static IP Setup\nSetup static IP Address for host. For LAN connections you can use DHCP reservation within your assigned range.\ne.g. 192.168.x.x. This will allow you to ssh to the host consistently, so the assigned IP address does\nnot change. It is preferred to set this through your router config.\n\n#### SSH Server Setup\n@note{Most distros have OpenSSH already installed. If it is not present, install OpenSSH using your package manager.}\n\n@tabs{\n  @tab{Debian based | ```bash\n    sudo apt update\n    sudo apt install openssh-server\n    ```}\n  @tab{Arch based | ```bash\n    sudo pacman -S openssh\n    # Install  openssh-<other_init> if you are not using SystemD\n    # e.g. sudo pacman -S openssh-runit\n    ```}\n  @tab{Alpine based | ```bash\n    sudo apk update\n    sudo apk add openssh\n    ```}\n  @tab{Fedora based (dnf) | ```bash\n    sudo dnf install openssh-server\n    ```}\n  @tab{Fedora based (yum) | ```bash\n    sudo yum install openssh-server\n    ```}\n}\n\nNext make sure the OpenSSH daemon is enabled to run when the system starts.\n\n@tabs{\n  @tab{SystemD | ```bash\n    sudo systemctl enable sshd.service\n    sudo systemctl start sshd.service  # Starts the service now\n    sudo systemctl status sshd.service  # See if the service is running\n    ```}\n  @tab{Runit | ```bash\n    sudo ln -s /etc/runit/sv/sshd /run/runit/service  # Enables the OpenSSH daemon to run when system starts\n    sudo sv start sshd  # Starts the service now\n    sudo sv status sshd  # See if the service is running\n    ```}\n  @tab{OpenRC | ```bash\n    rc-update add sshd  # Enables service\n    rc-status  # List services to verify sshd is enabled\n    rc-service sshd start  # Starts the service now\n    ```}\n}\n\n**Disabling PAM in sshd**\n\nI noticed when the ssh session is disconnected for any reason, `pulseaudio` would disconnect.\nThis is due to PAM handling sessions. When running `dmesg`, I noticed `elogind` would say removed user session.\nIn this [Gentoo Forums post](https://forums.gentoo.org/viewtopic-t-1090186-start-0.html),\nsomeone had a similar issue. Starting the X server in the background and exiting out of the console would cause your\nsession to be removed.\n\n@caution{According to this [article](https://devicetests.com/ssh-usepam-security-session-status)\ndisabling PAM increases security, but reduces certain functionality in terms of session handling.\n*Do so at your own risk!*}\n\nEdit the ``sshd_config`` file with the following to disable PAM.\n\n```txt\nusePAM no\n```\n\nAfter making changes to the `sshd_config`, restart the sshd service for changes to take effect.\n\n@tip{Run the command to check the ssh configuration prior to restarting the sshd service.\n```bash\nsudo sshd -t -f /etc/ssh/sshd_config\n```\n\nAn incorrect configuration will prevent the sshd service from starting, which might mean\nlosing SSH access to the server.}\n\n@tabs{\n  @tab{SystemD | ```bash\n    sudo systemctl restart sshd.service\n    ```}\n  @tab{Runit | ```bash\n    sudo sv restart sshd\n    ```}\n  @tab{OpenRC | ```bash\n    sudo rc-service sshd restart\n    ```}\n}\n\n#### Virtual Display Setup\nAs an alternative to a dummy dongle, you can use this config to create a virtual display.\n\n@important{This is only available for NVidia GPUs using Xorg.}\n\n@hint{Use ``xrandr`` to see name of your active display output. Usually it starts with ``DP`` or ``HDMI``. For me, it is ``DP-0``.\nPut this name for the ``ConnectedMonitor`` option under the ``Device`` section.\n\n```bash\nxrandr | grep \" connected\" | awk '{ print $1 }'\n```\n}\n\n```xorg\nSection \"ServerLayout\"\n   Identifier \"TwinLayout\"\n   Screen 0 \"metaScreen\" 0 0\nEndSection\n\nSection \"Monitor\"\n   Identifier \"Monitor0\"\n   Option \"Enable\" \"true\"\nEndSection\n\nSection \"Device\"\n   Identifier \"Card0\"\n   Driver \"nvidia\"\n   VendorName \"NVIDIA Corporation\"\n   Option \"MetaModes\" \"1920x1080\"\n   Option \"ConnectedMonitor\" \"DP-0\"\n   Option \"ModeValidation\" \"NoDFPNativeResolutionCheck,NoVirtualSizeCheck,NoMaxPClkCheck,NoHorizSyncCheck,NoVertRefreshCheck,NoWidthAlignmentCheck\"\nEndSection\n\nSection \"Screen\"\n   Identifier \"metaScreen\"\n   Device \"Card0\"\n   Monitor \"Monitor0\"\n   DefaultDepth 24\n   Option \"TwinView\" \"True\"\n   SubSection \"Display\"\n       Modes \"1920x1080\"\n   EndSubSection\nEndSection\n```\n\n@note{The `ConnectedMonitor` tricks the GPU into thinking a monitor is connected,\neven if there is none actually connected! This allows a virtual display to be created that is accelerated with\nyour GPU! The `ModeValidation` option disables valid resolution checks, so you can choose any\nresolution on the host!\n\n**References**\n\n* [issue comment on virtual-display-linux](https://github.com/dianariyanto/virtual-display-linux/issues/9#issuecomment-786389065)\n* [Nvidia Documentation on Configuring TwinView](https://download.nvidia.com/XFree86/Linux-x86/270.29/README/configtwinview.html)\n* [Arch Wiki Nvidia#TwinView](https://wiki.archlinux.org/title/NVIDIA#TwinView)\n* [Unix Stack Exchange - How to add virtual display monitor with Nvidia proprietary driver](https://unix.stackexchange.com/questions/559918/how-to-add-virtual-monitor-with-nvidia-proprietary-driver)\n}\n\n#### Uinput Permissions Workaround\n\n##### Steps\nWe can use `chown` to change the permissions from a script. Since this requires `sudo`,\nwe will need to update the sudo configuration to execute this without being prompted for a password.\n\n1. Create a `sunshine-setup.sh` script to update permissions on `/dev/uinput`. Since we aren't logged into the host,\n   the udev rule doesn't apply.\n2. Update user sudo configuration `/etc/sudoers.d/<user>` to allow the `sunshine-setup.sh`\n   script to be executed with `sudo`.\n\n@note{After I setup the :ref:`udev rule <about/setup:install>` to get access to `/dev/uinput`, I noticed when I sshed\ninto the host without physical login, the ACL permissions on `/dev/uinput` were not changed. So I asked\n[reddit](https://www.reddit.com/r/linux_gaming/comments/14htuzv/does_sshing_into_host_trigger_udev_rule_on_the).\nI discovered that SSH sessions are not the same as a physical login.\nI suppose it's not possible for SSH to trigger a udev rule or create a physical login session.}\n\n##### Setup Script\nThis script will take care of any preconditions prior to starting up Sunshine.\n\nRun the following to create a script named something like `sunshine-setup.sh`:\n\n```bash\necho \"chown $(id -un):$(id -gn) /dev/uinput\" > sunshine-setup.sh && \\\n  chmod +x sunshine-setup.sh\n```\n\n(**Optional**) To Ensure ethernet is being used for streaming, you can block Wi-Fi with `rfkill`.\n\nRun this command to append the rfkill block command to the script:\n\n```bash\necho \"rfkill block $(rfkill list | grep \"Wireless LAN\" \\\n  | sed 's/^\\([[:digit:]]\\).*/\\1/')\" >> sunshine-setup.sh\n```\n\n##### Sudo Configuration\nWe will manually change the permissions of `/dev/uinput` using `chown`.\nYou need to use `sudo` to make this change, so add/update the entry in `/etc/sudoers.d/${USER}`.\n\n@danger{Do so at your own risk! It is more secure to give sudo and no password prompt to a single script,\nthan a generic executable like chown.}\n\n@warning{Be very careful of messing this config up. If you make a typo, *YOU LOSE THE ABILITY TO USE SUDO*.\nFortunately, your system is not borked, you will need to login as root to fix the config.\nYou may want to setup a backup user / SSH into the host as root to fix the config if this happens.\nOtherwise, you will need to plug your machine back into a monitor and login as root to fix this.\nTo enable root login over SSH edit your SSHD config, and add `PermitRootLogin yes`, and restart the SSH server.}\n\n1. First make a backup of your `/etc/sudoers.d/${USER}` file.\n\n   ```bash\n   sudo cp /etc/sudoers.d/${USER} /etc/sudoers.d/${USER}.backup\n   ```\n\n2. `cd` to the parent dir of the `sunshine-setup.sh` script.\n3. Execute the following to update your sudoer config file.\n\n   ```bash\n   echo \"${USER} ALL=(ALL:ALL) ALL, NOPASSWD: $(pwd)/sunshine-setup.sh\" \\\n     | sudo tee /etc/sudoers.d/${USER}\n   ```\n\nThese changes allow the script to use sudo without being prompted with a password.\n\ne.g. `sudo $(pwd)/sunshine-setup.sh`\n\n#### Stream Launcher Script\nThis is the main entrypoint script that will run the `sunshine-setup.sh` script, start up X server, and Sunshine.\nThe client will call this script that runs on the host via ssh.\n\n\n##### Sunshine Startup Script\nThis guide will refer to this script as `~/scripts/sunshine.sh`.\nThe setup script will be referred as `~/scripts/sunshine-setup.sh`.\n\n```bash\n#!/bin/bash\nset -e\n\nexport DISPLAY=:0\n\n# Check existing X server\nps -e | grep X >/dev/null\n[[ ${?} -ne 0 ]] && {\n echo \"Starting X server\"\n startx &>/dev/null &\n [[ ${?} -eq 0 ]] && {\n   echo \"X server started successfully\"\n } || echo \"X server failed to start\"\n} || echo \"X server already running\"\n\n# Check if sunshine is already running\nps -e | grep -e .*sunshine$ >/dev/null\n[[ ${?} -ne 0 ]] && {\n sudo ~/scripts/sunshine-setup.sh\n echo \"Starting Sunshine!\"\n sunshine > /dev/null &\n [[ ${?} -eq 0 ]] && {\n   echo \"Sunshine started successfully\"\n } || echo \"Sunshine failed to start\"\n} || echo \"Sunshine is already running\"\n\n# Add any other Programs that you want to startup automatically\n# e.g.\n# steam &> /dev/null &\n# firefox &> /dev/null &\n# kdeconnect-app &> /dev/null &\n```\n\n#### SSH Client Setup\nWe will be setting up:\n\n1. [SSH Key Authentication Setup](#ssh-key-authentication-setup)\n2. [SSH Client Script (Optional)](#ssh-client-script-optional)\n\n##### SSH Key Authentication Setup\n1. Setup your SSH keys with `ssh-keygen` and use `ssh-copy-id` to authorize remote login to your host.\n   Run `ssh <user>@<ip_address>` to login to your host.\n   SSH keys automate login so you don't need to input your password!\n2. Optionally setup a `~/.ssh/config` file to simplify the `ssh` command\n\n   ```txt\n   Host <some_alias>\n       Hostname <ip_address>\n       User <username>\n       IdentityFile ~/.ssh/<your_private_key>\n   ```\n\n   Now you can use `ssh <some_alias>`.\n   `ssh <some_alias> <commands/script>` will execute the command or script on the remote host.\n\n##### Checkpoint\nAs a sanity check, let's make sure your setup is working so far!\n\n###### Test Steps\nWith your monitor still plugged into your Sunshine host PC:\n\n1. `ssh <alias>`\n2. `~/scripts/sunshine.sh`\n3. `nvidia-smi`\n\n   You should see the Sunshine and Xorg processing running:\n\n   ```bash\n   nvidia-smi\n   ```\n\n   *Output:*\n   ```txt\n   +---------------------------------------------------------------------------------------+\n   | NVIDIA-SMI 535.104.05             Driver Version: 535.104.05   CUDA Version: 12.2     |\n   |-----------------------------------------+----------------------+----------------------+\n   | GPU  Name                 Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |\n   | Fan  Temp   Perf          Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |\n   |                                         |                      |               MIG M. |\n   |=========================================+======================+======================|\n   |   0  NVIDIA GeForce RTX 3070        Off | 00000000:01:00.0  On |                  N/A |\n   | 30%   46C    P2              45W / 220W |    549MiB /  8192MiB |      2%      Default |\n   |                                         |                      |                  N/A |\n   +-----------------------------------------+----------------------+----------------------+\n\n   +---------------------------------------------------------------------------------------+\n   | Processes:                                                                            |\n   |  GPU   GI   CI        PID   Type   Process name                            GPU Memory |\n   |        ID   ID                                                             Usage      |\n   |=======================================================================================|\n   |    0   N/A  N/A      1393      G   /usr/lib/Xorg                                86MiB |\n   |    0   N/A  N/A      1440    C+G   sunshine                                    293MiB |\n   +---------------------------------------------------------------------------------------+\n   ```\n\n4. Check `/dev/uinput` permissions\n\n   ```bash\n   ls -l /dev/uinput\n   ```\n\n   *Output:*\n\n   ```console\n   crw------- 1 <user> <primary_group> 10, 223 Aug 29 17:31 /dev/uinput\n   ```\n\n5. Connect to Sunshine host from a moonlight client\n\nNow kill X and Sunshine by running `pkill X` on the host, unplug your monitors from your GPU, and repeat steps 1 - 5.\nYou should get the same result.\nWith this setup you don't need to modify the Xorg config regardless if monitors are plugged in or not.\n\n```bash\npkill X\n```\n\n##### SSH Client Script (Optional)\nAt this point you have a working setup! For convenience, I created this bash script to automate the\nstartup of the X server and Sunshine on the host.\nThis can be run on Unix systems, or on Windows using the `git-bash` or any bash shell.\n\nFor Android/iOS you can install Linux emulators, e.g. `Userland` for Android and `ISH` for iOS.\nThe neat part is that you can execute one script to launch Sunshine from your phone or tablet!\n\n```bash\n#!/bin/bash\nset -e\n\nssh_args=\"<user>@192.168.X.X\" # Or use alias set in ~/.ssh/config\n\ncheck_ssh(){\n  result=1\n  # Note this checks infinitely, you could update this to have a max # of retries\n  while [[ $result -ne 0 ]]\n  do\n    echo \"checking host...\"\n    ssh $ssh_args \"exit 0\" 2>/dev/null\n    result=$?\n    [[ $result -ne 0 ]] && {\n      echo \"Failed to ssh to $ssh_args, with exit code $result\"\n    }\n    sleep 3\n  done\n  echo \"Host is ready for streaming!\"\n}\n\nstart_stream(){\n  echo \"Starting sunshine server on host...\"\n  echo \"Start moonlight on your client of choice\"\n  # -f runs ssh in the background\n  ssh -f $ssh_args \"~/scripts/sunshine.sh &\"\n}\n\ncheck_ssh\nstart_stream\nexit_code=${?}\n\nsleep 3\nexit ${exit_code}\n```\n\n#### Next Steps\nCongratulations, you can now stream your desktop headless! When trying this the first time,\nkeep your monitors close by incase something isn't working right.\n\n@seealso{Now that you have a virtual display, you may want to automate changing the resolution\nand refresh rate prior to connecting to an app. See\n[Changing Resolution and Refresh Rate](md_docs_2app__examples#changing-resolution-and-refresh-rate)\nfor more information.}\n\n\n## macOS\n@todo{It's looking lonely here.}\n\n\n## Windows\n\n| Author     | [BeeLeDev](https://github.com/BeeLeDev) |\n|------------|-----------------------------------------|\n| Difficulty | Intermediate                            |\n\n### Discord call cancellation\nCancel Discord call audio with Voicemeeter (Standard)\n\n#### Voicemeeter Configuration\n1. Click \"Hardware Out\"\n2. Set the physical device you receive audio to as your Hardware Out with MME\n3. Turn on BUS A for the Virtual Input\n\n#### Windows Configuration\n1. Open the sound settings\n2. Set your default Playback as Voicemeeter Input\n\n@tip{Run audio in the background to find the device that your Virtual Input is using\n(Voicemeeter In #), you will see the bar to the right of the device have green bars\ngoing up and down. This device will be referred to as Voicemeeter Input.}\n\n#### Discord Configuration\n1. Open the settings\n2. Go to Voice & Video\n3. Set your Output Device as the physical device you receive audio to\n\n@tip{It is usually the same device you set for Hardware Out in Voicemeeter.}\n\n#### Sunshine Configuration\n1. Go to Configuration\n2. Go to the Audio/Video tab\n3. Set Virtual Sink as Voicemeeter Input\n\n@note{This should be the device you set as default previously in Playback.}\n\n<div class=\"section_buttons\">\n\n| Previous                        |                                        Next |\n|:--------------------------------|--------------------------------------------:|\n| [App Examples](app_examples.md) | [Performance Tuning](performance_tuning.md) |\n\n</div>\n"
  },
  {
    "path": "docs/legal.md",
    "content": "# Legal\n@attention{This documentation is for informational purposes only and is not intended as legal advice. If you have\nany legal questions or concerns about using Sunshine, we recommend consulting with a lawyer.}\n\nSunshine is licensed under the GPL-3.0 license, which allows for free use and modification of the software.\nThe full text of the license can be reviewed [here](https://github.com/LizardByte/Sunshine/blob/master/LICENSE).\n\n## Commercial Use\nSunshine can be used in commercial applications without any limitations. This means that businesses and organizations\ncan use Sunshine to create and sell products or services without needing to seek permission or pay a fee.\n\nHowever, it is important to note that the GPL-3.0 license does not grant any rights to distribute or sell the encoders\ncontained within Sunshine. If you plan to sell access to Sunshine as part of their distribution, you are responsible\nfor obtaining the necessary licenses to do so. This may include obtaining a license from the\nMotion Picture Experts Group (MPEG-LA) and/or any other necessary licensing requirements.\n\nIn summary, while Sunshine is free to use, it is the user's responsibility to ensure compliance with all applicable\nlicensing requirements when redistributing the software as part of a commercial offering. If you have any questions or\nconcerns about using Sunshine in a commercial setting, we recommend consulting with a lawyer.\n\n<div class=\"section_buttons\">\n\n| Previous                                        |                              Next |\n|:------------------------------------------------|----------------------------------:|\n| [Gamestream Migration](gamestream_migration.md) | [Configuration](configuration.md) |\n\n</div>\n"
  },
  {
    "path": "docs/performance_tuning.md",
    "content": "# Performance Tuning\nIn addition to the options available in the [Configuration](configuration.md) section, there are a few additional\nsystem options that can be used to help improve the performance of Sunshine.\n\n## AMD\n\nIn Windows, enabling *Enhanced Sync* in AMD's settings may help reduce the latency by an additional frame. This\napplies to `amfenc` and `libx264`.\n\n## NVIDIA\n\nEnabling *Fast Sync* in Nvidia settings may help reduce latency.\n\n<div class=\"section_buttons\">\n\n| Previous            |                                  Next |\n|:--------------------|--------------------------------------:|\n| [Guides](guides.md) | [Troubleshooting](troubleshooting.md) |\n\n</div>\n"
  },
  {
    "path": "docs/run_command_dev_guide.md",
    "content": "# `run_command` 函数开发说明\n\n## 1. 函数签名\n\n```cpp\nbp::child run_command(\n    bool elevated,                      // 是否以提升权限运行\n    bool interactive,                   // 是否为交互式进程（需要控制台窗口）\n    const std::string &cmd,             // 要执行的命令字符串\n    boost::filesystem::path &working_dir, // 工作目录\n    const bp::environment &env,         // 环境变量（包含 SUNSHINE_* 变量）\n    FILE *file,                         // 输出重定向文件（可为 nullptr）\n    std::error_code &ec,                // 错误码输出\n    bp::group *group                    // 进程组/作业对象（可为 nullptr）\n);\n```\n\n## 2. 功能概述\n\n`run_command` 是 Windows 平台的**统一进程启动接口**：\n\n- **跨权限执行**：SYSTEM 服务可以用户身份启动进程\n- **智能命令解析**：自动处理 URL、文件关联、可执行文件\n- **环境变量展开**：支持 `%SUNSHINE_CLIENT_WIDTH%` 等变量\n- **作业对象管理**：支持进程生命周期跟踪\n\n## 3. 执行流程\n\n```\nrun_command 入口\n      │\n      ▼\n1. 初始化：创建 STARTUPINFOEXW，克隆环境变量\n      │\n      ▼\n2. 环境变量展开：expand_env_vars_in_cmd(cmd, cloned_env)\n   - %SUNSHINE_CLIENT_WIDTH% → 1920\n   - %SUNSHINE_CLIENT_HEIGHT% → 1080\n      │\n      ▼\n3. 命令解析：resolve_command_string()\n   - URL → 查询注册表找默认浏览器\n   - .exe → 直接执行\n   - 其他文件 → 查询关联程序\n      │\n      ▼\n4. 进程创建\n   - SYSTEM 模式：CreateProcessAsUserW（模拟用户身份）\n   - 普通模式：CreateProcessW\n      │\n      ▼\n5. 返回 bp::child 对象\n```\n\n## 4. 环境变量展开机制\n\n### 4.1 Sunshine 内置环境变量\n\n在 `process.cpp` 和 `nvhttp.cpp` 中设置，存储于 `_env`，可在命令中使用 `%VAR%` 语法访问：\n\n#### 应用相关变量\n\n| 变量名 | 类型 | 说明 | 示例值 |\n|--------|------|------|--------|\n| `SUNSHINE_APP_ID` | 数字 | 应用 ID | `\"123\"` |\n| `SUNSHINE_APP_NAME` | 字符串 | 应用名称 | `\"Game\"` |\n\n#### 客户端标识相关变量\n\n| 变量名 | 类型 | 说明 | 示例值 |\n|--------|------|------|--------|\n| `SUNSHINE_CLIENT_ID` | 数字 | 客户端会话 ID | `\"12345\"` |\n| `SUNSHINE_CLIENT_UNIQUE_ID` | 字符串 | 客户端唯一标识符 | `\"unique-id-string\"` |\n| `SUNSHINE_CLIENT_NAME` | 字符串 | 客户端设备名称 | `\"iPhone\"` |\n\n#### 客户端显示相关变量\n\n| 变量名 | 类型 | 说明 | 示例值 |\n|--------|------|------|--------|\n| `SUNSHINE_CLIENT_WIDTH` | 数字 | 客户端屏幕宽度（像素） | `\"1920\"` |\n| `SUNSHINE_CLIENT_HEIGHT` | 数字 | 客户端屏幕高度（像素） | `\"1080\"` |\n| `SUNSHINE_CLIENT_FPS` | 数字 | 客户端刷新率（FPS） | `\"60\"` |\n| `SUNSHINE_CLIENT_HDR` | 布尔 | 是否启用 HDR | `\"true\"` 或 `\"false\"` |\n| `SUNSHINE_CLIENT_CUSTOM_SCREEN_MODE` | 数字 | 自定义屏幕模式 | `\"0\"` |\n\n#### 客户端音频相关变量\n\n| 变量名 | 类型 | 说明 | 示例值 |\n|--------|------|------|--------|\n| `SUNSHINE_CLIENT_AUDIO_CONFIGURATION` | 字符串 | 音频配置（仅在支持时设置） | `\"2.0\"`, `\"5.1\"`, `\"7.1\"` |\n| `SUNSHINE_CLIENT_AUDIO_SURROUND_PARAMS` | 字符串 | 环绕声参数 | 取决于客户端 |\n\n#### 客户端功能相关变量\n\n| 变量名 | 类型 | 说明 | 示例值 |\n|--------|------|------|--------|\n| `SUNSHINE_CLIENT_GCMAP` | 数字 | 游戏控制器映射 | `\"0\"` |\n| `SUNSHINE_CLIENT_HOST_AUDIO` | 布尔 | 是否使用主机音频 | `\"true\"` 或 `\"false\"` |\n| `SUNSHINE_CLIENT_ENABLE_SOPS` | 布尔 | 是否启用 SOPS | `\"true\"` 或 `\"false\"` |\n| `SUNSHINE_CLIENT_ENABLE_MIC` | 布尔 | 是否启用麦克风 | `\"true\"` 或 `\"false\"` |\n| `SUNSHINE_CLIENT_USE_VDD` | 布尔 | 是否使用虚拟显示器 | `\"true\"` 或 `\"false\"` |\n| `SUNSHINE_CLIENT_CERT_UUID` | 字符串 | 客户端证书 UUID（稳定的客户端标识符，仅在存在时设置） | `\"uuid-string\"` |\n\n#### 使用示例\n\n```bash\n# 使用分辨率变量\nqres.exe /x:%SUNSHINE_CLIENT_WIDTH% /y:%SUNSHINE_CLIENT_HEIGHT% /r:%SUNSHINE_CLIENT_FPS%\n\n# 使用 HDR 状态\nif \"%SUNSHINE_CLIENT_HDR%\"==\"true\" (\n    enable_hdr.exe\n)\n\n# 使用客户端名称\necho \"Connected from: %SUNSHINE_CLIENT_NAME%\"\n\n# 使用麦克风状态\nif \"%SUNSHINE_CLIENT_ENABLE_MIC%\"==\"true\" (\n    configure_mic.exe\n)\n```\n\n### 4.2 展开实现\n\n使用 `expand_env_vars_in_cmd` 函数（misc.cpp）：\n\n```cpp\nstd::string expand_env_vars_in_cmd(const std::string &cmd, const bp::environment &env) {\n    // 1. 快速检查：无 '%' 直接返回\n    // 2. 逐字符解析 %VAR% 模式\n    // 3. 先在 cloned_env 中查找（包含 SUNSHINE_* 变量）\n    // 4. 再在系统环境中查找\n    // 5. 找不到则保持原样\n}\n```\n\n### 4.3 为什么不用 `ExpandEnvironmentStringsW`\n\n- `ExpandEnvironmentStringsW` 只能访问**当前进程的环境变量**\n- `SUNSHINE_CLIENT_WIDTH` 等变量在 `cloned_env` 中，不在 Sunshine 进程环境中\n- 因此需要手动从 `cloned_env` 中查找并替换\n\n### 4.4 变量查找顺序\n\n1. 首先在 `cloned_env` 中查找（大小写不敏感）\n2. 然后在系统环境中查找（使用 `GetEnvironmentVariableA`）\n3. 找不到则保留原始 `%VAR%` 语法\n\n### 4.5 使用示例\n\n```bash\n# 直接使用环境变量（✅ 现在支持）\nunlocker.exe -screen-width %SUNSHINE_CLIENT_WIDTH% -screen-height %SUNSHINE_CLIENT_HEIGHT%\n\n# 展开后\nunlocker.exe -screen-width 1920 -screen-height 1080\n\n# 现有 cmd /C 用法仍然有效（向后兼容）\ncmd /C qres.exe /x:%SUNSHINE_CLIENT_WIDTH% /y:%SUNSHINE_CLIENT_HEIGHT%\n```\n\n### 4.6 特殊处理\n\n| 输入 | 输出 | 说明 |\n|------|------|------|\n| `%SUNSHINE_CLIENT_WIDTH%` | `1920` | 正常展开 |\n| `%UNKNOWN%` | `%UNKNOWN%` | 未找到，保持原样 |\n| `%%` | `%` | 转义字符 |\n| `100%` | `100%` | 无配对，保持原样 |\n\n## 5. URL 处理机制\n\n当命令是 URL 时，自动查询注册表找到默认浏览器：\n\n```\n输入: \"https://github.com/example\"\n  │\n  ▼\nPathIsURLW() → 是 URL\n  │\n  ▼\n提取 scheme: \"https\"\n  │\n  ▼\nAssocQueryStringW() 查询注册表\n  │\n  ▼\n获取默认浏览器命令: \"C:\\...\\chrome.exe\" \"%1\"\n  │\n  ▼\n替换 %1: \"C:\\...\\chrome.exe\" \"https://github.com/example\"\n```\n\n## 6. 注意事项\n\n1. **环境隔离**：传入的 `env` 会被克隆，不会污染其他调用\n2. **PATH 临时修改**：工作目录会临时添加到 PATH 前面\n3. **SYSTEM 模式**：作为服务运行时，自动模拟用户身份启动 GUI 进程\n4. **向后兼容**：现有 `cmd /C` 配置仍然有效\n\n---\n\n*最后更新: 2024*\n"
  },
  {
    "path": "docs/source/about/advanced_usage.rst",
    "content": "Advanced Usage\n==============\nSunshine will work with the default settings for most users. In some cases you may want to configure Sunshine further.\n\nPerformance Tips\n----------------\n\n.. tab:: AMD\n\n   In Windows, enabling `Enhanced Sync` in AMD's settings may help reduce the latency by an additional frame. This\n   applies to `amfenc` and `libx264`.\n\n.. tab:: NVIDIA\n\n   Enabling `Fast Sync` in Nvidia settings may help reduce latency.\n\nConfiguration\n-------------\nThe default location for the configuration file is listed below. You can use another location if you\nchoose, by passing in the full configuration file path as the first argument when you start Sunshine.\n\nThe default location of the ``apps.json`` is the same as the configuration file. You can use a custom\nlocation by modifying the configuration file.\n\n**Default File Location**\n\n.. table::\n   :widths: auto\n\n   =========   ===========\n   Value       Description\n   =========   ===========\n   Docker      /config/\n   Linux       ~/.config/sunshine/\n   macOS       ~/.config/sunshine/\n   Windows     %ProgramFiles%\\\\Sunshine\\\\config\n   =========   ===========\n\n**Example**\n   .. code-block:: bash\n\n      sunshine ~/sunshine_config.conf\n\nAlthough it is recommended to use the configuration UI, it is possible manually configure sunshine by\nediting the `conf` file in a text editor. Use the examples as reference.\n\n`General <https://localhost:47990/config/#general>`__\n-----------------------------------------------------\n\n`locale <https://localhost:47990/config/#locale>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The locale used for Sunshine's user interface.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =======   ===========\n   Value     Description\n   =======   ===========\n   de        German\n   en        English\n   en_GB     English (UK)\n   en_US     English (United States)\n   es        Spanish\n   fr        French\n   it        Italian\n   ja        Japanese\n   pt        Portuguese\n   ru        Russian\n   sv        Swedish\n   tr        Turkish\n   zh        Chinese (Simplified)\n   =======   ===========\n\n**Default**\n   ``en``\n\n**Example**\n   .. code-block:: text\n\n      locale = en\n\n`sunshine_name <https://localhost:47990/config/#sunshine_name>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The name displayed by Moonlight\n\n**Default**\n   PC hostname\n\n**Example**\n   .. code-block:: text\n\n      sunshine_name = Sunshine\n\n`min_log_level <https://localhost:47990/config/#min_log_level>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The minimum log level printed to standard out.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =======   ===========\n   Value     Description\n   =======   ===========\n   verbose   verbose logging\n   debug     debug logging\n   info      info logging\n   warning   warning logging\n   error     error logging\n   fatal     fatal logging\n   none      no logging\n   =======   ===========\n\n**Default**\n   ``info``\n\n**Example**\n   .. code-block:: text\n\n      min_log_level = info\n\n`channels <https://localhost:47990/config/#channels>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Sunshine can support multiple clients streaming simultaneously, at the cost of higher CPU and GPU usage.\n\n   .. note:: All connected clients share control of the same streaming session.\n\n   .. warning:: Some hardware encoders may have limitations that reduce performance with multiple streams.\n\n**Default**\n   ``1``\n\n**Example**\n   .. code-block:: text\n\n      channels = 1\n\n`global_prep_cmd <https://localhost:47990/config/#global_prep_cmd>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   A list of commands to be run before/after all applications. If any of the prep-commands fail, starting the application is aborted.\n\n**Default**\n   ``[]``\n\n**Example**\n   .. code-block:: text\n\n      global_prep_cmd = [{\"do\":\"nircmd.exe setdisplay 1280 720 32 144\",\"undo\":\"nircmd.exe setdisplay 2560 1440 32 144\"}]\n\n`notify_pre_releases <https://localhost:47990/config/#notify_pre_releases>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Whether to be notified of new pre-release versions of Sunshine.\n\n**Default**\n   ``disabled``\n\n**Example**\n   .. code-block:: text\n\n      notify_pre_releases = disabled\n\n`Input <https://localhost:47990/config/#input>`__\n-------------------------------------------------\n\n`controller <https://localhost:47990/config/#controller>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Whether to allow controller input from the client.\n\n**Example**\n   .. code-block:: text\n\n      controller = enabled\n\n`gamepad <https://localhost:47990/config/#gamepad>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The type of gamepad to emulate on the host.\n\n   .. caution:: Applies to Windows only.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =====     ===========\n   Value     Description\n   =====     ===========\n   auto      Selected based on information from client\n   x360      Xbox 360 controller\n   ds4       DualShock 4 controller (PS4)\n   =====     ===========\n\n**Default**\n   ``auto``\n\n**Example**\n   .. code-block:: text\n\n      gamepad = auto\n\n`ds4_back_as_touchpad_click <https://localhost:47990/config/#ds4_back_as_touchpad_click>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   .. hint:: Only applies when gamepad is set to ds4 manually. Unused in other gamepad modes.\n\n   Allow Select/Back inputs to also trigger DS4 touchpad click. Useful for clients looking to emulate touchpad click\n   on Xinput devices.\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      ds4_back_as_touchpad_click = enabled\n\n`motion_as_ds4 <https://localhost:47990/config/#motion_as_ds4>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   .. hint:: Only applies when gamepad is set to auto.\n\n   If a client reports that a connected gamepad has motion sensor support, emulate it on the host as a DS4 controller.\n\n   When disabled, motion sensors will not be taken into account during gamepad type selection.\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      motion_as_ds4 = enabled\n\n`touchpad_as_ds4 <https://localhost:47990/config/#touchpad_as_ds4>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   .. hint:: Only applies when gamepad is set to auto.\n\n   If a client reports that a connected gamepad has a touchpad, emulate it on the host as a DS4 controller.\n\n   When disabled, touchpad presence will not be taken into account during gamepad type selection.\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      touchpad_as_ds4 = enabled\n\n`back_button_timeout <https://localhost:47990/config/#back_button_timeout>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated.\n\n   .. tip:: If back_button_timeout < 0, then the Home/Guide button will not be emulated.\n\n**Default**\n   ``-1``\n\n**Example**\n   .. code-block:: text\n\n      back_button_timeout = 2000\n\n`keyboard <https://localhost:47990/config/#keyboard>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Whether to allow keyboard input from the client.\n\n**Example**\n   .. code-block:: text\n\n      keyboard = enabled\n\n`key_repeat_delay <https://localhost:47990/config/#key_repeat_delay>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The initial delay, in milliseconds, before repeating keys. Controls how fast keys will repeat themselves.\n\n**Default**\n   ``500``\n\n**Example**\n   .. code-block:: text\n\n      key_repeat_delay = 500\n\n`key_repeat_frequency <https://localhost:47990/config/#key_repeat_frequency>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   How often keys repeat every second.\n\n   .. tip:: This configurable option supports decimals.\n\n**Default**\n   ``24.9``\n\n**Example**\n   .. code-block:: text\n\n      key_repeat_frequency = 24.9\n\n`always_send_scancodes <https://localhost:47990/config/#always_send_scancodes>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Sending scancodes enhances compatibility with games and apps but may result in incorrect keyboard input\n   from certain clients that aren't using a US English keyboard layout.\n\n   Enable if keyboard input is not working at all in certain applications.\n\n   Disable if keys on the client are generating the wrong input on the host.\n\n   .. caution:: Applies to Windows only.\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      always_send_scancodes = enabled\n\n`key_rightalt_to_key_win <https://localhost:47990/config/#key_rightalt_to_key_win>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   It may be possible that you cannot send the Windows Key from Moonlight directly. In those cases it may be useful to\n   make Sunshine think the Right Alt key is the Windows key.\n\n**Default**\n   ``disabled``\n\n**Example**\n   .. code-block:: text\n\n      key_rightalt_to_key_win = enabled\n\n`mouse <https://localhost:47990/config/#mouse>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Whether to allow mouse input from the client.\n\n**Example**\n   .. code-block:: text\n\n      mouse = enabled\n\n`high_resolution_scrolling <https://localhost:47990/config/#high_resolution_scrolling>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   When enabled, Sunshine will pass through high resolution scroll events from Moonlight clients.\n\n   This can be useful to disable for older applications that scroll too fast with high resolution scroll events.\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      high_resolution_scrolling = enabled\n\n`native_pen_touch <https://localhost:47990/config/#native_pen_touch>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   When enabled, Sunshine will pass through native pen/touch events from Moonlight clients.\n\n   This can be useful to disable for older applications without native pen/touch support.\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      native_pen_touch = enabled\n\nkeybindings\n^^^^^^^^^^^\n\n**Description**\n   Sometimes it may be useful to map keybindings. Wayland won't allow clients to capture the Win Key for example.\n\n   .. tip:: See `virtual key codes <https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes>`__\n\n   .. hint:: keybindings needs to have a multiple of two elements.\n\n**Default**\n   .. code-block:: text\n\n      [\n        0x10, 0xA0,\n        0x11, 0xA2,\n        0x12, 0xA4\n      ]\n\n**Example**\n   .. code-block:: text\n\n      keybindings = [\n        0x10, 0xA0,\n        0x11, 0xA2,\n        0x12, 0xA4,\n        0x4A, 0x4B\n      ]\n\n.. note:: This option is not available in the UI. A PR would be welcome.\n\n`Audio/Video <https://localhost:47990/config/#audio-video>`__\n-------------------------------------------------------------\n\n`audio_sink <https://localhost:47990/config/#audio_sink>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The name of the audio sink used for audio loopback.\n\n   .. tip:: To find the name of the audio sink follow these instructions.\n\n      **Linux + pulseaudio**\n         .. code-block:: bash\n\n            pacmd list-sinks | grep \"name:\"\n\n      **Linux + pipewire**\n         .. code-block:: bash\n\n            pactl info | grep Source\n            # in some causes you'd need to use the `Sink` device, if `Source` doesn't work, so try:\n            pactl info | grep Sink\n\n      **macOS**\n         Sunshine can only access microphones on macOS due to system limitations. To stream system audio use\n         `Soundflower <https://github.com/mattingalls/Soundflower>`__ or\n         `BlackHole <https://github.com/ExistentialAudio/BlackHole>`__.\n\n      **Windows**\n         .. code-block:: batch\n\n            tools\\audio-info.exe\n\n         .. tip:: If you have multiple audio devices with identical names, use the Device ID instead.\n\n   .. tip:: If you want to mute the host speakers, use `virtual_sink`_ instead.\n\n**Default**\n   Sunshine will select the default audio device.\n\n**Examples**\n   **Linux**\n      .. code-block:: text\n\n         audio_sink = alsa_output.pci-0000_09_00.3.analog-stereo\n\n   **macOS**\n      .. code-block:: text\n\n         audio_sink = BlackHole 2ch\n\n   **Windows**\n      .. code-block:: text\n\n         audio_sink = Speakers (High Definition Audio Device)\n\n`virtual_sink <https://localhost:47990/config/#virtual_sink>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The audio device that's virtual, like Steam Streaming Speakers. This allows Sunshine to stream audio, while muting\n   the speakers.\n\n   .. tip:: See `audio_sink`_!\n\n   .. tip:: These are some options for virtual sound devices.\n\n      - Stream Streaming Speakers (Linux, macOS, Windows)\n\n        - Steam must be installed.\n        - Enable `install_steam_audio_drivers`_ or use Steam Remote Play at least once to install the drivers.\n\n      - `Virtual Audio Cable <https://vb-audio.com/Cable/>`__ (macOS, Windows)\n\n**Example**\n   .. code-block:: text\n\n      virtual_sink = Steam Streaming Speakers\n\n`install_steam_audio_drivers <https://localhost:47990/config/#install_steam_audio_drivers>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Installs the Steam Streaming Speakers driver (if Steam is installed) to support surround sound and muting host audio.\n\n   .. tip:: This option is only supported on Windows.\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      install_steam_audio_drivers = enabled\n\n`adapter_name <https://localhost:47990/config/#adapter_name>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Select the video card you want to stream.\n\n   .. tip:: To find the name of the appropriate values follow these instructions.\n\n      **Linux + VA-API**\n         Unlike with `amdvce` and `nvenc`, it doesn't matter if video encoding is done on a different GPU.\n\n         .. code-block:: bash\n\n            ls /dev/dri/renderD*  # to find all devices capable of VAAPI\n\n            # replace ``renderD129`` with the device from above to lists the name and capabilities of the device\n            vainfo --display drm --device /dev/dri/renderD129 | \\\n              grep -E \"((VAProfileH264High|VAProfileHEVCMain|VAProfileHEVCMain10).*VAEntrypointEncSlice)|Driver version\"\n\n         To be supported by Sunshine, it needs to have at the very minimum:\n         ``VAProfileH264High   : VAEntrypointEncSlice``\n\n      .. todo:: macOS\n\n      **Windows**\n         .. code-block:: batch\n\n            tools\\dxgi-info.exe\n\n         .. note:: For hybrid graphics systems, DXGI reports the outputs are connected to whichever graphics adapter\n            that the application is configured to use, so it's not a reliable indicator of how the display is\n            physically connected.\n\n**Default**\n   Sunshine will select the default video card.\n\n**Examples**\n   **Linux**\n      .. code-block:: text\n\n         adapter_name = /dev/dri/renderD128\n\n   .. todo:: macOS\n\n   **Windows**\n      .. code-block:: text\n\n         adapter_name = Radeon RX 580 Series\n\n`output_name <https://localhost:47990/config/#output_name>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Select the display you want to stream.\n\n   .. tip:: To find the name of the appropriate values follow these instructions.\n\n      **Linux**\n         During Sunshine startup, you should see the list of detected displays:\n\n         .. code-block:: text\n\n            Info: Detecting displays\n            Info: Detected display: DVI-D-0 (id: 0) connected: false\n            Info: Detected display: HDMI-0 (id: 1) connected: true\n            Info: Detected display: DP-0 (id: 2) connected: true\n            Info: Detected display: DP-1 (id: 3) connected: false\n            Info: Detected display: DVI-D-1 (id: 4) connected: false\n\n         You need to use the id value inside the parenthesis, e.g. ``1``.\n\n      **macOS**\n         During Sunshine startup, you should see the list of detected displays:\n\n         .. code-block:: text\n\n            Info: Detecting displays\n            Info: Detected display: Monitor-0 (id: 3) connected: true\n            Info: Detected display: Monitor-1 (id: 2) connected: true\n\n         You need to use the id value inside the parenthesis, e.g. ``3``.\n\n      **Windows**\n         During Sunshine startup, you should see the list of detected display devices:\n\n         .. code-block:: text\n\n            DEVICE ID: {de9bb7e2-186e-505b-9e93-f48793333810}\n            DISPLAY NAME: \\\\.\\DISPLAY1\n            FRIENDLY NAME: ROG PG279Q\n            DEVICE STATE: PRIMARY\n            HDR STATE: UNKNOWN\n            -----------------------\n            DEVICE ID: {3bd008cd-0465-547c-8da5-c28749c041e6}\n            DISPLAY NAME: NOT AVAILABLE\n            FRIENDLY NAME: IDD HDR\n            DEVICE STATE: INACTIVE\n            HDR STATE: UNKNOWN\n            -----------------------\n            DEVICE ID: {77f67f3e-754f-5d31-af64-ee037e18100a}\n            DISPLAY NAME: NOT AVAILABLE\n            FRIENDLY NAME: SunshineHDR\n            DEVICE STATE: INACTIVE\n            HDR STATE: UNKNOWN\n            -----------------------\n            DEVICE ID: {bc172e6d-86eb-5851-aeca-56525ed716e9}\n            DISPLAY NAME: NOT AVAILABLE\n            FRIENDLY NAME: ROG PG279Q\n            DEVICE STATE: INACTIVE\n            HDR STATE: UNKNOWN\n\n         You need to use the ``DEVICE ID`` value.\n\n**Default**\n   Sunshine will select the default display.\n\n**Examples**\n   **Linux**\n      .. code-block:: text\n\n         output_name = 0\n\n   **macOS**\n      .. code-block:: text\n\n         output_name = 3\n\n   **Windows**\n      .. code-block:: text\n\n         output_name = {de9bb7e2-186e-505b-9e93-f48793333810}\n\n`resolutions <https://localhost:47990/config/#resolutions>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The resolutions advertised by Sunshine.\n\n   .. note:: Some versions of Moonlight, such as Moonlight-nx (Switch), rely on this list to ensure that the requested\n      resolution is supported.\n\n**Default**\n   .. code-block:: text\n\n      [\n        352x240,\n        480x360,\n        858x480,\n        1280x720,\n        1920x1080,\n        2560x1080,\n        3440x1440,\n        1920x1200,\n        3840x2160,\n        3840x1600,\n      ]\n\n**Example**\n   .. code-block:: text\n\n      resolutions = [\n        352x240,\n        480x360,\n        858x480,\n        1280x720,\n        1920x1080,\n        2560x1080,\n        3440x1440,\n        1920x1200,\n        3840x2160,\n        3840x1600,\n      ]\n\n`fps <https://localhost:47990/config/#fps>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The fps modes advertised by Sunshine.\n\n   .. note:: Some versions of Moonlight, such as Moonlight-nx (Switch), rely on this list to ensure that the requested\n      fps is supported.\n\n**Default**\n   ``[10, 30, 60, 90, 120]``\n\n**Example**\n   .. code-block:: text\n\n      fps = [10, 30, 60, 90, 120]\n\nmin_fps_factor <https://localhost:47990/config/#min_fps_factor>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Sunshine will use this factor to calculate the minimum time between frames. Increasing this value may help when\n   streaming mostly static content.\n\n   .. Warning:: Higher values will consume more bandwidth.\n\n**Default**\n   ``1``\n\n**Range**\n   ``1-3``\n\n**Example**\n   .. code-block:: text\n\n      min_fps_factor = 1\n\n`Network <https://localhost:47990/config/#network>`__\n-----------------------------------------------------\n\n`upnp <https://localhost:47990/config/#upnp>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Sunshine will attempt to open ports for streaming over the internet.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =====     ===========\n   Value     Description\n   =====     ===========\n   on        enable UPnP\n   off       disable UPnP\n   =====     ===========\n\n**Default**\n   ``disabled``\n\n**Example**\n   .. code-block:: text\n\n      upnp = on\n\n`address_family <https://localhost:47990/config/#address_family>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Set the address family that Sunshine will use.\n\n.. table::\n   :widths: auto\n\n   =====     ===========\n   Value     Description\n   =====     ===========\n   ipv4      IPv4 only\n   both      IPv4+IPv6\n   =====     ===========\n\n**Default**\n   ``ipv4``\n\n**Example**\n   .. code-block:: text\n\n      address_family = both\n\n`port <https://localhost:47990/config/#port>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Set the family of ports used by Sunshine. Changing this value will offset other ports per the table below.\n\n.. table::\n   :widths: auto\n\n   ================ ============ ===========================\n   Port Description Default Port Difference from config port\n   ================ ============ ===========================\n   HTTPS            47984 TCP    -5\n   HTTP             47989 TCP    0\n   Web              47990 TCP    +1\n   RTSP             48010 TCP    +21\n   Video            47998 UDP    +9\n   Control          47999 UDP    +10\n   Audio            48000 UDP    +11\n   Mic (unused)     48002 UDP    +13\n   ================ ============ ===========================\n\n.. attention:: Custom ports may not be supported by all Moonlight clients.\n\n**Default**\n   ``47989``\n\n**Range**\n   ``1029-65514``\n\n**Example**\n   .. code-block:: text\n\n      port = 47989\n\n`origin_web_ui_allowed <https://localhost:47990/config/#origin_web_ui_allowed>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The origin of the remote endpoint address that is not denied for HTTPS Web UI.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =====     ===========\n   Value     Description\n   =====     ===========\n   pc        Only localhost may access the web ui\n   lan       Only LAN devices may access the web ui\n   wan       Anyone may access the web ui\n   =====     ===========\n\n**Default**\n   ``lan``\n\n**Example**\n   .. code-block:: text\n\n      origin_web_ui_allowed = lan\n\n`external_ip <https://localhost:47990/config/#external_ip>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   If no external IP address is given, Sunshine will attempt to automatically detect external ip-address.\n\n**Default**\n   Automatic\n\n**Example**\n   .. code-block:: text\n\n      external_ip = 123.456.789.12\n\n`lan_encryption_mode <https://localhost:47990/config/#lan_encryption_mode>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   This determines when encryption will be used when streaming over your local network.\n\n   .. warning:: Encryption can reduce streaming performance, particularly on less powerful hosts and clients.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =====     ===========\n   Value     Description\n   =====     ===========\n   0         encryption will not be used\n   1         encryption will be used if the client supports it\n   2         encryption is mandatory and unencrypted connections are rejected\n   =====     ===========\n\n**Default**\n   ``0``\n\n**Example**\n   .. code-block:: text\n\n      lan_encryption_mode = 0\n\n`wan_encryption_mode <https://localhost:47990/config/#wan_encryption_mode>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   This determines when encryption will be used when streaming over the Internet.\n\n   .. warning:: Encryption can reduce streaming performance, particularly on less powerful hosts and clients.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =====     ===========\n   Value     Description\n   =====     ===========\n   0         encryption will not be used\n   1         encryption will be used if the client supports it\n   2         encryption is mandatory and unencrypted connections are rejected\n   =====     ===========\n\n**Default**\n   ``1``\n\n**Example**\n   .. code-block:: text\n\n      wan_encryption_mode = 1\n\n`ping_timeout <https://localhost:47990/config/#ping_timeout>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   How long to wait, in milliseconds, for data from Moonlight before shutting down the stream.\n\n**Default**\n   ``10000``\n\n**Example**\n   .. code-block:: text\n\n      ping_timeout = 10000\n\n`Config Files <https://localhost:47990/config/#files>`__\n--------------------------------------------------------\n\n`file_apps <https://localhost:47990/config/#file_apps>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The application configuration file path. The file contains a json formatted list of applications that can be started\n   by Moonlight.\n\n**Default**\n   OS and package dependent\n\n**Example**\n   .. code-block:: text\n\n      file_apps = apps.json\n\n`credentials_file <https://localhost:47990/config/#credentials_file>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The file where user credentials for the UI are stored.\n\n**Default**\n   ``sunshine_state.json``\n\n**Example**\n   .. code-block:: text\n\n      credentials_file = sunshine_state.json\n\n`log_path <https://localhost:47990/config/#log_path>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The path where the sunshine log is stored.\n\n**Default**\n   ``sunshine.log``\n\n**Example**\n   .. code-block:: text\n\n      log_path = sunshine.log\n\n`pkey <https://localhost:47990/config/#pkey>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The private key used for the web UI and Moonlight client pairing. For best compatibility, this should be an RSA-2048 private key.\n\n   .. warning:: Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits.\n\n**Default**\n   ``credentials/cakey.pem``\n\n**Example**\n   .. code-block:: text\n\n      pkey = /dir/pkey.pem\n\n`cert <https://localhost:47990/config/#cert>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key.\n\n   .. warning:: Not all Moonlight clients support ECDSA keys or RSA key lengths other than 2048 bits.\n\n**Default**\n   ``credentials/cacert.pem``\n\n**Example**\n   .. code-block:: text\n\n      cert = /dir/cert.pem\n\n`file_state <https://localhost:47990/config/#file_state>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The file where current state of Sunshine is stored.\n\n**Default**\n   ``sunshine_state.json``\n\n**Example**\n   .. code-block:: text\n\n      file_state = sunshine_state.json\n\n`Advanced <https://localhost:47990/config/#advanced>`__\n-------------------------------------------------------\n\n`fec_percentage <https://localhost:47990/config/#fec_percentage>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Percentage of error correcting packets per data packet in each video frame.\n\n   .. warning:: Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage.\n\n**Default**\n   ``20``\n\n**Range**\n   ``1-255``\n\n**Example**\n   .. code-block:: text\n\n      fec_percentage = 20\n\n`qp <https://localhost:47990/config/#qp>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Quantization Parameter. Some devices don't support Constant Bit Rate. For those devices, QP is used instead.\n\n   .. warning:: Higher value means more compression, but less quality.\n\n**Default**\n   ``28``\n\n**Example**\n   .. code-block:: text\n\n      qp = 28\n\n`min_threads <https://localhost:47990/config/#min_threads>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Minimum number of CPU threads used for encoding.\n\n   .. note:: Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually worth it to gain\n      the use of more CPU cores for encoding. The ideal value is the lowest value that can reliably encode at your\n      desired streaming settings on your hardware.\n\n**Default**\n   ``2``\n\n**Example**\n   .. code-block:: text\n\n      min_threads = 2\n\n`hevc_mode <https://localhost:47990/config/#hevc_mode>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Allows the client to request HEVC Main or HEVC Main10 video streams.\n\n   .. warning:: HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software\n      encoding.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =====     ===========\n   Value     Description\n   =====     ===========\n   0         advertise support for HEVC based on encoder capabilities (recommended)\n   1         do not advertise support for HEVC\n   2         advertise support for HEVC Main profile\n   3         advertise support for HEVC Main and Main10 (HDR) profiles\n   =====     ===========\n\n**Default**\n   ``0``\n\n**Example**\n   .. code-block:: text\n\n      hevc_mode = 2\n\n`av1_mode <https://localhost:47990/config/#av1_mode>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Allows the client to request AV1 Main 8-bit or 10-bit video streams.\n\n   .. warning:: AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software\n      encoding.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =====     ===========\n   Value     Description\n   =====     ===========\n   0         advertise support for AV1 based on encoder capabilities (recommended)\n   1         do not advertise support for AV1\n   2         advertise support for AV1 Main 8-bit profile\n   3         advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles\n   =====     ===========\n\n**Default**\n   ``0``\n\n**Example**\n   .. code-block:: text\n\n      av1_mode = 2\n\n`capture <https://localhost:47990/config/#capture>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Force specific screen capture method.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =========  ========  ===========\n   Value      Platform  Description\n   =========  ========  ===========\n   nvfbc      Linux     Use NVIDIA Frame Buffer Capture to capture direct to GPU memory. This is usually the fastest method for\n                        NVIDIA cards. NvFBC does not have native Wayland support and does not work with XWayland.\n   wlr        Linux     Capture for wlroots based Wayland compositors via DMA-BUF.\n   kms        Linux     DRM/KMS screen capture from the kernel. This requires that sunshine has cap_sys_admin capability.\n                        See :ref:`Linux Setup <about/setup:install>`.\n   x11        Linux     Uses XCB. This is the slowest and most CPU intensive so should be avoided if possible.\n   ddx        Windows   Use DirectX Desktop Duplication API to capture the display. This is well-supported on Windows machines.\n   wgc        Windows   (beta feature) Use Windows.Graphics.Capture to capture the display.\n   =========  ========  ===========\n\n**Default**\n   Automatic. Sunshine will use the first capture method available in the order of the table above.\n\n**Example**\n   .. code-block:: text\n\n      capture = kms\n\n`encoder <https://localhost:47990/config/#encoder>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Force a specific encoder.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =========  ===========\n   Value      Description\n   =========  ===========\n   nvenc      For NVIDIA graphics cards\n   quicksync  For Intel graphics cards\n   amdvce     For AMD graphics cards\n   vaapi      Use Linux VA-API (AMD, Intel)\n   software   Encoding occurs on the CPU\n   =========  ===========\n\n**Default**\n   Sunshine will use the first encoder that is available.\n\n**Example**\n   .. code-block:: text\n\n      encoder = nvenc\n\n`NVIDIA NVENC Encoder <https://localhost:47990/config/#nvidia-nvenc-encoder>`__\n-------------------------------------------------------------------------------\n\n`nvenc_preset <https://localhost:47990/config/#nvenc_preset>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   NVENC encoder performance preset.\n   Higher numbers improve compression (quality at given bitrate) at the cost of increased encoding latency.\n   Recommended to change only when limited by network or decoder, otherwise similar effect can be accomplished by increasing bitrate.\n\n   .. note:: This option only applies when using NVENC `encoder`_.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   1          P1 (fastest)\n   2          P2\n   3          P3\n   4          P4\n   5          P5\n   6          P6\n   7          P7 (slowest)\n   ========== ===========\n\n**Default**\n   ``1``\n\n**Example**\n   .. code-block:: text\n\n      nvenc_preset = 1\n\n`nvenc_twopass <https://localhost:47990/config/#nvenc_twopass>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Enable two-pass mode in NVENC encoder.\n   This allows to detect more motion vectors, better distribute bitrate across the frame and more strictly adhere to bitrate limits.\n   Disabling it is not recommended since this can lead to occasional bitrate overshoot and subsequent packet loss.\n\n   .. note:: This option only applies when using NVENC `encoder`_.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =========== ===========\n   Value       Description\n   =========== ===========\n   disabled    One pass (fastest)\n   quarter_res Two passes, first pass at quarter resolution (faster)\n   full_res    Two passes, first pass at full resolution (slower)\n   =========== ===========\n\n**Default**\n   ``quarter_res``\n\n**Example**\n   .. code-block:: text\n\n      nvenc_twopass = quarter_res\n\n`nvenc_spatial_aq <https://localhost:47990/config/#nvenc_spatial_aq>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Assign higher QP values to flat regions of the video.\n   Recommended to enable when streaming at lower bitrates.\n\n   .. Note:: This option only applies when using NVENC `encoder`_.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   disabled   Don't enable Spatial AQ (faster)\n   enabled    Enable Spatial AQ (slower)\n   ========== ===========\n\n**Default**\n   ``disabled``\n\n**Example**\n   .. code-block:: text\n\n      nvenc_spatial_aq = disabled\n\n`nvenc_vbv_increase <https://localhost:47990/config/#nvenc_vbv_increase>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Single-frame VBV/HRD percentage increase.\n   By default sunshine uses single-frame VBV/HRD, which means any encoded video frame size is not expected to exceed requested bitrate divided by requested frame rate.\n   Relaxing this restriction can be beneficial and act as low-latency variable bitrate, but may also lead to packet loss if the network doesn't have buffer headroom to handle bitrate spikes.\n   Maximum accepted value is 400, which corresponds to 5x increased encoded video frame upper size limit.\n\n   .. Note:: This option only applies when using NVENC `encoder`_.\n\n   .. Warning:: Can lead to network packet loss.\n\n**Default**\n   ``0``\n\n**Range**\n   ``0-400``\n\n**Example**\n   .. code-block:: text\n\n      nvenc_vbv_increase = 0\n\n`nvenc_realtime_hags <https://localhost:47990/config/#nvenc_realtime_hags>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Use realtime gpu scheduling priority in NVENC when hardware accelerated gpu scheduling (HAGS) is enabled in Windows.\n   Currently NVIDIA drivers may freeze in encoder when HAGS is enabled, realtime priority is used and VRAM utilization is close to maximum.\n   Disabling this option lowers the priority to high, sidestepping the freeze at the cost of reduced capture performance when the GPU is heavily loaded.\n\n   .. note:: This option only applies when using NVENC `encoder`_.\n\n   .. caution:: Applies to Windows only.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   disabled   Use high priority\n   enabled    Use realtime priority\n   ========== ===========\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      nvenc_realtime_hags = enabled\n\n`nvenc_latency_over_power <https://localhost:47990/config/#nvenc_latency_over_power>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Adaptive P-State algorithm which NVIDIA drivers employ doesn't work well with low latency streaming, so sunshine requests high power mode explicitly.\n\n   .. Note:: This option only applies when using NVENC `encoder`_.\n\n   .. Warning:: Disabling it is not recommended since this can lead to significantly increased encoding latency.\n\n   .. Caution:: Applies to Windows only.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   disabled   Sunshine doesn't change GPU power preferences (not recommended)\n   enabled    Sunshine requests high power mode explicitly\n   ========== ===========\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      nvenc_latency_over_power = enabled\n\n`nvenc_opengl_vulkan_on_dxgi <https://localhost:47990/config/#nvenc_opengl_vulkan_on_dxgi>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Sunshine can't capture fullscreen OpenGL and Vulkan programs at full frame rate unless they present on top of DXGI.\n   This is system-wide setting that is reverted on sunshine program exit.\n\n   .. Note:: This option only applies when using NVENC `encoder`_.\n\n   .. Caution:: Applies to Windows only.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   disabled   Sunshine leaves global Vulkan/OpenGL present method unchanged\n   enabled    Sunshine changes global Vulkan/OpenGL present method to \"Prefer layered on DXGI Swapchain\"\n   ========== ===========\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      nvenc_opengl_vulkan_on_dxgi = enabled\n\n`nvenc_h264_cavlc <https://localhost:47990/config/#nvenc_h264_cavlc>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Prefer CAVLC entropy coding over CABAC in H.264 when using NVENC.\n   CAVLC is outdated and needs around 10% more bitrate for same quality, but provides slightly faster decoding when using software decoder.\n\n   .. note:: This option only applies when using H.264 format with NVENC `encoder`_.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   disabled   Prefer CABAC\n   enabled    Prefer CAVLC\n   ========== ===========\n\n**Default**\n   ``disabled``\n\n**Example**\n   .. code-block:: text\n\n      nvenc_h264_cavlc = disabled\n\n`Intel QuickSync Encoder <https://localhost:47990/config/#intel-quicksync-encoder>`__\n-------------------------------------------------------------------------------------\n\n`qsv_preset <https://localhost:47990/config/#qsv_preset>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The encoder preset to use.\n\n   .. note:: This option only applies when using quicksync `encoder`_.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   veryfast   fastest (lowest quality)\n   faster     faster (lower quality)\n   fast       fast (low quality)\n   medium     medium (default)\n   slow       slow (good quality)\n   slower     slower (better quality)\n   veryslow   slowest (best quality)\n   ========== ===========\n\n**Default**\n   ``medium``\n\n**Example**\n   .. code-block:: text\n\n      qsv_preset = medium\n\n`qsv_coder <https://localhost:47990/config/#qsv_coder>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The entropy encoding to use.\n\n   .. note:: This option only applies when using H264 with quicksync `encoder`_.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   auto       let ffmpeg decide\n   cabac      context adaptive binary arithmetic coding - higher quality\n   cavlc      context adaptive variable-length coding - faster decode\n   ========== ===========\n\n**Default**\n   ``auto``\n\n**Example**\n   .. code-block:: text\n\n      qsv_coder = auto\n\n`qsv_slow_hevc <https://localhost:47990/config/#qsv_slow_hevc>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   This options enables use of HEVC on older Intel GPUs that only support low power encoding for H.264.\n\n   .. Caution:: Streaming performance may be significantly reduced when this option is enabled.\n\n**Default**\n   ``disabled``\n\n**Example**\n   .. code-block:: text\n\n      qsv_slow_hevc = disabled\n\n`AMD AMF Encoder <https://localhost:47990/config/#amd-amf-encoder>`__\n---------------------------------------------------------------------\n\n`amd_usage <https://localhost:47990/config/#amd_usage>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The encoder usage profile is used to set the base set of encoding\n   parameters.\n\n   .. note:: This option only applies when using amdvce `encoder`_.\n\n   .. note:: The other AMF options that follow will override a subset\n      of the settings applied by your usage profile, but there are\n      hidden parameters set in usage profiles that cannot be\n      overridden elsewhere.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ======================= ===========\n   Value                   Description\n   ======================= ===========\n   transcoding             transcoding (slowest)\n   webcam                  webcam (slow)\n   lowlatency_high_quality low latency, high quality (fast)\n   lowlatency              low latency (faster)\n   ultralowlatency         ultra low latency (fastest)\n   ======================= ===========\n\n**Default**\n   ``ultralowlatency``\n\n**Example**\n   .. code-block:: text\n\n      amd_usage = ultralowlatency\n\n`amd_rc <https://localhost:47990/config/#amd_rc>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The encoder rate control.\n\n   .. note:: This option only applies when using amdvce `encoder`_.\n\n   .. warning:: The 'vbr_latency' option generally works best, but\n      some bitrate overshoots may still occur. Enabling HRD allows\n      all bitrate based rate controls to better constrain peak bitrate,\n      but may result in encoding artifacts depending on your card.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =========== ===========\n   Value       Description\n   =========== ===========\n   cqp         constant qp mode\n   cbr         constant bitrate\n   vbr_latency variable bitrate, latency constrained\n   vbr_peak    variable bitrate, peak constrained\n   =========== ===========\n\n**Default**\n   ``vbr_latency``\n\n**Example**\n   .. code-block:: text\n\n      amd_rc = vbr_latency\n\n`amd_enforce_hrd <https://localhost:47990/config/#amd_enforce_hrd>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Enable Hypothetical Reference Decoder (HRD) enforcement to help constrain the target bitrate.\n\n   .. note:: This option only applies when using amdvce `encoder`_.\n\n   .. warning:: HRD is known to cause encoding artifacts or negatively affect\n      encoding quality on certain cards.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ======== ===========\n   Value    Description\n   ======== ===========\n   enabled  enable HRD\n   disabled disable HRD\n   ======== ===========\n\n**Default**\n   ``disabled``\n\n**Example**\n   .. code-block:: text\n\n      amd_enforce_hrd = disabled\n\n`amd_quality <https://localhost:47990/config/#amd_quality>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The quality profile controls the tradeoff between\n   speed and quality of encoding.\n\n   .. note:: This option only applies when using amdvce `encoder`_.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   speed      prefer speed\n   balanced   balanced\n   quality    prefer quality\n   ========== ===========\n\n**Default**\n   ``balanced``\n\n**Example**\n   .. code-block:: text\n\n      amd_quality = balanced\n\n\n`amd_preanalysis <https://localhost:47990/config/#amd_preanalysis>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Preanalysis can increase encoding quality at the cost of latency.\n\n   .. note:: This option only applies when using amdvce `encoder`_.\n\n**Default**\n   ``disabled``\n\n**Example**\n   .. code-block:: text\n\n      amd_preanalysis = disabled\n\n`amd_vbaq <https://localhost:47990/config/#amd_vbaq>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Variance Based Adaptive Quantization (VBAQ) can increase subjective\n   visual quality by prioritizing allocation of more bits to smooth\n   areas compared to more textured areas.\n\n   .. note:: This option only applies when using amdvce `encoder`_.\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      amd_vbaq = enabled\n\n`amd_coder <https://localhost:47990/config/#amd_coder>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The entropy encoding to use.\n\n   .. note:: This option only applies when using H264 with amdvce `encoder`_.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   auto       let ffmpeg decide\n   cabac      context adaptive variable-length coding - higher quality\n   cavlc      context adaptive binary arithmetic coding - faster decode\n   ========== ===========\n\n**Default**\n   ``auto``\n\n**Example**\n   .. code-block:: text\n\n      amd_coder = auto\n\n`VideoToolbox Encoder <https://localhost:47990/config/#videotoolbox-encoder>`__\n-------------------------------------------------------------------------------\n\n`vt_coder <https://localhost:47990/config/#vt_coder>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The entropy encoding to use.\n\n   .. note:: This option only applies when using macOS.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   auto       let ffmpeg decide\n   cabac\n   cavlc\n   ========== ===========\n\n**Default**\n   ``auto``\n\n**Example**\n   .. code-block:: text\n\n      vt_coder = auto\n\n`vt_software <https://localhost:47990/config/#vt_software>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Force Video Toolbox to use software encoding.\n\n   .. note:: This option only applies when using macOS.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========== ===========\n   Value      Description\n   ========== ===========\n   auto       let ffmpeg decide\n   disabled   disable software encoding\n   allowed    allow software encoding\n   forced     force software encoding\n   ========== ===========\n\n**Default**\n   ``auto``\n\n**Example**\n   .. code-block:: text\n\n      vt_software = auto\n\n`vt_realtime <https://localhost:47990/config/#vt_realtime>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   Realtime encoding.\n\n   .. note:: This option only applies when using macOS.\n\n   .. warning:: Disabling realtime encoding might result in a delayed frame encoding or frame drop.\n\n**Default**\n   ``enabled``\n\n**Example**\n   .. code-block:: text\n\n      vt_realtime = enabled\n\n`Software Encoder <https://localhost:47990/config/#software-encoder>`__\n-----------------------------------------------------------------------\n\n`sw_preset <https://localhost:47990/config/#sw_preset>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The encoder preset to use.\n\n   .. note:: This option only applies when using software `encoder`_.\n\n   .. note:: From `FFmpeg <https://trac.ffmpeg.org/wiki/Encode/H.264#preset>`__.\n\n         A preset is a collection of options that will provide a certain encoding speed to compression ratio. A slower\n         preset will provide better compression (compression is quality per filesize). This means that, for example, if\n         you target a certain file size or constant bit rate, you will achieve better quality with a slower preset.\n         Similarly, for constant quality encoding, you will simply save bitrate by choosing a slower preset.\n\n         Use the slowest preset that you have patience for.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   ========= ===========\n   Value     Description\n   ========= ===========\n   ultrafast fastest\n   superfast\n   veryfast\n   faster\n   fast\n   medium\n   slow\n   slower\n   veryslow  slowest\n   ========= ===========\n\n**Default**\n   ``superfast``\n\n**Example**\n   .. code-block:: text\n\n      sw_preset = superfast\n\n`sw_tune <https://localhost:47990/config/#sw_tune>`__\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n**Description**\n   The tuning preset to use.\n\n   .. note:: This option only applies when using software `encoder`_.\n\n   .. note:: From `FFmpeg <https://trac.ffmpeg.org/wiki/Encode/H.264#preset>`__.\n\n         You can optionally use -tune to change settings based upon the specifics of your input.\n\n**Choices**\n\n.. table::\n   :widths: auto\n\n   =========== ===========\n   Value       Description\n   =========== ===========\n   film        use for high quality movie content; lowers deblocking\n   animation   good for cartoons; uses higher deblocking and more reference frames\n   grain       preserves the grain structure in old, grainy film material\n   stillimage  good for slideshow-like content\n   fastdecode  allows faster decoding by disabling certain filters\n   zerolatency good for fast encoding and low-latency streaming\n   =========== ===========\n\n**Default**\n   ``zerolatency``\n\n**Example**\n   .. code-block:: text\n\n      sw_tune = zerolatency\n"
  },
  {
    "path": "docs/source/source_code/src/display_device/display_device.rst",
    "content": "display_device\n==============\n\n.. todo:: Add display_device.h\n"
  },
  {
    "path": "docs/source/source_code/src/display_device/parsed_config.rst",
    "content": "parsed_config\n=============\n\n.. todo:: Add parsed_config.h\n"
  },
  {
    "path": "docs/source/source_code/src/display_device/session.rst",
    "content": "session\n=======\n\n.. todo:: Add session.h\n"
  },
  {
    "path": "docs/source/source_code/src/display_device/settings.rst",
    "content": "settings\n========\n\n.. todo:: Add settings.h\n"
  },
  {
    "path": "docs/source/source_code/src/display_device/to_string.rst",
    "content": "to_string\n=========\n\n.. todo:: Add to_string.h\n"
  },
  {
    "path": "docs/source/source_code/src/platform/windows/display_device/settings_data.rst",
    "content": "settings_data\n=============\n\n.. todo:: Add settings_data.h\n"
  },
  {
    "path": "docs/source/source_code/src/platform/windows/display_device/settings_topology.rst",
    "content": "settings_topology\n=================\n\n.. todo:: Add settings_topology.h\n"
  },
  {
    "path": "docs/source/source_code/src/platform/windows/display_device/windows_utils.rst",
    "content": "windows_utils\n=============\n\n.. todo:: Add windows_utils.h\n"
  },
  {
    "path": "docs/third_party_packages.md",
    "content": "# Third-Party Packages\n\n@danger{These packages are not maintained by LizardByte. Use at your own risk.}\n\n## Chocolatey\n[![Chocolatey](https://img.shields.io/badge/dynamic/xml.svg?color=orange&label=chocolatey&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27chocolatey%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=chocolatey)](https://community.chocolatey.org/packages/sunshine)\n\n## Flathub\n[![Flathub](https://img.shields.io/flathub/v/dev.lizardbyte.app.Sunshine?style=for-the-badge&logo=Flathub)](https://flathub.org/apps/dev.lizardbyte.app.Sunshine)\n\n## nixpkgs\n[![nixpkgs](https://img.shields.io/badge/dynamic/xml.svg?color=orange&label=nixpkgs&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27nix_unstable%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=nixos)](https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/sunshine/default.nix)\n\n## Scoop\n[![Scoop (extras bucket)](https://img.shields.io/scoop/v/sunshine.svg?bucket=extras&style=for-the-badge&logo=data:image/vnd.microsoft.icon;base64,AAABAAEAEBAAAAEAGAAhAwAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAAAuhJREFUOE9tk1tIFFEYx7+ZXdfbrhdMElJLFCykCxL20MUW9UkkqeiOFGSWYW75EvjgVlJmlpkaJV5SMtQlMYjEROqpQoiMMEpRW2/p6q67bTuXM2dmOjPu2moNDHPm4/v/Zs7//D9KlmUNAMjkBoqiJOVJapTyqqzXXn49tCohzbRSVERPSi7tokFOSkne2rmzoED4H6C0pHwjT2G2qspsU7U+wBuzWTs8M9mlpen0YEOoMS/73DjrnMuhXFyiLEmjwZH6vmufR5DDNtHBI7b9cWNNpw9AgcVCtw6+P8R43KdkjHMM+vDqI/tywyiN5oy46KQpLEogiG0149+7rG5HGRK5o01N9VYVoPxm/ZXCOMrD95NloihiOj4qhs1K3R8IbqQFogVJAuRifrXNT3wactkGmpvrbni9UregQu7nn87X0XB3w+ZYfcruHRAVJgNtE0EclmCGM8CYC2DE5UK8TJXtzT1ZZTRSeJUHiqOvW29Vb89KKw4kYgEvgIQFGHurg3l7AlitS8CzAohYZgQB5ZU9Ovx8FcBkMkdcKEx5GL1ee1yWGcKjgWMQfHgVDVOjNPD88qHwHAYOe57GbHOcLSoqQiunYC4tT4tL0NYmbwkOx1hO1ukABITg40AkOO0BJCgiYFEAl9sBjGj/pl+nyairq5xdAdy50xbKuH+eFyUMkijdJtHQCAIGxiOQYC0nguMYmJqeVJJW29vfU7wqSErDzeuV6aQ5lUPoIjn7RI5FRIRUMQkbLC05YN42txgaEpTd89IyuNZEaGlpCZqdXsjHAj5Avp7h+c2CIIiqGGMMMzNTgDD/oLev57I3vX+T6IttRUVNvNvpusey3EGeE5QtAkI82B12YFjmXagh5ER39zOrfw7UWfDPvcl0ddP0j+lGjucylDoiZhIbvkboDccsL9q/+Hr/2YI/JDMzZ4/IIyMhRyh1XYBmKCEptqOhoWFlyHwAZZxX/YHXNK/3/tiVUfcV6T8hxMYSf1PeGAAAAABJRU5ErkJggg==)](https://scoop.sh/#/apps?s=0&d=1&o=true&q=sunshine)\n\n## Solus\n[![Solus](https://img.shields.io/badge/dynamic/xml.svg?color=orange&label=Solus&style=for-the-badge&prefix=v&query=%2F%2Ftr%5B%40id%3D%27solus%27%5D%2Ftd%5B3%5D%2Fspan%2Fa&url=https%3A%2F%2Frepology.org%2Fproject%2Fsunshine%2Fversions&logo=solus)](https://dev.getsol.us/source/sunshine)\n\n<div class=\"section_buttons\">\n\n| Previous                      |                                            Next |\n|:------------------------------|------------------------------------------------:|\n| [Docker](../DOCKER_README.md) | [Gamestream Migration](gamestream_migration.md) |\n\n</div>\n"
  },
  {
    "path": "docs/troubleshooting.md",
    "content": "# Troubleshooting\n\n## General\n\n### Forgotten Credentials\nIf you forgot your credentials to the web UI, try this.\n\n@tabs{\n  @tab{General | ```bash\n    sunshine --creds {new-username} {new-password}\n    ```\n  }\n  @tab{AppImage | ```bash\n    ./sunshine.AppImage --creds {new-username} {new-password}\n    ```\n  }\n  @tab{Flatpak | ```bash\n    flatpak run --command=sunshine dev.lizardbyte.app.Sunshine --creds {new-username} {new-password}\n    ```\n  }\n}\n\n@tip{Don't forget to replace `{new-username}` and `{new-password}` with your new credentials.\nDo not include the curly braces.}\n\n### Web UI Access\nCan't access the web UI?\n\n1. Check firewall rules.\n\n### Controller works on Steam but not in games\nOne trick might be to change Steam settings and check or uncheck the configuration to support Xbox/Playstation\ncontrollers and leave only support for Generic controllers.\n\nAlso, if you have many controllers already directly connected to the host, it might help to disable them so that the\nSunshine provided controller (connected to the guest) is the \"first\" one. In Linux this can be accomplished on USB\ndevices by finding the device in `/sys/bus/usb/devices/` and writing `0` to the `authorized` file.\n\n### Network performance test\n\nFor real-time game streaming the most important characteristic of the network\npath between server and client is not pure bandwidth but rather stability and\nconsistency (low latency with low variance, minimal or no packet loss).\n\nThe network can be tested using the multi-platform tool [iPerf3](https://iperf.fr).\n\nOn the Sunshine host `iperf3` is started in server mode:\n\n```bash\niperf3 -s\n```\n\nOn the client device iperf3 is asked to perform a 60-second UDP test in reverse\ndirection (from server to client) at a given bitrate (e.g. 50 Mbps):\n\n```bash\niperf3 -c {HostIpAddress} -t 60 -u -R -b 50M\n```\n\nWatch the output on the client for packet loss and jitter values. Both should be\n(very) low. Ideally packet loss remains less than 5% and jitter below 1ms.\n\nFor Android clients use\n[PingMaster](https://play.google.com/store/apps/details?id=com.appplanex.pingmasternetworktools).\n\nFor iOS clients use [HE.NET Network Tools](https://apps.apple.com/us/app/he-net-network-tools/id858241710).\n\nIf you are testing a remote connection (over the internet) you will need to\nforward the port 5201 (TCP and UDP) from your host.\n\n### Packet loss (Buffer overrun)\nIf the host PC (running Sunshine) has a much faster connection to the network\nthan the slowest segment of the network path to the client device (running\nMoonlight), massive packet loss can occur: Sunshine emits its stream in bursts\nevery 16ms (for 60fps) but those bursts can't be passed on fast enough to the\nclient and must be buffered by one of the network devices inbetween. If the\nbitrate is high enough, these buffers will overflow and data will be discarded.\n\nThis can easily happen if e.g. the host has a 2.5 Gbit/s connection and the\nclient only 1 Gbit/s or Wi-Fi. Similarly, a 1 Gbps host may be too fast for a\nclient having only a 100 Mbps interface.\n\nAs a workaround the transmission speed of the host NIC can be reduced: 1 Gbps\ninstead of 2.5 or 100 Mbps instead of 1 Gbps. (A technically more advanced\nsolution would be to configure traffic shaping rules at the OS-level, so that\nonly Sunshine's traffic is slowed down.)\n\nSunshine versions > 0.23.1 include improved networking code that should\nalleviate or even solve this issue (without reducing the NIC speed).\n\n### Packet loss (MTU)\nAlthough unlikely, some guests might work better with a lower\n[MTU](https://en.wikipedia.org/wiki/Maximum_transmission_unit) from the host.\nFor example, a LG TV was found to have 30-60% packet loss when the host had MTU\nset to 1500 and 1472, but 0% packet loss with a MTU of 1428 set in the network card\nserving the stream (a Linux PC). It's unclear how that helped precisely, so it's a last\nresort suggestion.\n\n## Linux\n\n### Hardware Encoding fails\nDue to legal concerns, Mesa has disabled hardware decoding and encoding by default.\n\n```txt\nError: Could not open codec [h264_vaapi]: Function not implemented\n```\n\nIf you see the above error in the Sunshine logs, compiling *Mesa* manually, may be required. See the official Mesa3D\n[Compiling and Installing](https://docs.mesa3d.org/install.html) documentation for instructions.\n\n@important{You must re-enable the disabled encoders. You can do so, by passing the following argument to the build\nsystem. You may also want to enable decoders, however that is not required for Sunshine and is not covered here.\n```bash\n-Dvideo-codecs=h264enc,h265enc\n```\n}\n\n@note{Other build options are listed in the\n[meson options](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/meson_options.txt) file.}\n\n### KMS Streaming fails\nIf screencasting fails with KMS, you may need to run the following to force unprivileged screencasting.\n\n```bash\nsudo setcap -r $(readlink -f $(which sunshine))\n```\n\n@note{The above command will not work with the AppImage or Flatpak packages. Please refer to the\n[AppImage setup](md_docs_2getting__started.html#appimage) or\n[Flatpak setup](md_docs_2getting__started.html#flatpak) for more specific instructions.}\n\n### KMS streaming fails on Nvidia GPUs\nIf KMS screen capture results in a black screen being streamed, you may need to\nset the parameter `modeset=1` for Nvidia's kernel module. This can be done by\nadding the following directive to the kernel command line:\n\n```bash\nnvidia_drm.modeset=1\n```\n\nConsult your distribution's documentation for details on how to do this. (Most\noften grub is used to load the kernel and set its command line.)\n\n### AMD encoding latency issues\nIf you notice unexpectedly high encoding latencies (e.g. in Moonlight's\nperformance overlay) or strong fluctuations thereof, this is due to\n[missing support](https://gitlab.freedesktop.org/drm/amd/-/issues/3336)\nin Mesa/libva for AMD's low latency encoder mode. This is particularly\nproblematic at higher resolutions (4K).\n\nOnly the most recent development versions of mesa include support for this\nlow-latency mode. It will be included in Mesa-24.2.\n\nIn order to enable it, Sunshine has to be started with a special environment\nvariable:\n\n```bash\nAMD_DEBUG=lowlatencyenc sunshine\n```\n\nTo check whether low-latency mode is being used, one can watch the `VCLK` and\n`DCLK` frequencies in `amdgpu_top`. Without this encoder tuning both clock\nfrequencies will fluctuate strongly, whereas with active low-latency encoding\nthey will stay high as long as the encoder is used.\n\n### Gamescope compatibility\nSome users have reported stuttering issues when streaming games running within Gamescope.\n\n## macOS\n\n### Dynamic session lookup failed\nIf you get this error:\n\n> Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that\n> org.freedesktop.dbus-session.plist is loaded!\n\nTry this.\n```bash\nlaunchctl load -w /Library/LaunchAgents/org.freedesktop.dbus-session.plist\n```\n\n## Windows\n\n### No gamepad detected\nVerify that you've installed [Nefarius Virtual Gamepad](https://github.com/nefarius/ViGEmBus/releases/latest).\n\n### Permission denied\nSince Sunshine runs as a service on Windows, it may not have the same level of access that your regular user account\nhas. You may get permission denied errors when attempting to launch a game or application from a non system drive.\n\nYou will need to modify the security permissions on your disk. Ensure that user/principal SYSTEM has full\npermissions on the disk.\n\n<div class=\"section_buttons\">\n\n| Previous                                    |                    Next |\n|:--------------------------------------------|------------------------:|\n| [Performance Tuning](performance_tuning.md) | [Building](building.md) |\n\n</div>\n"
  },
  {
    "path": "docs/webhook_format.md",
    "content": "# Sunshine Webhook格式配置\n\n## 概述\n\nSunshine支持多种webhook格式，包括Markdown、纯文本和JSON格式，暂时没有适配的对象平台。\n\n## Webhook格式规范\n\n### 消息格式要求\n- **msgtype**: 消息类型，支持 `markdown`、`text`、`json`\n- **content**: 内容，最长不超过4096个字节，必须是utf8编码\n\n### 支持的Markdown语法\n- 支持标准Markdown语法\n- 支持HTML标签（如 `<font color=\"warning\">`）\n- 支持引用块（`>`）\n- 支持粗体（`**text**`）\n\n## 配置方法\n\n### 1. 自动配置（默认）\n```cpp\n// 系统会自动配置为Markdown格式\nconfigure_webhook_format(true);  // 使用Markdown格式\n```\n\n### 2. 手动配置\n```cpp\n// 配置为Markdown格式（推荐）\ng_webhook_format.set_format_type(format_type_t::MARKDOWN);\ng_webhook_format.set_use_colors(true);\ng_webhook_format.set_simplify_ip(true);\n\n// 或者配置为纯文本格式\nconfigure_webhook_format(false);\n```\n\n## 输出格式示例\n\n### 配置配对失败通知\n```json\n{\n    \"msgtype\": \"markdown\",\n    \"markdown\": {\n        \"content\": \"**Sunshine系统通知**\\n\\n<font color=\\\"warning\\\">**配置配对失败**</font>\\n\\n>主机名:<font color=\\\"comment\\\">sunshine</font>\\n>IP地址:<font color=\\\"comment\\\">IPv6 (本地链路)</font>\\n>客户端名称:<font color=\\\"comment\\\">1111</font>\\n>时间:<font color=\\\"comment\\\">2025-10-07 16:36:33</font>\\n>错误信息:<font color=\\\"warning\\\">PIN码验证失败</font>\"\n    }\n}\n```\n\n### 应用启动通知\n```json\n{\n    \"msgtype\": \"markdown\",\n    \"markdown\": {\n        \"content\": \"**Sunshine系统通知**\\n\\n<font color=\\\"info\\\">**应用启动**</font>\\n\\n>主机名:<font color=\\\"comment\\\">sunshine</font>\\n>IP地址:<font color=\\\"comment\\\">192.168.1.100</font>\\n>应用名称:<font color=\\\"comment\\\">Steam</font>\\n>应用ID:<font color=\\\"comment\\\">12345</font>\\n>客户端:<font color=\\\"comment\\\">Moonlight</font>\\n>客户端IP:<font color=\\\"comment\\\">192.168.1.50</font>\\n>分辨率:<font color=\\\"comment\\\">1920x1080</font>\\n>帧率:<font color=\\\"comment\\\">60</font>\\n>音频:<font color=\\\"comment\\\">启用</font>\\n>时间:<font color=\\\"comment\\\">2025-10-07 16:36:33</font>\"\n    }\n}\n```\n\n## 颜色规范\n\nSunshine webhook支持以下颜色：\n- `info` - 信息（绿色）\n- `warning` - 警告（橙色）\n- `error` - 错误（红色）\n- `comment` - 注释（灰色）\n\n## 内容长度限制\n\n- 最大长度：4096字节\n- 自动截断：超过限制时自动截断并添加省略号\n- 日志记录：截断时会记录警告日志\n\n## 自定义模板\n\n### 设置自定义模板\n```cpp\ng_webhook_format.set_format_type(format_type_t::CUSTOM);\ng_webhook_format.set_custom_template(\n    event_type_t::CONFIG_PIN_FAILED,\n    \"🚨 **配对失败通知**\\n\\n\"\n    \"**主机:** {{hostname}}\\n\"\n    \"**IP:** {{ip_address}}\\n\"\n    \"**客户端:** {{client_name}}\\n\"\n    \"**时间:** {{timestamp}}\\n\"\n    \"**错误:** {{error}}\"\n);\n```\n\n### 支持的模板变量\n- `{{hostname}}` - 主机名\n- `{{ip_address}}` - IP地址\n- `{{event_title}}` - 事件标题\n- `{{timestamp}}` - 时间戳\n- `{{client_name}}` - 客户端名称\n- `{{client_ip}}` - 客户端IP\n- `{{app_name}}` - 应用名称\n- `{{app_id}}` - 应用ID\n- `{{session_id}}` - 会话ID\n\n## 验证函数\n\n### 检查内容长度\n```cpp\nstd::string content = generate_content(event, is_chinese);\nif (validate_webhook_content_length(content)) {\n    // 内容长度符合要求\n} else {\n    // 内容过长，需要截断\n}\n```\n\n## 最佳实践\n\n1. **使用Markdown格式** - 提供更好的视觉效果\n2. **启用颜色** - 使用颜色区分不同类型的事件\n3. **简化IP显示** - 避免显示复杂的IPv6地址\n4. **控制内容长度** - 避免超过4096字节限制\n5. **使用引用块** - 提高信息层次感\n\n## 故障排除\n\n### 常见问题\n1. **内容过长** - 检查是否超过4096字节限制\n2. **格式错误** - 确保JSON格式正确\n3. **编码问题** - 确保使用UTF-8编码\n4. **颜色不显示** - 检查接收端是否支持HTML标签\n\n### 调试方法\n```cpp\n// 启用调试日志\nBOOST_LOG(debug) << \"Webhook content: \" << content;\nBOOST_LOG(debug) << \"Content length: \" << content.length();\n```\n"
  },
  {
    "path": "gh-pages-template/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <title>LizardByte - Sunshine</title>\n        <meta charset=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\" />\n        <meta name=\"description\" content=\"Self-hosted game stream host for Moonlight.\" />\n        <meta name=\"author\" content=\"\" />\n\n        <!-- Open Graph/Twitter metadata -->\n        <meta property=\"og:site_name\" content=\"LizardByte\" />\n        <meta property=\"og:title\" content=\"LizardByte | Sunshine\" />\n        <meta property=\"og:type\" content=\"website\" />\n        <meta property=\"og:image\" content=\"https://app.lizardbyte.dev/uno/github/openGraphImages/Sunshine_624x312.png\" />\n        <meta property=\"og:url\" content=\"https://app.lizardbyte.dev/Sunshine\" />\n        <meta property=\"og:description\" content=\"Self-hosted game stream host for Moonlight.\" />\n        <meta property=\"og:locale\" content=\"en-US\" />\n        <meta property=\"twitter:card\" content=\"summary_large_image\" />\n        <meta property=\"og:twitter_site\" content=\"@lizardbytedev\" />\n        <meta property=\"og:twitter_site:id\" content=\"@lizardbytedev\" />\n        <meta property=\"og:twitter_creator\" content=\"@lizardbytedev\" />\n        <meta property=\"og:twitter_creator:id\" content=\"@lizardbytedev\" />\n        <meta property=\"twitter:image\" content=\"https://app.lizardbyte.dev/uno/github/openGraphImages/Sunshine_624x312.png\" />\n\n        <!-- Favicon-->\n        <link rel=\"icon\" type=\"image/x-icon\" href=\"https://app.lizardbyte.dev/assets/images/favicon.ico\" />\n        <!-- FontAwesome-->\n        <link href=\"https://app.lizardbyte.dev/node_modules/@fortawesome/fontawesome-free/css/all.min.css\" rel=\"stylesheet\" />\n        <!-- Bootstrap theme-->\n        <link href=\"https://app.lizardbyte.dev/node_modules/bootstrap/dist/css/bootstrap.min.css\" rel=\"stylesheet\" />\n        <!-- Custom css-->\n        <link href=\"https://app.lizardbyte.dev/css/custom.css\" rel=\"stylesheet\" />\n\n        <script src=\"https://app.lizardbyte.dev/node_modules/jquery/dist/jquery.min.js\"></script>\n        <script src=\"https://app.lizardbyte.dev/node_modules/@popperjs/core/dist/umd/popper.min.js\"></script>\n\n        <!-- Crowdin widget -->\n        <script src=\"https://app.lizardbyte.dev/js/crowdin.js\"></script>\n        <script src=\"https://app.lizardbyte.dev/js/crowdin_web_widget.js\"></script>\n    </head>\n    <body class=\"d-flex flex-column h-100 bg-dark-gray\">\n        <main class=\"flex-shrink-0 overflow-hidden\">\n            <!-- Navigation-->\n            <nav id=\"nav-container\"></nav>\n\n            <!-- Header-->\n            <header class=\"bg-dark py-0\">\n                <section class=\"offset-anchor\" id=\"Top\">\n                    <div id=\"carousel1\" class=\"carousel slide carousel-fade\" data-bs-ride=\"carousel\"\n                         style=\"height:50vh\">\n                      <!--Indicators-->\n                      <div class=\"carousel-indicators\">\n                        <button type=\"button\" data-bs-target=\"#carousel1\" data-bs-slide-to=\"0\" class=\"active\"\n                                aria-current=\"true\" aria-label=\"Slide 1\"></button>\n                        <button type=\"button\" data-bs-target=\"#carousel1\" data-bs-slide-to=\"1\"\n                                aria-label=\"Slide 2\"></button>\n                        <button type=\"button\" data-bs-target=\"#carousel1\" data-bs-slide-to=\"2\"\n                                aria-label=\"Slide 3\"></button>\n                      </div>\n                      <!--/.Indicators-->\n                      <!--Slides-->\n                      <div class=\"carousel-inner\" role=\"listbox\">\n\n                        <div class=\"carousel-item active\">\n                          <div class=\"view\">\n                            <img  src=\"assets/images/AdobeStock_305732536_1920x1280.jpg\" alt=\"\">\n                            <div class=\"mask rgba-black-light\"></div>\n                          </div>\n<!--                              <div class=\"carousel-caption\">-->\n<!--                                <h3 class=\"h3-responsive\">1</h3>-->\n<!--                                <p>First text</p>-->\n<!--                              </div>-->\n                        </div>\n                        <div class=\"carousel-item\">\n                          <!--Mask color-->\n                          <div class=\"view\">\n                            <img  src=\"assets/images/AdobeStock_231616343_1920x1280.jpg\" alt=\"\">\n                            <div class=\"mask rgba-black-strong\"></div>\n                          </div>\n<!--                              <div class=\"carousel-caption\">-->\n<!--                                <h3 class=\"h3-responsive\">2</h3>-->\n<!--                                <p>Secondary text</p>-->\n<!--                              </div>-->\n                        </div>\n                        <div class=\"carousel-item\">\n                          <!--Mask color-->\n                          <div class=\"view\">\n                            <img  src=\"assets/images/AdobeStock_303330124_1920x1280.jpg\" alt=\"\">\n                            <div class=\"mask rgba-black-slight\"></div>\n                          </div>\n<!--                              <div class=\"carousel-caption\">-->\n<!--                                <h3 class=\"h3-responsive\">3</h3>-->\n<!--                                <p>Third text</p>-->\n<!--                              </div>-->\n                        </div>\n                      </div>\n                      <!--/.Slides-->\n                      <div>\n                          <h1 class=\"carousel-overlay-title display-5 fw-bolder text-white mb-2\">Sunshine</h1>\n                          <p class=\"carousel-overlay-subtitle lead fw-bolder text-white-50 mb-4\">\n                              A LizardByte project</p>\n                      </div>\n                      <!--Controls-->\n                      <button class=\"carousel-control-prev\" type=\"button\" data-bs-target=\"#carousel1\"\n                              data-bs-slide=\"prev\">\n                        <span class=\"carousel-control-prev-icon\" aria-hidden=\"true\"></span>\n                        <span class=\"visually-hidden\">Previous</span>\n                      </button>\n                      <button class=\"carousel-control-next\" type=\"button\" data-bs-target=\"#carousel1\"\n                              data-bs-slide=\"next\">\n                        <span class=\"carousel-control-next-icon\" aria-hidden=\"true\"></span>\n                        <span class=\"visually-hidden\">Next</span>\n                      </button>\n                      <!--/.Controls-->\n                    </div>\n                </section>\n            </header>\n\n            <!-- About section-->\n            <section class=\"offset-anchor py-5\" id=\"About\">\n                <div class=\"container px-auto\">\n                    <p class=\"lead text-center text-white mx-auto mt-0 mb-5\">\n                        Sunshine is a self-hosted game stream host for Moonlight. Offering low latency, cloud gaming\n                        server capabilities with support for AMD, Intel, and Nvidia GPUs for hardware encoding. Software\n                        encoding is also available. You can connect to Sunshine from any Moonlight client on a variety\n                        of devices. A web UI is provided to allow configuration, and client pairing, from your favorite\n                        web browser. Pair from the local server or any mobile device.\n                    </p>\n                </div>\n            </section>\n\n            <!-- Features section-->\n            <section class=\"offset-anchor bg-dark py-5\" id=\"Features\">\n                <div class=\"container px-auto\">\n                    <h2 class=\"text-center text-white fw-bolder my-5\">Features</h2>\n                    <!-- Create a card for each feature -->\n                    <div class=\"row gx-5\">\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fas fa-server\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">Self-hosted</h5>\n                                            <p class=\"mb-0\">\n                                                Run Sunshine on your own hardware. No need to pay monthly fees to a\n                                                cloud gaming provider.\n                                            </p>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <img height=\"40\" src=\"https://moonlight-stream.org/images/moonlight.svg\">\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">Moonlight Support</h5>\n                                            <p class=\"mb-0\">\n                                                Connect to Sunshine from any Moonlight client. Moonlight is available\n                                                for Windows, macOS, Linux, Android, iOS, Xbox, and more. See\n                                                <a class=\"text-white\" href=\"#Clients\">clients</a> for more information.\n                                            </p>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fas fa-microchip\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">Hardware Encoding</h5>\n                                            <p class=\"mb-0\">\n                                                Sunshine supports AMD, Intel, and Nvidia GPUs for hardware encoding.\n                                                Software encoding is also available.\n                                            </p>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                        <div class=\"col-md-6 col-lg-4 mb-5 mb-lg-0\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fas fa-globe\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">Low Latency</h5>\n                                            <p class=\"mb-0\">\n                                                Sunshine is designed to provide the lowest latency possible to achieve optimal gaming performance.\n                                            </p>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                        <div class=\"col-md-6 col-lg-4 mb-5 mb-lg-0\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fas fa-gamepad\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">Control</h5>\n                                            <p class=\"mb-0\">\n                                                Sunshine emulates an Xbox, PlayStation, or Nintendo Switch controller.\n                                                Use nearly any controller on your Moonlight client!<br>\n                                                <small>\n                                                  <ul>\n                                                    <li>Nintendo Switch emulation is only available on Linux.</li>\n                                                    <li>Gamepad emulation is not currently supported on macOS.</li>\n                                                  </ul>\n                                                </small>\n                                            </p>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                        <div class=\"col-md-6 col-lg-4 mb-5 mb-lg-0\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fas fa-gear\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">Configurable</h5>\n                                            <p class=\"mb-0\">\n                                                Sunshine offers many configuration options to customize your experience.\n                                            </p>\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </section>\n\n            <!-- Clients section-->\n            <section class=\"offset-anchor bg-dark py-5\" id=\"Clients\">\n                <div class=\"container px-auto\">\n                    <h2 class=\"text-center text-white fw-bolder my-5\">Clients</h2>\n                    <!-- Create a card for each client -->\n                    <div class=\"row gx-5\">\n\n                        <!-- Android -->\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fab fa-android\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">\n                                                <a href=\"https://github.com/moonlight-stream/moonlight-android\" target=\"_blank\" class=\"text-white text-decoration-none\">\n                                                    Android\n                                                </a>\n                                            </h5>\n                                        </div>\n                                        <div class=\"ms-auto\">\n                                            <span class=\"badge text-bg-info rounded-pill\">Official</span>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"card-footer p-3 ms-3\">\n                                    <div>\n                                        <a href=\"https://play.google.com/store/apps/details?id=com.limelight\" target=\"_blank\">\n                                            <img alt=\"Get it on Google Play\"\n                                                src=\"https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png\"\n                                                height=\"60\">\n                                        </a>\n                                    </div>\n                                    <div>\n                                        <a href=\"https://www.amazon.com/gp/product/B00JK4MFN2\" target=\"_blank\">\n                                            <img alt=\"Available at Amazon Appstore\"\n                                                 src=\"https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/devportal2/res/images/amazon-appstore-badge-english-black.png\"\n                                                 height=\"60\"\n                                                 style=\"padding: 10px;\">\n                                        </a>\n                                    </div>\n                                    <div>\n                                        <a href=\"https://f-droid.org/packages/com.limelight\" target=\"_blank\">\n                                            <img alt=\"Get it on F-Droid\"\n                                                 src=\"https://fdroid.gitlab.io/artwork/badge/get-it-on.png\"\n                                                 height=\"60\">\n                                        </a>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <!-- ChromeOS -->\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fab fa-chrome\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">\n                                                <a href=\"https://github.com/moonlight-stream/moonlight-chrome\" target=\"_blank\" class=\"text-white text-decoration-none\">\n                                                    ChromeOS\n                                                </a>\n                                            </h5>\n                                        </div>\n                                        <div class=\"ms-auto\">\n                                            <span class=\"badge text-bg-info rounded-pill\">Official</span>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"card-footer p-3 ms-3\">\n                                    <div class=\"pb-3\">\n                                        <a href=\"https://chrome.google.com/webstore/detail/moonlight-game-streaming/gemamigbbenahjlfnmlfdjhdnkpbkfjj\" target=\"_blank\" class=\"btn btn-outline-light\">\n                                            <img alt=\"Available in the Chrome Web Store\"\n                                                src=\"https://developer.chrome.com/static/docs/webstore/branding/image/206x58-chrome-web-043497a3d766e.png\"\n                                                height=\"30\">\n                                        </a>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <!-- iOS -->\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fab fa-apple\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">\n                                                <a href=\"https://github.com/moonlight-stream/moonlight-ios\" target=\"_blank\" class=\"text-white text-decoration-none\">\n                                                    iOS\n                                                </a>\n                                            </h5>\n                                        </div>\n                                        <div class=\"ms-auto\">\n                                            <span class=\"badge text-bg-info rounded-pill\">Official</span>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"card-footer p-3 ms-3\">\n                                    <div class=\"pb-3\">\n                                        <a href=\"https://apps.apple.com/us/app/moonlight-game-streaming/id1000551566\" target=\"_blank\">\n                                            <img alt=\"Download on the App Store\"\n                                                src=\"https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg\"\n                                                height=\"40\">\n                                        </a>\n                                    </div>\n                                    <div class=\"pb-3\">\n                                        <a href=\"https://apps.apple.com/us/app/moonlight-game-streaming/id1000551566\" target=\"_blank\">\n                                            <img alt=\"Download on Apple TV\"\n                                                src=\"https://developer.apple.com/app-store/marketing/guidelines/images/badge-download-on-apple-tv.svg\"\n                                                height=\"40\">\n                                        </a>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <!-- Moonlight-QT -->\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fab fa-linux\"></i>\n                                            <i class=\"fa-fw fa-2x fab fa-apple\"></i>\n                                            <i class=\"fa-fw fa-2x fab fa-windows\"></i>\n                                            <i class=\"fa-fw fa-2x fab fa-steam\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">\n                                                <a href=\"https://github.com/moonlight-stream/moonlight-qt\" target=\"_blank\" class=\"text-white text-decoration-none\">\n                                                    QT\n                                                </a>\n                                            </h5>\n                                        </div>\n                                        <div class=\"ms-auto\">\n                                            <span class=\"badge text-bg-info rounded-pill\">Official</span>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"card-footer p-3 ms-3\">\n                                    <div class=\"pb-3\">\n                                        <a href=\"https://github.com/moonlight-stream/moonlight-qt/releases\" target=\"_blank\" class=\"btn btn-info\">\n                                            <i class=\"fab fa-github\"></i> Download on GitHub\n                                        </a>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <!-- Embedded -->\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fas fa-microchip\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">\n                                                <a href=\"https://github.com/moonlight-stream/moonlight-embedded\" target=\"_blank\" class=\"text-white text-decoration-none\">\n                                                    Embedded\n                                                </a>\n                                            </h5>\n                                        </div>\n                                        <div class=\"ms-auto\">\n                                            <span class=\"badge text-bg-info rounded-pill\">Official</span>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"card-footer p-3 ms-3\">\n                                    <div class=\"pb-3\">\n                                        <a href=\"https://github.com/irtimmer/moonlight-embedded/wiki/Packages\" target=\"_blank\" class=\"btn btn-info\">\n                                            <i class=\"fas fa-download\"></i> Download\n                                        </a>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <!-- Xbox One/Series -->\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fab fa-xbox\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">\n                                                <a href=\"https://github.com/TheElixZammuto/moonlight-xbox\" target=\"_blank\" class=\"text-white text-decoration-none\">\n                                                    Xbox One/Series\n                                                </a>\n                                            </h5>\n                                        </div>\n                                        <div class=\"ms-auto\">\n                                            <span class=\"badge text-bg-warning rounded-pill\">Community</span>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"card-footer p-3 ms-3\">\n                                    <div class=\"pb-3\">\n                                        <a href=\"https://apps.microsoft.com/store/detail/moonlight-uwp/9MW1BS08ZBTH\" target=\"_blank\">\n                                            <img alt=\"Get it from Microsoft\"\n                                                 src=\"https://get.microsoft.com/images/en-us%20dark.svg\"\n                                                 height=\"40\">\n                                        </a>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <!-- PS Vita -->\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fab fa-playstation\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">\n                                                <a href=\"https://github.com/xyzz/vita-moonlight\" target=\"_blank\" class=\"text-white text-decoration-none\">\n                                                    PS Vita\n                                                </a>\n                                            </h5>\n                                        </div>\n                                        <div class=\"ms-auto\">\n                                            <span class=\"badge text-bg-warning rounded-pill\">Community</span>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"card-footer p-3 ms-3\">\n                                    <div class=\"pb-3\">\n                                        <a href=\"https://github.com/xyzz/vita-moonlight/releases\" target=\"_blank\" class=\"btn btn-info\">\n                                            <i class=\"fab fa-github\"></i> Download on GitHub\n                                        </a>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <!-- Nintendo Switch -->\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fas fa-code\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">\n                                                <a href=\"https://github.com/XITRIX/Moonlight-Switch\" target=\"_blank\" class=\"text-white text-decoration-none\">\n                                                    Nintendo Switch\n                                                </a>\n                                            </h5>\n                                        </div>\n                                        <div class=\"ms-auto\">\n                                            <span class=\"badge text-bg-warning rounded-pill\">Community</span>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"card-footer p-3 ms-3\">\n                                    <div class=\"pb-3\">\n                                        <a href=\"https://github.com/XITRIX/Moonlight-Switch/releases\" target=\"_blank\" class=\"btn btn-info\">\n                                            <i class=\"fab fa-github\"></i> Download on GitHub\n                                        </a>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <!-- Nintendo Wii U -->\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fas fa-code\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">\n                                                <a href=\"https://github.com/GaryOderNichts/moonlight-wiiu\" target=\"_blank\" class=\"text-white text-decoration-none\">\n                                                    Nintendo Wii U\n                                                </a>\n                                            </h5>\n                                        </div>\n                                        <div class=\"ms-auto\">\n                                            <span class=\"badge text-bg-warning rounded-pill\">Community</span>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"card-footer p-3 ms-3\">\n                                    <div class=\"pb-3\">\n                                        <a href=\"https://github.com/GaryOderNichts/moonlight-wiiu#quick-start\" target=\"_blank\" class=\"btn btn-info\">\n                                            <i class=\"fas fa-download\"></i> Download\n                                        </a>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n                        <!-- LG webOS TV -->\n                        <div class=\"col-md-6 col-lg-4 mb-5\">\n                            <div class=\"card bg-dark-gray text-white rounded-0\">\n                                <div class=\"card-body p-4\">\n                                    <div class=\"d-flex align-items-center\">\n                                        <div class=\"icon text-white\">\n                                            <i class=\"fa-fw fa-2x fas fa-code\"></i>\n                                        </div>\n                                        <div class=\"ms-3\">\n                                            <h5 class=\"fw-bolder mb-0\">\n                                                <a href=\"https://github.com/mariotaku/moonlight-tv\" target=\"_blank\" class=\"text-white text-decoration-none\">\n                                                    LG webOS TV\n                                                </a>\n                                            </h5>\n                                        </div>\n                                        <div class=\"ms-auto\">\n                                            <span class=\"badge text-bg-warning rounded-pill\">Community</span>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"card-footer p-3 ms-3\">\n                                    <div class=\"pb-3\">\n                                        <a href=\"https://github.com/mariotaku/moonlight-tv#download\" target=\"_blank\" class=\"btn btn-info\">\n                                            <i class=\"fas fa-download\"></i> Download\n                                        </a>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n\n\n                    </div>\n                </div>\n            </section>\n\n            <!-- More cards -->\n            <div class=\"container py-5 px-auto\">\n                <div class=\"container col-md-10\">\n                    <!-- Docs section -->\n                    <section class=\"offset-anchor py-4\" id=\"Docs\">\n                        <div class=\"card bg-dark text-white rounded-0\">\n                            <div class=\"card-body p-4\">\n                                <div class=\"d-flex align-items-center\">\n                                    <i class=\"fa-fw fa-2x fas fa-book\"></i>\n                                    <div class=\"ms-3\">\n                                        <h2 class=\"fw-bolder mb-0\">Documentation</h2>\n                                        <p class=\"mb-0\">\n                                            Read the documentation to learn how to install, use, and configure Sunshine.\n                                        </p>\n                                    </div>\n                                </div>\n                            </div>\n                            <div class=\"card-footer p-3 ms-3\">\n                                <a class=\"btn btn-outline-light me-3 mb-3\" href=\"https://docs.lizardbyte.dev/projects/sunshine\" target=\"_blank\">\n                                    <i class=\"fa-fw fas fa-book\"></i>\n                                    Read the Docs\n                                </a>\n                            </div>\n                        </div>\n                    </section>\n\n                    <!-- Download section -->\n                    <section class=\"offset-anchor py-4\" id=\"Download\">\n                        <div class=\"card bg-dark text-white rounded-0\">\n                            <div class=\"card-body p-4\">\n                                <div class=\"d-flex align-items-center\">\n                                    <i class=\"fa-fw fa-2x fas fa-download\"></i>\n                                    <div class=\"ms-3\">\n                                        <h2 class=\"fw-bolder mb-0\">Download</h2>\n                                        <p class=\"mb-0\">\n                                            Download Sunshine for your platform.\n                                        </p>\n                                    </div>\n                                </div>\n                            </div>\n                            <div class=\"card-footer p-3 ms-3\">\n                                <a class=\"latest-button btn btn-outline-light me-3 mb-3 d-none\" href=\"https://github.com/LizardByte/Sunshine/releases/latest\" target=\"_blank\">\n                                    <i class=\"fa-fw fab fa-github\"></i>\n                                        Latest: <span id=\"latest-version\"></span>\n                                </a>\n                                <a class=\"beta-button btn btn-outline-light me-3 mb-3 d-none\" href=\"#\" target=\"_blank\">\n                                    <i class=\"fa-fw fas fa-flask\"></i>\n                                        Beta: <span id=\"beta-version\"></span>\n                                </a>\n                                <a class=\"btn btn-outline-light me-3 mb-3\" href=\"https://github.com/LizardByte/pacman-repo\" target=\"_blank\">\n                                    <i class=\"fa-fw fab fa-linux\"></i>\n                                        ArchLinux\n                                </a>\n                                <a class=\"btn btn-outline-light me-3 mb-3\" href=\"https://hub.docker.com/r/lizardbyte/sunshine\" target=\"_blank\">\n                                    <i class=\"fa-fw fab fa-docker\"></i>\n                                        Docker\n                                </a>\n                                <a class=\"btn btn-outline-light me-3 mb-3\" href=\"https://github.com/LizardByte/homebrew-homebrew\" target=\"_blank\">\n                                    <i class=\"fa-fw fas fa-beer-mug-empty\"></i>\n                                        Homebrew\n                                </a>\n                            </div>\n                        </div>\n                    </section>\n                </div>\n            </div>\n\n            <!-- Donate section -->\n            <section class=\"offset-anchor bg-dark p-5\" id=\"Donate\">\n                <div class=\"container mb-5 shadow border-0 bg-dark-gray rounded-0 col-md-7\">\n                    <div class=\"d-table-row g-0 text-white\">\n                        <div class=\"d-table-cell px-3 align-middle text-center\">\n                            <h1>\n                                <i class=\"fa-fw fas fa-coins\"></i>\n                            </h1>\n                        </div>\n                        <div class=\"col-sm-auto border-white my-3 px-3 py-2 border-start\">\n                            <div class=\"container\">\n                                <h4 class=\"card-title mb-3 fw-bolder\">Donate</h4>\n                            </div>\n                            <a\n                                class=\"text-decoration-none\"\n                                href=\"https://github.com/sponsors/LizardByte\"\n                                target=\"_blank\">\n                                <img class=\"m-3\"\n                                     alt=\"GitHub Sponsors\"\n                                     src=\"https://img.shields.io/github/sponsors/lizardbyte?label=Github%20Sponsors&style=for-the-badge&color=green&logo=githubsponsors\"\n                                >\n                            </a>\n                            <a\n                                class=\"text-decoration-none\"\n                                href=\"https://www.patreon.com/LizardByte\"\n                                target=\"_blank\">\n                                <img class=\"m-3\"\n                                     alt=\"Patreon\"\n                                     src=\"https://img.shields.io/badge/dynamic/json?color=green&label=Patreon&style=for-the-badge&query=patron_count&url=https%3A%2F%2Fapp.lizardbyte.dev%2Funo%2Fpatreon%2FLizardByte.json&logo=patreon\"\n                                >\n                            </a>\n                            <a\n                                class=\"text-decoration-none\"\n                                href=\"https://www.paypal.com/paypalme/ReenigneArcher\"\n                                target=\"_blank\">\n                                <img class=\"m-3\"\n                                     alt=\"PayPal\"\n                                     src=\"https://img.shields.io/static/v1?style=for-the-badge&label=PayPal&message=Donate&color=green&logo=paypal\"\n                                >\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            </section>\n\n            <!-- Support -->\n            <section class=\"offset-anchor bg-dark p-5\" id=\"Support\">\n                <div class=\"col-md-7 mx-auto mb-5\">\n                    <div class=\"container shadow border-0 bg-primary rounded-0\">\n                        <div class=\"d-table-row g-0 text-white\">\n                            <div class=\"d-table-cell px-4 align-middle text-center\">\n                                <div class=\"fs-3 fw-bold text-white\">Support Center</div>\n                                <div class=\"text-white\">Find answers and ask questions.</div>\n                            </div>\n                            <div class=\"col-sm-auto border-white my-3 px-4 py-2 border-start\">\n                                <p class=\"card-text\">\n                                    <em>The one who knows all the answers has not been asked all the questions.</em>\n                                    – Confucius.\n                                </p>\n                                <div class=\"input-group mb-2\">\n                                    <a href=\"support\">\n                                        <button class=\"btn btn-outline-light rounded-0\"\n                                                id=\"button-support\"\n                                                type=\"button\"\n                                        >Support</button>\n                                    </a>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </section>\n\n        </main>\n        <!-- Footer-->\n        <footer class=\"bg-dark py-4 mt-auto\" id=\"footer-container\"></footer>\n\n        <!-- Audio player bottom navbar -->\n        <nav id=\"player-navbar\"></nav>\n\n        <!-- Bootstrap core JS-->\n        <script src=\"https://app.lizardbyte.dev/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js\"></script>\n\n        <!-- Get navbar -->\n        <script src=\"https://app.lizardbyte.dev/js/navbar.js\"></script>\n        <!-- Get footer -->\n        <script src=\"https://app.lizardbyte.dev/js/footer.js\"></script>\n        <!-- Discord WidgetBot -->\n        <script src=\"https://app.lizardbyte.dev/js/discord.js\"></script>\n\n        <!-- TODO: Move this to website repo, and make it accept arguments for the repo name -->\n        <script>\n          // Fetch the releases from the GitHub API\n          fetch('https://api.github.com/repos/LizardByte/Sunshine/releases')\n            .then(response => response.json())\n            .then(data => {\n              // Filter the releases to get only the pre-releases\n              const preReleases = data.filter(release => release.prerelease);\n              // Filter the releases to get only the stable releases\n              const stableReleases = data.filter(release => !release.prerelease);\n\n              // If there are no stable releases, hide the latest download button\n              if (stableReleases.length === 0) {\n                document.querySelector('.latest-button').classList.add('d-none');\n              } else {\n                // Show the latest download button\n                document.querySelector('.latest-button').classList.remove('d-none');\n\n                // Get the latest stable release\n                const latestStableRelease = stableReleases[0];\n                document.querySelector('#latest-version').textContent = latestStableRelease.tag_name;\n\n                // If there is a pre-release, update the href attribute of the anchor tag\n                if (preReleases.length > 0) {\n                  const latestPreRelease = preReleases[0];\n\n                  // Compare the date of the latest pre-release with the date of the latest stable release\n                  const preReleaseDate = new Date(latestPreRelease.published_at);\n                  const stableReleaseDate = new Date(latestStableRelease.published_at);\n\n                  // If the pre-release is newer, update the href attribute of the anchor tag\n                  if (preReleaseDate > stableReleaseDate) {\n                    document.querySelector('.beta-button').href = latestPreRelease.html_url;\n                    document.querySelector('#beta-version').textContent = latestPreRelease.tag_name;\n                    document.querySelector('.beta-button').classList.remove('d-none');\n                  } else {\n                    // If the pre-release is older, hide the button\n                    document.querySelector('.beta-button').classList.add('d-none');\n                  }\n                } else {\n                  // If there is no pre-release, hide the button\n                  document.querySelector('.beta-button').classList.add('d-none');\n                }\n              }\n            });\n        </script>\n\n    </body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"type\": \"module\",\n  \"scripts\": {\n    \"prebuild\": \"node ./src_assets/common/assets/web/scripts/extract-welcome-locales.js\",\n    \"build\": \"vite build --debug\",\n    \"build-clean\": \"vite build --debug --emptyOutDir\",\n    \"dev\": \"vite build --watch\",\n    \"dev-server\": \"vite serve --config vite.dev.config.js\",\n    \"dev-full\": \"node dev-server.js\",\n    \"preview\": \"vite preview\",\n    \"preview:build\": \"npm run build && npm run preview\",\n    \"i18n:validate\": \"node scripts/validate-i18n.js\",\n    \"i18n:sync\": \"node scripts/validate-i18n.js --sync\",\n    \"i18n:reverse-sync\": \"node scripts/reverse-sync-i18n.js --sync\",\n    \"i18n:reverse-check\": \"node scripts/reverse-sync-i18n.js\",\n    \"i18n:format\": \"node scripts/format-i18n.js\",\n    \"i18n:format:check\": \"node scripts/format-i18n.js --check\"\n  },\n  \"dependencies\": {\n    \"@fortawesome/fontawesome-free\": \"6.6.0\",\n    \"@popperjs/core\": \"2.11.8\",\n    \"bootstrap\": \"5.3.3\",\n    \"colorthief\": \"^2.6.0\",\n    \"firebase\": \"^12.3.0\",\n    \"marked\": \"^16.2.1\",\n    \"nanoid\": \"^5.0.7\",\n    \"qrcode\": \"^1.5.4\",\n    \"vue\": \"^3.5.26\",\n    \"vue-i18n\": \"^11.2.8\",\n    \"vuedraggable-es\": \"^4.1.1\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-vue\": \"^6.0.3\",\n    \"express\": \"^4.18.2\",\n    \"less\": \"^4.4.2\",\n    \"vite\": \"^8.0.8\",\n    \"vite-plugin-ejs\": \"1.6.4\",\n    \"vite-plugin-mkcert\": \"^1.17.8\"\n  }\n}\n"
  },
  {
    "path": "scripts/README_I18N.md",
    "content": "# i18n Tools\n\nThis directory contains scripts for managing internationalization (i18n) in the Sunshine project.\n\n## Overview\n\nThe i18n tools ensure that all locale files maintain consistency with the base English locale file (`en.json`), and that all files are properly formatted to minimize Git conflicts and improve code review.\n\n## Available Scripts\n\n### 1. validate-i18n.js\n\nValidates that all locale files have the same keys as the base locale file.\n\n**Usage:**\n```bash\n# Check all locale files for missing/extra keys\nnpm run i18n:validate\n\n# Auto-sync missing keys AND remove extra keys\nnpm run i18n:sync\n```\n\n**What it does:**\n- Compares all locale files against `en.json` (base file)\n- Reports missing keys that exist in `en.json` but not in other locales\n- Reports extra keys that exist in other locales but not in `en.json`\n- In `--sync` mode, automatically adds missing keys with English values AND removes extra keys\n\n**Example output:**\n```\n🔍 Validating i18n translations...\n\n📋 Base locale (en.json) has 606 keys\n\n✅ zh.json: All keys present (606 keys)\n❌ fr.json: Issues found\n   Missing 5 keys:\n     - config.new_setting\n     - apps.new_feature\n     ...\n```\n\n### 2. reverse-sync-i18n.js\n\nIdentifies keys that exist in other locale files but are missing from `en.json` and adds them.\n\n**Usage:**\n```bash\n# Check which keys are in other locales but missing from en.json\nnpm run i18n:reverse-check\n\n# Add missing keys to en.json (reverse sync)\nnpm run i18n:reverse-sync\n```\n\n**What it does:**\n- Scans all locale files to find keys not present in `en.json`\n- Reports which files contain these keys\n- In `--sync` mode, adds these keys to `en.json` using sample values\n- Useful when translations were added to other locales first\n\n**When to use:**\nUse this when you discover that translations were added to locale files (like zh.json, fr.json) but the base `en.json` file doesn't have those keys yet.\n\n**Workflow:**\n1. Run `npm run i18n:reverse-check` to see what's missing\n2. Run `npm run i18n:reverse-sync` to add keys to en.json\n3. Review and update the English translations in en.json\n4. Run `npm run i18n:sync` to propagate to all locales\n5. Run `npm run i18n:format` to ensure formatting\n\n### 3. format-i18n.js\n\nFormats and sorts all locale JSON files alphabetically.\n\n**Usage:**\n```bash\n# Format all locale files\nnpm run i18n:format\n\n# Check if files are properly formatted (for CI/CD)\nnpm run i18n:format:check\n```\n\n**What it does:**\n- Sorts all keys alphabetically (recursively for nested objects)\n- Applies consistent 2-space indentation\n- Ensures trailing newline\n- Works on all `.json` files in the locale directory\n\n**Why this is important:**\n- Alphabetical sorting reduces merge conflicts when multiple developers add keys\n- Consistent formatting makes code review easier\n- Makes it easy to spot missing translations by comparing files side-by-side\n\n## Workflow for Adding New Translations\n\n### Standard Workflow (Adding to en.json first)\n\n#### Step 1: Add keys to en.json\n\nAlways add new translation keys to `en.json` first:\n\n```json\n{\n  \"myapp\": {\n    \"new_feature\": \"New Feature Description\"\n  }\n}\n```\n\n#### Step 2: Sync to other locales\n\nRun the sync command to add the new key to all other locale files:\n\n```bash\nnpm run i18n:sync\n```\n\nThis will add the key with the English value as a placeholder in all locale files.\n\n#### Step 3: Format all files\n\nEnsure consistent formatting:\n\n```bash\nnpm run i18n:format\n```\n\n#### Step 4: Translate placeholders\n\nManually translate the English placeholders in each locale file to the appropriate language.\n\n#### Step 5: Validate\n\nBefore committing, verify everything is correct:\n\n```bash\nnpm run i18n:validate\n```\n\n#### Step 6: Commit changes\n\nCommit all locale file changes together:\n\n```bash\ngit add src_assets/common/assets/web/public/assets/locale/*.json\ngit commit -m \"Add translation keys for new feature\"\n```\n\n### Reverse Workflow (Fixing translations added to other locales first)\n\nSometimes translations are added to other locale files (zh.json, fr.json, etc.) before being added to en.json. Use this workflow to fix that:\n\n#### Step 1: Check for missing keys in en.json\n\n```bash\nnpm run i18n:reverse-check\n```\n\nThis will show which keys exist in other locales but are missing from en.json.\n\n#### Step 2: Add missing keys to en.json\n\n```bash\nnpm run i18n:reverse-sync\n```\n\nThis automatically adds the missing keys to en.json using sample values from other locale files.\n\n#### Step 3: Review and update English translations\n\nOpen `en.json` and review the newly added keys. Update them with proper English translations if the sample values aren't appropriate.\n\n#### Step 4: Sync to all locales\n\n```bash\nnpm run i18n:sync\n```\n\nThis ensures all locale files have the complete set of keys and removes any extra keys.\n\n#### Step 5: Format all files\n\n```bash\nnpm run i18n:format\n```\n\n#### Step 6: Validate\n\n```bash\nnpm run i18n:validate\n```\n\nShould show all green checkmarks!\n\n#### Step 7: Commit changes\n```\n\n## CI/CD Integration\n\nThe i18n validation is integrated into the GitHub Actions CI pipeline via `.github/workflows/i18n-validation.yml`.\n\n**The CI workflow will:**\n1. Check that all locale files are properly formatted\n2. Validate that all locale files have the same keys as `en.json`\n3. Fail the PR if validation fails\n4. Post a helpful comment on the PR with instructions to fix issues\n\n**To pass the CI checks:**\n- Run `npm run i18n:format` before committing\n- Run `npm run i18n:validate` to ensure no missing keys\n- Or run `npm run i18n:sync` to auto-add missing keys\n\n## Best Practices\n\n1. **Always edit en.json first**: Never add keys directly to other locale files\n2. **Use npm scripts**: Don't run the scripts directly, use the npm scripts\n3. **Format before committing**: Run `npm run i18n:format` before every commit\n4. **Validate before pushing**: Run `npm run i18n:validate` before pushing\n5. **Translate manually**: Don't use auto-translation for the synced placeholders - work with native speakers or use CrowdIn\n\n## File Structure\n\n```\nsrc_assets/common/assets/web/public/assets/locale/\n├── en.json       # Base locale (English) - source of truth\n├── zh.json       # Chinese (Simplified)\n├── zh_TW.json    # Chinese (Traditional)\n├── fr.json       # French\n├── de.json       # German\n├── es.json       # Spanish\n├── ja.json       # Japanese\n└── ...           # Other locales\n```\n\n## Troubleshooting\n\n### Missing keys error\n\n**Problem:** `npm run i18n:validate` reports missing keys\n\n**Solution:** \n```bash\nnpm run i18n:sync  # Auto-add missing keys\n# Then manually translate the placeholders\n```\n\n### Formatting check failed\n\n**Problem:** CI reports formatting issues\n\n**Solution:**\n```bash\nnpm run i18n:format  # Auto-format all files\ngit add src_assets/common/assets/web/public/assets/locale/*.json\ngit commit -m \"Format locale files\"\n```\n\n### Extra keys warning\n\n**Problem:** Locale file has keys not in `en.json`\n\n**Solution:** This usually means old keys that were removed from `en.json` but not from other locales. Remove them manually or update `en.json` if they should be kept.\n\n## Technical Details\n\n### Base Locale\n\nThe base locale is hardcoded as `en.json` in the scripts. All other locale files are compared against this file.\n\n### Key Comparison\n\nKeys are compared using dot notation (e.g., `config.general.name`). Nested objects are flattened for comparison.\n\n### Formatting Rules\n\n- 2-space indentation\n- No trailing commas\n- Unix line endings (LF)\n- Trailing newline at end of file\n- Alphabetically sorted keys at all levels\n\n## Contributing\n\nWhen contributing to i18n:\n\n1. Follow the workflow above\n2. Test your changes with the validation scripts\n3. Ensure CI passes before requesting review\n4. Document any new translation keys in your PR description\n\n## Related Documentation\n\n- [WEBUI_DEVELOPMENT.md](../../docs/WEBUI_DEVELOPMENT.md) - Comprehensive WebUI development guide\n- [contributing.md](../../docs/contributing.md) - General contribution guidelines\n"
  },
  {
    "path": "scripts/_locale.py",
    "content": "\"\"\"\n..\n   _locale.py\n\nFunctions related to building, initializing, updating, and compiling localization translations.\n\nBorrowed from RetroArcher.\n\"\"\"\n# standard imports\nimport argparse\nimport datetime\nimport os\nimport subprocess\n\nproject_name = 'Sunshine'\nproject_owner = 'LizardByte'\n\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nroot_dir = os.path.dirname(script_dir)\nlocale_dir = os.path.join(root_dir, 'locale')\nproject_dir = os.path.join(root_dir, 'src')\n\nyear = datetime.datetime.now().year\n\n# target locales\ntarget_locales = [\n    'bg',  # Bulgarian\n    'cs',  # Czech\n    'de',  # German\n    'en',  # English\n    'en_GB',  # English (United Kingdom)\n    'en_US',  # English (United States)\n    'es',  # Spanish\n    'fr',  # French\n    'it',  # Italian\n    'ja',  # Japanese\n    'pt',  # Portuguese\n    'ru',  # Russian\n    'sv',  # Swedish\n    'zh',  # Chinese\n    'zh_TW',  # Chinese (Traditional)\n]\n\n\ndef x_extract():\n    \"\"\"Executes `xgettext extraction` in subprocess.\"\"\"\n\n    pot_filepath = os.path.join(locale_dir, f'{project_name.lower()}.po')\n\n    commands = [\n        'xgettext',\n        '--keyword=translate:1,1t',\n        '--keyword=translate:1c,2,2t',\n        '--keyword=translate:1,2,3t',\n        '--keyword=translate:1c,2,3,4t',\n        '--keyword=gettext:1',\n        '--keyword=pgettext:1c,2',\n        '--keyword=ngettext:1,2',\n        '--keyword=npgettext:1c,2,3',\n        f'--default-domain={project_name.lower()}',\n        f'--output={pot_filepath}',\n        '--language=C++',\n        '--boost',\n        '--from-code=utf-8',\n        '-F',\n        f'--msgid-bugs-address=github.com/{project_owner.lower()}/{project_name.lower()}',\n        f'--copyright-holder={project_owner}',\n        f'--package-name={project_name}',\n        '--package-version=v0'\n    ]\n\n    extensions = ['cpp', 'h', 'm', 'mm']\n\n    # find input files\n    for root, dirs, files in os.walk(project_dir, topdown=True):\n        for name in files:\n            filename = os.path.join(root, name)\n            extension = filename.rsplit('.', 1)[-1]\n            if extension in extensions:  # append input files\n                commands.append(filename)\n\n    print(commands)\n    subprocess.check_output(args=commands, cwd=root_dir)\n\n    try:\n        # fix header\n        body = \"\"\n        with open(file=pot_filepath, mode='r') as file:\n            for line in file.readlines():\n                if line != '\"Language: \\\\n\"\\n':  # do not include this line\n                    if line == '# SOME DESCRIPTIVE TITLE.\\n':\n                        body += f'# Translations template for {project_name}.\\n'\n                    elif line.startswith('#') and 'YEAR' in line:\n                        body += line.replace('YEAR', str(year))\n                    elif line.startswith('#') and 'PACKAGE' in line:\n                        body += line.replace('PACKAGE', project_name)\n                    else:\n                        body += line\n\n        # rewrite pot file with updated header\n        with open(file=pot_filepath, mode='w+') as file:\n            file.write(body)\n    except FileNotFoundError:\n        pass\n\n\ndef babel_init(locale_code: str):\n    \"\"\"Executes `pybabel init` in subprocess.\n\n    :param locale_code: str - locale code\n    \"\"\"\n    commands = [\n        'pybabel',\n        'init',\n        '-i', os.path.join(locale_dir, f'{project_name.lower()}.po'),\n        '-d', locale_dir,\n        '-D', project_name.lower(),\n        '-l', locale_code\n    ]\n\n    print(commands)\n    subprocess.check_output(args=commands, cwd=root_dir)\n\n\ndef babel_update():\n    \"\"\"Executes `pybabel update` in subprocess.\"\"\"\n    commands = [\n        'pybabel',\n        'update',\n        '-i', os.path.join(locale_dir, f'{project_name.lower()}.po'),\n        '-d', locale_dir,\n        '-D', project_name.lower(),\n        '--update-header-comment'\n    ]\n\n    print(commands)\n    subprocess.check_output(args=commands, cwd=root_dir)\n\n\ndef babel_compile():\n    \"\"\"Executes `pybabel compile` in subprocess.\"\"\"\n    commands = [\n        'pybabel',\n        'compile',\n        '-d', locale_dir,\n        '-D', project_name.lower()\n    ]\n\n    print(commands)\n    subprocess.check_output(args=commands, cwd=root_dir)\n\n\nif __name__ == '__main__':\n    # Set up and gather command line arguments\n    parser = argparse.ArgumentParser(\n        description='Script helps update locale translations. Translations must be done manually.')\n\n    parser.add_argument('--extract', action='store_true', help='Extract messages from c++ files.')\n    parser.add_argument('--init', action='store_true', help='Initialize any new locales specified in target locales.')\n    parser.add_argument('--update', action='store_true', help='Update existing locales.')\n    parser.add_argument('--compile', action='store_true', help='Compile translated locales.')\n\n    args = parser.parse_args()\n\n    if args.extract:\n        x_extract()\n\n    if args.init:\n        for locale_id in target_locales:\n            if not os.path.isdir(os.path.join(locale_dir, locale_id)):\n                babel_init(locale_code=locale_id)\n\n    if args.update:\n        babel_update()\n\n    if args.compile:\n        babel_compile()\n"
  },
  {
    "path": "scripts/format-i18n.js",
    "content": "#!/usr/bin/env node\n/**\n * i18n JSON Sorting and Formatting Script\n * \n * This script sorts all locale JSON files alphabetically by key and applies\n * consistent formatting. This helps reduce Git conflicts and makes files easier\n * to review and maintain.\n * \n * Usage:\n *   node scripts/format-i18n.js                # Format all locale files\n *   node scripts/format-i18n.js --check        # Check if files are properly formatted (for CI)\n */\n\nimport fs from 'fs'\nimport path from 'path'\nimport { fileURLToPath } from 'url'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\nconst localeDir = path.join(__dirname, '../src_assets/common/assets/web/public/assets/locale')\n\n// Parse command line arguments\nconst args = process.argv.slice(2)\nconst checkMode = args.includes('--check')\n\n/**\n * Sort object keys recursively\n */\nfunction sortObjectKeys(obj) {\n  if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {\n    return obj\n  }\n  \n  const sorted = {}\n  const keys = Object.keys(obj).sort()\n  \n  for (const key of keys) {\n    sorted[key] = sortObjectKeys(obj[key])\n  }\n  \n  return sorted\n}\n\n/**\n * Format a JSON file with sorted keys\n */\nfunction formatJsonFile(filePath) {\n  try {\n    const content = fs.readFileSync(filePath, 'utf8')\n    const parsed = JSON.parse(content)\n    const sorted = sortObjectKeys(parsed)\n    const formatted = JSON.stringify(sorted, null, 2) + '\\n'\n    \n    // In check mode, compare formatted content with original\n    if (checkMode) {\n      return content === formatted\n    } else {\n      fs.writeFileSync(filePath, formatted, 'utf8')\n      return true\n    }\n  } catch (e) {\n    console.error(`❌ Error processing ${path.basename(filePath)}: ${e.message}`)\n    return false\n  }\n}\n\n/**\n * Main formatting function\n */\nfunction formatLocales() {\n  if (checkMode) {\n    console.log('🔍 Checking i18n file formatting...\\n')\n  } else {\n    console.log('🔄 Formatting i18n files...\\n')\n  }\n  \n  if (!fs.existsSync(localeDir)) {\n    console.error(`❌ Locale directory not found: ${localeDir}`)\n    process.exit(1)\n  }\n  \n  const localeFiles = fs.readdirSync(localeDir)\n    .filter(file => file.endsWith('.json'))\n    .sort()\n  \n  if (localeFiles.length === 0) {\n    console.error('❌ No locale JSON files found')\n    process.exit(1)\n  }\n  \n  let allFormatted = true\n  const results = []\n  \n  for (const localeFile of localeFiles) {\n    const filePath = path.join(localeDir, localeFile)\n    const isFormatted = formatJsonFile(filePath)\n    \n    if (checkMode) {\n      if (isFormatted) {\n        console.log(`✅ ${localeFile}: Properly formatted`)\n      } else {\n        console.log(`❌ ${localeFile}: Needs formatting`)\n        allFormatted = false\n      }\n    } else {\n      if (isFormatted) {\n        console.log(`✅ ${localeFile}: Formatted successfully`)\n      } else {\n        console.log(`❌ ${localeFile}: Failed to format`)\n        allFormatted = false\n      }\n    }\n    \n    results.push({ file: localeFile, formatted: isFormatted })\n  }\n  \n  // Summary\n  console.log('\\n' + '━'.repeat(60))\n  console.log('📊 Summary:')\n  console.log(`   Total files processed: ${localeFiles.length}`)\n  \n  if (checkMode) {\n    console.log(`   Properly formatted: ${results.filter(r => r.formatted).length}`)\n    console.log(`   Need formatting: ${results.filter(r => !r.formatted).length}`)\n  } else {\n    console.log(`   Successfully formatted: ${results.filter(r => r.formatted).length}`)\n    console.log(`   Failed to format: ${results.filter(r => !r.formatted).length}`)\n  }\n  \n  console.log('━'.repeat(60))\n  \n  if (checkMode && !allFormatted) {\n    console.log('\\n💡 Tip: Run without --check flag to auto-format all files')\n    console.error('\\n❌ Formatting check failed')\n    process.exit(1)\n  }\n  \n  if (!checkMode && allFormatted) {\n    console.log('\\n✅ All locale files have been formatted and sorted')\n  }\n  \n  if (!allFormatted && !checkMode) {\n    process.exit(1)\n  }\n}\n\n// Run formatting\nformatLocales()\n"
  },
  {
    "path": "scripts/generate-checksums.ps1",
    "content": "# Generate SHA256 checksums for release packages\n# Usage: .\\generate-checksums.ps1 [-Path <directory>] [-Output <file>]\n\nparam(\n    [string]$Path = \".\",\n    [string]$Output = \"SHA256SUMS.txt\"\n)\n\nWrite-Host \"Generating SHA256 checksums...\" -ForegroundColor Cyan\nWrite-Host \"Directory: $Path\" -ForegroundColor Gray\nWrite-Host \"\"\n\n# Get all package files\n$packages = Get-ChildItem -Path $Path -Include @(\n    \"*.exe\",\n    \"*.msi\", \n    \"*.zip\",\n    \"*.7z\"\n) -Recurse\n\nif ($packages.Count -eq 0) {\n    Write-Host \"No package files found!\" -ForegroundColor Red\n    exit 1\n}\n\n# Calculate checksums\n$checksums = @()\nforeach ($package in $packages) {\n    Write-Host \"Computing: $($package.Name)...\" -ForegroundColor Yellow\n    \n    $hash = Get-FileHash -Path $package.FullName -Algorithm SHA256\n    $relativePath = Resolve-Path -Path $package.FullName -Relative\n    \n    $checksums += [PSCustomObject]@{\n        Hash = $hash.Hash\n        File = $package.Name\n        Size = \"{0:N2} MB\" -f ($package.Length / 1MB)\n    }\n}\n\n# Create output content\n$content = @\"\n# Sunshine Release Package Checksums\n# Generated: $(Get-Date -Format \"yyyy-MM-dd HH:mm:ss UTC\")\n# Algorithm: SHA256\n\nVerify your downloads with:\n  Windows (PowerShell): Get-FileHash -Algorithm SHA256 <filename>\n  Linux/macOS:          shasum -a 256 <filename>\n\n================================================================================\n\n\"@\n\nforeach ($item in $checksums) {\n    $content += \"$($item.Hash.ToLower())  $($item.File)`n\"\n    Write-Host \"  $($item.Hash.ToLower())\" -ForegroundColor Green\n    Write-Host \"  $($item.File) ($($item.Size))\" -ForegroundColor White\n    Write-Host \"\"\n}\n\n# Write to file\n$outputPath = Join-Path -Path $Path -ChildPath $Output\n$content | Out-File -FilePath $outputPath -Encoding UTF8 -NoNewline\n\nWrite-Host \"Checksums saved to: $outputPath\" -ForegroundColor Cyan\nWrite-Host \"\"\nWrite-Host \"Summary:\" -ForegroundColor Cyan\nWrite-Host \"  Files processed: $($checksums.Count)\" -ForegroundColor White\nWrite-Host \"  Total size: $(\"{0:N2} MB\" -f (($packages | Measure-Object -Property Length -Sum).Sum / 1MB))\" -ForegroundColor White\n\n# Also create a JSON version for automation\n$jsonOutput = Join-Path -Path $Path -ChildPath \"checksums.json\"\n$checksums | ConvertTo-Json -Depth 10 | Out-File -FilePath $jsonOutput -Encoding UTF8\n\nWrite-Host \"  JSON output: $jsonOutput\" -ForegroundColor White\n\n"
  },
  {
    "path": "scripts/icons/convert_and_pack.sh",
    "content": "#!/bin/bash\r\n\r\nif ! [ -x \"$(command -v ./go-png2ico)\" ]; then\r\n    echo \"./go-png2ico not found\"\r\n    echo \"download the executable from https://github.com/J-Siu/go-png2ico\"\r\n    echo \"and drop it in this folder\"\r\n    exit 1\r\nfi\r\n\r\nif ! [ -x \"$(command -v ./oxipng)\" ]; then\r\n    echo \"./oxipng executable not found\"\r\n    echo \"download the executable from https://github.com/shssoichiro/oxipng\"\r\n    echo \"and drop it in this folder\"\r\n    exit 1\r\nfi\r\n\r\nif ! [ -x \"$(command -v inkscape)\" ]; then\r\n    echo \"inkscape executable not found\"\r\n    exit 1\r\nfi\r\n\r\nicon_base_sizes=(16 64)\r\nicon_sizes_keys=() # associative array to prevent duplicates\r\nicon_sizes_keys[256]=1\r\n\r\nfor icon_base_size in ${icon_base_sizes[@]}; do\r\n    # increment in 25% till 400%\r\n    icon_size_increment=$((icon_base_size / 4))\r\n    for ((i = 0; i <= 12; i++)); do\r\n        icon_sizes_keys[$((icon_base_size + i * icon_size_increment))]=1\r\n    done\r\ndone\r\n\r\n# convert to normal array\r\nicon_sizes=${!icon_sizes_keys[@]}\r\n\r\necho \"using icon sizes:\"\r\necho ${icon_sizes[@]}\r\n\r\nsrc_vectors=(\"../../src_assets/common/assets/web/public/images/sunshine-locked.svg\"\r\n             \"../../src_assets/common/assets/web/public/images/sunshine-pausing.svg\"\r\n             \"../../src_assets/common/assets/web/public/images/sunshine-playing.svg\"\r\n             \"../../sunshine.svg\")\r\n\r\necho \"using sources vectors:\"\r\necho ${src_vectors[@]}\r\n\r\nfor src_vector in ${src_vectors[@]}; do\r\n    file_name=`basename \"$src_vector\" .svg`\r\n    png_files=()\r\n    for icon_size in ${icon_sizes[@]}; do\r\n        png_file=\"${file_name}${icon_size}.png\"\r\n        echo \"converting ${png_file}\"\r\n        inkscape -w $icon_size -h $icon_size \"$src_vector\" --export-filename \"${png_file}\" &&\r\n        ./oxipng -o max --strip safe --alpha \"${png_file}\" &&\r\n        png_files+=(\"${png_file}\")\r\n    done\r\n\r\n    echo \"packing ${file_name}.ico\"\r\n    ./go-png2ico \"${png_files[@]}\" \"${file_name}.ico\"\r\ndone\r\n"
  },
  {
    "path": "scripts/linux_build.sh",
    "content": "#!/bin/bash\nset -e\n\n# Default value for arguments\nappimage_build=0\npublisher_name=\"Third Party Publisher\"\npublisher_website=\"\"\npublisher_issue_url=\"https://app.lizardbyte.dev/support\"\nskip_cleanup=0\nskip_cuda=0\nskip_libva=0\nskip_package=0\nsudo_cmd=\"sudo\"\nubuntu_test_repo=0\n\nfunction _usage() {\n  local exit_code=$1\n\n  cat <<EOF\nThis script installs the dependencies and builds the project.\nThe script is intended to be run on a Debian-based or Fedora-based system.\n\nUsage:\n  $0 [options]\n\nOptions:\n  -h, --help               Display this help message.\n  -s, --sudo-off           Disable sudo command.\n  --appimage-build         Compile for AppImage, this will not create the AppImage, just the executable.\n  --publisher-name         The name of the publisher (not developer) of the application.\n  --publisher-website      The URL of the publisher's website.\n  --publisher-issue-url    The URL of the publisher's support site or issue tracker.\n                           If you provide a modified version of Sunshine, we kindly request that you use your own url.\n  --skip-cleanup           Do not restore the original gcc alternatives, or the math-vector.h file.\n  --skip-cuda              Skip CUDA installation.\n  --skip-libva             Skip libva installation. This will automatically be enabled if passing --appimage-build.\n  --skip-package           Skip creating DEB, or RPM package.\n  --ubuntu-test-repo       Install ppa:ubuntu-toolchain-r/test repo on Ubuntu.\nEOF\n\n  exit \"$exit_code\"\n}\n\n# Parse named arguments\nwhile getopts \":hs-:\" opt; do\n  case ${opt} in\n    h ) _usage 0 ;;\n    s ) sudo_cmd=\"\" ;;\n    - )\n      case \"${OPTARG}\" in\n        help) _usage 0 ;;\n        appimage-build)\n          appimage_build=1\n          skip_libva=1\n          ;;\n        publisher-name=*)\n          publisher_name=\"${OPTARG#*=}\"\n          ;;\n        publisher-website=*)\n          publisher_website=\"${OPTARG#*=}\"\n          ;;\n        publisher-issue-url=*)\n          publisher_issue_url=\"${OPTARG#*=}\"\n          ;;\n        skip-cleanup) skip_cleanup=1 ;;\n        skip-cuda) skip_cuda=1 ;;\n        skip-libva) skip_libva=1 ;;\n        skip-package) skip_package=1 ;;\n        sudo-off) sudo_cmd=\"\" ;;\n        ubuntu-test-repo) ubuntu_test_repo=1 ;;\n        *)\n          echo \"Invalid option: --${OPTARG}\" 1>&2\n          _usage 1\n          ;;\n      esac\n      ;;\n    \\? )\n      echo \"Invalid option: -${OPTARG}\" 1>&2\n      _usage 1\n      ;;\n  esac\ndone\nshift $((OPTIND -1))\n\n# dependencies array to build out\ndependencies=()\n\nfunction add_debain_based_deps() {\n  dependencies+=(\n    \"bison\"  # required if we need to compile doxygen\n    \"build-essential\"\n    \"cmake\"\n    \"doxygen\"\n    \"flex\"  # required if we need to compile doxygen\n    \"gcc-${gcc_version}\"\n    \"g++-${gcc_version}\"\n    \"git\"\n    \"graphviz\"\n    \"libcap-dev\"  # KMS\n    \"libcurl4-openssl-dev\"\n    \"libdrm-dev\"  # KMS\n    \"libevdev-dev\"\n    \"libminiupnpc-dev\"\n    \"libnotify-dev\"\n    \"libnuma-dev\"\n    \"libopus-dev\"\n    \"libpulse-dev\"\n    \"libssl-dev\"\n    \"libwayland-dev\"  # Wayland\n    \"libx11-dev\"  # X11\n    \"libxcb-shm0-dev\"  # X11\n    \"libxcb-xfixes0-dev\"  # X11\n    \"libxcb1-dev\"  # X11\n    \"libxfixes-dev\"  # X11\n    \"libxrandr-dev\"  # X11\n    \"libxtst-dev\"  # X11\n    \"ninja-build\"\n    \"npm\"  # web-ui\n    \"udev\"\n    \"wget\"  # necessary for cuda install with `run` file\n    \"xvfb\"  # necessary for headless unit testing\n  )\n\n  if [ \"$skip_libva\" == 0 ]; then\n    dependencies+=(\n      \"libva-dev\"  # VA-API\n    )\n  fi\n}\n\nfunction add_debain_deps() {\n  add_debain_based_deps\n  dependencies+=(\n    \"libayatana-appindicator3-dev\"\n  )\n}\n\nfunction add_ubuntu_deps() {\n  if [ \"$ubuntu_test_repo\" == 1 ]; then\n    # allow newer gcc\n    ${sudo_cmd} add-apt-repository ppa:ubuntu-toolchain-r/test -y\n  fi\n\n  add_debain_based_deps\n  dependencies+=(\n    \"libappindicator3-dev\"\n  )\n}\n\nfunction add_fedora_deps() {\n  dependencies+=(\n    \"cmake\"\n    \"doxygen\"\n    \"gcc\"\n    \"g++\"\n    \"git\"\n    \"graphviz\"\n    \"libappindicator-gtk3-devel\"\n    \"libcap-devel\"\n    \"libcurl-devel\"\n    \"libdrm-devel\"\n    \"libevdev-devel\"\n    \"libnotify-devel\"\n    \"libX11-devel\"  # X11\n    \"libxcb-devel\"  # X11\n    \"libXcursor-devel\"  # X11\n    \"libXfixes-devel\"  # X11\n    \"libXi-devel\"  # X11\n    \"libXinerama-devel\"  # X11\n    \"libXrandr-devel\"  # X11\n    \"libXtst-devel\"  # X11\n    \"mesa-libGL-devel\"\n    \"miniupnpc-devel\"\n    \"ninja-build\"\n    \"npm\"\n    \"numactl-devel\"\n    \"openssl-devel\"\n    \"opus-devel\"\n    \"pulseaudio-libs-devel\"\n    \"rpm-build\"  # if you want to build an RPM binary package\n    \"wget\"  # necessary for cuda install with `run` file\n    \"which\"  # necessary for cuda install with `run` file\n    \"xorg-x11-server-Xvfb\"  # necessary for headless unit testing\n  )\n\n  if [ \"$skip_libva\" == 0 ]; then\n    dependencies+=(\n      \"libva-devel\"  # VA-API\n    )\n  fi\n}\n\nfunction install_cuda() {\n  # check if we need to install cuda\n  if [ -f \"${build_dir}/cuda/bin/nvcc\" ]; then\n    echo \"cuda already installed\"\n    return\n  fi\n\n  local cuda_prefix=\"https://developer.download.nvidia.com/compute/cuda/\"\n  local cuda_suffix=\"\"\n  if [ \"$architecture\" == \"aarch64\" ]; then\n    local cuda_suffix=\"_sbsa\"\n  fi\n\n  if [ \"$architecture\" == \"aarch64\" ]; then\n    # we need to patch the math-vector.h file for aarch64 fedora\n    # back up /usr/include/bits/math-vector.h\n    math_vector_file=\"\"\n    if [ \"$distro\" == \"ubuntu\" ] || [ \"$version\" == \"24.04\" ]; then\n      math_vector_file=\"/usr/include/aarch64-linux-gnu/bits/math-vector.h\"\n    elif [ \"$distro\" == \"fedora\" ]; then\n      math_vector_file=\"/usr/include/bits/math-vector.h\"\n    fi\n\n    if [ -n \"$math_vector_file\" ]; then\n      # patch headers https://bugs.launchpad.net/ubuntu/+source/mumax3/+bug/2032624\n      ${sudo_cmd} cp \"$math_vector_file\" \"$math_vector_file.bak\"\n      ${sudo_cmd} sed -i 's/__Float32x4_t/int/g' \"$math_vector_file\"\n      ${sudo_cmd} sed -i 's/__Float64x2_t/int/g' \"$math_vector_file\"\n      ${sudo_cmd} sed -i 's/__SVFloat32_t/float/g' \"$math_vector_file\"\n      ${sudo_cmd} sed -i 's/__SVFloat64_t/float/g' \"$math_vector_file\"\n      ${sudo_cmd} sed -i 's/__SVBool_t/int/g' \"$math_vector_file\"\n    fi\n  fi\n\n  local url=\"${cuda_prefix}${cuda_version}/local_installers/cuda_${cuda_version}_${cuda_build}_linux${cuda_suffix}.run\"\n  echo \"cuda url: ${url}\"\n  wget \"$url\" --progress=bar:force:noscroll -q --show-progress -O \"${build_dir}/cuda.run\"\n  chmod a+x \"${build_dir}/cuda.run\"\n  \"${build_dir}/cuda.run\" --silent --toolkit --toolkitpath=\"${build_dir}/cuda\" --no-opengl-libs --no-man-page --no-drm\n  rm \"${build_dir}/cuda.run\"\n}\n\nfunction check_version() {\n  local package_name=$1\n  local min_version=$2\n  local installed_version\n\n  echo \"Checking if $package_name is installed and at least version $min_version\"\n\n  if [ \"$distro\" == \"debian\" ] || [ \"$distro\" == \"ubuntu\" ]; then\n    installed_version=$(dpkg -s \"$package_name\" 2>/dev/null | grep '^Version:' | awk '{print $2}')\n  elif [ \"$distro\" == \"fedora\" ]; then\n    installed_version=$(rpm -q --queryformat '%{VERSION}' \"$package_name\" 2>/dev/null)\n  else\n    echo \"Unsupported Distro\"\n    return 1\n  fi\n\n  if [ -z \"$installed_version\" ]; then\n    echo \"Package not installed\"\n    return 1\n  fi\n\n  if [ \"$(printf '%s\\n' \"$installed_version\" \"$min_version\" | sort -V | head -n1)\" = \"$min_version\" ]; then\n    echo \"$package_name version $installed_version is at least $min_version\"\n    return 0\n  else\n    echo \"$package_name version $installed_version is less than $min_version\"\n    return 1\n  fi\n}\n\nfunction run_install() {\n  # prepare CMAKE args\n  cmake_args=(\n    \"-B=build\"\n    \"-G=Ninja\"\n    \"-S=.\"\n    \"-DBUILD_WERROR=ON\"\n    \"-DCMAKE_BUILD_TYPE=Release\"\n    \"-DCMAKE_INSTALL_PREFIX=/usr\"\n    \"-DSUNSHINE_ASSETS_DIR=share/sunshine\"\n    \"-DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine\"\n    \"-DSUNSHINE_ENABLE_WAYLAND=ON\"\n    \"-DSUNSHINE_ENABLE_X11=ON\"\n    \"-DSUNSHINE_ENABLE_DRM=ON\"\n  )\n\n  if [ \"$appimage_build\" == 1 ]; then\n    cmake_args+=(\"-DSUNSHINE_BUILD_APPIMAGE=ON\")\n  fi\n\n  # Publisher metadata\n  if [ -n \"$publisher_name\" ]; then\n    cmake_args+=(\"-DSUNSHINE_PUBLISHER_NAME='${publisher_name}'\")\n  fi\n  if [ -n \"$publisher_website\" ]; then\n    cmake_args+=(\"-DSUNSHINE_PUBLISHER_WEBSITE='${publisher_website}'\")\n  fi\n  if [ -n \"$publisher_issue_url\" ]; then\n    cmake_args+=(\"-DSUNSHINE_PUBLISHER_ISSUE_URL='${publisher_issue_url}'\")\n  fi\n\n  # Update the package list\n  $package_update_command\n\n  if [ \"$distro\" == \"debian\" ]; then\n    add_debain_deps\n  elif [ \"$distro\" == \"ubuntu\" ]; then\n    add_ubuntu_deps\n  elif [ \"$distro\" == \"fedora\" ]; then\n    add_fedora_deps\n    dnf group install \"Development Tools\" -y\n  fi\n\n  # Install the dependencies\n  $package_install_command \"${dependencies[@]}\"\n\n  # reload the environment\n  # shellcheck source=/dev/null\n  source ~/.bashrc\n\n  gcc_alternative_files=(\n    \"gcc\"\n    \"g++\"\n    \"gcov\"\n    \"gcc-ar\"\n    \"gcc-ranlib\"\n  )\n\n  # update alternatives for gcc and g++ if a debian based distro\n  if [ \"$distro\" == \"debian\" ] || [ \"$distro\" == \"ubuntu\" ]; then\n    for file in \"${gcc_alternative_files[@]}\"; do\n      file_path=\"/etc/alternatives/$file\"\n      if [ -e \"$file_path\" ]; then\n        mv \"$file_path\" \"$file_path.bak\"\n      fi\n    done\n\n    ${sudo_cmd} update-alternatives --install \\\n      /usr/bin/gcc gcc /usr/bin/gcc-${gcc_version} 100 \\\n      --slave /usr/bin/g++ g++ /usr/bin/g++-${gcc_version} \\\n      --slave /usr/bin/gcov gcov /usr/bin/gcov-${gcc_version} \\\n      --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-${gcc_version} \\\n      --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-${gcc_version}\n  fi\n\n  # compile cmake if the version is too low\n  cmake_min=\"3.25.0\"\n  target_cmake_version=\"3.30.1\"\n  if ! check_version \"cmake\" \"$cmake_min\"; then\n    cmake_prefix=\"https://github.com/Kitware/CMake/releases/download/v\"\n    if [ \"$architecture\" == \"x86_64\" ]; then\n      cmake_arch=\"x86_64\"\n    elif [ \"$architecture\" == \"aarch64\" ]; then\n      cmake_arch=\"aarch64\"\n    fi\n    url=\"${cmake_prefix}${target_cmake_version}/cmake-${target_cmake_version}-linux-${cmake_arch}.sh\"\n    echo \"cmake url: ${url}\"\n    wget \"$url\" --progress=bar:force:noscroll -q --show-progress -O \"${build_dir}/cmake.sh\"\n    ${sudo_cmd} sh \"${build_dir}/cmake.sh\" --skip-license --prefix=/usr/local\n    echo \"cmake installed, version:\"\n    cmake --version\n  fi\n\n  # compile doxygen if version is too low\n  doxygen_min=\"1.10.0\"\n  _doxygen_min=\"1_10_0\"\n  if ! check_version \"doxygen\" \"$doxygen_min\"; then\n    if [ \"${SUNSHINE_COMPILE_DOXYGEN}\" == \"true\" ]; then\n      echo \"Compiling doxygen\"\n      doxygen_url=\"https://github.com/doxygen/doxygen/releases/download/Release_${_doxygen_min}/doxygen-${doxygen_min}.src.tar.gz\"\n      echo \"doxygen url: ${doxygen_url}\"\n      wget \"$doxygen_url\" --progress=bar:force:noscroll -q --show-progress -O \"${build_dir}/doxygen.tar.gz\"\n      tar -xzf \"${build_dir}/doxygen.tar.gz\"\n      cd \"doxygen-${doxygen_min}\"\n      cmake -DCMAKE_BUILD_TYPE=Release -G=\"Ninja\" -B=\"build\" -S=\".\"\n      ninja -C \"build\"\n      ninja -C \"build\" install\n    else\n      echo \"Doxygen version too low, skipping docs\"\n      cmake_args+=(\"-DBUILD_DOCS=OFF\")\n    fi\n  fi\n\n  # install node from nvm\n  if [ \"$nvm_node\" == 1 ]; then\n    nvm_url=\"https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh\"\n    echo \"nvm url: ${nvm_url}\"\n    wget -qO- ${nvm_url} | bash\n    source \"$HOME/.nvm/nvm.sh\"\n    nvm install node\n    nvm use node\n  fi\n\n  # run the cuda install\n  if [ -n \"$cuda_version\" ] && [ \"$skip_cuda\" == 0 ]; then\n    install_cuda\n    cmake_args+=(\"-DSUNSHINE_ENABLE_CUDA=ON\")\n    cmake_args+=(\"-DCMAKE_CUDA_COMPILER:PATH=${build_dir}/cuda/bin/nvcc\")\n  fi\n\n  # Cmake stuff here\n  mkdir -p \"build\"\n  echo \"cmake args:\"\n  echo \"${cmake_args[@]}\"\n  cmake \"${cmake_args[@]}\"\n  ninja -C \"build\"\n\n  # Create the package\n  if [ \"$skip_package\" == 0 ]; then\n    if [ \"$distro\" == \"debian\" ] || [ \"$distro\" == \"ubuntu\" ]; then\n      cpack -G DEB --config ./build/CPackConfig.cmake\n    elif [ \"$distro\" == \"fedora\" ]; then\n      cpack -G RPM --config ./build/CPackConfig.cmake\n    fi\n  fi\n\n  if [ \"$skip_cleanup\" == 0 ]; then\n    # Restore the original gcc alternatives\n    if [ \"$distro\" == \"debian\" ] || [ \"$distro\" == \"ubuntu\" ]; then\n      for file in \"${gcc_alternative_files[@]}\"; do\n        if [ -e \"/etc/alternatives/$file.bak\" ]; then\n          ${sudo_cmd} mv \"/etc/alternatives/$file.bak\" \"/etc/alternatives/$file\"\n        else\n          ${sudo_cmd} rm \"/etc/alternatives/$file\"\n        fi\n      done\n    fi\n\n    # restore the math-vector.h file\n    if [ \"$architecture\" == \"aarch64\" ] && [ -n \"$math_vector_file\" ]; then\n      ${sudo_cmd} mv -f \"$math_vector_file.bak\" \"$math_vector_file\"\n    fi\n  fi\n}\n\n# Determine the OS and call the appropriate function\ncat /etc/os-release\nif grep -q \"Debian GNU/Linux 12 (bookworm)\" /etc/os-release; then\n  distro=\"debian\"\n  version=\"12\"\n  package_update_command=\"${sudo_cmd} apt-get update\"\n  package_install_command=\"${sudo_cmd} apt-get install -y\"\n  cuda_version=\"12.0.0\"\n  cuda_build=\"525.60.13\"\n  gcc_version=\"12\"\n  nvm_node=0\nelif grep -q \"PLATFORM_ID=\\\"platform:f39\\\"\" /etc/os-release; then\n  distro=\"fedora\"\n  version=\"39\"\n  package_update_command=\"${sudo_cmd} dnf update -y\"\n  package_install_command=\"${sudo_cmd} dnf install -y\"\n  cuda_version=\"12.4.0\"\n  cuda_build=\"550.54.14\"\n  gcc_version=\"13\"\n  nvm_node=0\nelif grep -q \"PLATFORM_ID=\\\"platform:f40\\\"\" /etc/os-release; then\n  distro=\"fedora\"\n  version=\"40\"\n  package_update_command=\"${sudo_cmd} dnf update -y\"\n  package_install_command=\"${sudo_cmd} dnf install -y\"\n  cuda_version=\n  cuda_build=\n  gcc_version=\"13\"\n  nvm_node=0\nelif grep -q \"Ubuntu 22.04\" /etc/os-release; then\n  distro=\"ubuntu\"\n  version=\"22.04\"\n  package_update_command=\"${sudo_cmd} apt-get update\"\n  package_install_command=\"${sudo_cmd} apt-get install -y\"\n  cuda_version=\"11.8.0\"\n  cuda_build=\"520.61.05\"\n  gcc_version=\"11\"\n  nvm_node=1\nelif grep -q \"Ubuntu 24.04\" /etc/os-release; then\n  distro=\"ubuntu\"\n  version=\"24.04\"\n  package_update_command=\"${sudo_cmd} apt-get update\"\n  package_install_command=\"${sudo_cmd} apt-get install -y\"\n  cuda_version=\"11.8.0\"\n  cuda_build=\"520.61.05\"\n  gcc_version=\"11\"\n  nvm_node=0\nelse\n  echo \"Unsupported Distro or Version\"\n  exit 1\nfi\n\narchitecture=$(uname -m)\n\necho \"Detected Distro: $distro\"\necho \"Detected Version: $version\"\necho \"Detected Architecture: $architecture\"\n\nif [ \"$architecture\" != \"x86_64\" ] && [ \"$architecture\" != \"aarch64\" ]; then\n  echo \"Unsupported Architecture\"\n  exit 1\nfi\n\n# get directory of this script\nscript_dir=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null 2>&1 && pwd )\"\nbuild_dir=\"$script_dir/../build\"\necho \"Script Directory: $script_dir\"\necho \"Build Directory: $build_dir\"\nmkdir -p \"$build_dir\"\n\nrun_install\n"
  },
  {
    "path": "scripts/requirements.txt",
    "content": "Babel==2.16.0\nclang-format\n"
  },
  {
    "path": "scripts/reverse-sync-i18n.js",
    "content": "#!/usr/bin/env node\n/**\n * Reverse i18n Sync Script\n * \n * This script identifies keys that exist in other locale files but are missing\n * from en.json (base file), and adds them to en.json.\n * \n * Usage:\n *   node scripts/reverse-sync-i18n.js              # Dry run - show what would be added\n *   node scripts/reverse-sync-i18n.js --sync       # Actually add missing keys to en.json\n */\n\nimport fs from 'fs'\nimport path from 'path'\nimport { fileURLToPath } from 'url'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\nconst localeDir = path.join(__dirname, '../src_assets/common/assets/web/public/assets/locale')\nconst baseLocale = 'en.json'\n\n// Parse command line arguments\nconst args = process.argv.slice(2)\nconst syncMode = args.includes('--sync')\n\n/**\n * Get all keys from a nested object\n */\nfunction getAllKeys(obj, prefix = '') {\n  const keys = []\n  for (const key in obj) {\n    const fullKey = prefix ? `${prefix}.${key}` : key\n    if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {\n      keys.push(...getAllKeys(obj[key], fullKey))\n    } else {\n      keys.push(fullKey)\n    }\n  }\n  return keys\n}\n\n/**\n * Get value from nested object using dot notation\n */\nfunction getValue(obj, path) {\n  return path.split('.').reduce((current, key) => current?.[key], obj)\n}\n\n/**\n * Set value in nested object using dot notation\n */\nfunction setValue(obj, path, value) {\n  const keys = path.split('.')\n  const lastKey = keys.pop()\n  const target = keys.reduce((current, key) => {\n    if (!current[key] || typeof current[key] !== 'object') {\n      current[key] = {}\n    }\n    return current[key]\n  }, obj)\n  target[lastKey] = value\n}\n\n/**\n * Main reverse sync function\n */\nfunction reverseSyncLocales() {\n  console.log('🔍 Checking for keys in other locales that are missing from en.json...\\n')\n  \n  // Read base locale\n  const baseLocalePath = path.join(localeDir, baseLocale)\n  if (!fs.existsSync(baseLocalePath)) {\n    console.error(`❌ Base locale file not found: ${baseLocale}`)\n    process.exit(1)\n  }\n  \n  const baseContent = JSON.parse(fs.readFileSync(baseLocalePath, 'utf8'))\n  const baseKeys = new Set(getAllKeys(baseContent))\n  \n  console.log(`📋 Base locale (${baseLocale}) has ${baseKeys.size} keys\\n`)\n  \n  // Get all locale files\n  const localeFiles = fs.readdirSync(localeDir)\n    .filter(file => file.endsWith('.json') && file !== baseLocale)\n    .sort()\n  \n  // Collect all keys from all locales\n  const allKeysMap = new Map() // key -> { files: Set, sampleValue: string }\n  \n  for (const localeFile of localeFiles) {\n    const localePath = path.join(localeDir, localeFile)\n    const content = JSON.parse(fs.readFileSync(localePath, 'utf8'))\n    const localeKeys = getAllKeys(content)\n    \n    for (const key of localeKeys) {\n      if (!baseKeys.has(key)) {\n        if (!allKeysMap.has(key)) {\n          allKeysMap.set(key, {\n            files: new Set(),\n            sampleValue: getValue(content, key)\n          })\n        }\n        allKeysMap.get(key).files.add(localeFile)\n      }\n    }\n  }\n  \n  if (allKeysMap.size === 0) {\n    console.log('✅ No missing keys found - en.json has all keys from other locales!\\n')\n    return\n  }\n  \n  console.log(`⚠️  Found ${allKeysMap.size} keys in other locales but missing from ${baseLocale}:\\n`)\n  \n  const sortedMissingKeys = Array.from(allKeysMap.entries()).sort((a, b) => a[0].localeCompare(b[0]))\n  \n  for (const [key, info] of sortedMissingKeys) {\n    console.log(`  📍 ${key}`)\n    console.log(`     Found in: ${Array.from(info.files).join(', ')}`)\n    console.log(`     Sample: \"${info.sampleValue}\"`)\n    console.log()\n  }\n  \n  if (syncMode) {\n    console.log('🔄 Adding missing keys to en.json...\\n')\n    \n    let addedCount = 0\n    for (const [key, info] of sortedMissingKeys) {\n      // Use the sample value as placeholder (it's likely English or close to it)\n      setValue(baseContent, key, info.sampleValue)\n      addedCount++\n      console.log(`   ✓ Added: ${key}`)\n    }\n    \n    // Write updated base locale\n    fs.writeFileSync(baseLocalePath, JSON.stringify(baseContent, null, 2) + '\\n', 'utf8')\n    \n    console.log(`\\n✅ Successfully added ${addedCount} keys to ${baseLocale}`)\n    console.log('\\n💡 Next steps:')\n    console.log('   1. Review the added keys in en.json')\n    console.log('   2. Update the values to proper English translations if needed')\n    console.log('   3. Run: npm run i18n:sync to sync these keys to all locales')\n    console.log('   4. Run: npm run i18n:format to ensure consistent formatting')\n  } else {\n    console.log('💡 This is a dry run. Use --sync flag to actually add these keys to en.json')\n    console.log('   Command: node scripts/reverse-sync-i18n.js --sync')\n  }\n}\n\n// Run reverse sync\nreverseSyncLocales()\n"
  },
  {
    "path": "scripts/update_clang_format.py",
    "content": "# standard imports\nimport os\nimport subprocess\n\n# variables\ndirectories = [\n    'src',\n    'tests',\n    'tools',\n    os.path.join('third-party', 'glad'),\n    os.path.join('third-party', 'nvfbc'),\n]\nfile_types = [\n    'cpp',\n    'h',\n    'm',\n    'mm'\n]\n\n\ndef clang_format(file: str):\n    print(f'Formatting {file} ...')\n    subprocess.run(['clang-format', '-i', file])\n\n\ndef main():\n    \"\"\"\n    Main entry point.\n    \"\"\"\n    # walk the directories\n    for directory in directories:\n        for root, dirs, files in os.walk(directory):\n            for file in files:\n                file_path = os.path.join(root, file)\n                if os.path.isfile(file_path) and file.rsplit('.')[-1] in file_types:\n                    clang_format(file=file_path)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "scripts/validate-i18n.js",
    "content": "#!/usr/bin/env node\n/**\n * i18n Translation Validation and Sync Script\n * \n * This script validates that all locale files have the same keys as the base locale (en.json).\n * It can also automatically add missing keys with placeholder values.\n * \n * Usage:\n *   node scripts/validate-i18n.js              # Validate only (report missing keys, exit with error code on failure)\n *   node scripts/validate-i18n.js --sync       # Auto-sync missing keys with English values\n */\n\nimport fs from 'fs'\nimport path from 'path'\nimport { fileURLToPath } from 'url'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\nconst localeDir = path.join(__dirname, '../src_assets/common/assets/web/public/assets/locale')\nconst baseLocale = 'en.json'\n\n// Parse command line arguments\nconst args = process.argv.slice(2)\nconst syncMode = args.includes('--sync')\n\n/**\n * Get all keys from a nested object\n */\nfunction getAllKeys(obj, prefix = '') {\n  const keys = []\n  for (const key in obj) {\n    const fullKey = prefix ? `${prefix}.${key}` : key\n    if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {\n      keys.push(...getAllKeys(obj[key], fullKey))\n    } else {\n      keys.push(fullKey)\n    }\n  }\n  return keys\n}\n\n/**\n * Get value from nested object using dot notation\n */\nfunction getValue(obj, path) {\n  return path.split('.').reduce((current, key) => current?.[key], obj)\n}\n\n/**\n * Set value in nested object using dot notation\n */\nfunction setValue(obj, path, value) {\n  const keys = path.split('.')\n  const lastKey = keys.pop()\n  const target = keys.reduce((current, key) => {\n    if (!current[key] || typeof current[key] !== 'object') {\n      current[key] = {}\n    }\n    return current[key]\n  }, obj)\n  target[lastKey] = value\n}\n\n/**\n * Remove a key from nested object using dot notation\n */\nfunction removeKey(obj, keyPath) {\n  const keys = keyPath.split('.')\n  const lastKey = keys.pop()\n  const target = keys.reduce((current, key) => {\n    if (!current || !current[key] || typeof current[key] !== 'object') {\n      return null\n    }\n    return current[key]\n  }, obj)\n  \n  if (target && target.hasOwnProperty(lastKey)) {\n    delete target[lastKey]\n    // Clean up empty objects\n    if (Object.keys(target).length === 0 && keys.length > 0) {\n      const parent = keys.reduce((current, key) => {\n        if (!current || !current[key] || typeof current[key] !== 'object') {\n          return null\n        }\n        return current[key]\n      }, obj)\n      if (parent && parent[keys[keys.length - 1]]) {\n        delete parent[keys[keys.length - 1]]\n      }\n    }\n    return true\n  }\n  return false\n}\n\n/**\n * Sort object keys recursively\n */\nfunction sortObjectKeys(obj) {\n  if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {\n    return obj\n  }\n  \n  const sorted = {}\n  const keys = Object.keys(obj).sort()\n  \n  for (const key of keys) {\n    sorted[key] = sortObjectKeys(obj[key])\n  }\n  \n  return sorted\n}\n\n/**\n * Get list of keys that should remain in English (technical terms, protocols, etc.)\n * These keys will be automatically overwritten with English values in sync mode\n */\nfunction getEnglishOnlyKeys() {\n  return [\n    \"address_family_both\", // IPv4+IPv6\n    \"port_tcp\", // TCP\n    \"port_udp\", // UDP\n    \"scan_result_filter_url\", // URL\n    \"webhook_url\", // Webhook URL (URL is technical term)\n    \"audio_sink_placeholder_macos\", // BlackHole 2ch (product name)\n    \"virtual_sink_placeholder\", // Steam Streaming Speakers (product name)\n    \"gamepad_ds4\", // DS4 (PS4) - product name\n    \"gamepad_ds5\", // DS5 (PS5) - product name\n    \"gamepad_switch\", // Nintendo Pro (Switch) - product name\n    \"gamepad_x360\", // X360 (Xbox 360) - product name\n    \"gamepad_xone\", // XOne (Xbox One) - product name\n    \"port_web_ui\", // Web UI\n    \"boom_sunshine\", // Boom!\n    \"boom_sunshine_title\", // Boom!\n    \"boom_sunshine_button\", // Boom!\n    \"boom_sunshine_button_desc\", // Boom!\n    \"boom_sunshine_button_title\", // Boom!\n    \"boom_sunshine_button_desc\", // Boom!\n    \"upnp\", // UPnP\n    \"scan_result_type_url\", // URL\n    \"scan_result_filter_url_title\", // URL\n    \"adapter_name_placeholder_windows\", // Radeon RX 580 Series\n  ]\n}\n\n/**\n * Check if a value should be excluded from translation check\n * (e.g., technical terms, protocol names, product names that are commonly kept in English)\n */\nfunction shouldSkipTranslationCheck(key, value) {\n  if (!value || typeof value !== 'string') {\n    return false\n  }\n  \n  const englishOnlyKeys = getEnglishOnlyKeys()\n  \n  // Check if key is in skip list\n  if (englishOnlyKeys.includes(key.split('.').pop())) {\n    return true\n  }\n  \n  // Check if value is a pure technical term (all uppercase, contains numbers/special chars)\n  // Examples: \"IPv4+IPv6\", \"TCP\", \"UDP\", \"URL\", \"DS4 (PS4)\"\n  const isTechnicalTerm = /^[A-Z0-9+\\-()\\s]+$/.test(value.trim()) && \n                          value.length < 50 && \n                          /[A-Z]/.test(value)\n  \n  // Check if value contains only product names or technical abbreviations\n  const isProductName = /^(DS4|DS5|X360|Nintendo|Steam|BlackHole|TCP|UDP|URL|IPv4|IPv6|Webhook URL)/i.test(value.trim())\n  \n  // Check if value ends with common technical terms that can stay in English\n  const hasTechnicalSuffix = /\\b(URL|TCP|UDP|IPv4|IPv6|UI|API|HTTP|HTTPS|DS4|DS5|X360)\\b/i.test(value)\n  \n  return isTechnicalTerm || isProductName || hasTechnicalSuffix\n}\n\n/**\n * Check for untranslated keys (keys that have the same value as the base locale)\n */\nfunction findUntranslatedKeys(baseContent, localeContent, localeFile) {\n  // Skip English variants\n  if (localeFile === 'en_GB.json' || localeFile === 'en_US.json') {\n    return []\n  }\n  \n  const baseKeys = getAllKeys(baseContent)\n  const untranslated = []\n  \n  for (const key of baseKeys) {\n    const baseValue = getValue(baseContent, key)\n    const localeValue = getValue(localeContent, key)\n    \n    // Check if the value is the same as the base (untranslated)\n    if (localeValue !== null && localeValue === baseValue) {\n      // Skip if this key/value should not be checked for translation\n      if (!shouldSkipTranslationCheck(key, localeValue)) {\n        untranslated.push(key)\n      }\n    }\n  }\n  \n  return untranslated\n}\n\n/**\n * Main validation function\n */\nfunction validateLocales() {\n  console.log('🔍 Validating i18n translations...\\n')\n  \n  // Read base locale\n  const baseLocalePath = path.join(localeDir, baseLocale)\n  if (!fs.existsSync(baseLocalePath)) {\n    console.error(`❌ Base locale file not found: ${baseLocale}`)\n    process.exit(1)\n  }\n  \n  const baseContent = JSON.parse(fs.readFileSync(baseLocalePath, 'utf8'))\n  const baseKeys = getAllKeys(baseContent).sort()\n  \n  console.log(`📋 Base locale (${baseLocale}) has ${baseKeys.length} keys\\n`)\n  \n  // Get all locale files\n  const localeFiles = fs.readdirSync(localeDir)\n    .filter(file => file.endsWith('.json') && file !== baseLocale)\n    .sort()\n  \n  let hasErrors = false\n  const results = []\n  \n  for (const localeFile of localeFiles) {\n    const localePath = path.join(localeDir, localeFile)\n    let content\n    \n    try {\n      content = JSON.parse(fs.readFileSync(localePath, 'utf8'))\n    } catch (e) {\n      console.error(`❌ Failed to parse ${localeFile}: ${e.message}`)\n      hasErrors = true\n      continue\n    }\n    \n    const localeKeys = getAllKeys(content).sort()\n    const missingKeys = baseKeys.filter(key => !localeKeys.includes(key))\n    const extraKeys = localeKeys.filter(key => !baseKeys.includes(key))\n    const untranslatedKeys = findUntranslatedKeys(baseContent, content, localeFile)\n    \n    const hasIssues = missingKeys.length > 0 || extraKeys.length > 0 || untranslatedKeys.length > 0\n    \n    if (!hasIssues) {\n      console.log(`✅ ${localeFile}: All keys present and translated (${localeKeys.length} keys)`)\n      results.push({ file: localeFile, status: 'ok', missing: 0, extra: 0, untranslated: 0 })\n      \n      // Still overwrite English-only keys even if no other issues\n      if (syncMode) {\n        let modified = false\n        const englishOnlyKeys = getEnglishOnlyKeys()\n        let overwrittenCount = 0\n        for (const key of baseKeys) {\n          const keyName = key.split('.').pop()\n          if (englishOnlyKeys.includes(keyName)) {\n            const baseValue = getValue(baseContent, key)\n            const currentValue = getValue(content, key)\n            if (currentValue !== baseValue) {\n              setValue(content, key, baseValue)\n              overwrittenCount++\n              modified = true\n            }\n          }\n        }\n        if (overwrittenCount > 0) {\n          console.log(`   🔄 Overwritten ${overwrittenCount} English-only keys with English values`)\n        }\n        // Always sort and write in sync mode, even if no changes were made\n        const sorted = sortObjectKeys(content)\n        const formatted = JSON.stringify(sorted, null, 2) + '\\n'\n        const original = fs.readFileSync(localePath, 'utf8')\n        \n        // Always write in sync mode to ensure consistent formatting\n        // Compare to detect if actual changes were made\n        let originalParsed\n        try {\n          originalParsed = JSON.parse(original)\n        } catch (e) {\n          originalParsed = null\n        }\n        \n        const keysChanged = originalParsed ? JSON.stringify(originalParsed) !== JSON.stringify(sorted) : true\n        const formatChanged = original.trim() !== formatted.trim()\n        \n        // Always write to ensure consistent formatting\n        fs.writeFileSync(localePath, formatted, 'utf8')\n        if (!modified) {\n          if (keysChanged) {\n            console.log(`   🔄 Sorted keys alphabetically`)\n          } else if (formatChanged) {\n            console.log(`   🔄 Reformatted file`)\n          } else {\n            // Even if no changes, we still write to ensure consistency\n            console.log(`   ✓ File is properly sorted and formatted`)\n          }\n        }\n      }\n    } else {\n      hasErrors = true\n      console.log(`❌ ${localeFile}: Issues found`)\n      \n      if (missingKeys.length > 0) {\n        console.log(`   Missing ${missingKeys.length} keys:`)\n        missingKeys.slice(0, 5).forEach(key => console.log(`     - ${key}`))\n        if (missingKeys.length > 5) {\n          console.log(`     ... and ${missingKeys.length - 5} more`)\n        }\n      }\n      \n      if (extraKeys.length > 0) {\n        console.log(`   Extra ${extraKeys.length} keys (not in base):`)\n        extraKeys.slice(0, 5).forEach(key => console.log(`     - ${key}`))\n        if (extraKeys.length > 5) {\n          console.log(`     ... and ${extraKeys.length - 5} more`)\n        }\n      }\n      \n      if (untranslatedKeys.length > 0) {\n        console.log(`   ⚠️  ${untranslatedKeys.length} untranslated keys (same as English):`)\n        untranslatedKeys.slice(0, 10).forEach(key => {\n          const value = getValue(content, key)\n          const displayValue = value && value.length > 50 ? value.substring(0, 50) + '...' : value\n          console.log(`     - ${key}: \"${displayValue}\"`)\n        })\n        if (untranslatedKeys.length > 10) {\n          console.log(`     ... and ${untranslatedKeys.length - 10} more`)\n        }\n      }\n      \n      results.push({ \n        file: localeFile, \n        status: 'error', \n        missing: missingKeys.length, \n        extra: extraKeys.length,\n        untranslated: untranslatedKeys.length,\n        missingKeys,\n        untranslatedKeys,\n        content\n      })\n      \n      // Auto-sync if requested\n      if (syncMode) {\n        let modified = false\n        \n        // Add missing keys\n        if (missingKeys.length > 0) {\n          console.log(`   🔄 Syncing missing keys...`)\n          for (const key of missingKeys) {\n            const baseValue = getValue(baseContent, key)\n            setValue(content, key, baseValue)\n          }\n          console.log(`   ✓ Added ${missingKeys.length} missing keys with English values`)\n          modified = true\n        }\n        \n        // Remove extra keys\n        if (extraKeys.length > 0) {\n          console.log(`   🗑️  Removing extra keys...`)\n          for (const key of extraKeys) {\n            removeKey(content, key)\n          }\n          console.log(`   ✓ Removed ${extraKeys.length} extra keys`)\n          modified = true\n        }\n        \n        // Overwrite English-only keys with English values (force overwrite even if different)\n        const englishOnlyKeys = getEnglishOnlyKeys()\n        let overwrittenCount = 0\n        for (const key of baseKeys) {\n          const keyName = key.split('.').pop()\n          if (englishOnlyKeys.includes(keyName)) {\n            const baseValue = getValue(baseContent, key)\n            const currentValue = getValue(content, key)\n            // Force overwrite English-only keys with English values\n            if (currentValue !== baseValue) {\n              setValue(content, key, baseValue)\n              overwrittenCount++\n            }\n          }\n        }\n        if (overwrittenCount > 0) {\n          console.log(`   🔄 Overwritten ${overwrittenCount} English-only keys with English values`)\n          modified = true\n        }\n        \n        if (modified) {\n          // Sort keys before writing\n          const sorted = sortObjectKeys(content)\n          fs.writeFileSync(localePath, JSON.stringify(sorted, null, 2) + '\\n', 'utf8')\n        }\n      }\n    }\n    console.log()\n  }\n  \n  // Summary\n  console.log('━'.repeat(60))\n  console.log('📊 Summary:')\n  console.log(`   Total locales checked: ${localeFiles.length}`)\n  console.log(`   Locales with all keys: ${results.filter(r => r.status === 'ok').length}`)\n  console.log(`   Locales with issues: ${results.filter(r => r.status === 'error').length}`)\n  \n  const totalUntranslated = results.reduce((sum, r) => sum + (r.untranslated || 0), 0)\n  if (totalUntranslated > 0) {\n    console.log(`   ⚠️  Total untranslated keys: ${totalUntranslated}`)\n  }\n  \n  if (syncMode) {\n    const synced = results.filter(r => r.status === 'error' && r.missing > 0)\n    if (synced.length > 0) {\n      console.log(`\\n✅ Synced ${synced.length} locale files with missing keys`)\n      console.log('   ⚠️  Remember to translate the English placeholder values!')\n    }\n  }\n  \n  console.log('━'.repeat(60))\n  \n  if (hasErrors && !syncMode) {\n    console.log('\\n💡 Tip: Run with --sync flag to automatically add missing keys')\n    console.error('\\n❌ Validation failed')\n    process.exit(1)\n  }\n}\n\n// Run validation\nvalidateLocales()\n"
  },
  {
    "path": "scripts/vmouse_smoke.ps1",
    "content": "param(\n    [string]$ProbeExe = \"\",\n    [int]$TimeoutMs = 5000,\n    [string]$MatchSubstring = \"VID_1ACE&PID_0002\",\n    [switch]$ManualInput,\n    [switch]$Quiet\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\nfunction Resolve-ProbeExe {\n    param([string]$ExplicitPath)\n\n    if ($ExplicitPath) {\n        return (Resolve-Path $ExplicitPath).Path\n    }\n\n    $candidates = @(\n        (Join-Path $PSScriptRoot \"..\\build\\tests\\vmouse_probe.exe\"),\n        (Join-Path $PSScriptRoot \"..\\build-test\\tests\\vmouse_probe.exe\"),\n        (Join-Path $PSScriptRoot \"..\\out\\build\\tests\\vmouse_probe.exe\")\n    )\n\n    foreach ($candidate in $candidates) {\n        if (Test-Path $candidate) {\n            return (Resolve-Path $candidate).Path\n        }\n    }\n\n    throw \"找不到 vmouse_probe.exe，请使用 -ProbeExe 显式指定。\"\n}\n\nfunction Get-VMousePnpInfo {\n    $device = Get-PnpDevice -InstanceId 'ROOT\\HIDCLASS\\*' -ErrorAction SilentlyContinue |\n        Where-Object {\n            ($_.FriendlyName -like '*Virtual Mouse*' -or $_.HardwareID -contains 'Root\\ZakoVirtualMouse') -and\n            $null -ne $_.FriendlyName -and\n            $_.FriendlyName -ne ''\n        } |\n        Select-Object -First 1 Status, FriendlyName, Problem, InstanceId\n\n    if ($null -eq $device) {\n        return [pscustomobject]@{\n            Installed  = $false\n            Running    = $false\n            StatusText = \"未安装\"\n            Device     = $null\n        }\n    }\n\n    $running = $device.Status -eq \"OK\" -and $device.Problem -eq 0\n    $statusText = if ($running) {\n        \"$($device.FriendlyName) - 正常运行\"\n    }\n    elseif ($device.Problem -eq 21) {\n        \"$($device.FriendlyName) - 需要重启\"\n    }\n    else {\n        \"$($device.FriendlyName) - Status=$($device.Status), Problem=$($device.Problem)\"\n    }\n\n    return [pscustomobject]@{\n        Installed  = $true\n        Running    = $running\n        StatusText = $statusText\n        Device     = $device\n    }\n}\n\nfunction Invoke-Probe {\n    param([string]$ExePath, [string[]]$Arguments)\n\n    $lines = & $ExePath @Arguments 2>&1\n    $exitCode = $LASTEXITCODE\n    $values = @{}\n\n    foreach ($line in $lines) {\n        if ($line -match '^([A-Z_]+)=(.*)$') {\n            $values[$matches[1]] = $matches[2]\n        }\n    }\n\n    return [pscustomobject]@{\n        ExitCode = $exitCode\n        Lines    = $lines\n        Values   = $values\n    }\n}\n\nfunction Get-IntValue {\n    param(\n        [hashtable]$Table,\n        [string]$Key\n    )\n\n    if ($Table.ContainsKey($Key)) {\n        return [int]$Table[$Key]\n    }\n\n    return 0\n}\n\n$probePath = Resolve-ProbeExe -ExplicitPath $ProbeExe\n$pnpInfo = Get-VMousePnpInfo\n\nWrite-Host \"PnP 状态: $($pnpInfo.StatusText)\"\nif (-not $pnpInfo.Installed) {\n    throw \"未检测到 Root\\ZakoVirtualMouse。请先安装驱动。\"\n}\n\n$listResult = Invoke-Probe -ExePath $probePath -Arguments @(\n    \"--list-only\",\n    \"--match-substring\", $MatchSubstring\n)\n\nif (-not $Quiet) {\n    $listResult.Lines | ForEach-Object { Write-Host $_ }\n}\n\nif ((Get-IntValue -Table $listResult.Values -Key \"MATCHED_DEVICE_PRESENT\") -ne 1) {\n    throw \"Raw Input 设备枚举中未发现匹配的虚拟鼠标。\"\n}\n\n$probeArgs = @(\n    \"--timeout-ms\", \"$TimeoutMs\",\n    \"--match-substring\", $MatchSubstring,\n    \"--require-device\",\n    \"--require-events\"\n)\n\nif ($ManualInput) {\n    Write-Host \"请在接下来的 $TimeoutMs ms 内，通过 Sunshine/Moonlight 触发一次虚拟鼠标移动或点击。\"\n}\nelse {\n    $probeArgs += \"--send-test-sequence\"\n}\n\nif ($Quiet) {\n    $probeArgs += \"--quiet\"\n}\n\n$runResult = Invoke-Probe -ExePath $probePath -Arguments $probeArgs\nif (-not $Quiet) {\n    $runResult.Lines | ForEach-Object { Write-Host $_ }\n}\n\nif ($runResult.ExitCode -ne 0) {\n    throw \"探针运行失败，退出码 $($runResult.ExitCode)。\"\n}\n\n$matchedEvents = Get-IntValue -Table $runResult.Values -Key \"MATCHED_EVENT_COUNT\"\n$sendOk = Get-IntValue -Table $runResult.Values -Key \"SEND_SEQUENCE_OK\"\n\nif (-not $ManualInput -and $sendOk -ne 1) {\n    throw \"虚拟鼠标发送序列失败，无法连接驱动或发送报告。\"\n}\n\nif ($matchedEvents -le 0) {\n    throw \"未观察到来自虚拟鼠标驱动的 Raw Input 事件。\"\n}\n\nWrite-Host \"验证通过：驱动已安装、Raw Input 枚举可见、并成功收到虚拟鼠标事件。\"\n"
  },
  {
    "path": "src/abr.cpp",
    "content": "/**\n * @file src/abr.cpp\n * @brief Adaptive Bitrate (ABR) decision engine using LLM AI.\n *\n * Architecture: two-tier bitrate control —\n *   1. Real-time fallback: threshold-based reactions to network conditions\n *      (always active, rate-limited to every 3 seconds).\n *   2. Event-driven LLM: queries the configured LLM API for optimal target\n *      bitrate on app switches and network recovery events.\n *\n * The fallback layer handles immediate network degradation, while the LLM\n * provides intelligent per-game bitrate targets that guide probe-up behavior.\n */\n\n#include \"abr.h\"\n#include \"config.h\"\n#include \"confighttp.h\"\n#include \"logging.h\"\n\n#include <algorithm>\n#include <cmath>\n#include <filesystem>\n#include <fstream>\n#include <nlohmann/json.hpp>\n#include <sstream>\n#include <thread>\n\n#ifdef _WIN32\n  #include <Windows.h>\n  #include <Psapi.h>\n#endif\n\nusing json = nlohmann::json;\n\nnamespace abr {\n\n  static std::mutex sessions_mutex;\n  static std::unordered_map<std::string, session_state_t> sessions;\n\n  /**\n   * @brief Sanitize client-provided network feedback values.\n   */\n  static network_feedback_t\n  sanitize_feedback(const network_feedback_t &raw) {\n    return {\n      std::clamp(raw.packet_loss, 0.0, 100.0),\n      std::max(raw.rtt_ms, 0.0),\n      std::max(raw.decode_fps, 0.0),\n      std::max(raw.dropped_frames, 0),\n      std::max(raw.current_bitrate_kbps, 0),\n    };\n  }\n\n  /**\n   * @brief Detect the actual foreground window title and process name.\n   *\n   * When users launch games through Steam/Epic/etc., the app_name from config\n   * is just \"Steam Big Picture\". This function gets the real active window info.\n   *\n   * @return Pair of (window_title, exe_name), or empty strings on failure.\n   */\n  struct foreground_info_t {\n    std::string window_title;\n    std::string exe_name;\n    uint32_t pid = 0;\n  };\n\n  static foreground_info_t\n  detect_foreground_app() {\n#ifdef _WIN32\n    HWND hwnd = GetForegroundWindow();\n    if (!hwnd) return {};\n\n    // Get window title\n    wchar_t title_buf[256] = {};\n    int len = GetWindowTextW(hwnd, title_buf, sizeof(title_buf) / sizeof(title_buf[0]));\n    std::string window_title;\n    if (len > 0) {\n      // Convert wide string to UTF-8\n      int utf8_len = WideCharToMultiByte(CP_UTF8, 0, title_buf, len, nullptr, 0, nullptr, nullptr);\n      if (utf8_len > 0) {\n        window_title.resize(utf8_len);\n        WideCharToMultiByte(CP_UTF8, 0, title_buf, len, window_title.data(), utf8_len, nullptr, nullptr);\n      }\n    }\n\n    // Get process ID and executable name\n    std::string exe_name;\n    DWORD pid = 0;\n    GetWindowThreadProcessId(hwnd, &pid);\n    if (pid > 0) {\n      HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);\n      if (hProcess) {\n        wchar_t exe_buf[MAX_PATH] = {};\n        DWORD buf_size = MAX_PATH;\n        if (QueryFullProcessImageNameW(hProcess, 0, exe_buf, &buf_size)) {\n          // Extract just the filename\n          std::wstring full_path(exe_buf, buf_size);\n          auto last_sep = full_path.find_last_of(L\"\\\\/\");\n          std::wstring name = (last_sep != std::wstring::npos) ? full_path.substr(last_sep + 1) : full_path;\n\n          int utf8_len = WideCharToMultiByte(CP_UTF8, 0, name.c_str(), static_cast<int>(name.size()), nullptr, 0, nullptr, nullptr);\n          if (utf8_len > 0) {\n            exe_name.resize(utf8_len);\n            WideCharToMultiByte(CP_UTF8, 0, name.c_str(), static_cast<int>(name.size()), exe_name.data(), utf8_len, nullptr, nullptr);\n          }\n        }\n        CloseHandle(hProcess);\n      }\n    }\n\n    return { window_title, exe_name, pid };\n#else\n    return {};\n#endif\n  }\n\n  /// Convert mode enum to string\n  static std::string\n  mode_to_string(mode_e mode) {\n    switch (mode) {\n      case mode_e::QUALITY:\n        return \"quality\";\n      case mode_e::LOW_LATENCY:\n        return \"lowLatency\";\n      case mode_e::BALANCED:\n      default:\n        return \"balanced\";\n    }\n  }\n\n  /**\n   * @brief Load prompt template from external file.\n   * Search order: config dir (user override) → assets dir (bundled default).\n   * Cached after first successful load. Returns empty string if not found.\n   */\n  static const std::string &\n  load_prompt_template() {\n    static std::string cached;\n    static bool loaded = false;\n    if (loaded) return cached;\n\n    // Search paths: config dir first (user override), then assets dir (bundled default)\n    std::vector<std::filesystem::path> search_paths;\n    try {\n      search_paths.push_back(std::filesystem::path(config::sunshine.config_file).parent_path() / \"abr_prompt.md\");\n    }\n    catch (...) {}\n    search_paths.push_back(std::filesystem::path(SUNSHINE_ASSETS_DIR) / \"abr_prompt.md\");\n\n    for (const auto &path : search_paths) {\n      try {\n        std::ifstream file(path);\n        if (file.is_open()) {\n          cached.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());\n          BOOST_LOG(info) << \"ABR: loaded prompt template from \" << path;\n          loaded = true;\n          return cached;\n        }\n      }\n      catch (...) {}\n    }\n\n    BOOST_LOG(warning) << \"ABR: prompt template not found, LLM decisions will be unavailable\";\n    loaded = true;\n    return cached;\n  }\n\n  /**\n   * @brief Replace all occurrences of {{key}} with value in a string.\n   */\n  static std::string\n  replace_placeholders(std::string tmpl, const std::vector<std::pair<std::string, std::string>> &vars) {\n    for (const auto &[key, value] : vars) {\n      std::string placeholder = \"{{\" + key + \"}}\";\n      size_t pos = 0;\n      while ((pos = tmpl.find(placeholder, pos)) != std::string::npos) {\n        tmpl.replace(pos, placeholder.size(), value);\n        pos += value.size();\n      }\n    }\n    return tmpl;\n  }\n\n  /**\n   * @brief Build the LLM prompt by filling the template with session state.\n   */\n  static std::string\n  build_llm_prompt(const session_state_t &state) {\n    // Format recent feedback\n    std::ostringstream feedback_ss;\n    for (auto it = state.recent_feedback.rbegin(); it != state.recent_feedback.rend(); ++it) {\n      auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(\n                       std::chrono::steady_clock::now() - it->timestamp)\n                       .count();\n      feedback_ss << \"- [\" << elapsed << \"s ago] \"\n                  << \"loss=\" << it->feedback.packet_loss << \"%, \"\n                  << \"rtt=\" << it->feedback.rtt_ms << \"ms, \"\n                  << \"fps=\" << it->feedback.decode_fps << \", \"\n                  << \"dropped=\" << it->feedback.dropped_frames << \", \"\n                  << \"bitrate=\" << it->feedback.current_bitrate_kbps << \"Kbps\\n\";\n    }\n\n    int max_br = state.config.max_bitrate_kbps;\n\n    return replace_placeholders(load_prompt_template(), {\n      { \"FOREGROUND_TITLE\",  !state.foreground_title.empty() ? state.foreground_title\n                             : !state.app_name.empty()       ? state.app_name\n                             : \"Unknown\" },\n      { \"FOREGROUND_EXE\",    !state.foreground_exe.empty() ? state.foreground_exe\n                             : !state.app_name.empty()     ? state.app_name\n                             : \"Unknown\" },\n      { \"MODE\",              mode_to_string(state.config.mode) },\n      { \"CURRENT_BITRATE\",   std::to_string(state.current_bitrate_kbps) },\n      { \"MIN_BITRATE\",       std::to_string(state.config.min_bitrate_kbps) },\n      { \"MAX_BITRATE\",       std::to_string(max_br) },\n      { \"RECENT_FEEDBACK\",   feedback_ss.str() },\n      { \"FPS_RANGE\",         std::to_string(int(max_br * 0.8)) + \"-\" + std::to_string(max_br) },\n      { \"ACTION_RANGE\",      std::to_string(int(max_br * 0.6)) + \"-\" + std::to_string(int(max_br * 0.8)) },\n      { \"STRATEGY_RANGE\",    std::to_string(int(max_br * 0.4)) + \"-\" + std::to_string(int(max_br * 0.6)) },\n      { \"DESKTOP_RANGE\",     std::to_string(int(max_br * 0.2)) + \"-\" + std::to_string(int(max_br * 0.3)) },\n    });\n  }\n\n  /**\n   * @brief Load AI model parameters from ai_config.json (with defaults).\n   * Reads: system_prompt, temperature, max_tokens.\n   */\n  struct llm_params_t {\n    std::string system_prompt = \"You are a streaming bitrate optimizer. Always respond with valid JSON only.\";\n    double temperature = 0.1;\n    int max_tokens = 150;\n  };\n\n  static const llm_params_t &\n  load_llm_params() {\n    static llm_params_t cached;\n    static bool loaded = false;\n    if (loaded) return cached;\n\n    try {\n      auto config_dir = std::filesystem::path(config::sunshine.config_file).parent_path();\n      auto path = config_dir / \"ai_config.json\";\n      std::ifstream file(path);\n      if (file.is_open()) {\n        auto cfg = json::parse(file);\n        if (cfg.contains(\"system_prompt\"))  cached.system_prompt = cfg[\"system_prompt\"].get<std::string>();\n        if (cfg.contains(\"temperature\"))    cached.temperature = cfg[\"temperature\"].get<double>();\n        if (cfg.contains(\"max_tokens\"))     cached.max_tokens = cfg[\"max_tokens\"].get<int>();\n      }\n    }\n    catch (...) {}\n\n    loaded = true;\n    return cached;\n  }\n\n  /**\n   * @brief Build the OpenAI-compatible request body for the LLM.\n   */\n  static std::string\n  build_llm_request(const std::string &prompt) {\n    const auto &params = load_llm_params();\n\n    json request;\n    request[\"messages\"] = json::array({\n      { { \"role\", \"system\" }, { \"content\", params.system_prompt } },\n      { { \"role\", \"user\" }, { \"content\", prompt } },\n    });\n    request[\"temperature\"] = params.temperature;\n    request[\"max_tokens\"] = params.max_tokens;\n    request[\"stream\"] = false;\n    return request.dump();\n  }\n\n  /**\n   * @brief Parse the LLM response to extract bitrate decision.\n   * @return action_t with new_bitrate_kbps and reason.\n   */\n  static action_t\n  parse_llm_response(const std::string &response_body, const session_state_t &state) {\n    action_t action;\n    try {\n      auto resp = json::parse(response_body);\n\n      // Extract the assistant's message content\n      std::string content;\n      if (resp.contains(\"choices\") && !resp[\"choices\"].empty()) {\n        content = resp[\"choices\"][0][\"message\"][\"content\"].get<std::string>();\n      }\n      else {\n        action.reason = \"llm_parse_error: no choices in response\";\n        return action;\n      }\n\n      // Strip markdown code fence if present\n      if (content.find(\"```\") != std::string::npos) {\n        auto start = content.find('{');\n        auto end = content.rfind('}');\n        if (start != std::string::npos && end != std::string::npos) {\n          content = content.substr(start, end - start + 1);\n        }\n      }\n\n      auto decision = json::parse(content);\n\n      int bitrate = decision.value(\"bitrate\", 0);\n      action.reason = decision.value(\"reason\", \"llm_decision\");\n\n      if (bitrate > 0) {\n        // Clamp to configured range\n        bitrate = std::clamp(bitrate, state.config.min_bitrate_kbps, state.config.max_bitrate_kbps);\n\n        // Always record the LLM's recommended target\n        action.target_bitrate_kbps = bitrate;\n\n        // Only signal an immediate action if the change is significant (>= 2%)\n        if (std::abs(bitrate - state.current_bitrate_kbps) >= state.current_bitrate_kbps / 50) {\n          action.new_bitrate_kbps = bitrate;\n        }\n        else {\n          action.reason = \"no_change: delta too small\";\n        }\n      }\n    }\n    catch (const json::exception &e) {\n      action.reason = std::string(\"llm_parse_error: \") + e.what();\n      BOOST_LOG(warning) << \"ABR LLM parse error: \" << e.what() << \" body: \" << response_body.substr(0, 200);\n    }\n\n    return action;\n  }\n\n  /**\n   * @brief Simple fallback when LLM is unavailable.\n   */\n  static action_t\n  fallback_decision(session_state_t &state, const network_feedback_t &feedback) {\n    action_t action;\n\n    if (feedback.packet_loss > 5.0) {\n      state.consecutive_high_loss++;\n      state.stable_ticks = 0;\n      int new_bitrate = static_cast<int>(state.current_bitrate_kbps * 0.70);\n      new_bitrate = std::clamp(new_bitrate, state.config.min_bitrate_kbps, state.config.max_bitrate_kbps);\n      action.new_bitrate_kbps = new_bitrate;\n      action.reason = \"fallback: emergency_drop\";\n    }\n    else if (feedback.packet_loss > 2.0) {\n      state.consecutive_high_loss = 0;\n      state.stable_ticks = 0;\n      int new_bitrate = static_cast<int>(state.current_bitrate_kbps * 0.90);\n      new_bitrate = std::clamp(new_bitrate, state.config.min_bitrate_kbps, state.config.max_bitrate_kbps);\n      action.new_bitrate_kbps = new_bitrate;\n      action.reason = \"fallback: moderate_drop\";\n    }\n    else if (feedback.packet_loss < 0.5) {\n      state.consecutive_high_loss = 0;\n      state.stable_ticks++;\n      if (state.stable_ticks >= 5) {\n        int new_bitrate = static_cast<int>(state.current_bitrate_kbps * 1.05);\n        new_bitrate = std::clamp(new_bitrate, state.config.min_bitrate_kbps, state.config.max_bitrate_kbps);\n        if (new_bitrate != state.current_bitrate_kbps) {\n          action.new_bitrate_kbps = new_bitrate;\n          action.reason = \"fallback: probe_up\";\n        }\n      }\n    }\n\n    return action;\n  }\n\n  void\n  enable(const std::string &client_name, const config_t &cfg, int initial_bitrate_kbps, const std::string &app_name) {\n    std::lock_guard lock(sessions_mutex);\n\n    auto &state = sessions[client_name];\n    state.config = cfg;\n    state.config.enabled = true;\n    state.initial_bitrate_kbps = initial_bitrate_kbps;\n    state.current_bitrate_kbps = initial_bitrate_kbps;\n    state.app_name = app_name;\n    state.recent_feedback.clear();\n    state.consecutive_high_loss = 0;\n    state.stable_ticks = 0;\n    state.llm_target_bitrate_kbps = 0;\n    state.app_changed = true;  // Trigger initial LLM call for app classification\n    state.network_recovered = false;\n    state.last_llm_call = std::chrono::steady_clock::time_point {};\n    state.last_fallback_time = std::chrono::steady_clock::time_point {};\n    state.last_fg_detect = std::chrono::steady_clock::time_point {};\n    state.last_fg_pid = 0;\n    state.llm_in_flight = false;\n    static uint64_t generation_counter = 0;\n    state.generation = ++generation_counter;\n    state.created_time = std::chrono::steady_clock::now();\n\n    // Initial foreground detection\n    auto fg = detect_foreground_app();\n    if (!fg.window_title.empty()) {\n      state.foreground_title = fg.window_title;\n      state.foreground_exe = fg.exe_name;\n      state.last_fg_pid = fg.pid;\n      state.last_fg_detect = std::chrono::steady_clock::now();\n    }\n\n    // Apply mode presets if min/max not explicitly configured\n    if (cfg.min_bitrate_kbps <= 0 || cfg.max_bitrate_kbps <= 0) {\n      switch (cfg.mode) {\n        case mode_e::QUALITY:\n          state.config.min_bitrate_kbps = std::max(5000, initial_bitrate_kbps / 2);\n          state.config.max_bitrate_kbps = std::min(150000, initial_bitrate_kbps * 3 / 2);\n          break;\n        case mode_e::LOW_LATENCY:\n          state.config.min_bitrate_kbps = 2000;\n          state.config.max_bitrate_kbps = initial_bitrate_kbps * 6 / 5;\n          break;\n        case mode_e::BALANCED:\n        default:\n          state.config.min_bitrate_kbps = std::max(3000, initial_bitrate_kbps * 3 / 10);\n          state.config.max_bitrate_kbps = std::min(150000, initial_bitrate_kbps * 2);\n          break;\n      }\n      // Guard against inverted range when initial_bitrate is very low\n      if (state.config.min_bitrate_kbps > state.config.max_bitrate_kbps) {\n        state.config.min_bitrate_kbps = state.config.max_bitrate_kbps;\n      }\n    }\n\n    // Clamp current bitrate to the computed range\n    state.current_bitrate_kbps = std::clamp(\n      state.current_bitrate_kbps,\n      state.config.min_bitrate_kbps,\n      state.config.max_bitrate_kbps);\n\n    BOOST_LOG(info) << \"ABR enabled for client '\" << client_name\n                    << \"': app=\" << app_name\n                    << \" mode=\" << mode_to_string(cfg.mode)\n                    << \" bitrate=\" << initial_bitrate_kbps\n                    << \" range=[\" << state.config.min_bitrate_kbps\n                    << \",\" << state.config.max_bitrate_kbps << \"] Kbps\";\n  }\n\n  void\n  disable(const std::string &client_name) {\n    std::lock_guard lock(sessions_mutex);\n    sessions.erase(client_name);\n    BOOST_LOG(info) << \"ABR disabled for client '\" << client_name << \"'\";\n  }\n\n  bool\n  is_enabled(const std::string &client_name) {\n    std::lock_guard lock(sessions_mutex);\n    auto it = sessions.find(client_name);\n    return it != sessions.end() && it->second.config.enabled;\n  }\n\n  /**\n   * @brief Background worker: calls LLM and stores target bitrate recommendation.\n   * Spawned as detached thread; communicates via sessions map.\n   * Unlike old design, the LLM sets a target (not an immediate action).\n   */\n  static void\n  llm_worker(const std::string &client_name, uint64_t generation, std::string request_body) {\n    auto result = confighttp::processAiChat(request_body);\n\n    std::lock_guard lock(sessions_mutex);\n    auto it = sessions.find(client_name);\n    if (it == sessions.end() || it->second.generation != generation) {\n      return;  // Session was cleaned up or re-created while LLM was in flight\n    }\n    auto &state = it->second;\n    state.llm_in_flight = false;\n\n    if (result.httpCode != 200) {\n      BOOST_LOG(warning) << \"ABR LLM call failed (HTTP \" << result.httpCode << \")\";\n      return;\n    }\n\n    auto action = parse_llm_response(result.body, state);\n    if (action.target_bitrate_kbps > 0) {\n      state.llm_target_bitrate_kbps = action.target_bitrate_kbps;\n      BOOST_LOG(info) << \"ABR LLM target for '\" << client_name\n                      << \"': \" << action.target_bitrate_kbps << \" Kbps\"\n                      << \" (\" << action.reason << \")\";\n    }\n  }\n\n  action_t\n  process_feedback(const std::string &client_name, const network_feedback_t &raw_feedback) {\n    auto feedback = sanitize_feedback(raw_feedback);\n\n    std::lock_guard lock(sessions_mutex);\n\n    auto it = sessions.find(client_name);\n    if (it == sessions.end() || !it->second.config.enabled) {\n      return { .reason = \"ABR not enabled\" };\n    }\n\n    auto &state = it->second;\n    auto now = std::chrono::steady_clock::now();\n\n    // Update current bitrate from client report, clamped to session range\n    if (feedback.current_bitrate_kbps > 0) {\n      state.current_bitrate_kbps = std::clamp(\n        feedback.current_bitrate_kbps,\n        state.config.min_bitrate_kbps,\n        state.config.max_bitrate_kbps);\n    }\n\n    // Add to feedback history\n    state.recent_feedback.push_back({ feedback, now });\n    while (state.recent_feedback.size() > session_state_t::MAX_FEEDBACK_HISTORY) {\n      state.recent_feedback.pop_front();\n    }\n\n    // ── Phase 1: Foreground detection (rate limited) ──\n    auto since_last_fg = std::chrono::duration_cast<std::chrono::seconds>(now - state.last_fg_detect).count();\n    if (since_last_fg >= session_state_t::FG_DETECT_INTERVAL_SECONDS) {\n      auto fg = detect_foreground_app();\n      state.last_fg_detect = now;\n      if (fg.pid > 0 && fg.pid != state.last_fg_pid) {\n        state.foreground_title = fg.window_title;\n        state.foreground_exe = fg.exe_name;\n        state.last_fg_pid = fg.pid;\n        state.app_changed = true;\n        BOOST_LOG(info) << \"ABR: foreground changed to '\" << fg.window_title\n                        << \"' (\" << fg.exe_name << \") pid=\" << fg.pid;\n      }\n      else if (fg.pid == state.last_fg_pid && !fg.window_title.empty()) {\n        state.foreground_title = fg.window_title;\n      }\n    }\n\n    // ── Phase 2: Fallback decisions (real-time, rate-limited to FALLBACK_INTERVAL_SECONDS) ──\n    action_t result_action;\n    auto since_last_fallback = std::chrono::duration_cast<std::chrono::seconds>(now - state.last_fallback_time).count();\n    bool can_fallback = since_last_fallback >= session_state_t::FALLBACK_INTERVAL_SECONDS;\n\n    // Emergency: high packet loss — immediate, no rate limit\n    if (feedback.packet_loss > 5.0) {\n      auto action = fallback_decision(state, feedback);\n      if (action.new_bitrate_kbps > 0) {\n        state.current_bitrate_kbps = action.new_bitrate_kbps;\n        state.last_fallback_time = now;\n        result_action = action;\n      }\n      return result_action;\n    }\n\n    // Regular fallback: moderate loss, probe-up, etc.\n    if (can_fallback) {\n      state.last_fallback_time = now;\n      auto action = fallback_decision(state, feedback);\n      if (action.new_bitrate_kbps > 0) {\n        // When probing up with LLM target, don't exceed the target\n        if (state.llm_target_bitrate_kbps > 0 && action.reason.find(\"probe_up\") != std::string::npos) {\n          action.new_bitrate_kbps = std::min(action.new_bitrate_kbps, state.llm_target_bitrate_kbps);\n          if (action.new_bitrate_kbps <= state.current_bitrate_kbps) {\n            action.new_bitrate_kbps = 0;  // Already at or above LLM target, don't probe further\n          }\n        }\n        if (action.new_bitrate_kbps > 0) {\n          state.current_bitrate_kbps = action.new_bitrate_kbps;\n          BOOST_LOG(info) << \"ABR fallback for '\" << client_name\n                          << \"': \" << action.new_bitrate_kbps << \" Kbps\"\n                          << \" (\" << action.reason << \")\";\n          result_action = action;\n        }\n      }\n    }\n\n    // Track network recovery: edge-trigger when stable_ticks crosses threshold\n    if (state.stable_ticks == 5 && state.consecutive_high_loss == 0) {\n      state.network_recovered = true;  // Signal for LLM (set once on transition)\n    }\n\n    // ── Phase 3: LLM trigger (event-driven, NOT periodic) ──\n    bool should_trigger_llm = (state.app_changed || state.network_recovered)\n                              && !state.llm_in_flight\n                              && confighttp::isAiEnabled()\n                              && !load_prompt_template().empty();\n\n    if (should_trigger_llm) {\n      auto since_last_llm = std::chrono::duration_cast<std::chrono::seconds>(now - state.last_llm_call).count();\n      if (since_last_llm >= session_state_t::LLM_MIN_INTERVAL_SECONDS) {\n        state.app_changed = false;\n        state.network_recovered = false;\n        state.last_llm_call = now;\n        state.llm_in_flight = true;\n\n        auto prompt = build_llm_prompt(state);\n        auto request_body = build_llm_request(prompt);\n\n        std::thread(llm_worker, client_name, state.generation, std::move(request_body)).detach();\n      }\n    }\n\n    return result_action;\n  }\n\n  capabilities_t\n  get_capabilities() {\n    return { true, 1 };\n  }\n\n  void\n  cleanup(const std::string &client_name) {\n    disable(client_name);\n  }\n\n}  // namespace abr\n"
  },
  {
    "path": "src/abr.h",
    "content": "/**\n * @file src/abr.h\n * @brief Adaptive Bitrate (ABR) decision engine for server-side bitrate control.\n *\n * Two-tier architecture:\n *   1. Real-time fallback: immediate threshold-based reactions to packet loss.\n *   2. Event-driven LLM: intelligent per-game target bitrate via LLM AI,\n *      triggered on app switches and network recovery.\n *\n * Clients POST network metrics periodically; the server responds with\n * bitrate adjustments from the fallback layer, while the LLM asynchronously\n * sets the optimal target bitrate ceiling.\n */\n#pragma once\n\n#include <chrono>\n#include <deque>\n#include <mutex>\n#include <string>\n#include <unordered_map>\n\nnamespace abr {\n\n  /// ABR operating mode\n  enum class mode_e {\n    QUALITY,      ///< Prioritize visual quality\n    BALANCED,     ///< Balance quality and latency\n    LOW_LATENCY,  ///< Prioritize low latency\n  };\n\n  /// ABR configuration per client session\n  struct config_t {\n    bool enabled = false;\n    int min_bitrate_kbps = 2000;\n    int max_bitrate_kbps = 150000;\n    mode_e mode = mode_e::BALANCED;\n  };\n\n  /// Network feedback from client\n  struct network_feedback_t {\n    double packet_loss;       ///< Packet loss percentage (0-100)\n    double rtt_ms;            ///< Round-trip time in ms\n    double decode_fps;        ///< Client-side decoded FPS\n    int dropped_frames;       ///< Number of dropped frames since last report\n    int current_bitrate_kbps; ///< Client's view of current bitrate\n  };\n\n  /// Server decision sent back to client\n  struct action_t {\n    int new_bitrate_kbps = 0;   ///< 0 means no change (immediate action)\n    int target_bitrate_kbps = 0; ///< LLM-recommended optimal bitrate (always set when LLM responds)\n    std::string reason;\n  };\n\n  /// ABR capabilities reported to client\n  struct capabilities_t {\n    bool supported = true;\n    int version = 1;\n  };\n\n  /// Recent feedback snapshot for LLM context window\n  struct feedback_snapshot_t {\n    network_feedback_t feedback;\n    std::chrono::steady_clock::time_point timestamp;\n  };\n\n  /// Per-client ABR session state\n  struct session_state_t {\n    config_t config;\n    int current_bitrate_kbps = 0;\n    int initial_bitrate_kbps = 0;\n    std::string app_name;            ///< Game/app from config (may be launcher name)\n    std::string foreground_title;     ///< Actual foreground window title (detected at runtime)\n    std::string foreground_exe;       ///< Actual foreground process executable name\n\n    /// Rolling window of recent feedback for LLM context (last N seconds)\n    std::deque<feedback_snapshot_t> recent_feedback;\n    static constexpr size_t MAX_FEEDBACK_HISTORY = 10;\n\n    /// LLM target bitrate: the ideal bitrate recommended by LLM for current app/conditions.\n    /// Fallback probe-up will move toward this target. 0 = no recommendation yet.\n    int llm_target_bitrate_kbps = 0;\n\n    /// Rate limiting: minimum interval between LLM calls\n    std::chrono::steady_clock::time_point last_llm_call;\n    static constexpr int LLM_MIN_INTERVAL_SECONDS = 10;\n\n    /// Foreground window detection rate limiting\n    std::chrono::steady_clock::time_point last_fg_detect;\n    static constexpr int FG_DETECT_INTERVAL_SECONDS = 10;\n    uint32_t last_fg_pid = 0;  ///< Cached PID to detect window changes\n\n    /// Fallback: simple threshold-based decisions (always active)\n    int consecutive_high_loss = 0;\n    int stable_ticks = 0;\n    static constexpr int FALLBACK_INTERVAL_SECONDS = 3;\n    std::chrono::steady_clock::time_point last_fallback_time;\n\n    /// LLM trigger flags\n    bool app_changed = false;         ///< Foreground app changed since last LLM call\n    bool network_recovered = false;   ///< Network recovered from turbulence\n\n    /// Async LLM state\n    bool llm_in_flight = false;  ///< Prevents concurrent LLM calls (protected by sessions_mutex)\n    uint64_t generation = 0;     ///< Monotonic counter to detect stale worker results\n\n    std::chrono::steady_clock::time_point created_time;\n  };\n\n  /**\n   * @brief Enable ABR for a client session.\n   * @param client_name Client identifier.\n   * @param cfg ABR configuration.\n   * @param initial_bitrate_kbps The bitrate at stream start.\n   * @param app_name The game/application currently running.\n   */\n  void\n  enable(const std::string &client_name, const config_t &cfg, int initial_bitrate_kbps, const std::string &app_name);\n\n  /**\n   * @brief Disable ABR for a client session.\n   */\n  void\n  disable(const std::string &client_name);\n\n  /**\n   * @brief Check if ABR is enabled for a client.\n   */\n  bool\n  is_enabled(const std::string &client_name);\n\n  /**\n   * @brief Process network feedback and produce a bitrate action.\n   *\n   * Runs fallback logic (threshold-based) on every call for real-time response.\n   * Triggers LLM asynchronously on app change or network recovery events.\n   *\n   * @param client_name Client identifier.\n   * @param feedback Network metrics from the client.\n   * @return Bitrate adjustment action (new_bitrate_kbps == 0 means no change).\n   */\n  action_t\n  process_feedback(const std::string &client_name, const network_feedback_t &feedback);\n\n  /**\n   * @brief Get ABR capabilities for capability negotiation.\n   */\n  capabilities_t\n  get_capabilities();\n\n  /**\n   * @brief Remove all ABR state for a client (call on session end).\n   */\n  void\n  cleanup(const std::string &client_name);\n\n}  // namespace abr\n"
  },
  {
    "path": "src/amf/amf_config.h",
    "content": "/**\n * @file src/amf/amf_config.h\n * @brief Declarations for AMF encoder configuration.\n */\n#pragma once\n\n#include <cstdint>\n#include <optional>\n\nnamespace amf {\n\n  /**\n   * @brief HDR metadata for AMF encoder.\n   */\n  struct amf_hdr_metadata {\n    struct {\n      uint16_t x;  // Normalized to 50,000\n      uint16_t y;  // Normalized to 50,000\n    } displayPrimaries[3];  // RGB order\n\n    struct {\n      uint16_t x;  // Normalized to 50,000\n      uint16_t y;  // Normalized to 50,000\n    } whitePoint;\n\n    uint16_t maxDisplayLuminance;        // Nits\n    uint16_t minDisplayLuminance;        // 1/10000th of a nit\n    uint16_t maxContentLightLevel;       // Nits\n    uint16_t maxFrameAverageLightLevel;  // Nits\n  };\n\n  /**\n   * @brief AMF encoder configuration.\n   * Integer values correspond directly to AMF SDK enum values.\n   */\n  struct amf_config {\n    // Usage preset (AMF_VIDEO_ENCODER_USAGE_ENUM values)\n    std::optional<int> usage;\n\n    // Quality preset (AMF_VIDEO_ENCODER_QUALITY_PRESET_ENUM values)\n    std::optional<int> quality_preset;\n\n    // Rate control mode (AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_ENUM values)\n    std::optional<int> rc_mode;\n\n    // Pre-analysis enable\n    std::optional<int> preanalysis;\n\n    // VBAQ enable\n    std::optional<int> vbaq;\n\n    // H.264 entropy coding (0=CAVLC, 1=CABAC)\n    int h264_cabac = 1;\n\n    // Enforce HRD\n    std::optional<int> enforce_hrd;\n\n    // Number of LTR frames for RFI\n    int max_ltr_frames = 1;\n\n    // --- Pre-Analysis sub-system ---\n    // PAQ mode (AMF_PA_PAQ_MODE_ENUM): 0=none, 1=CAQ\n    std::optional<int> pa_paq_mode;\n    // TAQ mode (AMF_PA_TAQ_MODE_ENUM): 0=none, 1=mode1, 2=mode2\n    std::optional<int> pa_taq_mode;\n    // CAQ strength (AMF_PA_CAQ_STRENGTH_ENUM): 0=low, 1=medium, 2=high\n    std::optional<int> pa_caq_strength;\n    // Lookahead buffer depth (0=disabled)\n    std::optional<int> pa_lookahead_depth;\n    // Scene change detection sensitivity (AMF_PA_SCENE_CHANGE_DETECTION_SENSITIVITY_ENUM): 0=low, 1=medium, 2=high\n    std::optional<int> pa_scene_change_sensitivity;\n    // High motion quality boost mode (AMF_PA_HIGH_MOTION_QUALITY_BOOST_MODE_ENUM): 0=none, 1=auto\n    std::optional<int> pa_high_motion_quality_boost;\n    // Initial QP after scene change (0-51, 0=auto)\n    std::optional<int> pa_initial_qp_after_scene_change;\n    // Activity type (AMF_PA_ACTIVITY_TYPE_ENUM): 0=Y, 1=YUV\n    std::optional<int> pa_activity_type;\n\n    // --- QVBR quality level ---\n    // For QVBR rate control mode: quality level 1-51 (lower=better)\n    std::optional<int> qvbr_quality_level;\n\n    // --- AV1 Encoding Latency Mode ---\n    // AMF_VIDEO_ENCODER_AV1_ENCODING_LATENCY_MODE_ENUM: 0=none, 1=power saving RT, 2=RT, 3=lowest latency\n    std::optional<int> av1_encoding_latency_mode;\n\n    // --- AV1 Screen Content Tools ---\n    std::optional<bool> av1_screen_content_tools;\n    std::optional<bool> av1_palette_mode;\n    std::optional<bool> av1_force_integer_mv;\n\n    // --- Intra Refresh ---\n    // H.264: number of MBs per slot; HEVC: number of CTBs per slot; AV1: mode enum\n    std::optional<int> intra_refresh_mbs;\n    // AV1-specific: intra refresh mode (AMF_VIDEO_ENCODER_AV1_INTRA_REFRESH_MODE_ENUM)\n    std::optional<int> av1_intra_refresh_mode;\n    // AV1-specific: number of stripes for intra refresh\n    std::optional<int> av1_intra_refresh_stripes;\n\n    // --- Statistics feedback ---\n    bool enable_statistics_feedback = false;\n    bool enable_psnr_feedback = false;\n    bool enable_ssim_feedback = false;\n\n    // --- High Motion Quality Boost (encoder-level, separate from PA) ---\n    std::optional<bool> high_motion_quality_boost_enable;\n  };\n\n}  // namespace amf\n"
  },
  {
    "path": "src/amf/amf_d3d11.cpp",
    "content": "/**\n * @file src/amf/amf_d3d11.cpp\n * @brief Implementation of standalone AMF encoder with D3D11 texture input.\n */\n\n#include \"amf_d3d11.h\"\n\n#include <chrono>\n#include <thread>\n\n#include <AMF/components/ColorSpace.h>\n#include <AMF/components/PreAnalysis.h>\n#include <AMF/components/VideoEncoderAV1.h>\n#include <AMF/components/VideoEncoderHEVC.h>\n#include <AMF/components/VideoEncoderVCE.h>\n#include <AMF/core/Surface.h>\n\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/utility.h\"\n\nnamespace amf {\n\n  // AMF DLL function types\n  typedef AMF_RESULT(AMF_CDECL_CALL *AMFInit_Fn)(amf_uint64 version, ::amf::AMFFactory **ppFactory);\n  typedef AMF_RESULT(AMF_CDECL_CALL *AMFQueryVersion_Fn)(amf_uint64 *pVersion);\n\n  amf_d3d11::amf_d3d11(ID3D11Device *d3d_device):\n      device(d3d_device) {\n  }\n\n  amf_d3d11::~amf_d3d11() {\n    destroy_encoder();\n  }\n\n  bool\n  amf_d3d11::init_amf_library() {\n    if (factory) return true;\n\n    amf_dll = LoadLibraryA(AMF_DLL_NAMEA);\n    if (!amf_dll) {\n      BOOST_LOG(error) << \"AMF: failed to load \" << AMF_DLL_NAMEA;\n      return false;\n    }\n\n    auto amf_query_version = reinterpret_cast<AMFQueryVersion_Fn>(GetProcAddress(amf_dll, AMF_QUERY_VERSION_FUNCTION_NAME));\n    auto amf_init = reinterpret_cast<AMFInit_Fn>(GetProcAddress(amf_dll, AMF_INIT_FUNCTION_NAME));\n\n    if (!amf_query_version || !amf_init) {\n      BOOST_LOG(error) << \"AMF: missing entry points in \" << AMF_DLL_NAMEA;\n      FreeLibrary(amf_dll);\n      amf_dll = nullptr;\n      return false;\n    }\n\n    amf_uint64 version = 0;\n    if (amf_query_version(&version) != AMF_OK) {\n      BOOST_LOG(error) << \"AMF: failed to query runtime version\";\n      FreeLibrary(amf_dll);\n      amf_dll = nullptr;\n      return false;\n    }\n\n    BOOST_LOG(info) << \"AMF runtime version: \"\n                    << AMF_GET_MAJOR_VERSION(version) << \".\"\n                    << AMF_GET_MINOR_VERSION(version) << \".\"\n                    << AMF_GET_SUBMINOR_VERSION(version) << \".\"\n                    << AMF_GET_BUILD_VERSION(version);\n\n    if (amf_init(AMF_FULL_VERSION, &factory) != AMF_OK || !factory) {\n      BOOST_LOG(error) << \"AMF: AMFInit failed\";\n      FreeLibrary(amf_dll);\n      amf_dll = nullptr;\n      return false;\n    }\n\n    return true;\n  }\n\n  AMF_SURFACE_FORMAT\n  amf_d3d11::get_amf_format(platf::pix_fmt_e buffer_format, int bit_depth) {\n    switch (buffer_format) {\n      case platf::pix_fmt_e::nv12:\n        return AMF_SURFACE_NV12;\n      case platf::pix_fmt_e::p010:\n        return AMF_SURFACE_P010;\n      default:\n        return (bit_depth == 10) ? AMF_SURFACE_P010 : AMF_SURFACE_NV12;\n    }\n  }\n\n  const wchar_t *\n  amf_d3d11::get_codec_id() {\n    switch (video_format) {\n      case 0:\n        return AMFVideoEncoderVCE_AVC;\n      case 1:\n        return AMFVideoEncoder_HEVC;\n      case 2:\n        return AMFVideoEncoder_AV1;\n      default:\n        return AMFVideoEncoderVCE_AVC;\n    }\n  }\n\n  bool\n  amf_d3d11::set_ltr_property(const wchar_t *name, int64_t value) {\n    auto res = encoder->SetProperty(name, value);\n    if (res != AMF_OK) {\n      BOOST_LOG(warning) << \"AMF: failed to set LTR property, error: \" << res;\n      return false;\n    }\n    return true;\n  }\n\n  // Helper to set a codec-specific property with the right prefix\n  template<typename T>\n  void\n  amf_d3d11::set_codec_property(const wchar_t *h264_name, const wchar_t *hevc_name, const wchar_t *av1_name, T value) {\n    const wchar_t *name = (video_format == 0) ? h264_name :\n                          (video_format == 1) ? hevc_name : av1_name;\n    if (name) {\n      encoder->SetProperty(name, value);\n    }\n  }\n\n  bool\n  amf_d3d11::configure_encoder(const amf_config &config,\n    const video::config_t &client_config,\n    const video::sunshine_colorspace_t &colorspace) {\n    auto bitrate = static_cast<int64_t>(client_config.bitrate) * 1000;\n    auto framerate = AMFConstructRate(client_config.framerate, 1);\n\n    if (video_format == 0) {\n      // H.264\n      if (config.usage) encoder->SetProperty(AMF_VIDEO_ENCODER_USAGE, (amf_int64) *config.usage);\n      if (config.quality_preset) encoder->SetProperty(AMF_VIDEO_ENCODER_QUALITY_PRESET, (amf_int64) *config.quality_preset);\n      if (config.rc_mode) encoder->SetProperty(AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD, (amf_int64) *config.rc_mode);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_TARGET_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_PEAK_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_VBV_BUFFER_SIZE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_FRAMERATE, framerate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_FILLER_DATA_ENABLE, false);\n      if (config.enforce_hrd) encoder->SetProperty(AMF_VIDEO_ENCODER_ENFORCE_HRD, !!(*config.enforce_hrd));\n      encoder->SetProperty(AMF_VIDEO_ENCODER_IDR_PERIOD, (amf_int64) 0);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_DE_BLOCKING_FILTER, true);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_CABAC_ENABLE, (amf_int64)(config.h264_cabac ? AMF_VIDEO_ENCODER_CABAC : AMF_VIDEO_ENCODER_CALV));\n      if (config.preanalysis) encoder->SetProperty(AMF_VIDEO_ENCODER_PRE_ANALYSIS_ENABLE, !!(*config.preanalysis));\n      if (config.vbaq) encoder->SetProperty(AMF_VIDEO_ENCODER_ENABLE_VBAQ, !!(*config.vbaq));\n      encoder->SetProperty(AMF_VIDEO_ENCODER_B_PIC_PATTERN, (amf_int64) 0);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_LOWLATENCY_MODE, true);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_INPUT_QUEUE_SIZE, (amf_int64) 1);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_QUERY_TIMEOUT, (amf_int64) 1);\n\n      // LTR for RFI\n      max_ltr_frames = config.max_ltr_frames;\n      if (max_ltr_frames > 0) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_MAX_LTR_FRAMES, (amf_int64) max_ltr_frames);\n        encoder->SetProperty(AMF_VIDEO_ENCODER_LTR_MODE, (amf_int64) AMF_VIDEO_ENCODER_LTR_MODE_RESET_UNUSED);\n      }\n\n      // QVBR quality level\n      if (config.qvbr_quality_level) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_QVBR_QUALITY_LEVEL, (amf_int64) *config.qvbr_quality_level);\n      }\n\n      // High motion quality boost\n      if (config.high_motion_quality_boost_enable) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HIGH_MOTION_QUALITY_BOOST_ENABLE, *config.high_motion_quality_boost_enable);\n      }\n\n      // Intra refresh\n      if (config.intra_refresh_mbs) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_INTRA_REFRESH_NUM_MBS_PER_SLOT, (amf_int64) *config.intra_refresh_mbs);\n      }\n\n      // Slices per frame\n      if (client_config.slicesPerFrame > 1) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_SLICES_PER_FRAME, (amf_int64) client_config.slicesPerFrame);\n      }\n\n      // Statistics feedback\n      if (config.enable_statistics_feedback) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_STATISTICS_FEEDBACK, true);\n      }\n      if (config.enable_psnr_feedback) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_PSNR_FEEDBACK, true);\n      }\n      if (config.enable_ssim_feedback) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_SSIM_FEEDBACK, true);\n      }\n    }\n    else if (video_format == 1) {\n      // HEVC\n      if (config.usage) encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_USAGE, (amf_int64) *config.usage);\n      if (config.quality_preset) encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET, (amf_int64) *config.quality_preset);\n      if (config.rc_mode) encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD, (amf_int64) *config.rc_mode);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_TARGET_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_PEAK_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_VBV_BUFFER_SIZE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_FRAMERATE, framerate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_FILLER_DATA_ENABLE, false);\n      if (config.enforce_hrd) encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_ENFORCE_HRD, !!(*config.enforce_hrd));\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_NUM_GOPS_PER_IDR, (amf_int64) 1);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_GOP_SIZE, (amf_int64) 0);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_HEADER_INSERTION_MODE, (amf_int64) AMF_VIDEO_ENCODER_HEVC_HEADER_INSERTION_MODE_IDR_ALIGNED);\n      if (config.preanalysis) encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_PRE_ANALYSIS_ENABLE, !!(*config.preanalysis));\n      if (config.vbaq) encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_ENABLE_VBAQ, !!(*config.vbaq));\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_LOWLATENCY_MODE, true);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_INPUT_QUEUE_SIZE, (amf_int64) 1);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT, (amf_int64) 1);\n\n      if (colorspace.bit_depth == 10) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_PROFILE, (amf_int64) AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN_10);\n      }\n      else {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_PROFILE, (amf_int64) AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN);\n      }\n\n      max_ltr_frames = config.max_ltr_frames;\n      if (max_ltr_frames > 0) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_MAX_LTR_FRAMES, (amf_int64) max_ltr_frames);\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_LTR_MODE, (amf_int64) AMF_VIDEO_ENCODER_HEVC_LTR_MODE_RESET_UNUSED);\n      }\n\n      // QVBR quality level\n      if (config.qvbr_quality_level) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_QVBR_QUALITY_LEVEL, (amf_int64) *config.qvbr_quality_level);\n      }\n\n      // High motion quality boost\n      if (config.high_motion_quality_boost_enable) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_HIGH_MOTION_QUALITY_BOOST_ENABLE, *config.high_motion_quality_boost_enable);\n      }\n\n      // Intra refresh\n      if (config.intra_refresh_mbs) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_INTRA_REFRESH_NUM_CTBS_PER_SLOT, (amf_int64) *config.intra_refresh_mbs);\n      }\n\n      // Slices per frame\n      if (client_config.slicesPerFrame > 1) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_SLICES_PER_FRAME, (amf_int64) client_config.slicesPerFrame);\n      }\n\n      // Statistics feedback\n      if (config.enable_statistics_feedback) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_STATISTICS_FEEDBACK, true);\n      }\n      if (config.enable_psnr_feedback) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_PSNR_FEEDBACK, true);\n      }\n      if (config.enable_ssim_feedback) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_SSIM_FEEDBACK, true);\n      }\n    }\n    else {\n      // AV1\n      if (config.usage) encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_USAGE, (amf_int64) *config.usage);\n      if (config.quality_preset) encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET, (amf_int64) *config.quality_preset);\n      if (config.rc_mode) encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD, (amf_int64) *config.rc_mode);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_TARGET_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_PEAK_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_VBV_BUFFER_SIZE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_FRAMERATE, framerate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_FILLER_DATA, false);\n      if (config.enforce_hrd) encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_ENFORCE_HRD, !!(*config.enforce_hrd));\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_ALIGNMENT_MODE, (amf_int64) AMF_VIDEO_ENCODER_AV1_ALIGNMENT_MODE_NO_RESTRICTIONS);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_GOP_SIZE, (amf_int64) 0);\n      if (config.preanalysis) encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_PRE_ANALYSIS_ENABLE, !!(*config.preanalysis));\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_INPUT_QUEUE_SIZE, (amf_int64) 1);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_QUERY_TIMEOUT, (amf_int64) 1);\n      if (config.av1_encoding_latency_mode) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_ENCODING_LATENCY_MODE, (amf_int64) *config.av1_encoding_latency_mode);\n      }\n      else {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_ENCODING_LATENCY_MODE, (amf_int64) AMF_VIDEO_ENCODER_AV1_ENCODING_LATENCY_MODE_LOWEST_LATENCY);\n      }\n\n      // AV1 Screen Content Tools\n      if (config.av1_screen_content_tools) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_SCREEN_CONTENT_TOOLS, *config.av1_screen_content_tools);\n      }\n      if (config.av1_palette_mode) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_PALETTE_MODE, *config.av1_palette_mode);\n      }\n      if (config.av1_force_integer_mv) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_FORCE_INTEGER_MV, *config.av1_force_integer_mv);\n      }\n\n      // AV1 high motion quality boost\n      if (config.high_motion_quality_boost_enable) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_HIGH_MOTION_QUALITY_BOOST, *config.high_motion_quality_boost_enable);\n      }\n\n      // AV1 AQ mode (Content Adaptive Quantization)\n      if (config.pa_paq_mode) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_AQ_MODE, (amf_int64) *config.pa_paq_mode);\n      }\n\n      max_ltr_frames = config.max_ltr_frames;\n      if (max_ltr_frames > 0) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_MAX_LTR_FRAMES, (amf_int64) max_ltr_frames);\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_LTR_MODE, (amf_int64) AMF_VIDEO_ENCODER_AV1_LTR_MODE_RESET_UNUSED);\n      }\n\n      // QVBR quality level\n      if (config.qvbr_quality_level) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_QVBR_QUALITY_LEVEL, (amf_int64) *config.qvbr_quality_level);\n      }\n\n      // Intra refresh\n      if (config.av1_intra_refresh_mode) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_INTRA_REFRESH_MODE, (amf_int64) *config.av1_intra_refresh_mode);\n        if (config.av1_intra_refresh_stripes) {\n          encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_INTRAREFRESH_STRIPES, (amf_int64) *config.av1_intra_refresh_stripes);\n        }\n      }\n\n      // Tiles per frame\n      if (client_config.slicesPerFrame > 1) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_TILES_PER_FRAME, (amf_int64) client_config.slicesPerFrame);\n      }\n\n      // Statistics feedback\n      if (config.enable_statistics_feedback) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_STATISTICS_FEEDBACK, true);\n      }\n      if (config.enable_psnr_feedback) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_PSNR_FEEDBACK, true);\n      }\n      if (config.enable_ssim_feedback) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_SSIM_FEEDBACK, true);\n      }\n    }\n\n    // Color space properties\n    if (video_format == 0) {\n      encoder->SetProperty(AMF_VIDEO_ENCODER_FULL_RANGE_COLOR, colorspace.full_range);\n    }\n    else if (video_format == 1) {\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_NOMINAL_RANGE, (amf_int64)(colorspace.full_range ? AMF_VIDEO_ENCODER_HEVC_NOMINAL_RANGE_FULL : AMF_VIDEO_ENCODER_HEVC_NOMINAL_RANGE_STUDIO));\n    }\n    else {\n      // AV1: amf_bool type\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_OUTPUT_FULL_RANGE_COLOR, colorspace.full_range);\n    }\n\n    // Color properties for bitstream metadata.\n    // Only set OUTPUT properties, matching FFmpeg's approach.\n    // Do NOT set INPUT_COLOR_xxx — setting them may trigger AMF's internal color converter.\n    amf_int64 amf_primaries;\n    amf_int64 amf_transfer;\n    amf_int64 amf_color_profile;\n\n    switch (colorspace.colorspace) {\n      case video::colorspace_e::rec601:\n        amf_primaries = AMF_COLOR_PRIMARIES_SMPTE170M;\n        amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_SMPTE170M;\n        amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_601 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_601;\n        break;\n      case video::colorspace_e::rec709:\n        amf_primaries = AMF_COLOR_PRIMARIES_BT709;\n        amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_BT709;\n        amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_709 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_709;\n        break;\n      case video::colorspace_e::bt2020sdr:\n        amf_primaries = AMF_COLOR_PRIMARIES_BT2020;\n        amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_BT2020_10;\n        amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_2020 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020;\n        break;\n      case video::colorspace_e::bt2020:\n        amf_primaries = AMF_COLOR_PRIMARIES_BT2020;\n        amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_SMPTE2084;\n        amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_2020 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020;\n        break;\n      case video::colorspace_e::bt2020hlg:\n        amf_primaries = AMF_COLOR_PRIMARIES_BT2020;\n        amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_ARIB_STD_B67;\n        amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_2020 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020;\n        break;\n      default:\n        amf_primaries = AMF_COLOR_PRIMARIES_BT709;\n        amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_BT709;\n        amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_709 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_709;\n        break;\n    }\n\n    auto amf_bit_depth = (amf_int64)((colorspace.bit_depth == 10) ? AMF_COLOR_BIT_DEPTH_10 : AMF_COLOR_BIT_DEPTH_8);\n\n    if (video_format == 0) {\n      encoder->SetProperty(AMF_VIDEO_ENCODER_COLOR_BIT_DEPTH, amf_bit_depth);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_OUTPUT_COLOR_PROFILE, amf_color_profile);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_OUTPUT_TRANSFER_CHARACTERISTIC, amf_transfer);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_OUTPUT_COLOR_PRIMARIES, amf_primaries);\n    }\n    else if (video_format == 1) {\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_COLOR_BIT_DEPTH, amf_bit_depth);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_OUTPUT_COLOR_PROFILE, amf_color_profile);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_OUTPUT_TRANSFER_CHARACTERISTIC, amf_transfer);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_OUTPUT_COLOR_PRIMARIES, amf_primaries);\n    }\n    else {\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_COLOR_BIT_DEPTH, amf_bit_depth);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_OUTPUT_COLOR_PROFILE, amf_color_profile);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_OUTPUT_TRANSFER_CHARACTERISTIC, amf_transfer);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_OUTPUT_COLOR_PRIMARIES, amf_primaries);\n    }\n\n    // Save statistics feedback state for encode_frame()\n    statistics_enabled = config.enable_statistics_feedback;\n    psnr_enabled = config.enable_psnr_feedback;\n    ssim_enabled = config.enable_ssim_feedback;\n\n    // Pre-Analysis sub-system properties (set on encoder when PA is enabled)\n    if (config.preanalysis && *config.preanalysis) {\n      if (config.pa_paq_mode) {\n        encoder->SetProperty(AMF_PA_PAQ_MODE, (amf_int64) *config.pa_paq_mode);\n      }\n      if (config.pa_taq_mode) {\n        encoder->SetProperty(AMF_PA_TAQ_MODE, (amf_int64) *config.pa_taq_mode);\n      }\n      if (config.pa_caq_strength) {\n        encoder->SetProperty(AMF_PA_CAQ_STRENGTH, (amf_int64) *config.pa_caq_strength);\n      }\n      if (config.pa_lookahead_depth) {\n        encoder->SetProperty(AMF_PA_LOOKAHEAD_BUFFER_DEPTH, (amf_int64) *config.pa_lookahead_depth);\n      }\n      if (config.pa_scene_change_sensitivity) {\n        encoder->SetProperty(AMF_PA_SCENE_CHANGE_DETECTION_SENSITIVITY, (amf_int64) *config.pa_scene_change_sensitivity);\n      }\n      if (config.pa_high_motion_quality_boost) {\n        encoder->SetProperty(AMF_PA_HIGH_MOTION_QUALITY_BOOST_MODE, (amf_int64) *config.pa_high_motion_quality_boost);\n      }\n      if (config.pa_initial_qp_after_scene_change) {\n        encoder->SetProperty(AMF_PA_INITIAL_QP_AFTER_SCENE_CHANGE, (amf_int64) *config.pa_initial_qp_after_scene_change);\n      }\n      if (config.pa_activity_type) {\n        encoder->SetProperty(AMF_PA_ACTIVITY_TYPE, (amf_int64) *config.pa_activity_type);\n      }\n    }\n\n    // Low-latency mode (matching FFmpeg's \"latency\"=1 option)\n    if (video_format == 0) {\n      encoder->SetProperty(AMF_VIDEO_ENCODER_LOWLATENCY_MODE, true);\n    }\n    else if (video_format == 1) {\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_LOWLATENCY_MODE, true);\n    }\n\n    return true;\n  }\n\n  bool\n  amf_d3d11::create_encoder(const amf_config &config,\n    const video::config_t &client_config,\n    const video::sunshine_colorspace_t &colorspace,\n    platf::pix_fmt_e buffer_format) {\n    // Determine video format from client config\n    video_format = client_config.videoFormat;\n    current_config = client_config;\n\n    // Initialize AMF library\n    if (!init_amf_library()) return false;\n\n    // Create AMF context\n    auto res = factory->CreateContext(&context);\n    if (res != AMF_OK || !context) {\n      BOOST_LOG(error) << \"AMF: CreateContext failed, error: \" << res;\n      return false;\n    }\n\n    // Set surface cache size to match FFmpeg's hwcontext_amf initialization\n    context->SetProperty(L\"DeviceSurfaceCacheSize\", (amf_int64) 50);\n\n    // Initialize D3D11 in AMF context with DX11_1 (matching FFmpeg)\n    res = context->InitDX11(device, AMF_DX11_1);\n    if (res != AMF_OK) {\n      BOOST_LOG(error) << \"AMF: InitDX11 failed, error: \" << res;\n      return false;\n    }\n\n    // Create encoder component\n    res = factory->CreateComponent(context, get_codec_id(), &encoder);\n    if (res != AMF_OK || !encoder) {\n      BOOST_LOG(error) << \"AMF: CreateComponent failed for codec \" << video_format << \", error: \" << res;\n      return false;\n    }\n\n    // Configure encoder properties (before Init)\n    if (!configure_encoder(config, client_config, colorspace)) {\n      return false;\n    }\n\n    // Initialize encoder\n    auto amf_format = get_amf_format(buffer_format, colorspace.bit_depth);\n    surface_format = amf_format;\n    encode_width = client_config.width;\n    encode_height = client_config.height;\n    res = encoder->Init(amf_format, client_config.width, client_config.height);\n    if (res != AMF_OK && config.rc_mode) {\n      // Init failed with custom RC mode - retry without it (driver may not support it)\n      BOOST_LOG(warning) << \"AMF: Init failed with rc_mode=\" << *config.rc_mode << \", retrying with default RC\";\n      encoder->Terminate();\n      encoder = nullptr;\n      res = factory->CreateComponent(context, get_codec_id(), &encoder);\n      if (res == AMF_OK && encoder) {\n        auto config_fallback = config;\n        config_fallback.rc_mode = std::nullopt;\n        if (configure_encoder(config_fallback, client_config, colorspace)) {\n          res = encoder->Init(amf_format, client_config.width, client_config.height);\n        }\n      }\n    }\n    if (res != AMF_OK) {\n      BOOST_LOG(error) << \"AMF: encoder Init failed, error: \" << res;\n      return false;\n    }\n\n    // Check if driver supports QUERY_TIMEOUT by reading back the property (FFmpeg pattern)\n    {\n      const wchar_t *qt_prop = (video_format == 0) ? AMF_VIDEO_ENCODER_QUERY_TIMEOUT :\n                               (video_format == 1) ? AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT :\n                                                     AMF_VIDEO_ENCODER_AV1_QUERY_TIMEOUT;\n      amf_int64 qt_val = 0;\n      auto qt_res = encoder->GetProperty(qt_prop, &qt_val);\n      query_timeout_supported = (qt_res == AMF_OK && qt_val > 0);\n      BOOST_LOG(info) << \"AMF: QUERY_TIMEOUT \" << (query_timeout_supported ? \"supported\" : \"not supported\") << \" (value=\" << qt_val << \")\";\n    }\n\n    // Create input texture for the rendering pipeline to write to.\n    // Must match the YUV format that the shader pipeline outputs (NV12/P010).\n    DXGI_FORMAT dxgi_fmt;\n    switch (buffer_format) {\n      case platf::pix_fmt_e::nv12:\n        dxgi_fmt = DXGI_FORMAT_NV12;\n        break;\n      case platf::pix_fmt_e::p010:\n        dxgi_fmt = DXGI_FORMAT_P010;\n        break;\n      default:\n        dxgi_fmt = (colorspace.bit_depth == 10) ? DXGI_FORMAT_P010 : DXGI_FORMAT_NV12;\n        break;\n    }\n\n    D3D11_TEXTURE2D_DESC desc = {};\n    desc.Width = client_config.width;\n    desc.Height = client_config.height;\n    desc.MipLevels = 1;\n    desc.ArraySize = 1;\n    desc.Format = dxgi_fmt;\n    desc.SampleDesc.Count = 1;\n    desc.Usage = D3D11_USAGE_DEFAULT;\n    desc.BindFlags = D3D11_BIND_RENDER_TARGET;\n\n    auto hr = device->CreateTexture2D(&desc, nullptr, &input_texture);\n    if (FAILED(hr)) {\n      BOOST_LOG(error) << \"AMF: failed to create input texture, HRESULT: 0x\" << std::hex << hr;\n      return false;\n    }\n\n\n\n    // Clamp effective LTR slots to what the encoder actually reserves\n    effective_ltr_slots = (max_ltr_frames > 0) ? std::min(max_ltr_frames, MAX_LTR_SLOTS) : 0;\n\n    // Reset LTR state\n    for (auto &valid : ltr_slots_valid) valid = false;\n    for (auto &fi : ltr_slot_frame_index) fi = 0;\n    current_ltr_slot = 0;\n    rfi_pending = false;\n\n    auto codec_name = (video_format == 0) ? \"H.264\" :\n                      (video_format == 1) ? \"HEVC\" :\n                      (video_format == 2) ? \"AV1\" : \"Unknown\";\n    BOOST_LOG(info) << \"AMF: standalone \" << codec_name << \" encoder created (\"\n                    << client_config.width << \"x\" << client_config.height << \" @ \"\n                    << client_config.framerate << \"fps, LTR=\" << max_ltr_frames\n                    << \", slices=\" << client_config.slicesPerFrame << \")\";\n    return true;\n  }\n\n  void\n  amf_d3d11::destroy_encoder() {\n    pending_output = nullptr;\n    if (encoder) {\n      encoder->Terminate();\n      encoder = nullptr;\n    }\n    if (context) {\n      context->Terminate();\n      context = nullptr;\n    }\n    if (input_texture) {\n      input_texture->Release();\n      input_texture = nullptr;\n    }\n\n    if (amf_dll) {\n      FreeLibrary(amf_dll);\n      amf_dll = nullptr;\n    }\n    factory = nullptr;\n  }\n\n  amf_encoded_frame\n  amf_d3d11::encode_frame(uint64_t frame_index, bool force_idr) {\n    amf_encoded_frame result;\n    result.frame_index = frame_index;\n\n    if (!encoder || !input_texture) return result;\n\n    // Set the texture array index via private data, as FFmpeg does.\n    // AMF uses this GUID to determine which slice of a texture array to encode.\n    static const GUID AMFTextureArrayIndexGUID = { 0x28115527, 0xe7c3, 0x4b66, { 0x99, 0xd3, 0x4f, 0x2a, 0xe6, 0xb4, 0x7f, 0xaf } };\n    int array_index = 0;\n    input_texture->SetPrivateData(AMFTextureArrayIndexGUID, sizeof(array_index), &array_index);\n\n    // Wrap the D3D11 texture as AMF surface (zero-copy)\n    ::amf::AMFSurfacePtr surface;\n    auto res = context->CreateSurfaceFromDX11Native(input_texture, &surface, nullptr);\n    if (res != AMF_OK || !surface) {\n      BOOST_LOG(error) << \"AMF: CreateSurfaceFromDX11Native failed, error: \" << res;\n      // Check if the D3D11 device is lost (TDR, driver crash, etc.)\n      if (device) {\n        auto removed_reason = device->GetDeviceRemovedReason();\n        if (removed_reason != S_OK) {\n          BOOST_LOG(error) << \"AMF: D3D11 device lost, reason: 0x\" << util::hex(removed_reason).to_string_view();\n        }\n      }\n      return result;\n    }\n\n    // Set crop to actual frame dimensions (hw surfaces can be vertically aligned by 16)\n    surface->SetCrop(0, 0, encode_width, encode_height);\n\n    // Set per-frame properties\n    if (force_idr) {\n      if (video_format == 0) {\n        surface->SetProperty(AMF_VIDEO_ENCODER_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_PICTURE_TYPE_IDR);\n        surface->SetProperty(AMF_VIDEO_ENCODER_INSERT_SPS, true);\n        surface->SetProperty(AMF_VIDEO_ENCODER_INSERT_PPS, true);\n      }\n      else if (video_format == 1) {\n        surface->SetProperty(AMF_VIDEO_ENCODER_HEVC_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_HEVC_PICTURE_TYPE_IDR);\n        surface->SetProperty(AMF_VIDEO_ENCODER_HEVC_INSERT_HEADER, true);\n      }\n      else {\n        surface->SetProperty(AMF_VIDEO_ENCODER_AV1_FORCE_FRAME_TYPE, AMF_VIDEO_ENCODER_AV1_FORCE_FRAME_TYPE_KEY);\n        surface->SetProperty(AMF_VIDEO_ENCODER_AV1_FORCE_INSERT_SEQUENCE_HEADER, true);\n      }\n\n      // After IDR, mark LTR slot 0 for RFI baseline\n      if (effective_ltr_slots > 0) {\n        if (video_format == 0) {\n          surface->SetProperty(AMF_VIDEO_ENCODER_MARK_CURRENT_WITH_LTR_INDEX, (amf_int64) 0);\n        }\n        else if (video_format == 1) {\n          surface->SetProperty(AMF_VIDEO_ENCODER_HEVC_MARK_CURRENT_WITH_LTR_INDEX, (amf_int64) 0);\n        }\n        else {\n          surface->SetProperty(AMF_VIDEO_ENCODER_AV1_MARK_CURRENT_WITH_LTR_INDEX, (amf_int64) 0);\n        }\n        ltr_slots_valid[0] = true;\n        ltr_slot_frame_index[0] = frame_index;\n        current_ltr_slot = 1 % effective_ltr_slots;\n      }\n    }\n    else if (rfi_pending && effective_ltr_slots > 0) {\n      // After RFI: force reference to the saved LTR frame\n      int64_t ltr_bitfield = 1LL << last_rfi_ltr_index;\n\n      if (video_format == 0) {\n        surface->SetProperty(AMF_VIDEO_ENCODER_FORCE_LTR_REFERENCE_BITFIELD, ltr_bitfield);\n      }\n      else if (video_format == 1) {\n        surface->SetProperty(AMF_VIDEO_ENCODER_HEVC_FORCE_LTR_REFERENCE_BITFIELD, ltr_bitfield);\n      }\n      else {\n        surface->SetProperty(AMF_VIDEO_ENCODER_AV1_FORCE_LTR_REFERENCE_BITFIELD, ltr_bitfield);\n      }\n\n      rfi_pending = false;\n      result.after_ref_frame_invalidation = true;\n    }\n    else if (effective_ltr_slots > 0 && (frame_index % LTR_MARK_INTERVAL) == 0) {\n      // Periodically mark current frame as LTR for future RFI use\n      // Only mark every LTR_MARK_INTERVAL frames to avoid limiting encoder reference freedom\n      // Only use slots < effective_ltr_slots (clamped to max_ltr_frames)\n      if (video_format == 0) {\n        surface->SetProperty(AMF_VIDEO_ENCODER_MARK_CURRENT_WITH_LTR_INDEX, (amf_int64) current_ltr_slot);\n      }\n      else if (video_format == 1) {\n        surface->SetProperty(AMF_VIDEO_ENCODER_HEVC_MARK_CURRENT_WITH_LTR_INDEX, (amf_int64) current_ltr_slot);\n      }\n      else {\n        surface->SetProperty(AMF_VIDEO_ENCODER_AV1_MARK_CURRENT_WITH_LTR_INDEX, (amf_int64) current_ltr_slot);\n      }\n      ltr_slots_valid[current_ltr_slot] = true;\n      ltr_slot_frame_index[current_ltr_slot] = frame_index;\n      current_ltr_slot = (current_ltr_slot + 1) % effective_ltr_slots;\n    }\n\n    // Submit input — retry with output draining if input queue is full (like FFmpeg)\n    res = encoder->SubmitInput(surface);\n    if (res == AMF_INPUT_FULL) {\n      // Drain output to free up space in the encoder queue, then retry\n      for (int retry = 0; retry < 20 && res == AMF_INPUT_FULL; ++retry) {\n        ::amf::AMFDataPtr drain_data;\n        auto drain_res = encoder->QueryOutput(&drain_data);\n        if (drain_data) {\n          // Stash the output for later retrieval\n          pending_output = drain_data;\n        }\n        if (drain_res != AMF_OK && !drain_data) {\n          if (!query_timeout_supported) {\n            std::this_thread::sleep_for(std::chrono::milliseconds(1));\n          }\n        }\n        res = encoder->SubmitInput(surface);\n      }\n      if (res == AMF_INPUT_FULL) {\n        BOOST_LOG(warning) << \"AMF: SubmitInput still AMF_INPUT_FULL after retries, dropping frame \" << frame_index;\n        return result;\n      }\n    }\n    if (res != AMF_OK) {\n      BOOST_LOG(error) << \"AMF: SubmitInput failed, error: \" << res;\n      // Check if the D3D11 device is lost (TDR, driver crash, etc.)\n      if (device) {\n        auto removed_reason = device->GetDeviceRemovedReason();\n        if (removed_reason != S_OK) {\n          BOOST_LOG(error) << \"AMF: D3D11 device lost after SubmitInput, reason: 0x\" << util::hex(removed_reason).to_string_view();\n        }\n      }\n      return result;\n    }\n\n    // Query output — if we already drained output during SubmitInput retry, use that\n    ::amf::AMFDataPtr output_data;\n    if (pending_output) {\n      output_data = pending_output;\n      pending_output = nullptr;\n    }\n    else {\n      // Poll with retry: encoder may need a moment after SubmitInput\n      for (int poll = 0; poll < 10; ++poll) {\n        res = encoder->QueryOutput(&output_data);\n        if (output_data || (res != AMF_REPEAT && res != AMF_NEED_MORE_INPUT)) {\n          break;\n        }\n        // Only sleep manually if driver doesn't support QUERY_TIMEOUT;\n        // when supported, QueryOutput() blocks internally for up to 1ms\n        if (!query_timeout_supported) {\n          std::this_thread::sleep_for(std::chrono::milliseconds(1));\n        }\n      }\n      if (!output_data) {\n        // Encoder needs more input or no output yet (pipeline filling)\n        return result;\n      }\n    }\n\n    // Extract encoded bitstream\n    ::amf::AMFBufferPtr buffer(output_data);\n    if (!buffer) {\n      BOOST_LOG(error) << \"AMF: output is not a buffer\";\n      return result;\n    }\n\n    auto data_ptr = static_cast<uint8_t *>(buffer->GetNative());\n    auto data_size = buffer->GetSize();\n    result.data.assign(data_ptr, data_ptr + data_size);\n\n    // Check if output frame is IDR\n    amf_int64 output_type = 0;\n    if (video_format == 0) {\n      if (output_data->GetProperty(AMF_VIDEO_ENCODER_OUTPUT_DATA_TYPE, &output_type) == AMF_OK) {\n        result.idr = (output_type == AMF_VIDEO_ENCODER_OUTPUT_DATA_TYPE_IDR);\n      }\n    }\n    else if (video_format == 1) {\n      if (output_data->GetProperty(AMF_VIDEO_ENCODER_HEVC_OUTPUT_DATA_TYPE, &output_type) == AMF_OK) {\n        result.idr = (output_type == AMF_VIDEO_ENCODER_HEVC_OUTPUT_DATA_TYPE_IDR);\n      }\n    }\n    else {\n      if (output_data->GetProperty(AMF_VIDEO_ENCODER_AV1_OUTPUT_FRAME_TYPE, &output_type) == AMF_OK) {\n        result.idr = (output_type == AMF_VIDEO_ENCODER_AV1_OUTPUT_FRAME_TYPE_KEY);\n      }\n    }\n\n    // Statistics feedback logging (only if enabled and at debug level)\n    if (statistics_enabled) {\n      amf_int64 avg_qp = 0;\n      const wchar_t *avg_qp_prop = (video_format == 0) ? AMF_VIDEO_ENCODER_STATISTIC_AVERAGE_QP :\n                                   (video_format == 1) ? AMF_VIDEO_ENCODER_HEVC_STATISTIC_AVERAGE_QP :\n                                                         AMF_VIDEO_ENCODER_AV1_STATISTIC_AVERAGE_Q_INDEX;\n      if (output_data->GetProperty(avg_qp_prop, &avg_qp) == AMF_OK) {\n        BOOST_LOG(debug) << \"AMF: frame \" << frame_index << \" avg_qp=\" << avg_qp << \" size=\" << data_size;\n      }\n    }\n    if (psnr_enabled) {\n      double psnr_y = 0;\n      const wchar_t *psnr_prop = (video_format == 0) ? AMF_VIDEO_ENCODER_STATISTIC_PSNR_Y :\n                                 (video_format == 1) ? AMF_VIDEO_ENCODER_HEVC_STATISTIC_PSNR_Y :\n                                                       AMF_VIDEO_ENCODER_AV1_STATISTIC_PSNR_Y;\n      if (output_data->GetProperty(psnr_prop, &psnr_y) == AMF_OK) {\n        BOOST_LOG(debug) << \"AMF: frame \" << frame_index << \" PSNR_Y=\" << psnr_y;\n      }\n    }\n    if (ssim_enabled) {\n      double ssim_y = 0;\n      const wchar_t *ssim_prop = (video_format == 0) ? AMF_VIDEO_ENCODER_STATISTIC_SSIM_Y :\n                                 (video_format == 1) ? AMF_VIDEO_ENCODER_HEVC_STATISTIC_SSIM_Y :\n                                                       AMF_VIDEO_ENCODER_AV1_STATISTIC_SSIM_Y;\n      if (output_data->GetProperty(ssim_prop, &ssim_y) == AMF_OK) {\n        BOOST_LOG(debug) << \"AMF: frame \" << frame_index << \" SSIM_Y=\" << ssim_y;\n      }\n    }\n\n    return result;\n  }\n\n  bool\n  amf_d3d11::invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) {\n    if (!encoder || effective_ltr_slots <= 0) return false;\n\n    // Find a valid LTR slot whose frame was marked BEFORE the invalidation range.\n    // This ensures we reference a frame that predates the corrupted frames.\n    int best_ltr = -1;\n    uint64_t best_frame = 0;\n    for (int i = 0; i < effective_ltr_slots; i++) {\n      if (ltr_slots_valid[i] && ltr_slot_frame_index[i] < first_frame) {\n        if (best_ltr < 0 || ltr_slot_frame_index[i] > best_frame) {\n          best_ltr = i;\n          best_frame = ltr_slot_frame_index[i];\n        }\n      }\n    }\n\n    if (best_ltr < 0) {\n      BOOST_LOG(warning) << \"AMF: RFI failed, no valid LTR frame before frame \" << first_frame;\n      return false;\n    }\n\n    // Invalidate all LTR slots that overlap the invalidation range\n    for (int i = 0; i < effective_ltr_slots; i++) {\n      if (ltr_slots_valid[i] && ltr_slot_frame_index[i] >= first_frame && ltr_slot_frame_index[i] <= last_frame) {\n        ltr_slots_valid[i] = false;\n      }\n    }\n\n    last_rfi_ltr_index = best_ltr;\n    rfi_pending = true;\n\n    BOOST_LOG(info) << \"AMF: RFI pending, using LTR index \" << best_ltr\n                    << \" (frame \" << best_frame << \") for invalidated frames \" << first_frame << \"-\" << last_frame;\n    return true;\n  }\n\n  void\n  amf_d3d11::set_bitrate(int bitrate_kbps) {\n    if (!encoder) return;\n\n    auto bitrate = static_cast<int64_t>(bitrate_kbps) * 1000;\n    AMF_RESULT res;\n\n    if (video_format == 0) {\n      res = encoder->SetProperty(AMF_VIDEO_ENCODER_TARGET_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_PEAK_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_VBV_BUFFER_SIZE, bitrate);\n    }\n    else if (video_format == 1) {\n      res = encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_TARGET_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_PEAK_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_VBV_BUFFER_SIZE, bitrate);\n    }\n    else {\n      res = encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_TARGET_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_PEAK_BITRATE, bitrate);\n      encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_VBV_BUFFER_SIZE, bitrate);\n    }\n\n    if (res == AMF_OK) {\n      BOOST_LOG(info) << \"AMF: bitrate dynamically changed to \" << bitrate_kbps << \" Kbps\";\n    }\n    else {\n      BOOST_LOG(warning) << \"AMF: set_bitrate failed, error: \" << res;\n    }\n  }\n\n  void\n  amf_d3d11::set_hdr_metadata(const std::optional<amf_hdr_metadata> &metadata) {\n    if (!encoder || !context) return;\n\n    if (metadata && video_format >= 0) {\n      // Create AMFBuffer containing AMFHDRMetadata\n      ::amf::AMFBufferPtr hdr_buffer;\n      auto res = context->AllocBuffer(::amf::AMF_MEMORY_HOST, sizeof(AMFHDRMetadata), &hdr_buffer);\n      if (res != AMF_OK || !hdr_buffer) {\n        BOOST_LOG(warning) << \"AMF: failed to allocate HDR metadata buffer\";\n        return;\n      }\n\n      auto *amf_hdr = static_cast<AMFHDRMetadata *>(hdr_buffer->GetNative());\n      // Display primaries: both normalized to 50,000\n      amf_hdr->redPrimary[0] = metadata->displayPrimaries[0].x;\n      amf_hdr->redPrimary[1] = metadata->displayPrimaries[0].y;\n      amf_hdr->greenPrimary[0] = metadata->displayPrimaries[1].x;\n      amf_hdr->greenPrimary[1] = metadata->displayPrimaries[1].y;\n      amf_hdr->bluePrimary[0] = metadata->displayPrimaries[2].x;\n      amf_hdr->bluePrimary[1] = metadata->displayPrimaries[2].y;\n      amf_hdr->whitePoint[0] = metadata->whitePoint.x;\n      amf_hdr->whitePoint[1] = metadata->whitePoint.y;\n      // maxMasteringLuminance: AMF expects nits * 10000, SS_HDR_METADATA provides nits\n      amf_hdr->maxMasteringLuminance = static_cast<amf_uint32>(metadata->maxDisplayLuminance) * 10000;\n      // minMasteringLuminance: both in 1/10000th of a nit\n      amf_hdr->minMasteringLuminance = metadata->minDisplayLuminance;\n      amf_hdr->maxContentLightLevel = metadata->maxContentLightLevel;\n      amf_hdr->maxFrameAverageLightLevel = metadata->maxFrameAverageLightLevel;\n\n      // Set HDR metadata on encoder\n      if (video_format == 0) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_INPUT_HDR_METADATA, hdr_buffer);\n      }\n      else if (video_format == 1) {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_INPUT_HDR_METADATA, hdr_buffer);\n      }\n      else {\n        encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_INPUT_HDR_METADATA, hdr_buffer);\n      }\n\n      BOOST_LOG(info) << \"AMF: HDR metadata set (max luminance: \" << metadata->maxDisplayLuminance << \" nits)\";\n    }\n  }\n\n  void *\n  amf_d3d11::get_input_texture() {\n    return input_texture;\n  }\n\n  std::unique_ptr<amf_d3d11>\n  create_amf_d3d11(ID3D11Device *d3d_device) {\n    if (!d3d_device) return nullptr;\n\n    auto enc = std::make_unique<amf_d3d11>(d3d_device);\n    return enc;\n  }\n\n}  // namespace amf\n"
  },
  {
    "path": "src/amf/amf_d3d11.h",
    "content": "/**\n * @file src/amf/amf_d3d11.h\n * @brief Declarations for AMF D3D11 encoder.\n */\n#pragma once\n\n#include \"amf_encoder.h\"\n\n#include <d3d11.h>\n#include <memory>\n#include <string>\n\n#include <AMF/components/Component.h>\n#include <AMF/core/Context.h>\n#include <AMF/core/Data.h>\n#include <AMF/core/Factory.h>\n\nnamespace amf {\n\n  /**\n   * @brief AMF encoder using D3D11 for texture input.\n   */\n  class amf_d3d11: public amf_encoder {\n  public:\n    explicit amf_d3d11(ID3D11Device *d3d_device);\n    ~amf_d3d11();\n\n    bool\n    create_encoder(const amf_config &config,\n      const video::config_t &client_config,\n      const video::sunshine_colorspace_t &colorspace,\n      platf::pix_fmt_e buffer_format) override;\n\n    void\n    destroy_encoder() override;\n\n    amf_encoded_frame\n    encode_frame(uint64_t frame_index, bool force_idr) override;\n\n    bool\n    invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) override;\n\n    void\n    set_bitrate(int bitrate_kbps) override;\n\n    void\n    set_hdr_metadata(const std::optional<amf_hdr_metadata> &metadata) override;\n\n    void *\n    get_input_texture() override;\n\n  private:\n    bool\n    init_amf_library();\n\n    bool\n    configure_encoder(const amf_config &config,\n      const video::config_t &client_config,\n      const video::sunshine_colorspace_t &colorspace);\n\n    AMF_SURFACE_FORMAT\n    get_amf_format(platf::pix_fmt_e buffer_format, int bit_depth);\n\n    const wchar_t *\n    get_codec_id();\n\n    bool\n    set_ltr_property(const wchar_t *name, int64_t value);\n\n    template<typename T>\n    void\n    set_codec_property(const wchar_t *h264_name, const wchar_t *hevc_name, const wchar_t *av1_name, T value);\n\n    ID3D11Device *device = nullptr;\n    ::amf::AMFFactory *factory = nullptr;\n    ::amf::AMFContextPtr context;\n    ::amf::AMFComponentPtr encoder;\n    HMODULE amf_dll = nullptr;\n\n    // Input texture that the rendering pipeline writes to\n    ID3D11Texture2D *input_texture = nullptr;\n\n    // Encoder state\n    video::config_t current_config {};\n    int video_format = 0;  // 0=H264, 1=HEVC, 2=AV1\n    AMF_SURFACE_FORMAT surface_format = AMF_SURFACE_NV12;\n    int encode_width = 0;\n    int encode_height = 0;\n    bool rfi_pending = false;\n    uint64_t last_rfi_ltr_index = 0;\n    int max_ltr_frames = 0;\n\n    // Whether the driver supports QUERY_TIMEOUT (FFmpeg-style safety check)\n    bool query_timeout_supported = false;\n\n    // Current LTR state for RFI\n    static constexpr int MAX_LTR_SLOTS = 2;\n    static constexpr uint64_t LTR_MARK_INTERVAL = 30;  // Mark LTR every N frames\n    int effective_ltr_slots = 0;    // Clamped to min(max_ltr_frames, MAX_LTR_SLOTS)\n    int current_ltr_slot = 0;      // Which LTR slot to mark next\n    bool ltr_slots_valid[MAX_LTR_SLOTS] = {};\n    uint64_t ltr_slot_frame_index[MAX_LTR_SLOTS] = {};  // Frame index when each LTR slot was marked\n\n    // Pending output stashed during SubmitInput retry\n    ::amf::AMFDataPtr pending_output;\n\n    // Statistics feedback state\n    bool statistics_enabled = false;\n    bool psnr_enabled = false;\n    bool ssim_enabled = false;\n\n    std::string last_error_string;\n  };\n\n  /**\n   * @brief Create an AMF D3D11 encoder instance.\n   * @param d3d_device The D3D11 device to use.\n   * @return AMF encoder or nullptr on failure.\n   */\n  std::unique_ptr<amf_d3d11>\n  create_amf_d3d11(ID3D11Device *d3d_device);\n\n}  // namespace amf\n"
  },
  {
    "path": "src/amf/amf_encoded_frame.h",
    "content": "/**\n * @file src/amf/amf_encoded_frame.h\n * @brief Declarations for AMF encoded frame.\n */\n#pragma once\n\n#include <cstdint>\n#include <vector>\n\nnamespace amf {\n\n  /**\n   * @brief Encoded frame from AMF encoder.\n   */\n  struct amf_encoded_frame {\n    std::vector<uint8_t> data;\n    uint64_t frame_index = 0;\n    bool idr = false;\n    bool after_ref_frame_invalidation = false;\n  };\n\n}  // namespace amf\n"
  },
  {
    "path": "src/amf/amf_encoder.h",
    "content": "/**\n * @file src/amf/amf_encoder.h\n * @brief Declarations for standalone AMF encoder interface.\n */\n#pragma once\n\n#include \"amf_config.h\"\n#include \"amf_encoded_frame.h\"\n\n#include \"src/platform/common.h\"\n#include \"src/video.h\"\n#include \"src/video_colorspace.h\"\n\nnamespace amf {\n\n  /**\n   * @brief Standalone AMF encoder interface.\n   */\n  class amf_encoder {\n  public:\n    virtual ~amf_encoder() = default;\n\n    /**\n     * @brief Create the encoder.\n     * @param config AMF encoder configuration.\n     * @param client_config Stream configuration requested by the client.\n     * @param colorspace YUV colorspace.\n     * @param buffer_format Platform-agnostic input surface format.\n     * @return `true` on success, `false` on error\n     */\n    virtual bool\n    create_encoder(const amf_config &config,\n      const video::config_t &client_config,\n      const video::sunshine_colorspace_t &colorspace,\n      platf::pix_fmt_e buffer_format) = 0;\n\n    /**\n     * @brief Destroy the encoder.\n     */\n    virtual void\n    destroy_encoder() = 0;\n\n    /**\n     * @brief Encode the next frame using platform-specific input surface.\n     * @param frame_index Unique frame identifier.\n     * @param force_idr Whether to encode frame as forced IDR.\n     * @return Encoded frame.\n     */\n    virtual amf_encoded_frame\n    encode_frame(uint64_t frame_index, bool force_idr) = 0;\n\n    /**\n     * @brief Perform reference frame invalidation (RFI).\n     * @param first_frame First frame index of the invalidation range.\n     * @param last_frame Last frame index of the invalidation range.\n     * @return `true` on success, `false` on error (caller should force IDR).\n     */\n    virtual bool\n    invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) = 0;\n\n    /**\n     * @brief Set the bitrate for the encoder dynamically.\n     * @param bitrate_kbps Bitrate in kilobits per second.\n     */\n    virtual void\n    set_bitrate(int bitrate_kbps) = 0;\n\n    /**\n     * @brief Set HDR metadata for the encoder.\n     * @param metadata HDR metadata, or nullopt to disable.\n     */\n    virtual void\n    set_hdr_metadata(const std::optional<amf_hdr_metadata> &metadata) = 0;\n\n    /**\n     * @brief Get the D3D11 input texture the encoder reads from.\n     * @return Pointer to ID3D11Texture2D, or nullptr.\n     */\n    virtual void *\n    get_input_texture() = 0;\n  };\n\n}  // namespace amf\n"
  },
  {
    "path": "src/assets/abr_prompt.md",
    "content": "You are an adaptive bitrate controller for a game streaming server. Analyze the network metrics and active application to decide the optimal encoding bitrate.\n\n## Current State\n- Active Window: {{FOREGROUND_TITLE}}\n- Active Process: {{FOREGROUND_EXE}}\n- Mode: {{MODE}}\n- Current bitrate: {{CURRENT_BITRATE}} Kbps\n- Allowed range: [{{MIN_BITRATE}}, {{MAX_BITRATE}}] Kbps\n\n## Recent Network Feedback (newest first)\n{{RECENT_FEEDBACK}}\n\n## Application-Aware Bitrate Target\nIdentify the running application from Active Window and Process name.\n\n### Step 1: Determine base target by interaction type (within allowed range)\n- Fast-paced FPS/Racing (CS2, Forza, Apex): base = 80-100% of max -> {{FPS_RANGE}}\n- Action/Adventure (Elden Ring, GTA V): base = 60-80% of max -> {{ACTION_RANGE}}\n- Strategy/Turn-based (Civilization, XCOM): base = 40-60% of max -> {{STRATEGY_RANGE}}\n- Desktop/Productivity (explorer.exe, chrome, browsers): base = 20-30% of max -> {{DESKTOP_RANGE}}\n\n### Step 2: Adjust for visual complexity (apply to the base range)\n- Anime/cel-shaded (Genshin Impact, Honkai, Persona): reduce by 10-20% (flat colors, repetitive textures compress well)\n- Pixel art/2D (Terraria, Stardew Valley, retro games): reduce by 20-30% (extremely compressible)\n- Photorealistic/high-detail (RDR2, Flight Simulator, Forza): keep at upper end (complex textures compress poorly)\n- Dark/horror scenes (Resident Evil, Dead Space): keep moderate (dark gradients show artifacts at low bitrate)\n- Unknown application: use base target without adjustment\n\nIMPORTANT: If current bitrate differs significantly from the adjusted target, you MUST adjust toward it.\n\n## Adjustment Rules\n1. Max change per decision: 15% of current bitrate (for stability)\n2. If network loss > 5%: override max change, reduce by 25-35%\n3. If network loss 2-5% sustained: reduce by 10-20%\n4. If network stable and current != target: adjust toward target by up to 10% per step\n5. Never exceed allowed range [{{MIN_BITRATE}}, {{MAX_BITRATE}}]\n\n## Response\nJSON only: {\"bitrate\": <integer_kbps>, \"reason\": \"<reason>\"}\nSet bitrate to 0 ONLY if current is within 5% of the type-appropriate target AND network is stable.\n"
  },
  {
    "path": "src/audio.cpp",
    "content": "/**\n * @file src/audio.cpp\n * @brief Definitions for audio capture and encoding.\n */\n// standard includes\n#include <thread>\n\n// lib includes\n#include <opus/opus_multistream.h>\n\n// local includes\n#include \"audio.h\"\n#include \"config.h\"\n#include \"globals.h\"\n#include \"logging.h\"\n#include \"platform/common.h\"\n#include \"thread_safe.h\"\n#include \"utility.h\"\n\nnamespace audio {\n  using namespace std::literals;\n  using opus_t = util::safe_ptr<OpusMSEncoder, opus_multistream_encoder_destroy>;\n  using sample_queue_t = std::shared_ptr<safe::queue_t<std::vector<float>>>;\n\n  static int start_audio_control(audio_ctx_t &ctx);\n  static void stop_audio_control(audio_ctx_t &);\n  static void apply_surround_params(opus_stream_config_t &stream, const stream_params_t &params);\n\n  int map_stream(int channels, bool quality);\n\n  constexpr auto SAMPLE_RATE = 48000;\n\n  // NOTE: If you adjust the bitrates listed here, make sure to update the\n  // corresponding bitrate adjustment logic in rtsp_stream::cmd_announce()\n  opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {\n    {\n      SAMPLE_RATE,\n      2,\n      1,\n      1,\n      platf::speaker::map_stereo,\n      96000,\n    },\n    {\n      SAMPLE_RATE,\n      2,\n      1,\n      1,\n      platf::speaker::map_stereo,\n      512000,\n    },\n    {\n      SAMPLE_RATE,\n      6,\n      4,\n      2,\n      platf::speaker::map_surround51,\n      256000,\n    },\n    {\n      SAMPLE_RATE,\n      6,\n      6,\n      0,\n      platf::speaker::map_surround51,\n      1536000,\n    },\n    {\n      SAMPLE_RATE,\n      8,\n      5,\n      3,\n      platf::speaker::map_surround71,\n      450000,\n    },\n    {\n      SAMPLE_RATE,\n      8,\n      8,\n      0,\n      platf::speaker::map_surround71,\n      2048000,\n    },\n    {\n      SAMPLE_RATE,\n      12,\n      8,\n      4,\n      platf::speaker::map_surround714,\n      600000,\n    },\n    {\n      SAMPLE_RATE,\n      12,\n      12,\n      0,\n      platf::speaker::map_surround714,\n      3072000,\n    },\n  };\n\n  void encodeThread(sample_queue_t samples, config_t config, void *channel_data) {\n    auto packets = mail::man->queue<packet_t>(mail::audio_packets);\n    auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];\n    if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) {\n      apply_surround_params(stream, config.customStreamParams);\n    }\n\n    // Encoding takes place on this thread\n    platf::adjust_thread_priority(platf::thread_priority_e::high);\n\n    opus_t opus {opus_multistream_encoder_create(\n      stream.sampleRate,\n      stream.channelCount,\n      stream.streams,\n      stream.coupledStreams,\n      stream.mapping,\n      OPUS_APPLICATION_RESTRICTED_LOWDELAY,\n      nullptr\n    )};\n\n    opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(stream.bitrate));\n    opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0));\n    opus_multistream_encoder_ctl(opus.get(), OPUS_SET_COMPLEXITY(10));\n\n    // Note: In-band FEC (OPUS_SET_INBAND_FEC) is a SILK-only feature and has no effect\n    // in RESTRICTED_LOWDELAY mode (CELT-only). DRED is the CELT equivalent.\n\n#ifdef OPUS_SET_DRED_DURATION_REQUEST  // Opus >= 1.5.0\n    // DRED (Deep REDundancy): ML-based redundancy for graceful packet loss recovery\n    // Works with CELT mode (RESTRICTED_LOWDELAY). Embeds redundancy in each packet\n    // allowing the decoder to recover up to 100ms of lost audio from subsequent packets.\n    opus_multistream_encoder_ctl(opus.get(), OPUS_SET_DRED_DURATION(100));\n    BOOST_LOG(info) << \"Opus DRED enabled: 100ms redundancy\"sv;\n#endif\n\n    BOOST_LOG(info) << \"Opus initialized: \"sv << stream.sampleRate / 1000 << \" kHz, \"sv\n                    << stream.channelCount << \" channels, \"sv\n                    << stream.bitrate / 1000 << \" kbps (total), LOWDELAY\"sv;\n\n    auto frame_size = config.packetDuration * stream.sampleRate / 1000;\n    while (auto sample = samples->pop()) {\n      buffer_t packet {1400};\n\n      int bytes = opus_multistream_encode_float(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size());\n      if (bytes < 0) {\n        BOOST_LOG(error) << \"Couldn't encode audio: \"sv << opus_strerror(bytes);\n        packets->stop();\n\n        return;\n      }\n\n      packet.fake_resize(bytes);\n      packets->raise(channel_data, std::move(packet));\n    }\n  }\n\n  void capture(safe::mail_t mail, config_t config, void *channel_data) {\n    auto shutdown_event = mail->event<bool>(mail::shutdown);\n    if (!config::audio.stream) {\n      BOOST_LOG(info) << \"Audio streaming is disabled in configuration\";\n      shutdown_event->view();\n      return;\n    }\n    auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];\n    if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) {\n      apply_surround_params(stream, config.customStreamParams);\n    }\n\n    BOOST_LOG(debug) << \"Audio capture: acquiring context reference\";\n    auto ref = get_audio_ctx_ref();\n    if (!ref) {\n      BOOST_LOG(error) << \"Audio capture: failed to get context reference\";\n      return;\n    }\n    BOOST_LOG(debug) << \"Audio capture: context reference acquired successfully\";\n\n    auto init_failure_fg = util::fail_guard([&shutdown_event]() {\n      BOOST_LOG(error) << \"Unable to initialize audio capture. The stream will not have audio.\"sv;\n\n      // Wait for shutdown to be signalled if we fail init.\n      // This allows streaming to continue without audio.\n      shutdown_event->view();\n    });\n\n    auto &control = ref->control;\n    if (!control) {\n      BOOST_LOG(error) << \"Audio capture: control is null\";\n      return;\n    }\n\n    // Order of priority:\n    // 1. Virtual sink\n    // 2. Audio sink\n    // 3. Host\n    std::string *sink = &ref->sink.host;\n    if (!config::audio.sink.empty()) {\n      sink = &config::audio.sink;\n    }\n\n    // Prefer the virtual sink if host playback is disabled or there's no other sink\n    if (ref->sink.null && (!config.flags[config_t::HOST_AUDIO] || sink->empty())) {\n      auto &null = *ref->sink.null;\n      switch (stream.channelCount) {\n        case 2:\n          sink = &null.stereo;\n          break;\n        case 6:\n          sink = &null.surround51;\n          break;\n        case 8:\n          sink = &null.surround71;\n          break;\n        case 12:\n          if (!null.surround714.empty()) {\n            sink = &null.surround714;\n          }\n          break;\n      }\n    }\n\n    // Only the first to start a session may change the default sink\n    if (!ref->sink_flag->exchange(true, std::memory_order_acquire)) {\n      // If the selected sink is different than the current one, change sinks.\n      ref->restore_sink = ref->sink.host != *sink;\n      if (ref->restore_sink) {\n        if (control->set_sink(*sink)) {\n          return;\n        }\n      }\n    }\n\n    auto frame_size = config.packetDuration * stream.sampleRate / 1000;\n    auto mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size);\n    if (!mic) {\n      BOOST_LOG(error) << \"Audio capture: failed to initialize microphone\";\n      return;\n    }\n\n    // Audio is initialized, so we don't want to print the failure message\n    init_failure_fg.disable();\n    BOOST_LOG(info) << \"Audio capture initialized successfully, entering sampling loop\";\n\n    // Capture takes place on this thread\n    platf::adjust_thread_priority(platf::thread_priority_e::critical);\n\n    auto samples = std::make_shared<sample_queue_t::element_type>(30);\n    std::thread thread {encodeThread, samples, config, channel_data};\n\n    auto fg = util::fail_guard([&]() {\n      samples->stop();\n      thread.join();\n\n      shutdown_event->view();\n    });\n\n    int samples_per_frame = frame_size * stream.channelCount;\n\n    while (!shutdown_event->peek()) {\n      std::vector<float> sample_buffer;\n      sample_buffer.resize(samples_per_frame);\n\n      auto status = mic->sample(sample_buffer);\n      switch (status) {\n        case platf::capture_e::ok:\n          break;\n        case platf::capture_e::timeout:\n          continue;\n        case platf::capture_e::reinit:\n          BOOST_LOG(info) << \"Reinitializing audio capture\"sv;\n          mic.reset();\n          do {\n            mic = control->microphone(stream.mapping, stream.channelCount, stream.sampleRate, frame_size);\n            if (!mic) {\n              BOOST_LOG(warning) << \"Couldn't re-initialize audio input\"sv;\n            }\n          } while (!mic && !shutdown_event->view(5s));\n          continue;\n        default:\n          return;\n      }\n\n      samples->raise(std::move(sample_buffer));\n    }\n    \n    BOOST_LOG(info) << \"Audio capture sampling loop ended (shutdown requested)\";\n  }\n\n  // 确保唯一实例\n  namespace {\n    auto control_shared = safe::make_shared<audio_ctx_t>(start_audio_control, stop_audio_control);\n  }\n\n  audio_ctx_ref_t get_audio_ctx_ref() {\n    return control_shared.ref();\n  }\n\n  // 检查音频上下文是否有活动的引用，不触发构造\n  bool has_audio_ctx_ref() {\n    return control_shared.has_ref();\n  }\n\n  bool is_audio_ctx_sink_available(const audio_ctx_t &ctx) {\n    if (!ctx.control) {\n      return false;\n    }\n\n    const std::string &sink = ctx.sink.host.empty() ? config::audio.sink : ctx.sink.host;\n    if (sink.empty()) {\n      return false;\n    }\n\n    return ctx.control->is_sink_available(sink);\n  }\n\n  int map_stream(int channels, bool quality) {\n    int shift = quality ? 1 : 0;\n    switch (channels) {\n      case 2:\n        return STEREO + shift;\n      case 6:\n        return SURROUND51 + shift;\n      case 8:\n        return SURROUND71 + shift;\n      case 12:\n        return SURROUND714 + shift;\n    }\n    if (channels >= 12) {\n      return SURROUND714 + shift;\n    }\n    if (channels >= 8) {\n      return SURROUND71 + shift;\n    }\n    return STEREO;\n  }\n\n  int start_audio_control(audio_ctx_t &ctx) {\n    auto fg = util::fail_guard([]() {\n      BOOST_LOG(warning) << \"There will be no audio\"sv;\n    });\n\n    ctx.sink_flag = std::make_unique<std::atomic_bool>(false);\n\n    // The default sink has not been replaced yet.\n    ctx.restore_sink = false;\n\n    if (!(ctx.control = platf::audio_control())) {\n      return 0;\n    }\n\n    auto sink = ctx.control->sink_info();\n    if (!sink) {\n      // Let the calling code know it failed\n      ctx.control.reset();\n      return 0;\n    }\n\n    ctx.sink = std::move(*sink);\n\n    fg.disable();\n    return 0;\n  }\n\n  void stop_audio_control(audio_ctx_t &ctx) {\n    // restore audio-sink if applicable\n    if (!ctx.restore_sink) {\n      return;\n    }\n\n    // 检查 control 是否存在，如果不存在则无法恢复 sink\n    if (!ctx.control) {\n      BOOST_LOG(debug) << \"Audio control not available, skipping sink restoration\";\n      return;\n    }\n\n    // Change back to the host sink, unless there was none\n    const std::string &sink = ctx.sink.host.empty() ? config::audio.sink : ctx.sink.host;\n    if (!sink.empty()) {\n      // Best effort, it's allowed to fail\n      ctx.control->set_sink(sink);\n    }\n  }\n\n  void apply_surround_params(opus_stream_config_t &stream, const stream_params_t &params) {\n    stream.channelCount = params.channelCount;\n    stream.streams = params.streams;\n    stream.coupledStreams = params.coupledStreams;\n    stream.mapping = params.mapping;\n  }\n\n  int init_mic_redirect_device() {\n    // 关键修复：先检查是否有活动的引用，避免触发 start_audio_control\n    // 如果没有活动的引用，说明音频上下文没有启动，不应该初始化麦克风设备\n    if (!has_audio_ctx_ref()) {\n      BOOST_LOG(debug) << \"Audio context not active, skipping microphone device initialization\";\n      return -1;\n    }\n    \n    auto ref = get_audio_ctx_ref();\n    if (!ref || !ref->control) {\n      BOOST_LOG(error) << \"Audio context not available for microphone data writing\";\n      return -1;\n    }\n    return ref->control->init_mic_redirect_device();\n  }\n\n  void release_mic_redirect_device() {\n    // 关键修复：先检查是否有活动的引用，避免触发 start_audio_control\n    // 如果没有活动的引用，说明音频上下文没有启动，不需要释放\n    if (!has_audio_ctx_ref()) {\n      BOOST_LOG(debug) << \"Audio context not active, skipping microphone device release\";\n      return;\n    }\n    \n    auto ref = get_audio_ctx_ref();\n    if (!ref || !ref->control) {\n      BOOST_LOG(warning) << \"Audio context not available for microphone device release\";\n      return;\n    }\n    ref->control->release_mic_redirect_device();\n  }\n\n  int write_mic_data(const std::uint8_t *data, size_t size, uint16_t seq) {\n    // 先检查是否有活动引用，避免不必要地触发 start_audio_control\n    // 如果音频捕获线程正在运行，它会持有引用，这里会返回 true\n    if (!has_audio_ctx_ref()) {\n      BOOST_LOG(debug) << \"Audio context not active, skipping microphone data write\";\n      // 注意：这不是错误，而是正常情况\n      // 可能音频捕获还没有启动，或者已经停止\n      return -1;\n    }\n    \n    auto ref = get_audio_ctx_ref();\n    if (!ref || !ref->control) {\n      BOOST_LOG(warning) << \"Audio context reference invalid for microphone data writing\";\n      return -1;\n    }\n\n    return ref->control->write_mic_data(reinterpret_cast<const char*>(data), size, seq);\n  }\n}  // namespace audio"
  },
  {
    "path": "src/audio.h",
    "content": "/**\n * @file src/audio.h\n * @brief Declarations for audio capture and encoding.\n */\n#pragma once\n\n// local includes\n#include \"platform/common.h\"\n#include \"thread_safe.h\"\n#include \"utility.h\"\n\n#include <bitset>\n\nnamespace audio {\n  enum stream_config_e : int {\n    STEREO,  ///< Stereo\n    HIGH_STEREO,  ///< High stereo\n    SURROUND51,  ///< Surround 5.1\n    HIGH_SURROUND51,  ///< High surround 5.1\n    SURROUND71,  ///< Surround 7.1\n    HIGH_SURROUND71,  ///< High surround 7.1\n    SURROUND714,  ///< Surround 7.1.4\n    HIGH_SURROUND714,  ///< High surround 7.1.4\n    MAX_STREAM_CONFIG  ///< Maximum audio stream configuration\n  };\n\n  struct opus_stream_config_t {\n    std::int32_t sampleRate;\n    int channelCount;\n    int streams;\n    int coupledStreams;\n    const std::uint8_t *mapping;\n    int bitrate;\n  };\n\n  struct stream_params_t {\n    int channelCount;\n    int streams;\n    int coupledStreams;\n    std::uint8_t mapping[platf::speaker::MAX_SPEAKERS];\n  };\n\n  extern opus_stream_config_t stream_configs[MAX_STREAM_CONFIG];\n\n  struct config_t {\n    enum flags_e : int {\n      HIGH_QUALITY,  ///< High quality audio\n      HOST_AUDIO,  ///< Host audio\n      CUSTOM_SURROUND_PARAMS,  ///< Custom surround parameters\n      MAX_FLAGS  ///< Maximum number of flags\n    };\n\n    int packetDuration;\n    int channels;\n    int mask;\n\n    stream_params_t customStreamParams;\n\n    std::bitset<MAX_FLAGS> flags;\n  };\n\n  struct audio_ctx_t {\n    // We want to change the sink for the first stream only\n    std::unique_ptr<std::atomic_bool> sink_flag;\n\n    std::unique_ptr<platf::audio_control_t> control;\n\n    bool restore_sink;\n    platf::sink_t sink;\n  };\n\n  using buffer_t = util::buffer_t<std::uint8_t>;\n  using packet_t = std::pair<void *, buffer_t>;\n  using audio_ctx_ref_t = safe::shared_t<audio_ctx_t>::ptr_t;\n\n  void\n  capture(safe::mail_t mail, config_t config, void *channel_data);\n\n  /**\n   * @brief Get the reference to the audio context.\n   * @returns A shared pointer reference to audio context.\n   * @note Aside from the configuration purposes, it can be used to extend the\n   *       audio sink lifetime to capture sink earlier and restore it later.\n   *\n   * @examples\n   * audio_ctx_ref_t audio = get_audio_ctx_ref()\n   * @examples_end\n   */\n  audio_ctx_ref_t\n  get_audio_ctx_ref();\n\n  /**\n   * @brief Check if there are any active references to the audio context without creating a new one.\n   * @returns True if there are active references, false otherwise.\n   * @note This function does not trigger the audio context construction, unlike get_audio_ctx_ref().\n   */\n  bool\n  has_audio_ctx_ref();\n\n  /**\n   * @brief Check if the audio sink held by audio context is available.\n   * @returns True if available (and can probably be restored), false otherwise.\n   * @note Useful for delaying the release of audio context shared pointer (which\n   *       tries to restore original sink).\n   *\n   * @examples\n   * audio_ctx_ref_t audio = get_audio_ctx_ref()\n   * if (audio.get()) {\n   *     return is_audio_ctx_sink_available(*audio.get());\n   * }\n   * return false;\n   * @examples_end\n   */\n  bool\n  is_audio_ctx_sink_available(const audio_ctx_t &ctx);\n\n  /**\n   * @brief Start the microphone redirect device.\n   * @returns 0 on success, -1 on error.\n   */\n  int\n  init_mic_redirect_device();\n\n  /**\n   * @brief Release the microphone redirect device.\n   */\n  void\n  release_mic_redirect_device();\n\n  /**\n   * @brief Write microphone data to the virtual audio device.\n   * @param data Pointer to the audio data.\n   * @param size Size of the audio data in bytes.\n   * @param seq Sequence number for FEC recovery (0 = unknown)\n   * @returns Number of bytes written, or -1 on error.\n   */\n  int\n  write_mic_data(const std::uint8_t *data, size_t size, uint16_t seq = 0);\n}  // namespace audio"
  },
  {
    "path": "src/cbs.cpp",
    "content": "/**\n * @file src/cbs.cpp\n * @brief Definitions for FFmpeg Coded Bitstream API.\n */\nextern \"C\" {\n#include <libavcodec/avcodec.h>\n#include <libavcodec/cbs_h264.h>\n#include <libavcodec/cbs_h265.h>\n#include <libavcodec/h264_levels.h>\n#include <libavutil/pixdesc.h>\n}\n\n#include \"cbs.h\"\n#include \"logging.h\"\n#include \"utility.h\"\n\nusing namespace std::literals;\nnamespace cbs {\n  void\n  close(CodedBitstreamContext *c) {\n    ff_cbs_close(&c);\n  }\n\n  using ctx_t = util::safe_ptr<CodedBitstreamContext, close>;\n\n  class frag_t: public CodedBitstreamFragment {\n  public:\n    frag_t(frag_t &&o) {\n      std::copy((std::uint8_t *) &o, (std::uint8_t *) (&o + 1), (std::uint8_t *) this);\n\n      o.data = nullptr;\n      o.units = nullptr;\n    };\n\n    frag_t() {\n      std::fill_n((std::uint8_t *) this, sizeof(*this), 0);\n    }\n\n    frag_t &\n    operator=(frag_t &&o) {\n      std::copy((std::uint8_t *) &o, (std::uint8_t *) (&o + 1), (std::uint8_t *) this);\n\n      o.data = nullptr;\n      o.units = nullptr;\n\n      return *this;\n    };\n\n    ~frag_t() {\n      if (data || units) {\n        ff_cbs_fragment_free(this);\n      }\n    }\n  };\n\n  util::buffer_t<std::uint8_t>\n  write(cbs::ctx_t &cbs_ctx, std::uint8_t nal, void *uh, AVCodecID codec_id) {\n    cbs::frag_t frag;\n    auto err = ff_cbs_insert_unit_content(&frag, -1, nal, uh, nullptr);\n    if (err < 0) {\n      char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n      BOOST_LOG(error) << \"Could not insert NAL unit SPS: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);\n\n      return {};\n    }\n\n    err = ff_cbs_write_fragment_data(cbs_ctx.get(), &frag);\n    if (err < 0) {\n      char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n      BOOST_LOG(error) << \"Could not write fragment data: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);\n\n      return {};\n    }\n\n    // frag.data_size * 8 - frag.data_bit_padding == bits in fragment\n    util::buffer_t<std::uint8_t> data { frag.data_size };\n    std::copy_n(frag.data, frag.data_size, std::begin(data));\n\n    return data;\n  }\n\n  util::buffer_t<std::uint8_t>\n  write(std::uint8_t nal, void *uh, AVCodecID codec_id) {\n    cbs::ctx_t cbs_ctx;\n    ff_cbs_init(&cbs_ctx, codec_id, nullptr);\n\n    return write(cbs_ctx, nal, uh, codec_id);\n  }\n\n  h264_t\n  make_sps_h264(const AVCodecContext *avctx, const AVPacket *packet) {\n    cbs::ctx_t ctx;\n    if (ff_cbs_init(&ctx, AV_CODEC_ID_H264, nullptr)) {\n      return {};\n    }\n\n    cbs::frag_t frag;\n\n    int err = ff_cbs_read_packet(ctx.get(), &frag, packet);\n    if (err < 0) {\n      char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n      BOOST_LOG(error) << \"Couldn't read packet: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);\n\n      return {};\n    }\n\n    auto sps_p = ((CodedBitstreamH264Context *) ctx->priv_data)->active_sps;\n\n    // This is a very large struct that cannot safely be stored on the stack\n    auto sps = std::make_unique<H264RawSPS>(*sps_p);\n\n    if (avctx->refs > 0) {\n      sps->max_num_ref_frames = avctx->refs;\n    }\n\n    sps->vui_parameters_present_flag = 1;\n\n    auto &vui = sps->vui;\n    std::memset(&vui, 0, sizeof(vui));\n\n    vui.video_format = 5;\n    vui.colour_description_present_flag = 1;\n    vui.video_signal_type_present_flag = 1;\n    vui.video_full_range_flag = avctx->color_range == AVCOL_RANGE_JPEG;\n    vui.colour_primaries = avctx->color_primaries;\n    vui.transfer_characteristics = avctx->color_trc;\n    vui.matrix_coefficients = avctx->colorspace;\n\n    vui.low_delay_hrd_flag = 1 - vui.fixed_frame_rate_flag;\n\n    vui.bitstream_restriction_flag = 1;\n    vui.motion_vectors_over_pic_boundaries_flag = 1;\n    vui.log2_max_mv_length_horizontal = 16;\n    vui.log2_max_mv_length_vertical = 16;\n    vui.max_num_reorder_frames = 0;\n    vui.max_dec_frame_buffering = sps->max_num_ref_frames;\n\n    cbs::ctx_t write_ctx;\n    ff_cbs_init(&write_ctx, AV_CODEC_ID_H264, nullptr);\n\n    return h264_t {\n      write(write_ctx, sps->nal_unit_header.nal_unit_type, (void *) &sps->nal_unit_header, AV_CODEC_ID_H264),\n      write(ctx, sps_p->nal_unit_header.nal_unit_type, (void *) &sps_p->nal_unit_header, AV_CODEC_ID_H264)\n    };\n  }\n\n  hevc_t\n  make_sps_hevc(const AVCodecContext *avctx, const AVPacket *packet) {\n    cbs::ctx_t ctx;\n    if (ff_cbs_init(&ctx, AV_CODEC_ID_H265, nullptr)) {\n      return {};\n    }\n\n    cbs::frag_t frag;\n\n    int err = ff_cbs_read_packet(ctx.get(), &frag, packet);\n    if (err < 0) {\n      char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n      BOOST_LOG(error) << \"Couldn't read packet: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);\n\n      return {};\n    }\n\n    auto vps_p = ((CodedBitstreamH265Context *) ctx->priv_data)->active_vps;\n    auto sps_p = ((CodedBitstreamH265Context *) ctx->priv_data)->active_sps;\n\n    // These are very large structs that cannot safely be stored on the stack\n    auto sps = std::make_unique<H265RawSPS>(*sps_p);\n    auto vps = std::make_unique<H265RawVPS>(*vps_p);\n\n    vps->profile_tier_level.general_profile_compatibility_flag[4] = 1;\n    sps->profile_tier_level.general_profile_compatibility_flag[4] = 1;\n\n    auto &vui = sps->vui;\n    std::memset(&vui, 0, sizeof(vui));\n\n    sps->vui_parameters_present_flag = 1;\n\n    // skip sample aspect ratio\n\n    vui.video_format = 5;\n    vui.colour_description_present_flag = 1;\n    vui.video_signal_type_present_flag = 1;\n    vui.video_full_range_flag = avctx->color_range == AVCOL_RANGE_JPEG;\n    vui.colour_primaries = avctx->color_primaries;\n    vui.transfer_characteristics = avctx->color_trc;\n    vui.matrix_coefficients = avctx->colorspace;\n\n    vui.vui_timing_info_present_flag = vps->vps_timing_info_present_flag;\n    vui.vui_num_units_in_tick = vps->vps_num_units_in_tick;\n    vui.vui_time_scale = vps->vps_time_scale;\n    vui.vui_poc_proportional_to_timing_flag = vps->vps_poc_proportional_to_timing_flag;\n    vui.vui_num_ticks_poc_diff_one_minus1 = vps->vps_num_ticks_poc_diff_one_minus1;\n    vui.vui_hrd_parameters_present_flag = 0;\n\n    vui.bitstream_restriction_flag = 1;\n    vui.motion_vectors_over_pic_boundaries_flag = 1;\n    vui.restricted_ref_pic_lists_flag = 1;\n    vui.max_bytes_per_pic_denom = 0;\n    vui.max_bits_per_min_cu_denom = 0;\n    vui.log2_max_mv_length_horizontal = 15;\n    vui.log2_max_mv_length_vertical = 15;\n\n    cbs::ctx_t write_ctx;\n    ff_cbs_init(&write_ctx, AV_CODEC_ID_H265, nullptr);\n\n    return hevc_t {\n      nal_t {\n        write(write_ctx, vps->nal_unit_header.nal_unit_type, (void *) &vps->nal_unit_header, AV_CODEC_ID_H265),\n        write(ctx, vps_p->nal_unit_header.nal_unit_type, (void *) &vps_p->nal_unit_header, AV_CODEC_ID_H265),\n      },\n\n      nal_t {\n        write(write_ctx, sps->nal_unit_header.nal_unit_type, (void *) &sps->nal_unit_header, AV_CODEC_ID_H265),\n        write(ctx, sps_p->nal_unit_header.nal_unit_type, (void *) &sps_p->nal_unit_header, AV_CODEC_ID_H265),\n      },\n    };\n  }\n\n  /**\n   * This function initializes a Coded Bitstream Context and reads the packet into a Coded Bitstream Fragment.\n   * It then checks if the SPS->VUI (Video Usability Information) is present in the active SPS of the packet.\n   * This is done for both H264 and H265 codecs.\n   */\n  bool\n  validate_sps(const AVPacket *packet, int codec_id) {\n    cbs::ctx_t ctx;\n    if (ff_cbs_init(&ctx, (AVCodecID) codec_id, nullptr)) {\n      return false;\n    }\n\n    cbs::frag_t frag;\n\n    int err = ff_cbs_read_packet(ctx.get(), &frag, packet);\n    if (err < 0) {\n      char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n      BOOST_LOG(error) << \"Couldn't read packet: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);\n\n      return false;\n    }\n\n    if (codec_id == AV_CODEC_ID_H264) {\n      auto h264 = (CodedBitstreamH264Context *) ctx->priv_data;\n\n      if (!h264->active_sps->vui_parameters_present_flag) {\n        return false;\n      }\n\n      return true;\n    }\n\n    return ((CodedBitstreamH265Context *) ctx->priv_data)->active_sps->vui_parameters_present_flag;\n  }\n}  // namespace cbs"
  },
  {
    "path": "src/cbs.h",
    "content": "/**\n * @file src/cbs.h\n * @brief Declarations for FFmpeg Coded Bitstream API.\n */\n#pragma once\n\n#include \"utility.h\"\n\nstruct AVPacket;\nstruct AVCodecContext;\n\nnamespace cbs {\n\n  struct nal_t {\n    util::buffer_t<std::uint8_t> _new;\n    util::buffer_t<std::uint8_t> old;\n  };\n\n  struct hevc_t {\n    nal_t vps;\n    nal_t sps;\n  };\n\n  struct h264_t {\n    nal_t sps;\n  };\n\n  hevc_t\n  make_sps_hevc(const AVCodecContext *ctx, const AVPacket *packet);\n  h264_t\n  make_sps_h264(const AVCodecContext *ctx, const AVPacket *packet);\n\n  /**\n   * @brief Validates the Sequence Parameter Set (SPS) of a given packet.\n   * @param packet The packet to validate.\n   * @param codec_id The ID of the codec used (either AV_CODEC_ID_H264 or AV_CODEC_ID_H265).\n   * @return True if the SPS->VUI is present in the active SPS of the packet, false otherwise.\n   */\n  bool\n  validate_sps(const AVPacket *packet, int codec_id);\n}  // namespace cbs\n"
  },
  {
    "path": "src/config.cpp",
    "content": "/**\n * @file src/config.cpp\n * @brief Definitions for the configuration of Sunshine.\n */\n#include <algorithm>\n#include <chrono>\n#include <filesystem>\n#include <fstream>\n#include <functional>\n#include <iostream>\n#include <thread>\n#include <unordered_map>\n#include <utility>\n\n#include <boost/asio.hpp>\n#include <boost/filesystem.hpp>\n#include <boost/property_tree/json_parser.hpp>\n#include <boost/property_tree/ptree.hpp>\n\n#include \"config.h\"\n#include \"entry_handler.h\"\n#include \"file_handler.h\"\n#include \"logging.h\"\n#include \"nvhttp.h\"\n#include \"rtsp.h\"\n#include \"utility.h\"\n#include \"globals.h\"\n\n#include \"display_device/parsed_config.h\"\n#include \"platform/common.h\"\n\n#ifdef _WIN32\n  #include <shellapi.h>\n  #include \"platform/windows/misc.h\"\n#endif\n\n#ifndef __APPLE__\n  // For NVENC legacy constants\n  #include <ffnvcodec/nvEncodeAPI.h>\n#endif\n\nnamespace fs = std::filesystem;\nusing namespace std::literals;\n\n#define CA_DIR \"credentials\"\n#define PRIVATE_KEY_FILE CA_DIR \"/cakey.pem\"\n#define CERTIFICATE_FILE CA_DIR \"/cacert.pem\"\n\n#define APPS_JSON_PATH platf::appdata().string() + \"/apps.json\"\nnamespace config {\n\n  namespace nv {\n\n    nvenc::nvenc_two_pass\n    twopass_from_view(const std::string_view &preset) {\n      if (preset == \"disabled\") return nvenc::nvenc_two_pass::disabled;\n      if (preset == \"quarter_res\") return nvenc::nvenc_two_pass::quarter_resolution;\n      if (preset == \"full_res\") return nvenc::nvenc_two_pass::full_resolution;\n      BOOST_LOG(warning) << \"config: unknown nvenc_twopass value: \" << preset;\n      return nvenc::nvenc_two_pass::quarter_resolution;\n    }\n\n    nvenc::nvenc_split_frame_encoding\n    split_encode_from_view(const std::string_view &preset) {\n      using enum nvenc::nvenc_split_frame_encoding;\n      if (preset == \"disabled\") return disabled;\n      if (preset == \"driver_decides\") return driver_decides;\n      if (preset == \"enabled\") return force_enabled;\n      if (preset == \"two_strips\") return two_strips;\n      if (preset == \"three_strips\") return three_strips;\n      if (preset == \"four_strips\") return four_strips;\n      BOOST_LOG(warning) << \"config: unknown nvenc_split_encode value: \" << preset;\n      return driver_decides;\n    }\n\n    nvenc::nvenc_lookahead_level\n    lookahead_level_from_view(const std::string_view &level) {\n      using enum nvenc::nvenc_lookahead_level;\n      if (level == \"disabled\" || level == \"0\") return disabled;\n      if (level == \"1\") return level_1;\n      if (level == \"2\") return level_2;\n      if (level == \"3\") return level_3;\n      if (level == \"autoselect\" || level == \"auto\") return autoselect;\n      BOOST_LOG(warning) << \"config: unknown nvenc_lookahead_level value: \" << level;\n      return disabled;\n    }\n\n    nvenc::nvenc_temporal_filter_level\n    temporal_filter_level_from_view(const std::string_view &level) {\n      using enum nvenc::nvenc_temporal_filter_level;\n      if (level == \"disabled\" || level == \"0\") return disabled;\n      if (level == \"4\") return level_4;\n      BOOST_LOG(warning) << \"config: unknown nvenc_temporal_filter_level value: \" << level;\n      return disabled;\n    }\n\n    nvenc::nvenc_rate_control_mode\n    rate_control_mode_from_view(const std::string_view &mode) {\n      using enum nvenc::nvenc_rate_control_mode;\n      if (mode == \"cbr\") return cbr;\n      if (mode == \"vbr\") return vbr;\n      BOOST_LOG(warning) << \"config: unknown nvenc_rate_control_mode value: \" << mode;\n      return cbr;\n    }\n\n  }  // namespace nv\n\n  namespace amd {\n#if !defined(_WIN32) || defined(DOXYGEN)\n  // values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build\n  #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_SPEED 100\n  #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_QUALITY 30\n  #define AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_BALANCED 70\n  #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED 10\n  #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY 0\n  #define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED 5\n  #define AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED 1\n  #define AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY 2\n  #define AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED 0\n  #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CONSTANT_QP 0\n  #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR 3\n  #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2\n  #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 1\n  #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP 0\n  #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR 3\n  #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2\n  #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 1\n  #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP 0\n  #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR 1\n  #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2\n  #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 3\n  #define AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING 0\n  #define AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY 1\n  #define AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY 2\n  #define AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM 3\n  #define AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY_HIGH_QUALITY 5\n  #define AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING 0\n  #define AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY 1\n  #define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY 2\n  #define AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM 3\n  #define AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY_HIGH_QUALITY 5\n  #define AMF_VIDEO_ENCODER_USAGE_TRANSCODING 0\n  #define AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY 1\n  #define AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY 2\n  #define AMF_VIDEO_ENCODER_USAGE_WEBCAM 3\n  #define AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY_HIGH_QUALITY 5\n  #define AMF_VIDEO_ENCODER_UNDEFINED 0\n  #define AMF_VIDEO_ENCODER_CABAC 1\n  #define AMF_VIDEO_ENCODER_CALV 2\n#else\n  #ifdef _GLIBCXX_USE_C99_INTTYPES\n    #undef _GLIBCXX_USE_C99_INTTYPES\n  #endif\n  #include <AMF/components/VideoEncoderAV1.h>\n  #include <AMF/components/VideoEncoderHEVC.h>\n  #include <AMF/components/VideoEncoderVCE.h>\n#endif\n\n    enum class quality_av1_e : int {\n      speed = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_SPEED,  ///< Speed preset\n      quality = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_QUALITY,  ///< Quality preset\n      balanced = AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET_BALANCED  ///< Balanced preset\n    };\n\n    enum class quality_hevc_e : int {\n      speed = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED,  ///< Speed preset\n      quality = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY,  ///< Quality preset\n      balanced = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED  ///< Balanced preset\n    };\n\n    enum class quality_h264_e : int {\n      speed = AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED,  ///< Speed preset\n      quality = AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY,  ///< Quality preset\n      balanced = AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED  ///< Balanced preset\n    };\n\n    enum class rc_av1_e : int {\n      cbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR,  ///< CBR\n      cqp = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CONSTANT_QP,  ///< CQP\n      vbr_latency = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR,  ///< VBR with latency constraints\n      vbr_peak = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR,  ///< VBR with peak constraints\n      qvbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_QUALITY_VBR,  ///< Quality VBR\n      hqvbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_HIGH_QUALITY_VBR,  ///< High Quality VBR\n      hqcbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_HIGH_QUALITY_CBR  ///< High Quality CBR\n    };\n\n    enum class rc_hevc_e : int {\n      cbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR,  ///< CBR\n      cqp = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP,  ///< CQP\n      vbr_latency = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR,  ///< VBR with latency constraints\n      vbr_peak = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR,  ///< VBR with peak constraints\n      qvbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_QUALITY_VBR,  ///< Quality VBR\n      hqvbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_HIGH_QUALITY_VBR,  ///< High Quality VBR\n      hqcbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_HIGH_QUALITY_CBR  ///< High Quality CBR\n    };\n\n    enum class rc_h264_e : int {\n      cbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR,  ///< CBR\n      cqp = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP,  ///< CQP\n      vbr_latency = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR,  ///< VBR with latency constraints\n      vbr_peak = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR,  ///< VBR with peak constraints\n      qvbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_QUALITY_VBR,  ///< Quality VBR\n      hqvbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_HIGH_QUALITY_VBR,  ///< High Quality VBR\n      hqcbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_HIGH_QUALITY_CBR  ///< High Quality CBR\n    };\n\n    enum class usage_av1_e : int {\n      transcoding = AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING,  ///< Transcoding preset\n      webcam = AMF_VIDEO_ENCODER_AV1_USAGE_WEBCAM,  ///< Webcam preset\n      lowlatency_high_quality = AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY_HIGH_QUALITY,  ///< Low latency high quality preset\n      lowlatency = AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY,  ///< Low latency preset\n      ultralowlatency = AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY  ///< Ultra low latency preset\n    };\n\n    enum class usage_hevc_e : int {\n      transcoding = AMF_VIDEO_ENCODER_HEVC_USAGE_TRANSCODING,  ///< Transcoding preset\n      webcam = AMF_VIDEO_ENCODER_HEVC_USAGE_WEBCAM,  ///< Webcam preset\n      lowlatency_high_quality = AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY_HIGH_QUALITY,  ///< Low latency high quality preset\n      lowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_LOW_LATENCY,  ///< Low latency preset\n      ultralowlatency = AMF_VIDEO_ENCODER_HEVC_USAGE_ULTRA_LOW_LATENCY  ///< Ultra low latency preset\n    };\n\n    enum class usage_h264_e : int {\n      transcoding = AMF_VIDEO_ENCODER_USAGE_TRANSCODING,  ///< Transcoding preset\n      webcam = AMF_VIDEO_ENCODER_USAGE_WEBCAM,  ///< Webcam preset\n      lowlatency_high_quality = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY_HIGH_QUALITY,  ///< Low latency high quality preset\n      lowlatency = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY,  ///< Low latency preset\n      ultralowlatency = AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY  ///< Ultra low latency preset\n    };\n\n    enum coder_e : int {\n      _auto = AMF_VIDEO_ENCODER_UNDEFINED,  ///< Auto\n      cabac = AMF_VIDEO_ENCODER_CABAC,  ///< CABAC\n      cavlc = AMF_VIDEO_ENCODER_CALV  ///< CAVLC\n    };\n\n    template <class T>\n    std::optional<int>\n    quality_from_view(const std::string_view &quality_type, const std::optional<int>(&original)) {\n#define _CONVERT_(x) \\\n  if (quality_type == #x##sv) return (int) T::x\n      _CONVERT_(balanced);\n      _CONVERT_(quality);\n      _CONVERT_(speed);\n#undef _CONVERT_\n      return original;\n    }\n\n    template <class T>\n    std::optional<int>\n    rc_from_view(const std::string_view &rc, const std::optional<int>(&original)) {\n#define _CONVERT_(x) \\\n  if (rc == #x##sv) return (int) T::x\n      _CONVERT_(cbr);\n      _CONVERT_(cqp);\n      _CONVERT_(vbr_latency);\n      _CONVERT_(vbr_peak);\n      _CONVERT_(qvbr);\n      _CONVERT_(hqvbr);\n      _CONVERT_(hqcbr);\n#undef _CONVERT_\n      return original;\n    }\n\n    template <class T>\n    std::optional<int>\n    usage_from_view(const std::string_view &usage, const std::optional<int>(&original)) {\n#define _CONVERT_(x) \\\n  if (usage == #x##sv) return (int) T::x\n      _CONVERT_(lowlatency);\n      _CONVERT_(lowlatency_high_quality);\n      _CONVERT_(transcoding);\n      _CONVERT_(ultralowlatency);\n      _CONVERT_(webcam);\n#undef _CONVERT_\n      return original;\n    }\n\n    int\n    coder_from_view(const std::string_view &coder) {\n      if (coder == \"auto\"sv) return _auto;\n      if (coder == \"cabac\"sv || coder == \"ac\"sv) return cabac;\n      if (coder == \"cavlc\"sv || coder == \"vlc\"sv) return cavlc;\n\n      return _auto;\n    }\n  }  // namespace amd\n\n  namespace qsv {\n    enum preset_e : int {\n      veryslow = 1,  ///< veryslow preset\n      slower = 2,  ///< slower preset\n      slow = 3,  ///< slow preset\n      medium = 4,  ///< medium preset\n      fast = 5,  ///< fast preset\n      faster = 6,  ///< faster preset\n      veryfast = 7  ///< veryfast preset\n    };\n\n    enum cavlc_e : int {\n      _auto = false,  ///< Auto\n      enabled = true,  ///< Enabled\n      disabled = false  ///< Disabled\n    };\n\n    std::optional<int>\n    preset_from_view(const std::string_view &preset) {\n#define _CONVERT_(x) \\\n  if (preset == #x##sv) return x\n      _CONVERT_(veryslow);\n      _CONVERT_(slower);\n      _CONVERT_(slow);\n      _CONVERT_(medium);\n      _CONVERT_(fast);\n      _CONVERT_(faster);\n      _CONVERT_(veryfast);\n#undef _CONVERT_\n      return std::nullopt;\n    }\n\n    std::optional<int>\n    coder_from_view(const std::string_view &coder) {\n      if (coder == \"auto\"sv) return _auto;\n      if (coder == \"cabac\"sv || coder == \"ac\"sv) return disabled;\n      if (coder == \"cavlc\"sv || coder == \"vlc\"sv) return enabled;\n      return std::nullopt;\n    }\n\n  }  // namespace qsv\n\n  namespace vt {\n\n    enum coder_e : int {\n      _auto = 0,  ///< Auto\n      cabac,  ///< CABAC\n      cavlc  ///< CAVLC\n    };\n\n    int\n    coder_from_view(const std::string_view &coder) {\n      if (coder == \"auto\"sv) return _auto;\n      if (coder == \"cabac\"sv || coder == \"ac\"sv) return cabac;\n      if (coder == \"cavlc\"sv || coder == \"vlc\"sv) return cavlc;\n\n      return -1;\n    }\n\n    int\n    allow_software_from_view(const std::string_view &software) {\n      if (software == \"allowed\"sv || software == \"forced\") return 1;\n\n      return 0;\n    }\n\n    int\n    force_software_from_view(const std::string_view &software) {\n      if (software == \"forced\") return 1;\n\n      return 0;\n    }\n\n    int\n    rt_from_view(const std::string_view &rt) {\n      if (rt == \"disabled\" || rt == \"off\" || rt == \"0\") return 0;\n\n      return 1;\n    }\n\n  }  // namespace vt\n\n  namespace sw {\n    int\n    svtav1_preset_from_view(const std::string_view &preset) {\n#define _CONVERT_(x, y) \\\n  if (preset == #x##sv) return y\n      _CONVERT_(veryslow, 1);\n      _CONVERT_(slower, 2);\n      _CONVERT_(slow, 4);\n      _CONVERT_(medium, 5);\n      _CONVERT_(fast, 7);\n      _CONVERT_(faster, 9);\n      _CONVERT_(veryfast, 10);\n      _CONVERT_(superfast, 11);\n      _CONVERT_(ultrafast, 12);\n#undef _CONVERT_\n      return 11;  // Default to superfast\n    }\n  }  // namespace sw\n\n  video_t video {\n    28,  // qp\n\n    0,  // hevc_mode\n    0,  // av1_mode\n\n    0,  // max_bitrate\n    2,  // min_threads\n    {\n      \"superfast\"s,  // preset\n      \"zerolatency\"s,  // tune\n      11,  // superfast\n    },  // software\n\n    {},  // nv\n    true,  // nv_realtime_hags\n    true,  // nv_opengl_vulkan_on_dxgi\n    true,  // nv_sunshine_high_power_mode\n    false,  // vdd_keep_enabled\n    false,  // vdd_headless_create_enabled\n    false,  // vdd_reuse (default: recreate VDD for each client)\n    {},  // nv_legacy\n\n    {\n      qsv::medium,  // preset\n      qsv::_auto,  // cavlc\n      false,  // slow_hevc\n    },  // qsv\n\n    {\n      (int) amd::usage_h264_e::ultralowlatency,  // usage (h264)\n      (int) amd::usage_hevc_e::ultralowlatency,  // usage (hevc)\n      (int) amd::usage_av1_e::ultralowlatency,  // usage (av1)\n      (int) amd::rc_h264_e::vbr_latency,  // rate control (h264)\n      (int) amd::rc_hevc_e::vbr_latency,  // rate control (hevc)\n      (int) amd::rc_av1_e::vbr_latency,  // rate control (av1)\n      0,  // enforce_hrd\n      (int) amd::quality_h264_e::balanced,  // quality (h264)\n      (int) amd::quality_hevc_e::balanced,  // quality (hevc)\n      (int) amd::quality_av1_e::balanced,  // quality (av1)\n      0,  // preanalysis\n      1,  // vbaq\n      (int) amd::coder_e::_auto,  // coder\n      23,  // qvbr_quality (1-51, default 23)\n    },  // amd\n\n    {\n      0,\n      0,\n      1,\n      -1,\n    },  // vt\n\n    {\n      false,  // strict_rc_buffer\n    },  // vaapi\n\n    {},  // capture\n    {},  // encoder\n    {},  // adapter_name\n    {},  // output_name\n    {},  // capture_target (default: empty, will be set to \"display\" in apply_config)\n    {},  // window_title\n    (int) display_device::parsed_config_t::device_prep_e::no_operation,  // display_device_prep\n    (int) display_device::parsed_config_t::resolution_change_e::automatic,  // resolution_change\n    {},  // manual_resolution\n    (int) display_device::parsed_config_t::refresh_rate_change_e::automatic,  // refresh_rate_change\n    {},  // manual_refresh_rate\n    (int) display_device::parsed_config_t::hdr_prep_e::automatic,  // hdr_prep\n    {},  // display_mode_remapping\n    false,  // variable_refresh_rate\n    0,  // minimum_fps_target (0 = auto, about half the stream FPS)\n    \"balanced\"s,  // downscaling_quality (default: bicubic for best quality/performance balance)\n    false,  // hdr_luminance_analysis (disabled by default to avoid GPU overhead)\n    false,  // wgc_disable_secure_desktop (disabled by default for security)\n  };\n\n  audio_t audio {\n    {},  // audio_sink\n    {},  // virtual_sink\n    true,  // stream audio\n    true,  // stream_mic (enable microphone streaming from client)\n    true,  // install_steam_drivers\n  };\n\n  stream_t stream {\n    10s,  // ping_timeout\n\n    APPS_JSON_PATH,\n\n    20,  // fecPercentage\n\n    ENCRYPTION_MODE_NEVER,  // lan_encryption_mode\n    ENCRYPTION_MODE_OPPORTUNISTIC,  // wan_encryption_mode\n  };\n\n  nvhttp_t nvhttp {\n    \"lan\",  // origin web manager\n\n    PRIVATE_KEY_FILE,\n    CERTIFICATE_FILE,\n\n    platf::get_host_name(),  // sunshine_name,\n    \"[]\",\n    \"sunshine_state.json\"s,  // file_state\n    {},  // external_ip\n    {\n      \"1280x720\"s,\n      \"1920x1080\"s,\n      \"2560x1080\"s,\n      \"2560x1440\"s,\n      \"2560x1600\"s,\n      \"3440x1440\"s,\n      \"1920x1200\"s,\n      \"3840x2160\"s,\n      \"3840x1600\"s,\n    },  // supported resolutions\n\n    { \"60\", \"90\", \"120\", \"144\" },  // supported fps (支持小数刷新率)\n\n    SLEEP_MODE_SUSPEND,  // sleep_mode: default to S3 suspend\n  };\n\n  webhook_t webhook {\n    false,  // enabled\n    {},     // url\n    false,  // skip_ssl_verify\n    1000ms, // timeout\n  };\n\n  input_t input {\n    {\n      { 0x10, 0xA0 },\n      { 0x11, 0xA2 },\n      { 0x12, 0xA4 },\n    },\n    -1ms,  // back_button_timeout\n    500ms,  // key_repeat_delay\n    std::chrono::duration<double> { 1 / 24.9 },  // key_repeat_period\n\n    {\n      platf::supported_gamepads(nullptr).front().name.data(),\n      platf::supported_gamepads(nullptr).front().name.size(),\n    },  // Default gamepad\n    true,  // back as touchpad click enabled (manual DS4 only)\n    true,  // client gamepads with motion events are emulated as DS4\n    true,  // client gamepads with touchpads are emulated as DS4\n    true,  // ds5_inputtino_randomize_mac\n    false, // enable_dsu_server - disabled by default\n    26760, // dsu_server_port - default DSU server port\n\n    true,  // keyboard enabled\n    true,  // mouse enabled\n    true,  // controller enabled\n    true,  // always send scancodes\n    true,  // high resolution scrolling\n    true,  // native pen/touch support\n    true,  // virtual mouse (use driver if available)\n  };\n\n  sunshine_t sunshine {\n    \"en\",  // locale\n    \"en\",  // tray_locale (托盘菜单语言)\n    2,  // min_log_level\n    0,  // flags\n    {},  // User file\n    {},  // Username\n    {},  // Password\n    {},  // Password Salt\n    platf::appdata().string() + \"/sunshine.conf\",  // config file\n    {},  // cmd args\n    47989,  // Base port number\n    \"ipv4\",  // Address family\n    {},  // Bind address\n    platf::appdata().string() + \"/sunshine.log\",  // log file\n    false,  // restore_log - 默认不恢复日志文件\n    50,  // max_log_size_mb - 默认50MB，超过自动轮转\n    false,  // notify_pre_releases\n    true,  // system_tray\n    {},  // prep commands\n  };\n\n  bool\n  endline(char ch) {\n    return ch == '\\r' || ch == '\\n';\n  }\n\n  bool\n  space_tab(char ch) {\n    return ch == ' ' || ch == '\\t';\n  }\n\n  bool\n  whitespace(char ch) {\n    return space_tab(ch) || endline(ch);\n  }\n\n  std::string\n  to_string(const char *begin, const char *end) {\n    std::string result;\n\n    KITTY_WHILE_LOOP(auto pos = begin, pos != end, {\n      auto comment = std::find(pos, end, '#');\n      auto endl = std::find_if(comment, end, endline);\n\n      result.append(pos, comment);\n\n      pos = endl;\n    })\n\n    return result;\n  }\n\n  template <class It>\n  It\n  skip_list(It skipper, It end) {\n    int stack = 1;\n    while (skipper != end && stack) {\n      if (*skipper == '[') {\n        ++stack;\n      }\n      if (*skipper == ']') {\n        --stack;\n      }\n\n      ++skipper;\n    }\n\n    return skipper;\n  }\n\n  std::pair<\n    std::string_view::const_iterator,\n    std::optional<std::pair<std::string, std::string>>>\n  parse_option(std::string_view::const_iterator begin, std::string_view::const_iterator end) {\n    begin = std::find_if_not(begin, end, whitespace);\n    auto endl = std::find_if(begin, end, endline);\n    auto endc = std::find(begin, endl, '#');\n    endc = std::find_if(std::make_reverse_iterator(endc), std::make_reverse_iterator(begin), std::not_fn(whitespace)).base();\n\n    auto eq = std::find(begin, endc, '=');\n    if (eq == endc || eq == begin) {\n      return std::make_pair(endl, std::nullopt);\n    }\n\n    auto end_name = std::find_if_not(std::make_reverse_iterator(eq), std::make_reverse_iterator(begin), space_tab).base();\n    auto begin_val = std::find_if_not(eq + 1, endc, space_tab);\n\n    if (begin_val == endl) {\n      return std::make_pair(endl, std::nullopt);\n    }\n\n    // Lists might contain newlines\n    if (*begin_val == '[') {\n      endl = skip_list(begin_val + 1, end);\n\n      // Check if we reached the end of the file without finding a closing bracket\n      // We know we have a valid closing bracket if:\n      // 1. We didn't reach the end, or\n      // 2. We reached the end but the last character was the matching closing bracket\n      if (endl == end && end == begin_val + 1) {\n        BOOST_LOG(warning) << \"config: Missing ']' in config option: \" << to_string(begin, end_name);\n        return std::make_pair(endl, std::nullopt);\n      }\n    }\n\n    return std::make_pair(\n      endl,\n      std::make_pair(to_string(begin, end_name), to_string(begin_val, endl)));\n  }\n\n  std::unordered_map<std::string, std::string>\n  parse_config(const std::string_view &file_content) {\n    std::unordered_map<std::string, std::string> vars;\n\n    auto pos = std::begin(file_content);\n    auto end = std::end(file_content);\n\n    while (pos < end) {\n      // auto newline = std::find_if(pos, end, [](auto ch) { return ch == '\\n' || ch == '\\r'; });\n      TUPLE_2D(endl, var, parse_option(pos, end));\n\n      pos = endl;\n      if (pos != end) {\n        pos += (*pos == '\\r') ? 2 : 1;\n      }\n\n      if (!var) {\n        continue;\n      }\n\n      vars.emplace(std::move(*var));\n    }\n\n    return vars;\n  }\n\n  void\n  string_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input) {\n    auto it = vars.find(name);\n    if (it == std::end(vars)) {\n      return;\n    }\n\n    input = std::move(it->second);\n\n    vars.erase(it);\n  }\n\n  template <typename T, typename F>\n  void\n  generic_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, T &input, F &&f) {\n    std::string tmp;\n    string_f(vars, name, tmp);\n    if (!tmp.empty()) {\n      input = f(tmp);\n    }\n  }\n\n  void\n  string_restricted_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input, const std::vector<std::string_view> &allowed_vals) {\n    std::string temp;\n    string_f(vars, name, temp);\n\n    for (auto &allowed_val : allowed_vals) {\n      if (temp == allowed_val) {\n        input = std::move(temp);\n        return;\n      }\n    }\n  }\n\n  void\n  path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, fs::path &input) {\n    // appdata needs to be retrieved once only\n    static auto appdata = platf::appdata();\n\n    std::string temp;\n    string_f(vars, name, temp);\n\n    if (!temp.empty()) {\n      input = temp;\n    }\n\n    if (input.is_relative()) {\n      input = appdata / input;\n    }\n\n    auto dir = input;\n    dir.remove_filename();\n\n    // Ensure the directories exists\n    if (!fs::exists(dir)) {\n      fs::create_directories(dir);\n    }\n  }\n\n  void\n  path_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::string &input) {\n    fs::path temp = input;\n\n    path_f(vars, name, temp);\n\n    input = temp.string();\n  }\n\n  void\n  int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input) {\n    auto it = vars.find(name);\n\n    if (it == std::end(vars)) {\n      return;\n    }\n\n    std::string_view val = it->second;\n\n    // If value is something like: \"756\" instead of 756\n    if (val.size() >= 2 && val[0] == '\"') {\n      val = val.substr(1, val.size() - 2);\n    }\n\n    // If that integer is in hexadecimal\n    if (val.size() >= 2 && val.substr(0, 2) == \"0x\"sv) {\n      input = util::from_hex<int>(val.substr(2));\n    }\n    else {\n      input = util::from_view(val);\n    }\n\n    vars.erase(it);\n  }\n\n  void\n  int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input) {\n    auto it = vars.find(name);\n\n    if (it == std::end(vars)) {\n      return;\n    }\n\n    std::string_view val = it->second;\n\n    // If value is something like: \"756\" instead of 756\n    if (val.size() >= 2 && val[0] == '\"') {\n      val = val.substr(1, val.size() - 2);\n    }\n\n    // If that integer is in hexadecimal\n    if (val.size() >= 2 && val.substr(0, 2) == \"0x\"sv) {\n      input = util::from_hex<int>(val.substr(2));\n    }\n    else {\n      input = util::from_view(val);\n    }\n\n    vars.erase(it);\n  }\n\n  template <class F>\n  void\n  int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, F &&f) {\n    std::string tmp;\n    string_f(vars, name, tmp);\n    if (!tmp.empty()) {\n      input = f(tmp);\n    }\n  }\n\n  template <class F>\n  void\n  int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::optional<int> &input, F &&f) {\n    std::string tmp;\n    string_f(vars, name, tmp);\n    if (!tmp.empty()) {\n      input = f(tmp);\n    }\n  }\n\n  void\n  int_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, int &input, const std::pair<int, int> &range) {\n    int temp = input;\n\n    int_f(vars, name, temp);\n\n    TUPLE_2D_REF(lower, upper, range);\n    if (temp >= lower && temp <= upper) {\n      input = temp;\n    }\n  }\n\n  bool\n  to_bool(std::string &boolean) {\n    std::for_each(std::begin(boolean), std::end(boolean), [](char ch) { return (char) std::tolower(ch); });\n\n    return boolean == \"true\"sv ||\n           boolean == \"yes\"sv ||\n           boolean == \"enable\"sv ||\n           boolean == \"enabled\"sv ||\n           boolean == \"on\"sv ||\n           (std::find(std::begin(boolean), std::end(boolean), '1') != std::end(boolean));\n  }\n\n  void\n  bool_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, bool &input) {\n    std::string tmp;\n    string_f(vars, name, tmp);\n\n    if (tmp.empty()) {\n      return;\n    }\n\n    input = to_bool(tmp);\n  }\n\n  void\n  double_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input) {\n    std::string tmp;\n    string_f(vars, name, tmp);\n\n    if (tmp.empty()) {\n      return;\n    }\n\n    char *c_str_p;\n    auto val = std::strtod(tmp.c_str(), &c_str_p);\n\n    if (c_str_p == tmp.c_str()) {\n      return;\n    }\n\n    input = val;\n  }\n\n  void\n  double_between_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, double &input, const std::pair<double, double> &range) {\n    double temp = input;\n\n    double_f(vars, name, temp);\n\n    TUPLE_2D_REF(lower, upper, range);\n    if (temp >= lower && temp <= upper) {\n      input = temp;\n    }\n  }\n\n  void\n  list_string_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<std::string> &input) {\n    std::string string;\n    string_f(vars, name, string);\n\n    if (string.empty()) {\n      return;\n    }\n\n    input.clear();\n\n    auto begin = std::cbegin(string);\n    if (*begin == '[') {\n      ++begin;\n    }\n\n    begin = std::find_if_not(begin, std::cend(string), whitespace);\n    if (begin == std::cend(string)) {\n      return;\n    }\n\n    auto pos = begin;\n    while (pos < std::cend(string)) {\n      if (*pos == '[') {\n        pos = skip_list(pos + 1, std::cend(string)) + 1;\n      }\n      else if (*pos == ']') {\n        break;\n      }\n      else if (*pos == ',') {\n        input.emplace_back(begin, pos);\n        pos = begin = std::find_if_not(pos + 1, std::cend(string), whitespace);\n      }\n      else {\n        ++pos;\n      }\n    }\n\n    if (pos != begin) {\n      input.emplace_back(begin, pos);\n    }\n  }\n\n  void\n  list_display_mode_remapping_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<video_t::display_mode_remapping_t> &input) {\n    std::string string;\n    string_f(vars, name, string);\n\n    std::stringstream jsonStream;\n\n    // check if string is empty, i.e. when the value doesn't exist in the config file\n    if (string.empty()) {\n      return;\n    }\n\n    // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it.\n    jsonStream << \"{\\\"display_mode_remapping\\\":\" << string << \"}\";\n\n    boost::property_tree::ptree jsonTree;\n    boost::property_tree::read_json(jsonStream, jsonTree);\n\n    for (auto &[_, entry] : jsonTree.get_child(\"display_mode_remapping\"s)) {\n      auto type = entry.get_optional<std::string>(\"type\"s);\n      auto received_resolution = entry.get_optional<std::string>(\"received_resolution\"s);\n      auto received_fps = entry.get_optional<std::string>(\"received_fps\"s);\n      auto final_resolution = entry.get_optional<std::string>(\"final_resolution\"s);\n      auto final_refresh_rate = entry.get_optional<std::string>(\"final_refresh_rate\"s);\n\n      input.push_back(video_t::display_mode_remapping_t {\n        type.value_or(\"\"),\n        received_resolution.value_or(\"\"),\n        received_fps.value_or(\"\"),\n        final_resolution.value_or(\"\"),\n        final_refresh_rate.value_or(\"\") });\n    }\n  }\n\n  void\n  list_prep_cmd_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<prep_cmd_t> &input) {\n    std::string string;\n    string_f(vars, name, string);\n\n    std::stringstream jsonStream;\n\n    // check if string is empty, i.e. when the value doesn't exist in the config file\n    if (string.empty()) {\n      return;\n    }\n\n    // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it.\n    jsonStream << \"{\\\"prep_cmd\\\":\" << string << \"}\";\n\n    boost::property_tree::ptree jsonTree;\n    boost::property_tree::read_json(jsonStream, jsonTree);\n\n    for (auto &[_, prep_cmd] : jsonTree.get_child(\"prep_cmd\"s)) {\n      auto do_cmd = prep_cmd.get_optional<std::string>(\"do\"s);\n      auto undo_cmd = prep_cmd.get_optional<std::string>(\"undo\"s);\n      auto elevated = prep_cmd.get_optional<bool>(\"elevated\"s);\n\n      input.emplace_back(do_cmd.value_or(\"\"), undo_cmd.value_or(\"\"), elevated.value_or(false));\n    }\n  }\n\n  void\n  list_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::vector<int> &input) {\n    std::vector<std::string> list;\n    list_string_f(vars, name, list);\n\n    // check if list is empty, i.e. when the value doesn't exist in the config file\n    if (list.empty()) {\n      return;\n    }\n\n    // The framerate list must be cleared before adding values from the file configuration.\n    // If the list is not cleared, then the specified parameters do not affect the behavior of the sunshine server.\n    // That is, if you set only 30 fps in the configuration file, it will not work because by default, during initialization the list includes 10, 30, 60, 90 and 120 fps.\n    input.clear();\n    for (auto &el : list) {\n      std::string_view val = el;\n\n      // If value is something like: \"756\" instead of 756\n      if (val.size() >= 2 && val[0] == '\"') {\n        val = val.substr(1, val.size() - 2);\n      }\n\n      int tmp;\n\n      // If the integer is a hexadecimal\n      if (val.size() >= 2 && val.substr(0, 2) == \"0x\"sv) {\n        tmp = util::from_hex<int>(val.substr(2));\n      }\n      else {\n        tmp = util::from_view(val);\n      }\n      input.emplace_back(tmp);\n    }\n  }\n\n  void\n  map_int_int_f(std::unordered_map<std::string, std::string> &vars, const std::string &name, std::unordered_map<int, int> &input) {\n    std::vector<int> list;\n    list_int_f(vars, name, list);\n\n    // The list needs to be a multiple of 2\n    if (list.size() % 2) {\n      BOOST_LOG(warning) << \"config: expected \"sv << name << \" to have a multiple of two elements --> not \"sv << list.size();\n      return;\n    }\n\n    int x = 0;\n    while (x < list.size()) {\n      auto key = list[x++];\n      auto val = list[x++];\n\n      input.emplace(key, val);\n    }\n  }\n\n  int\n  apply_flags(const char *line) {\n    int ret = 0;\n    while (*line != '\\0') {\n      switch (*line) {\n        case '0':\n          config::sunshine.flags[config::flag::PIN_STDIN].flip();\n          break;\n        case '1':\n          config::sunshine.flags[config::flag::FRESH_STATE].flip();\n          break;\n        case '2':\n          config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE].flip();\n          break;\n        case 'p':\n          config::sunshine.flags[config::flag::UPNP].flip();\n          break;\n        default:\n          BOOST_LOG(warning) << \"config: Unrecognized flag: [\"sv << *line << ']' << std::endl;\n          ret = -1;\n      }\n\n      ++line;\n    }\n\n    return ret;\n  }\n\n  std::vector<std::string_view> &\n  get_supported_gamepad_options() {\n    const auto options = platf::supported_gamepads(nullptr);\n    static std::vector<std::string_view> opts {};\n    opts.reserve(options.size());\n    for (auto &opt : options) {\n      opts.emplace_back(opt.name);\n    }\n    return opts;\n  }\n\n  void apply_config(std::unordered_map<std::string, std::string> &&vars) {\n    for (auto &[name, val] : vars) {\n      BOOST_LOG(info) << \"config: '\"sv << name << \"' = \"sv << val;\n      modified_config_settings[name] = val;\n    }\n\n    int_f(vars, \"qp\", video.qp);\n    int_f(vars, \"min_threads\", video.min_threads);\n    int_between_f(vars, \"hevc_mode\", video.hevc_mode, { 0, 3 });\n    int_between_f(vars, \"av1_mode\", video.av1_mode, { 0, 3 });\n    string_f(vars, \"sw_preset\", video.sw.sw_preset);\n    if (!video.sw.sw_preset.empty()) {\n      video.sw.svtav1_preset = sw::svtav1_preset_from_view(video.sw.sw_preset);\n    }\n    string_f(vars, \"sw_tune\", video.sw.sw_tune);\n\n    int_between_f(vars, \"nvenc_preset\", video.nv.quality_preset, { 1, 7 });\n    int_between_f(vars, \"nvenc_vbv_increase\", video.nv.vbv_percentage_increase, { 0, 400 });\n    bool_f(vars, \"nvenc_spatial_aq\", video.nv.adaptive_quantization);\n    bool_f(vars, \"nvenc_temporal_aq\", video.nv.enable_temporal_aq);\n    generic_f(vars, \"nvenc_twopass\", video.nv.two_pass, nv::twopass_from_view);\n    bool_f(vars, \"nvenc_h264_cavlc\", video.nv.h264_cavlc);\n    generic_f(vars, \"nvenc_split_encode\", video.nv.split_frame_encoding, nv::split_encode_from_view);\n    int_between_f(vars, \"nvenc_lookahead_depth\", video.nv.lookahead_depth, { 0, 32 });\n    generic_f(vars, \"nvenc_lookahead_level\", video.nv.lookahead_level, nv::lookahead_level_from_view);\n    generic_f(vars, \"nvenc_temporal_filter\", video.nv.temporal_filter_level, nv::temporal_filter_level_from_view);\n    generic_f(vars, \"nvenc_rate_control\", video.nv.rate_control_mode, nv::rate_control_mode_from_view);\n    int_between_f(vars, \"nvenc_target_quality\", video.nv.target_quality, { 0, 63 });\n    bool_f(vars, \"nvenc_realtime_hags\", video.nv_realtime_hags);\n    bool_f(vars, \"nvenc_opengl_vulkan_on_dxgi\", video.nv_opengl_vulkan_on_dxgi);\n    bool_f(vars, \"nvenc_latency_over_power\", video.nv_sunshine_high_power_mode);\n\n#if !defined(__ANDROID__) && !defined(__APPLE__)\n    video.nv_legacy.preset = video.nv.quality_preset + 11;\n    video.nv_legacy.multipass = video.nv.two_pass == nvenc::nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION :\n                                video.nv.two_pass == nvenc::nvenc_two_pass::full_resolution    ? NV_ENC_TWO_PASS_FULL_RESOLUTION :\n                                                                                                 NV_ENC_MULTI_PASS_DISABLED;\n    video.nv_legacy.h264_coder = video.nv.h264_cavlc ? NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC : NV_ENC_H264_ENTROPY_CODING_MODE_CABAC;\n    video.nv_legacy.aq = video.nv.adaptive_quantization;\n    video.nv_legacy.vbv_percentage_increase = video.nv.vbv_percentage_increase;\n#endif\n\n    int_f(vars, \"qsv_preset\", video.qsv.qsv_preset, qsv::preset_from_view);\n    int_f(vars, \"qsv_coder\", video.qsv.qsv_cavlc, qsv::coder_from_view);\n    bool_f(vars, \"qsv_slow_hevc\", video.qsv.qsv_slow_hevc);\n\n    std::string quality;\n    string_f(vars, \"amd_quality\", quality);\n    if (!quality.empty()) {\n      video.amd.amd_quality_h264 = amd::quality_from_view<amd::quality_h264_e>(quality, video.amd.amd_quality_h264);\n      video.amd.amd_quality_hevc = amd::quality_from_view<amd::quality_hevc_e>(quality, video.amd.amd_quality_hevc);\n      video.amd.amd_quality_av1 = amd::quality_from_view<amd::quality_av1_e>(quality, video.amd.amd_quality_av1);\n    }\n\n    std::string rc;\n    string_f(vars, \"amd_rc\", rc);\n    int_f(vars, \"amd_coder\", video.amd.amd_coder, amd::coder_from_view);\n    if (!rc.empty()) {\n      video.amd.amd_rc_h264 = amd::rc_from_view<amd::rc_h264_e>(rc, video.amd.amd_rc_h264);\n      video.amd.amd_rc_hevc = amd::rc_from_view<amd::rc_hevc_e>(rc, video.amd.amd_rc_hevc);\n      video.amd.amd_rc_av1 = amd::rc_from_view<amd::rc_av1_e>(rc, video.amd.amd_rc_av1);\n    }\n\n    std::string usage;\n    string_f(vars, \"amd_usage\", usage);\n    if (!usage.empty()) {\n      video.amd.amd_usage_h264 = amd::usage_from_view<amd::usage_h264_e>(usage, video.amd.amd_usage_h264);\n      video.amd.amd_usage_hevc = amd::usage_from_view<amd::usage_hevc_e>(usage, video.amd.amd_usage_hevc);\n      video.amd.amd_usage_av1 = amd::usage_from_view<amd::usage_av1_e>(usage, video.amd.amd_usage_av1);\n    }\n\n    // HQVBR/HQCBR requires two-pass encoding, incompatible with Ultra Low Latency usage.\n    // Auto-upgrade usage to Low Latency High Quality when necessary.\n    // RC values: HIGH_QUALITY_VBR=5, HIGH_QUALITY_CBR=6 (same across H.264/HEVC/AV1)\n    // Usage values: ULTRA_LOW_LATENCY=1(H264/HEVC)/2(AV1), LOW_LATENCY_HIGH_QUALITY=5 (all codecs)\n    auto adjust_usage_for_hq_rc = [](const std::optional<int> &rc, std::optional<int> &usage, int ull_val, int llhq_val, const char *codec) {\n      if (rc && (*rc == 5 || *rc == 6) && usage && *usage == ull_val) {\n        BOOST_LOG(warning) << \"AMD \" << codec << \": HQVBR/HQCBR is incompatible with Ultra Low Latency usage, \"\n                           << \"auto-switching to Low Latency High Quality\";\n        usage = llhq_val;\n      }\n    };\n    adjust_usage_for_hq_rc(video.amd.amd_rc_h264, video.amd.amd_usage_h264, 1, 5, \"H.264\");\n    adjust_usage_for_hq_rc(video.amd.amd_rc_hevc, video.amd.amd_usage_hevc, 1, 5, \"HEVC\");\n    adjust_usage_for_hq_rc(video.amd.amd_rc_av1, video.amd.amd_usage_av1, 2, 5, \"AV1\");\n\n    bool_f(vars, \"amd_preanalysis\", (bool &) video.amd.amd_preanalysis);\n    bool_f(vars, \"amd_vbaq\", (bool &) video.amd.amd_vbaq);\n    bool_f(vars, \"amd_enforce_hrd\", (bool &) video.amd.amd_enforce_hrd);\n    int_between_f(vars, \"amd_qvbr_quality\", video.amd.amd_qvbr_quality, { 1, 51 });\n    int_between_f(vars, \"amd_ltr_frames\", video.amd.amd_ltr_frames, { 0, 4 });\n    int_between_f(vars, \"amd_slices_per_frame\", video.amd.amd_slices_per_frame, { 0, 4 });\n\n    int_f(vars, \"vt_coder\", video.vt.vt_coder, vt::coder_from_view);\n    int_f(vars, \"vt_software\", video.vt.vt_allow_sw, vt::allow_software_from_view);\n    int_f(vars, \"vt_software\", video.vt.vt_require_sw, vt::force_software_from_view);\n    int_f(vars, \"vt_realtime\", video.vt.vt_realtime, vt::rt_from_view);\n\n    bool_f(vars, \"vaapi_strict_rc_buffer\", video.vaapi.strict_rc_buffer);\n\n    string_f(vars, \"capture\", video.capture);\n    \n#ifdef _WIN32\n    // Check if WGC is selected and we're running in service mode\n    // If so, automatically switch to DDX since WGC doesn't work in system mode\n    if (!video.capture.empty() && video.capture == \"wgc\") {\n      if (is_running_as_system_user) {\n        BOOST_LOG(warning) << \"WGC capture requires user session mode. Automatically switching to DDX capture.\"sv;\n        video.capture = \"ddx\";\n      }\n    }\n#endif\n    \n    string_f(vars, \"encoder\", video.encoder);\n    string_f(vars, \"adapter_name\", video.adapter_name);\n    string_f(vars, \"output_name\", video.output_name);\n    \n#ifdef _WIN32\n    // Capture target: \"display\" (default) or \"window\"\n    string_f(vars, \"capture_target\", video.capture_target);\n    if (video.capture_target.empty()) {\n      video.capture_target = \"display\";  // Default to display capture\n    }\n    \n    // Window title for window capture\n    string_f(vars, \"window_title\", video.window_title);\n    \n    // Validate capture_target\n    if (video.capture_target != \"display\" && video.capture_target != \"window\") {\n      BOOST_LOG(warning) << \"Invalid capture_target: [\"sv << video.capture_target << \"], defaulting to 'display'\"sv;\n      video.capture_target = \"display\";\n    }\n    \n    // If window capture is selected, ensure window_title is provided\n    if (video.capture_target == \"window\" && video.window_title.empty()) {\n      BOOST_LOG(warning) << \"capture_target=window but window_title is empty. Window capture may fail.\"sv;\n    }\n#endif\n    int_f(vars, \"display_device_prep\", video.display_device_prep, display_device::parsed_config_t::device_prep_from_view);\n    int_f(vars, \"resolution_change\", video.resolution_change, display_device::parsed_config_t::resolution_change_from_view);\n    string_f(vars, \"manual_resolution\", video.manual_resolution);\n    list_display_mode_remapping_f(vars, \"display_mode_remapping\", video.display_mode_remapping);\n    int_f(vars, \"refresh_rate_change\", video.refresh_rate_change, display_device::parsed_config_t::refresh_rate_change_from_view);\n    string_f(vars, \"manual_refresh_rate\", video.manual_refresh_rate);\n    int_f(vars, \"hdr_prep\", video.hdr_prep, display_device::parsed_config_t::hdr_prep_from_view);\n    int_f(vars, \"max_bitrate\", video.max_bitrate);\n    bool_f(vars, \"variable_refresh_rate\", video.variable_refresh_rate);\n    int_between_f(vars, \"minimum_fps_target\", video.minimum_fps_target, { 0, 1000 });\n    bool_f(vars, \"hdr_luminance_analysis\", video.hdr_luminance_analysis);\n    bool_f(vars, \"wgc_disable_secure_desktop\", video.wgc_disable_secure_desktop);\n    bool_f(vars, \"vdd_keep_enabled\", video.vdd_keep_enabled);\n    bool_f(vars, \"vdd_headless_create\", video.vdd_headless_create_enabled);\n    bool_f(vars, \"vdd_reuse\", video.vdd_reuse);\n\n    // Downscaling quality: \"fast\" (bilinear+8pt average), \"balanced\" (bicubic), \"high_quality\" (future: lanczos)\n    string_f(vars, \"downscaling_quality\", video.downscaling_quality);\n    if (video.downscaling_quality.empty()) {\n      video.downscaling_quality = \"balanced\";  // Default to bicubic\n    }\n    // Validate downscaling_quality\n    if (video.downscaling_quality != \"fast\" && \n        video.downscaling_quality != \"balanced\" && \n        video.downscaling_quality != \"high_quality\") {\n      BOOST_LOG(warning) << \"Invalid downscaling_quality: [\"sv << video.downscaling_quality \n                         << \"], valid options are: fast, balanced, high_quality. Defaulting to 'balanced'\"sv;\n      video.downscaling_quality = \"balanced\";\n    }\n\n    path_f(vars, \"pkey\", nvhttp.pkey);\n    path_f(vars, \"cert\", nvhttp.cert);\n    string_f(vars, \"sunshine_name\", nvhttp.sunshine_name);\n    string_f(vars, \"clients\", nvhttp.clients);\n    path_f(vars, \"log_path\", config::sunshine.log_file);\n    path_f(vars, \"file_state\", nvhttp.file_state);\n\n    // Must be run after \"file_state\"\n    config::sunshine.credentials_file = config::nvhttp.file_state;\n    path_f(vars, \"credentials_file\", config::sunshine.credentials_file);\n\n    string_f(vars, \"external_ip\", nvhttp.external_ip);\n    list_string_f(vars, \"resolutions\"s, nvhttp.resolutions);\n    list_string_f(vars, \"fps\"s, nvhttp.fps);\n    int_between_f(vars, \"sleep_mode\", nvhttp.sleep_mode, { SLEEP_MODE_SUSPEND, SLEEP_MODE_AWAY });\n    list_prep_cmd_f(vars, \"global_prep_cmd\", config::sunshine.prep_cmds);\n\n    string_f(vars, \"audio_sink\", audio.sink);\n    string_f(vars, \"virtual_sink\", audio.virtual_sink);\n    bool_f(vars, \"stream_audio\", audio.stream);\n    bool_f(vars, \"stream_mic\", audio.stream_mic);\n    bool_f(vars, \"install_steam_audio_drivers\", audio.install_steam_drivers);\n\n    string_restricted_f(vars, \"origin_web_ui_allowed\", nvhttp.origin_web_ui_allowed, { \"pc\"sv, \"lan\"sv, \"wan\"sv });\n\n    int to = -1;\n    int_between_f(vars, \"ping_timeout\", to, { -1, std::numeric_limits<int>::max() });\n    if (to != -1) {\n      stream.ping_timeout = std::chrono::milliseconds(to);\n    }\n\n    int_between_f(vars, \"lan_encryption_mode\", stream.lan_encryption_mode, { 0, 2 });\n    int_between_f(vars, \"wan_encryption_mode\", stream.wan_encryption_mode, { 0, 2 });\n\n    path_f(vars, \"file_apps\", stream.file_apps);\n#ifndef __ANDROID__\n    // TODO: Android can possibly support this\n    if (!fs::exists(stream.file_apps.c_str())) {\n      fs::copy_file(SUNSHINE_ASSETS_DIR \"/apps.json\", stream.file_apps);\n      fs::permissions(\n        stream.file_apps,\n        fs::perms::owner_read | fs::perms::owner_write,\n        fs::perm_options::add\n      );\n    }\n#endif\n\n    int_between_f(vars, \"fec_percentage\", stream.fec_percentage, {1, 255});\n\n    map_int_int_f(vars, \"keybindings\"s, input.keybindings);\n\n    // This config option will only be used by the UI\n    // When editing in the config file itself, use \"keybindings\"\n    bool map_rightalt_to_win = false;\n    bool_f(vars, \"key_rightalt_to_key_win\", map_rightalt_to_win);\n\n    if (map_rightalt_to_win) {\n      input.keybindings.emplace(0xA5, 0x5B);\n    }\n\n    to = std::numeric_limits<int>::min();\n    int_f(vars, \"back_button_timeout\", to);\n\n    if (to > std::numeric_limits<int>::min()) {\n      input.back_button_timeout = std::chrono::milliseconds { to };\n    }\n\n    double repeat_frequency { 0 };\n    double_between_f(vars, \"key_repeat_frequency\", repeat_frequency, { 0, std::numeric_limits<double>::max() });\n\n    if (repeat_frequency > 0) {\n      config::input.key_repeat_period = std::chrono::duration<double> { 1 / repeat_frequency };\n    }\n\n    to = -1;\n    int_f(vars, \"key_repeat_delay\", to);\n    if (to >= 0) {\n      input.key_repeat_delay = std::chrono::milliseconds { to };\n    }\n\n    string_restricted_f(vars, \"gamepad\"s, input.gamepad, get_supported_gamepad_options());\n    bool_f(vars, \"ds4_back_as_touchpad_click\", input.ds4_back_as_touchpad_click);\n    bool_f(vars, \"motion_as_ds4\", input.motion_as_ds4);\n    bool_f(vars, \"touchpad_as_ds4\", input.touchpad_as_ds4);\n    bool_f(vars, \"enable_dsu_server\", input.enable_dsu_server);\n    \n    int temp_port = static_cast<int>(input.dsu_server_port);\n    int_between_f(vars, \"dsu_server_port\", temp_port, { 1024, 65535 });\n    input.dsu_server_port = static_cast<uint16_t>(temp_port);\n\n    bool_f(vars, \"mouse\", input.mouse);\n    bool_f(vars, \"keyboard\", input.keyboard);\n    bool_f(vars, \"controller\", input.controller);\n\n    bool_f(vars, \"always_send_scancodes\", input.always_send_scancodes);\n\n    bool_f(vars, \"high_resolution_scrolling\", input.high_resolution_scrolling);\n    bool_f(vars, \"native_pen_touch\", input.native_pen_touch);\n    bool_f(vars, \"virtual_mouse\", input.virtual_mouse);\n    bool_f(vars, \"amf_draw_mouse_cursor\", input.amf_draw_mouse_cursor);\n\n    bool_f(vars, \"notify_pre_releases\", sunshine.notify_pre_releases);\n\n    bool_f(vars, \"system_tray\", sunshine.system_tray);\n\n    int port = sunshine.port;\n    int_between_f(vars, \"port\"s, port, { 1024 + nvhttp::PORT_HTTPS, 65535 - rtsp_stream::RTSP_SETUP_PORT });\n    sunshine.port = (std::uint16_t) port;\n\n    string_restricted_f(vars, \"address_family\", sunshine.address_family, {\"ipv4\"sv, \"both\"sv});\n    string_f(vars, \"bind_address\", sunshine.bind_address);\n\n    bool upnp = false;\n    bool_f(vars, \"upnp\"s, upnp);\n\n    if (upnp) {\n      config::sunshine.flags[config::flag::UPNP].flip();\n    }\n\n    bool close_verify_safe = false;\n    bool_f(vars, \"close_verify_safe\"s, close_verify_safe);\n\n    if (close_verify_safe) {\n      config::sunshine.flags[config::flag::CLOSE_VERIFY_SAFE].flip();\n    }\n\n    bool mdns_broadcast = true;\n    bool_f(vars, \"mdns_broadcast\"s, mdns_broadcast);\n\n    if (mdns_broadcast) {\n      config::sunshine.flags[config::flag::MDNS_BROADCAST].flip();\n    }\n\n    bool_f(vars, \"restore_log\"s, sunshine.restore_log);\n    int_f(vars, \"max_log_size_mb\"s, sunshine.max_log_size_mb);\n    if (sunshine.max_log_size_mb < 0) {\n      sunshine.max_log_size_mb = 0;\n    }\n\n    // Webhook configuration\n    bool_f(vars, \"webhook_enabled\"s, webhook.enabled);\n    string_f(vars, \"webhook_url\"s, webhook.url);\n    bool_f(vars, \"webhook_skip_ssl_verify\"s, webhook.skip_ssl_verify);\n    \n    int webhook_timeout = 1000;\n    int_between_f(vars, \"webhook_timeout\"s, webhook_timeout, { 100, 5000 });\n    webhook.timeout = std::chrono::milliseconds(webhook_timeout);\n\n    string_restricted_f(vars, \"locale\", config::sunshine.locale, {\n                                                                   \"bg\"sv,  // Bulgarian\n                                                                   \"cs\"sv,  // Czech\n                                                                   \"de\"sv,  // German\n                                                                   \"en\"sv,  // English\n                                                                   \"en_GB\"sv,  // English (UK)\n                                                                   \"en_US\"sv,  // English (US)\n                                                                   \"es\"sv,  // Spanish\n                                                                   \"fr\"sv,  // French\n                                                                   \"it\"sv,  // Italian\n                                                                   \"ja\"sv,  // Japanese\n                                                                   \"pt\"sv,  // Portuguese\n                                                                   \"ru\"sv,  // Russian\n                                                                   \"sv\"sv,  // Swedish\n                                                                   \"tr\"sv,  // Turkish\n                                                                   \"zh\"sv,  // Chinese\n                                                                   \"zh_TW\"sv,  // Chinese (Traditional)\n                                                                 });\n\n    // 托盘菜单语言设置\n    string_restricted_f(vars, \"tray_locale\", config::sunshine.tray_locale, {\n                                                                   \"en\"sv,  // English\n                                                                   \"zh\"sv,  // Chinese (Simplified)\n                                                                   \"ja\"sv,  // Japanese\n                                                                 });\n\n    std::string log_level_string;\n    string_f(vars, \"min_log_level\", log_level_string);\n\n    if (!log_level_string.empty()) {\n      if (log_level_string == \"verbose\"sv) {\n        sunshine.min_log_level = 0;\n      }\n      else if (log_level_string == \"debug\"sv) {\n        sunshine.min_log_level = 1;\n      }\n      else if (log_level_string == \"info\"sv) {\n        sunshine.min_log_level = 2;\n      }\n      else if (log_level_string == \"warning\"sv) {\n        sunshine.min_log_level = 3;\n      }\n      else if (log_level_string == \"error\"sv) {\n        sunshine.min_log_level = 4;\n      }\n      else if (log_level_string == \"fatal\"sv) {\n        sunshine.min_log_level = 5;\n      }\n      else if (log_level_string == \"none\"sv) {\n        sunshine.min_log_level = 6;\n      }\n      else {\n        // accept digit directly\n        auto val = log_level_string[0];\n        if (val >= '0' && val < '7') {\n          sunshine.min_log_level = val - '0';\n        }\n      }\n    }\n\n    auto it = vars.find(\"flags\"s);\n    if (it != std::end(vars)) {\n      apply_flags(it->second.c_str());\n\n      vars.erase(it);\n    }\n\n    if (sunshine.min_log_level <= 3) {\n      for (auto &[var, _] : vars) {\n        std::cout << \"Warning: Unrecognized configurable option [\"sv << var << ']' << std::endl;\n      }\n    }\n  }\n\n  int\n  parse(int argc, char *argv[]) {\n    std::unordered_map<std::string, std::string> cmd_vars;\n#ifdef _WIN32\n    bool shortcut_launch = false;\n    bool service_admin_launch = false;\n#endif\n\n    for (auto x = 1; x < argc; ++x) {\n      auto line = argv[x];\n\n      if (line == \"--help\"sv) {\n        logging::print_help(*argv);\n        return 1;\n      }\n#ifdef _WIN32\n      else if (line == \"--shortcut\"sv) {\n        shortcut_launch = true;\n      }\n      else if (line == \"--shortcut-admin\"sv) {\n        service_admin_launch = true;\n      }\n#endif\n      else if (*line == '-') {\n        if (*(line + 1) == '-') {\n          sunshine.cmd.name = line + 2;\n          sunshine.cmd.argc = argc - x - 1;\n          sunshine.cmd.argv = argv + x + 1;\n\n          break;\n        }\n        if (apply_flags(line + 1)) {\n          logging::print_help(*argv);\n          return -1;\n        }\n      }\n      else {\n        auto line_end = line + strlen(line);\n\n        auto pos = std::find(line, line_end, '=');\n        if (pos == line_end) {\n          sunshine.config_file = line;\n        }\n        else {\n          TUPLE_EL(var, 1, parse_option(line, line_end));\n          if (!var) {\n            logging::print_help(*argv);\n            return -1;\n          }\n\n          TUPLE_EL_REF(name, 0, *var);\n\n          auto it = cmd_vars.find(name);\n          if (it != std::end(cmd_vars)) {\n            cmd_vars.erase(it);\n          }\n\n          cmd_vars.emplace(std::move(*var));\n        }\n      }\n    }\n\n    bool config_loaded = false;\n    try {\n      // Create appdata folder if it does not exist\n      file_handler::make_directory(platf::appdata().string());\n\n      // Create empty config file if it does not exist\n      if (!fs::exists(sunshine.config_file)) {\n        std::ofstream { sunshine.config_file };\n      }\n\n      // Read config file\n      auto vars = parse_config(file_handler::read_file(sunshine.config_file.c_str()));\n\n      for (auto &[name, value] : cmd_vars) {\n        vars.insert_or_assign(std::move(name), std::move(value));\n      }\n\n      // Apply the config. Note: This will try to create any paths\n      // referenced in the config, so we may receive exceptions if\n      // the path is incorrect or inaccessible.\n      apply_config(std::move(vars));\n      config_loaded = true;\n    }\n    catch (const std::filesystem::filesystem_error &err) {\n      BOOST_LOG(fatal) << \"Failed to apply config: \"sv << err.what();\n    }\n    catch (const boost::filesystem::filesystem_error &err) {\n      BOOST_LOG(fatal) << \"Failed to apply config: \"sv << err.what();\n    }\n\n#ifdef _WIN32\n    // UCRT64 raises an access denied exception if launching from the shortcut\n    // as non-admin and the config folder is not yet present; we can defer\n    // so that service instance will do the work instead.\n\n    if (!config_loaded && !shortcut_launch) {\n      BOOST_LOG(fatal) << \"To relaunch Sunshine successfully, use the shortcut in the Start Menu. Do not run Sunshine.exe manually.\"sv;\n      BOOST_LOG(fatal) << \"要成功重新启动 Sunshine, 请使用开始菜单中的快捷方式。不要手动运行 Sunshine.exe\"sv;\n      std::this_thread::sleep_for(10s);\n#else\n    if (!config_loaded) {\n#endif\n      return -1;\n    }\n\n#ifdef _WIN32\n    // We have to wait until the config is loaded to handle these launches,\n    // because we need to have the correct base port loaded in our config.\n    // Exception: UCRT64 shortcut_launch instances may have no config loaded due to\n    // insufficient permissions to create folder; port defaults will be acceptable.\n    if (service_admin_launch) {\n      // This is a relaunch as admin to start the service\n      service_ctrl::start_service();\n\n      // Always return 1 to ensure Sunshine doesn't start normally\n      return 1;\n    }\n    else if (shortcut_launch) {\n      if (!service_ctrl::is_service_running()) {\n        // If the service isn't running, relaunch ourselves as admin to start it\n        WCHAR executable[MAX_PATH];\n        GetModuleFileNameW(NULL, executable, ARRAYSIZE(executable));\n\n        SHELLEXECUTEINFOW shell_exec_info {};\n        shell_exec_info.cbSize = sizeof(shell_exec_info);\n        shell_exec_info.fMask = SEE_MASK_NOASYNC | SEE_MASK_NO_CONSOLE | SEE_MASK_NOCLOSEPROCESS;\n        shell_exec_info.lpVerb = L\"runas\";\n        shell_exec_info.lpFile = executable;\n        shell_exec_info.lpParameters = L\"--shortcut-admin\";\n        shell_exec_info.nShow = SW_NORMAL;\n        if (!ShellExecuteExW(&shell_exec_info)) {\n          auto winerr = GetLastError();\n          BOOST_LOG(error) << \"Failed executing shell command: \" << winerr << std::endl;\n          return 1;\n        }\n\n        // Wait for the elevated process to finish starting the service\n        WaitForSingleObject(shell_exec_info.hProcess, INFINITE);\n        CloseHandle(shell_exec_info.hProcess);\n\n        // Wait for the UI to be ready for connections\n        service_ctrl::wait_for_ui_ready();\n      }\n\n      // Launch the web UI\n      launch_ui();\n\n      // Always return 1 to ensure Sunshine doesn't start normally\n      return 1;\n    }\n#endif\n\n    return 0;\n  }\n\n  bool\n  update_config(const std::map<std::string, std::string> &updates) {\n    try {\n      // 读取现有配置文件\n      std::map<std::string, std::string> configMap;\n      try {\n        std::string fileContent = file_handler::read_file(sunshine.config_file.c_str());\n        auto existingConfig = parse_config(fileContent);\n        configMap.insert(existingConfig.begin(), existingConfig.end());\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(debug) << \"Failed to read existing config: \" << e.what();\n      }\n\n      // 更新配置项，同时检查是否有变化\n      bool hasChanged = false;\n      for (const auto &[key, value] : updates) {\n        auto it = configMap.find(key);\n        if (value.empty() || value == \"null\") {\n          // 空值则删除该配置项\n          if (it != configMap.end()) {\n            configMap.erase(it);\n            hasChanged = true;\n          }\n        }\n        else {\n          // 只有值不同时才更新\n          if (it == configMap.end() || it->second != value) {\n            configMap[key] = value;\n            hasChanged = true;\n          }\n        }\n      }\n\n      if (!hasChanged) {\n        BOOST_LOG(info) << \"Config unchanged, skip writing\";\n        return false;\n      }\n\n      // 按字母顺序写入配置文件\n      std::stringstream configStream;\n      for (const auto &[key, value] : configMap) {\n        if (!value.empty() && value != \"null\") {\n          configStream << key << \" = \" << value << std::endl;\n        }\n      }\n\n      file_handler::write_file(sunshine.config_file.c_str(), configStream.str());\n      BOOST_LOG(info) << \"Config updated successfully\";\n      return true;\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(warning) << \"Failed to update config: \" << e.what();\n      return false;\n    }\n  }\n\n  bool\n  update_full_config(const std::map<std::string, std::string> &fullConfig) {\n    try {\n      // 不需要保存到配置文件的只读字段（API响应字段，不是配置项）\n      const std::set<std::string> readonlyFields = {\n        \"status\",           // API响应状态，不是配置项\n        \"platform\",         // 平台信息，编译时确定，只读\n        \"version\",          // 版本号，只读\n        \"display_devices\",  // 显示设备列表，运行时枚举，只读\n        \"adapters\",         // 适配器列表，运行时枚举，只读\n        \"pair_name\",        // 配对名称，由系统生成，只读\n      };\n\n      // 受保护的字段：由系统托盘控制，需要保留本地配置文件中的原有值\n      const std::set<std::string> protectedFields = {\n        \"vdd_keep_enabled\",       // 由系统托盘控制，不通过Web UI修改\n        \"vdd_headless_create\",    // 由系统托盘控制，不通过Web UI修改\n        \"tray_locale\",            // 由系统托盘控制，不通过Web UI修改\n      };\n\n      // 读取现有配置文件（用于获取受保护字段的值和后续对比）\n      std::map<std::string, std::string> originalMap;\n      try {\n        std::string originalFileContent = file_handler::read_file(sunshine.config_file.c_str());\n        auto existingConfig = parse_config(originalFileContent);\n        originalMap.insert(existingConfig.begin(), existingConfig.end());\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(debug) << \"Failed to read existing config: \" << e.what();\n      }\n\n      // 使用 std::map 保证按字母顺序保存\n      std::map<std::string, std::string> resultMap;\n\n      // 收集传入的配置（跳过只读字段和受保护字段）\n      for (const auto &[key, value] : fullConfig) {\n        // 跳过只读字段\n        if (readonlyFields.count(key)) {\n          continue;\n        }\n        // 跳过受保护字段（稍后用本地值覆盖）\n        if (protectedFields.count(key)) {\n          continue;\n        }\n        // 跳过空值和 null\n        if (value.empty() || value == \"null\") {\n          continue;\n        }\n        resultMap[key] = value;\n      }\n\n      // 添加受保护字段（使用本地配置文件中的原有值）\n      for (const auto &field : protectedFields) {\n        auto it = originalMap.find(field);\n        if (it != originalMap.end() && !it->second.empty() && it->second != \"null\") {\n          resultMap[field] = it->second;\n        }\n      }\n\n      // 对比新配置与原始配置，相同则跳过写入\n      if (resultMap != originalMap) {\n        // 按字母顺序写入配置文件\n        std::stringstream configStream;\n        for (const auto &[key, value] : resultMap) {\n          configStream << key << \" = \" << value << std::endl;\n        }\n        file_handler::write_file(sunshine.config_file.c_str(), configStream.str());\n        BOOST_LOG(info) << \"Config saved successfully\";\n        return true;\n      }\n      else {\n        BOOST_LOG(info) << \"Config unchanged, skip writing\";\n        return false;\n      }\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(warning) << \"Failed to save config: \" << e.what();\n      return false;\n    }\n  }\n}  // namespace config"
  },
  {
    "path": "src/config.h",
    "content": "/**\n * @file src/config.h\n * @brief Declarations for the configuration of Sunshine.\n */\n#pragma once\n\n#include <bitset>\n#include <chrono>\n#include <map>\n#include <optional>\n#include <set>\n#include <string>\n#include <unordered_map>\n#include <vector>\n\n#include \"nvenc/nvenc_config.h\"\n\nnamespace config {\n  // track modified config options\n  inline std::unordered_map<std::string, std::string> modified_config_settings;\n\n  struct video_t {\n    // ffmpeg params\n    int qp;  // higher == more compression and less quality\n\n    int hevc_mode;\n    int av1_mode;\n\n    int max_bitrate;  // Maximum bitrate, sets ceiling in kbps for bitrate requested from client\n    int min_threads;  // Minimum number of threads/slices for CPU encoding\n    struct {\n      std::string sw_preset;\n      std::string sw_tune;\n      std::optional<int> svtav1_preset;\n    } sw;\n\n    nvenc::nvenc_config nv;\n    bool nv_realtime_hags;\n    bool nv_opengl_vulkan_on_dxgi;\n    bool nv_sunshine_high_power_mode;\n    bool vdd_keep_enabled;\n    /** When true, after stream end if no display is found (headless), create Zako VDD automatically. Default false. */\n    bool vdd_headless_create_enabled;\n    /** When true, reuse existing VDD on client switch instead of destroying and recreating. Default true. */\n    bool vdd_reuse;\n\n    struct {\n      int preset;\n      int multipass;\n      int h264_coder;\n      int aq;\n      int vbv_percentage_increase;\n    } nv_legacy;\n\n    struct {\n      std::optional<int> qsv_preset;\n      std::optional<int> qsv_cavlc;\n      bool qsv_slow_hevc;\n    } qsv;\n\n    struct {\n      std::optional<int> amd_usage_h264;\n      std::optional<int> amd_usage_hevc;\n      std::optional<int> amd_usage_av1;\n      std::optional<int> amd_rc_h264;\n      std::optional<int> amd_rc_hevc;\n      std::optional<int> amd_rc_av1;\n      std::optional<int> amd_enforce_hrd;\n      std::optional<int> amd_quality_h264;\n      std::optional<int> amd_quality_hevc;\n      std::optional<int> amd_quality_av1;\n      std::optional<int> amd_preanalysis;\n      std::optional<int> amd_vbaq;\n      int amd_coder;\n      int amd_qvbr_quality = 23;  // QVBR quality level 1-51 (lower=better, default=23)\n      int amd_ltr_frames = 1;  // LTR frames for RFI (0=disabled, default=1)\n      int amd_slices_per_frame = 0;  // Slices/tiles per frame (0=client decides, 1-4=minimum)\n    } amd;\n\n    struct {\n      int vt_allow_sw;\n      int vt_require_sw;\n      int vt_realtime;\n      int vt_coder;\n    } vt;\n\n    struct {\n      bool strict_rc_buffer;\n    } vaapi;\n\n    std::string capture;\n    std::string encoder;\n    std::string adapter_name;\n\n    struct display_mode_remapping_t {\n      std::string type;\n      std::string received_resolution;\n      std::string received_fps;\n      std::string final_resolution;\n      std::string final_refresh_rate;\n    };\n\n    std::string output_name;\n    std::string capture_target;  // \"display\" or \"window\" - determines whether to capture display or window\n    std::string window_title;     // Window title to capture when capture_target=\"window\"\n    int display_device_prep;\n    int resolution_change;\n    std::string manual_resolution;\n    int refresh_rate_change;\n    std::string manual_refresh_rate;\n    int hdr_prep;\n    std::vector<display_mode_remapping_t> display_mode_remapping;\n    bool variable_refresh_rate;  // Allow video stream framerate to match render framerate for VRR support\n    int minimum_fps_target;  // Minimum FPS target (0 = auto, 1-1000 = minimum FPS to maintain)\n    std::string downscaling_quality;  // Downscaling quality: \"fast\" (bilinear+8pt), \"balanced\" (bicubic), \"high_quality\" (future: lanczos)\n    bool hdr_luminance_analysis;  // Enable per-frame HDR luminance analysis for dynamic metadata\n    bool wgc_disable_secure_desktop;  // Auto-disable UAC secure desktop when using WGC capture\n  };\n\n  struct audio_t {\n    std::string sink;\n    std::string virtual_sink;\n    bool stream;\n    bool stream_mic;\n    bool install_steam_drivers;\n  };\n\n  constexpr int ENCRYPTION_MODE_NEVER = 0;  // Never use video encryption, even if the client supports it\n  constexpr int ENCRYPTION_MODE_OPPORTUNISTIC = 1;  // Use video encryption if available, but stream without it if not supported\n  constexpr int ENCRYPTION_MODE_MANDATORY = 2;  // Always use video encryption and refuse clients that can't encrypt\n\n  struct stream_t {\n    std::chrono::milliseconds ping_timeout;\n\n    std::string file_apps;\n\n    int fec_percentage;\n\n    // Video encryption settings for LAN and WAN streams\n    int lan_encryption_mode;\n    int wan_encryption_mode;\n  };\n\n  // Sleep mode options for PC sleep command\n  constexpr int SLEEP_MODE_SUSPEND = 0;   // S3 Sleep (traditional suspend via SetSuspendState)\n  constexpr int SLEEP_MODE_HIBERNATE = 1;  // S4 Hibernate (deeper sleep, saves to disk)\n  constexpr int SLEEP_MODE_AWAY = 2;       // Away Mode (display off, system stays on, instant wake)\n\n  struct nvhttp_t {\n    // Could be any of the following values:\n    // pc|lan|wan\n    std::string origin_web_ui_allowed;\n\n    std::string pkey;\n    std::string cert;\n\n    std::string sunshine_name;\n    std::string clients;\n\n    std::string file_state;\n\n    std::string external_ip;\n    std::vector<std::string> resolutions;\n    std::vector<std::string> fps;  // 支持小数刷新率，如 \"119.88\"\n\n    int sleep_mode;  // Sleep mode: 0=suspend(S3), 1=hibernate(S4), 2=away_mode\n  };\n\n  struct webhook_t {\n    bool enabled;\n    std::string url;\n    bool skip_ssl_verify;\n    std::chrono::milliseconds timeout;\n  };\n\n  struct input_t {\n    std::unordered_map<int, int> keybindings;\n\n    std::chrono::milliseconds back_button_timeout;\n    std::chrono::milliseconds key_repeat_delay;\n    std::chrono::duration<double> key_repeat_period;\n\n    std::string gamepad;\n    bool ds4_back_as_touchpad_click;\n    bool motion_as_ds4;\n    bool touchpad_as_ds4;\n    bool ds5_inputtino_randomize_mac;\n    bool enable_dsu_server;\n    uint16_t dsu_server_port;\n\n    bool keyboard;\n    bool mouse;\n    bool controller;\n\n    bool always_send_scancodes;\n\n    bool high_resolution_scrolling;\n    bool native_pen_touch;\n    bool virtual_mouse;\n    bool amf_draw_mouse_cursor;\n  };\n\n  namespace flag {\n    enum flag_e : std::size_t {\n      PIN_STDIN = 0,  ///< Read PIN from stdin instead of http\n      FRESH_STATE,  ///< Do not load or save state\n      FORCE_VIDEO_HEADER_REPLACE,  ///< force replacing headers inside video data\n      UPNP,  ///< Try Universal Plug 'n Play\n      CONST_PIN,  ///< Use \"universal\" pin\n      CLOSE_VERIFY_SAFE,  ///< Close verify certificate chain safely\n      MDNS_BROADCAST,  ///< Enable mDNS broadcast\n      FLAG_SIZE  ///< Number of flags\n    };\n  }\n\n  struct prep_cmd_t {\n    prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd, bool &&elevated):\n        do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)), elevated(std::move(elevated)) {}\n    explicit prep_cmd_t(std::string &&do_cmd, bool &&elevated):\n        do_cmd(std::move(do_cmd)), elevated(std::move(elevated)) {}\n    std::string do_cmd;\n    std::string undo_cmd;\n    bool elevated;\n  };\n  struct sunshine_t {\n    std::string locale;\n    std::string tray_locale;\n    int min_log_level;\n    std::bitset<flag::FLAG_SIZE> flags;\n    std::string credentials_file;\n\n    std::string username;\n    std::string password;\n    std::string salt;\n\n    std::string config_file;\n\n    struct cmd_t {\n      std::string name;\n      int argc;\n      char **argv;\n    } cmd;\n\n    std::uint16_t port;\n    std::string address_family;\n    std::string bind_address;\n\n    std::string log_file;\n    bool restore_log;  // 是否恢复日志文件（true=恢复，false=覆盖）\n    int max_log_size_mb;  // 日志文件最大大小（MB），超过后自动轮转，0=不限制\n    bool notify_pre_releases;\n    bool system_tray;\n    std::vector<prep_cmd_t> prep_cmds;\n  };\n\n  extern video_t video;\n  extern audio_t audio;\n  extern stream_t stream;\n  extern nvhttp_t nvhttp;\n  extern webhook_t webhook;\n  extern input_t input;\n  extern sunshine_t sunshine;\n\n  int\n  parse(int argc, char *argv[]);\n  std::unordered_map<std::string, std::string>\n  parse_config(const std::string_view &file_content);\n\n  bool\n  update_config(const std::map<std::string, std::string> &updates);\n\n  bool\n  update_full_config(const std::map<std::string, std::string> &fullConfig);\n}  // namespace config"
  },
  {
    "path": "src/confighttp.cpp",
    "content": "/**\n * @file src/confighttp.cpp\n * @brief Definitions for the Web UI Config HTTP server.\n *\n * @todo Authentication, better handling of routes common to nvhttp, cleanup\n */\n#define BOOST_BIND_GLOBAL_PLACEHOLDERS\n\n#include \"process.h\"\n\n#include <filesystem>\n#include <fstream>\n#include <iomanip>\n#include <algorithm>\n#include <atomic>\n#include <mutex>\n#include <stdexcept>\n#include <random>\n#include <map>\n#include <set>\n#include <sstream>\n#include <cstdio>\n#include <ctime>\n#include <openssl/evp.h>\n#include <openssl/sha.h>\n\n#include <boost/property_tree/json_parser.hpp>\n#include <boost/property_tree/ptree.hpp>\n#include <boost/property_tree/xml_parser.hpp>\n\n#include <boost/algorithm/string.hpp>\n\n#include <boost/asio/ssl/context.hpp>\n\n#include <boost/filesystem.hpp>\n#include <nlohmann/json.hpp>\n#include <Simple-Web-Server/crypto.hpp>\n#include <Simple-Web-Server/server_https.hpp>\n#include <boost/asio/ssl/context_base.hpp>\n\n#include \"config.h\"\n#include \"confighttp.h\"\n#include \"crypto.h\"\n#include \"display_device/session.h\"\n#include \"file_handler.h\"\n#include \"globals.h\"\n#include \"httpcommon.h\"\n#include \"logging.h\"\n#include \"network.h\"\n#include \"nvhttp.h\"\n#include \"platform/common.h\"\n#include \"platform/run_command.h\"\n#include \"rtsp.h\"\n#include \"src/display_device/display_device.h\"\n#include \"src/display_device/to_string.h\"\n#include \"stream.h\"\n#include \"utility.h\"\n#include \"uuid.h\"\n#include \"video.h\"\n#include \"version.h\"\n#include \"webhook.h\"\n\n#ifdef _WIN32\n  #include <iphlpapi.h>\n#endif\n\nusing namespace std::literals;\nusing json = nlohmann::json;\n\nnamespace confighttp {\n  namespace fs = std::filesystem;\n  namespace pt = boost::property_tree;\n\n  // Prevent saveApp/deleteApp concurrent write to file_apps causing file corruption, non-blocking\n  // return busy if not acquired\n  static std::atomic<bool> apps_writing { false };\n\n  using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;\n\n  using args_t = SimpleWeb::CaseInsensitiveMultimap;\n  using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;\n  using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;\n\n  enum class op_e {\n    ADD,  ///< Add client\n    REMOVE  ///< Remove client\n  };\n\n  void\n  print_req(const req_https_t &request) {\n    std::ostringstream log_stream;\n    log_stream << \"Request - TUNNEL: HTTPS\"\n               << \", METHOD: \" << request->method\n               << \", PATH: \" << request->path;\n    \n    // Headers\n    if (!request->header.empty()) {\n      log_stream << \", HEADERS: \";\n      bool first = true;\n      for (auto &[name, val] : request->header) {\n        if (!first) log_stream << \", \";\n        log_stream << name << \"=\" << (name == \"Authorization\" ? \"CREDENTIALS REDACTED\" : val);\n        first = false;\n      }\n    }\n    \n    // Query parameters\n    auto query_params = request->parse_query_string();\n    if (!query_params.empty()) {\n      log_stream << \", PARAMS: \";\n      bool first = true;\n      for (auto &[name, val] : query_params) {\n        if (!first) log_stream << \"&\";\n        log_stream << name << \"=\" << val;\n        first = false;\n      }\n    }\n    \n    BOOST_LOG(verbose) << log_stream.str();\n  }\n\n  /**\n   * @brief Send a response.\n   * @param response The HTTP response object.\n   * @param output_tree The JSON tree to send.\n   */\n  void send_response(resp_https_t response, const nlohmann::json &output_tree) {\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"application/json\");\n    headers.emplace(\"X-Frame-Options\", \"DENY\");\n    headers.emplace(\"Content-Security-Policy\", \"frame-ancestors 'none';\");\n    response->write(output_tree.dump(), headers);\n  }\n\n  void\n  send_unauthorized(resp_https_t response, req_https_t request) {\n    auto address = net::addr_to_normalized_string(request->remote_endpoint().address());\n    BOOST_LOG(error) << \"Web UI: [\"sv << address << \"] -- not authorized\"sv;\n    const SimpleWeb::CaseInsensitiveMultimap headers {\n      { \"WWW-Authenticate\", R\"(Basic realm=\"Sunshine Gamestream Host\", charset=\"UTF-8\")\" }\n    };\n    response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);\n  }\n\n  /**\n   * Logout endpoint: for localhost (PC) returns 200 so the browser does not show\n   * a password dialog; for other allowed origins returns 401 with WWW-Authenticate\n   * so the browser shows the credential prompt. Denied origins receive 403.\n   */\n  void\n  handleLogout(resp_https_t response, req_https_t request) {\n    auto address = net::addr_to_normalized_string(request->remote_endpoint().address());\n    auto ip_type = net::from_address(address);\n\n    if (ip_type > http::origin_web_ui_allowed) {\n      BOOST_LOG(error) << \"Web UI: [\"sv << address << \"] -- logout denied\"sv;\n      response->write(SimpleWeb::StatusCode::client_error_forbidden);\n      return;\n    }\n\n    if (ip_type == net::PC) {\n      BOOST_LOG(info) << \"Web UI: [\"sv << address << \"] -- logout (local), responding 200\"sv;\n      response->write(SimpleWeb::StatusCode::success_ok, \"\");\n      return;\n    }\n\n    BOOST_LOG(info) << \"Web UI: [\"sv << address << \"] -- logout requested, responding 401\"sv;\n    send_unauthorized(response, request);\n  }\n\n  void\n  send_redirect(resp_https_t response, req_https_t request, const char *path) {\n    auto address = net::addr_to_normalized_string(request->remote_endpoint().address());\n    BOOST_LOG(error) << \"Web UI: [\"sv << address << \"] -- not authorized, redirect\"sv;\n    const SimpleWeb::CaseInsensitiveMultimap headers {\n      { \"Location\", path }\n    };\n    response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers);\n  }\n\n  bool\n  authenticate(resp_https_t response, req_https_t request) {\n    auto address = net::addr_to_normalized_string(request->remote_endpoint().address());\n    auto ip_type = net::from_address(address);\n\n    if (ip_type > http::origin_web_ui_allowed) {\n      BOOST_LOG(error) << \"Web UI: [\"sv << address << \"] -- denied\"sv;\n      response->write(SimpleWeb::StatusCode::client_error_forbidden);\n      return false;\n    }\n\n    // If credentials are shown, redirect the user to a /welcome page\n    if (config::sunshine.username.empty()) {\n      send_redirect(response, request, \"/welcome\");\n      return false;\n    }\n\n    if (ip_type == net::PC) {\n      return true;\n    }\n\n    auto fg = util::fail_guard([&]() {\n      send_unauthorized(response, request);\n    });\n\n    auto auth = request->header.find(\"authorization\");\n    if (auth == request->header.end()) {\n      return false;\n    }\n\n    auto &rawAuth = auth->second;\n    constexpr auto basicPrefix = \"Basic \"sv;\n    if (rawAuth.length() <= basicPrefix.length() || \n        rawAuth.substr(0, basicPrefix.length()) != basicPrefix) {\n      return false;\n    }\n    \n    std::string authData;\n    try {\n      authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr(basicPrefix.length()));\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(debug) << \"Authentication: Base64 decode failed: \" << e.what();\n      return false;\n    }\n\n    int index = authData.find(':');\n    if (index >= authData.size() - 1) {\n      return false;\n    }\n\n    auto username = authData.substr(0, index);\n    auto password = authData.substr(index + 1);\n    auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();\n\n    if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {\n      return false;\n    }\n\n    fg.disable();\n    return true;\n  }\n\n  void\n  not_found(resp_https_t response, req_https_t request) {\n    pt::ptree tree;\n    tree.put(\"root.<xmlattr>.status_code\", 404);\n\n    std::ostringstream data;\n\n    pt::write_xml(data, tree);\n    response->write(data.str());\n\n    *response << \"HTTP/1.1 404 NOT FOUND\\r\\n\"\n              << data.str();\n  }\n\n  void\n  close_connection(resp_https_t response, req_https_t request) {\n      *response << \"HTTP/1.1 444 No Response\\r\\n\";\n      response->close_connection_after_response = true;\n      return;\n  }\n\n  void\n  getHtmlPage(resp_https_t response, req_https_t request, const std::string& pageName, bool requireAuth = true) {\n    if (requireAuth && !authenticate(response, request)) return;\n\n    print_req(request);\n\n    std::string content = file_handler::read_file((std::string(WEB_DIR) + pageName).c_str());\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"text/html; charset=utf-8\");\n    if (pageName == \"apps.html\") {\n      headers.emplace(\"Access-Control-Allow-Origin\", \"https://images.igdb.com/\");\n    }\n    response->write(content, headers);\n  }\n\n  void\n  getIndexPage(resp_https_t response, req_https_t request) {\n    getHtmlPage(response, request, \"index.html\");\n  }\n\n  void\n  getPinPage(resp_https_t response, req_https_t request) {\n    getHtmlPage(response, request, \"pin.html\");\n  }\n\n  void\n  getAppsPage(resp_https_t response, req_https_t request) {\n    getHtmlPage(response, request, \"apps.html\");\n  }\n\n  void\n  getClientsPage(resp_https_t response, req_https_t request) {\n    getHtmlPage(response, request, \"clients.html\");\n  }\n\n  void\n  getConfigPage(resp_https_t response, req_https_t request) {\n    getHtmlPage(response, request, \"config.html\");\n  }\n\n  void\n  getPasswordPage(resp_https_t response, req_https_t request) {\n    getHtmlPage(response, request, \"password.html\");\n  }\n\n  void\n  getWelcomePage(resp_https_t response, req_https_t request) {\n    // 如果已经有用户名，要求认证后才能访问（防止未授权访问）\n    // 认证通过后重定向到首页，认证失败则拒绝访问\n    if (!config::sunshine.username.empty()) {\n      if (!authenticate(response, request)) {\n        return; // authenticate已经发送了401响应\n      }\n      // 认证通过，重定向到首页\n      send_redirect(response, request, \"/\");\n      return;\n    }\n    // 只有在没有用户名时才显示welcome页面（首次设置）\n    getHtmlPage(response, request, \"welcome.html\", false);\n  }\n\n  void\n  getTroubleshootingPage(resp_https_t response, req_https_t request) {\n    getHtmlPage(response, request, \"troubleshooting.html\");\n  }\n\n  /**\n   * 处理静态资源文件\n   */\n  void\n  getStaticResource(resp_https_t response, req_https_t request, const std::string& path, const std::string& contentType) {\n    // print_req(request);\n\n    std::ifstream in(path, std::ios::binary);\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", contentType);\n    response->write(SimpleWeb::StatusCode::success_ok, in, headers);\n  }\n\n  void\n  getFaviconImage(resp_https_t response, req_https_t request) {\n    getStaticResource(response, request, WEB_DIR \"images/sunshine.ico\", \"image/x-icon\");\n  }\n\n  void\n  getSunshineLogoImage(resp_https_t response, req_https_t request) {\n    getStaticResource(response, request, WEB_DIR \"images/logo-sunshine-256.png\", \"image/png\");\n  }\n\n  /**\n   * @brief 检查 child 是否是 parent 目录的子路径（防止路径穿越）\n   */\n  bool\n  isChildPath(fs::path const &child, fs::path const &parent) {\n    auto relPath = fs::relative(child, parent);\n    return *(relPath.begin()) != fs::path(\"..\");\n  }\n\n  void\n  getBoxArt(resp_https_t response, req_https_t request) {\n    print_req(request);\n\n    // Extract image filename from request path\n    std::string path = request->path;\n    if (path.find(\"/boxart/\") == 0) {\n      path = path.substr(8); // Remove \"/boxart/\" prefix\n    }\n\n    BOOST_LOG(debug) << \"getBoxArt: Requested file: \" << path;\n\n    static const fs::path assetsRoot = fs::weakly_canonical(fs::path(SUNSHINE_ASSETS_DIR));\n    static const fs::path coversRoot = fs::weakly_canonical(platf::appdata() / \"covers\");\n\n    // First try to find in SUNSHINE_ASSETS_DIR\n    fs::path targetPath = fs::weakly_canonical(assetsRoot / path);\n    fs::path finalPath;\n    bool found = false;\n\n    // Strict check: Allow only files directly in assets root, no subdirectories\n    if (targetPath.parent_path() == assetsRoot && fs::exists(targetPath) && fs::is_regular_file(targetPath)) {\n      finalPath = targetPath;\n      found = true;\n      BOOST_LOG(debug) << \"Found in boxart: \" << finalPath.string();\n    }\n    \n    // If not found in boxart, try covers directory\n    if (!found) {\n      targetPath = fs::weakly_canonical(coversRoot / path);\n      // For covers, we use isChildPath which allows subdirectories but prevents traversal out of root\n      if (isChildPath(targetPath, coversRoot) && fs::exists(targetPath) && fs::is_regular_file(targetPath)) {\n        finalPath = targetPath;\n        found = true;\n        BOOST_LOG(debug) << \"Found in covers: \" << finalPath.string();\n      }\n    }\n\n    if (!found) {\n      // If still not found or invalid path, use default image\n      BOOST_LOG(debug) << \"Not found or invalid path, using default box.png\";\n      finalPath = assetsRoot / \"box.png\";\n      // Ensure default file exists, otherwise we might fail later\n      if (!fs::exists(finalPath)) {\n        BOOST_LOG(warning) << \"Default box.png not found at: \" << finalPath.string();\n        response->write(SimpleWeb::StatusCode::client_error_not_found, \"Image not found\");\n        return;\n      }\n    }\n\n    std::string imagePath = finalPath.string();\n\n    // Get file size\n    std::error_code ec;\n    auto fileSize = fs::file_size(imagePath, ec);\n    if (ec) {\n      BOOST_LOG(warning) << \"Failed to get file size for: \" << imagePath;\n      response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Failed to read image file\");\n      return;\n    }\n\n    // Determine Content-Type from file extension\n    std::string ext = fs::path(imagePath).extension().string();\n    if (!ext.empty() && ext[0] == '.') {\n      ext = ext.substr(1);\n    }\n    \n    auto mimeType = mime_types.find(ext);\n    std::string contentType = \"image/png\"; // Default type\n    \n    if (mimeType != mime_types.end()) {\n      contentType = mimeType->second;\n    }\n    \n    BOOST_LOG(debug) << \"Serving boxart: \" << imagePath << \" (Content-Type: \" << contentType << \", Size: \" << fileSize << \" bytes)\";\n\n    // Return image resource\n    std::ifstream in(imagePath, std::ios::binary);\n    if (!in.is_open()) {\n      BOOST_LOG(warning) << \"Failed to open image file: \" << imagePath;\n      response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Failed to open image file\");\n      return;\n    }\n\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", contentType);\n    headers.emplace(\"Content-Length\", std::to_string(fileSize));\n    headers.emplace(\"Cache-Control\", \"max-age=3600\"); // Add caching to reduce load\n    \n    response->write(SimpleWeb::StatusCode::success_ok, in, headers);\n  }\n\n  void\n  getNodeModules(resp_https_t response, req_https_t request) {\n    // print_req(request);\n    fs::path webDirPath(WEB_DIR);\n    fs::path nodeModulesPath(webDirPath / \"assets\");\n\n    // .relative_path is needed to shed any leading slash that might exist in the request path\n    auto filePath = fs::weakly_canonical(webDirPath / fs::path(request->path).relative_path());\n\n    // Don't do anything if file does not exist or is outside the assets directory\n    if (!isChildPath(filePath, nodeModulesPath)) {\n      BOOST_LOG(warning) << \"Someone requested a path \" << filePath << \" that is outside the assets folder\";\n      response->write(SimpleWeb::StatusCode::client_error_bad_request, \"Bad Request\");\n    }\n    else if (!fs::exists(filePath)) {\n      response->write(SimpleWeb::StatusCode::client_error_not_found);\n    }\n    else {\n      auto relPath = fs::relative(filePath, webDirPath);\n      // get the mime type from the file extension mime_types map\n      // remove the leading period from the extension\n      auto mimeType = mime_types.find(relPath.extension().string().substr(1));\n      // check if the extension is in the map at the x position\n      if (mimeType != mime_types.end()) {\n        // if it is, set the content type to the mime type\n        SimpleWeb::CaseInsensitiveMultimap headers;\n        headers.emplace(\"Content-Type\", mimeType->second);\n        std::ifstream in(filePath.string(), std::ios::binary);\n        response->write(SimpleWeb::StatusCode::success_ok, in, headers);\n      }\n      // do not return any file if the type is not in the map\n    }\n  }\n\n  void\n  getApps(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    std::string content = file_handler::read_file(config::stream.file_apps.c_str());\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"application/json\");\n    response->write(content, headers);\n  }\n\n  /**\n   * @brief Snapshot of log cache state, swapped atomically via shared_ptr.\n   */\n  struct LogCacheSnapshot {\n    std::shared_ptr<const std::string> content;  ///< Cached log content (nullptr in offset-only mode)\n    std::uintmax_t file_size = 0;                ///< Actual file size on disk when snapshot was taken\n    std::intmax_t mtime_ns = 0;                  ///< File mtime when snapshot was taken\n    std::uintmax_t start_offset = 0;             ///< File byte position where content begins\n  };\n\n  /**\n   * @brief Try to read only the new tail of the log file and append to existing content.\n   * @return New content on success, nullptr on any failure (caller should fall back to full read).\n   */\n  static std::shared_ptr<const std::string> try_incremental_log_read(\n    const std::filesystem::path &log_path,\n    std::uintmax_t prev_size,\n    std::uintmax_t current_size,\n    const std::shared_ptr<const std::string> &old_content) {\n    if (current_size <= prev_size || prev_size == 0 || !old_content) {\n      return nullptr;\n    }\n    std::ifstream in(log_path.string(), std::ios::binary);\n    if (!in || !in.seekg(static_cast<std::streamoff>(prev_size))) {\n      return nullptr;\n    }\n    const auto tail_len = current_size - prev_size;\n    std::string tail(tail_len, '\\0');\n    if (!in.read(tail.data(), static_cast<std::streamsize>(tail_len))) {\n      return nullptr;\n    }\n    return std::make_shared<const std::string>(*old_content + tail);\n  }\n\n  /**\n   * @brief Read a specific byte range [offset, offset+length) from a file.\n   * @return Content string on success, nullptr on failure.\n   */\n  static std::shared_ptr<const std::string>\n  read_file_range(const std::filesystem::path &path, std::uintmax_t offset, std::uintmax_t length) {\n    std::ifstream in(path.string(), std::ios::binary);\n    if (!in || !in.seekg(static_cast<std::streamoff>(offset))) {\n      return nullptr;\n    }\n    std::string content(static_cast<std::size_t>(length), '\\0');\n    if (!in.read(content.data(), static_cast<std::streamsize>(length))) {\n      return nullptr;\n    }\n    return std::make_shared<const std::string>(std::move(content));\n  }\n\n  /**\n   * @brief Get the logs from the log file.\n   * @param response The HTTP response object.\n   * @param request The HTTP request object.\n   *\n   * Dual mode via X-Log-Offset header:\n   *   - Without header: stream full log file from disk (for download)\n   *   - With header:    cached or offset-based incremental support (for live viewer)\n   *\n   * When the log file is small (≤ 4 MB), the entire file is cached in memory for fast serving.\n   * When the log file exceeds 4 MB, no content is cached; reads go directly to disk at the\n   * client's offset (offset-only mode), avoiding unbounded memory growth.\n   *\n   * @api_examples{/api/logs| GET| null}\n   */\n  void\n  getLogs(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) {\n      return;\n    }\n\n    //print_req(request);\n\n    const std::filesystem::path log_path(config::sunshine.log_file);\n\n    // --- Mode 1: No X-Log-Offset header → stream full file from disk (download) ---\n    auto offset_it = request->header.find(\"X-Log-Offset\");\n    if (offset_it == request->header.end()) {\n      std::ifstream in(log_path.string(), std::ios::binary);\n      if (!in.is_open()) {\n        response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Failed to open log file\");\n        return;\n      }\n      SimpleWeb::CaseInsensitiveMultimap headers;\n      headers.emplace(\"Content-Type\", \"text/plain; charset=utf-8\");\n      headers.emplace(\"Content-Disposition\", \"attachment; filename=\\\"sunshine.log\\\"\");\n      // Omit Content-Length: log file is actively written (TOCTOU risk)\n      response->write(SimpleWeb::StatusCode::success_ok, in, headers);\n      return;\n    }\n\n    // --- Mode 2: X-Log-Offset present → cached or offset-only mode for live viewer ---\n\n    // When file ≤ MAX_LOG_CACHE_SIZE: cached in memory.  Otherwise: disk reads only.\n    static constexpr std::uintmax_t MAX_LOG_CACHE_SIZE = 4 * 1024 * 1024;   // 4 MB\n    static constexpr std::uintmax_t MAX_RESPONSE_SIZE  = 4 * 1024 * 1024;   // 4 MB\n\n    static std::atomic<std::shared_ptr<const LogCacheSnapshot>> log_cache;\n\n    // Check file status\n    std::error_code ec;\n    auto current_size = std::filesystem::file_size(log_path, ec);\n    if (ec) {\n      response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Failed to read log file\");\n      return;\n    }\n    auto current_mtime = std::filesystem::last_write_time(log_path, ec);\n    if (ec) {\n      response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Failed to read log file\");\n      return;\n    }\n    auto current_mtime_ns = current_mtime.time_since_epoch().count();\n\n    // Refresh cache if file changed\n    auto snapshot = log_cache.load();\n    const bool cache_stale = !snapshot || current_size != snapshot->file_size || current_mtime_ns != snapshot->mtime_ns;\n    if (cache_stale) {\n      auto new_snap = std::make_shared<LogCacheSnapshot>();\n      new_snap->file_size = current_size;\n      new_snap->mtime_ns = current_mtime_ns;\n\n      if (current_size <= MAX_LOG_CACHE_SIZE) {\n        // --- Cached mode: file fits in memory ---\n        std::shared_ptr<const std::string> new_content;\n        if (snapshot && snapshot->content && snapshot->file_size > 0 && current_size > snapshot->file_size) {\n          new_content = try_incremental_log_read(log_path, snapshot->file_size, current_size, snapshot->content);\n        }\n        if (!new_content) {\n          // Use sampled current_size to avoid TOCTOU unsigned underflow at start_offset\n          auto read_len = std::min(current_size, MAX_LOG_CACHE_SIZE);\n          auto read_start = current_size - read_len;\n          new_content = read_file_range(log_path, read_start, read_len);\n        }\n        if (!new_content) {\n          if (current_size > 0) {\n            response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Failed to read log file\");\n            return;\n          }\n          new_content = std::make_shared<const std::string>();\n        }\n        new_snap->content = std::move(new_content);\n        new_snap->start_offset = current_size - new_snap->content->size();\n      }\n      else {\n        // --- Offset-only mode: file too large, don't cache content ---\n        new_snap->content = nullptr;\n        new_snap->start_offset = 0;\n      }\n\n      // CAS publish: avoid overwriting a newer snapshot from a concurrent thread\n      if (!log_cache.compare_exchange_strong(snapshot, new_snap)) {\n        // CAS failed: snapshot already updated by compare_exchange_strong\n      }\n      else {\n        snapshot = std::move(new_snap);\n      }\n    }\n\n    // Parse client offset\n    std::uintmax_t client_offset = 0;\n    try {\n      std::string offset_str(offset_it->second);\n      boost::algorithm::trim(offset_str);\n      if (!offset_str.empty()) {\n        client_offset = std::stoull(offset_str);\n      }\n    }\n    catch (const std::invalid_argument &) {\n      client_offset = 0;\n    }\n    catch (const std::out_of_range &) {\n      client_offset = 0;\n    }\n\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"text/plain\");\n    headers.emplace(\"X-Log-Size\", std::to_string(snapshot->file_size));\n    headers.emplace(\"X-Frame-Options\", \"DENY\");\n    headers.emplace(\"Content-Security-Policy\", \"frame-ancestors 'none';\");\n\n    // No change in logs → 304\n    if (client_offset > 0 && client_offset == snapshot->file_size) {\n      headers.emplace(\"X-Log-Range\", \"unchanged\");\n      response->write(SimpleWeb::StatusCode::redirection_not_modified, headers);\n      return;\n    }\n\n    if (snapshot->content) {\n      // === Cached mode: serve from memory ===\n      if (client_offset > 0 && client_offset >= snapshot->start_offset && client_offset < snapshot->file_size) {\n        auto cache_pos = client_offset - snapshot->start_offset;\n        headers.emplace(\"X-Log-Range\", \"incremental\");\n        auto delta = snapshot->content->substr(static_cast<std::size_t>(cache_pos));\n        response->write(SimpleWeb::StatusCode::success_ok, delta, headers);\n      }\n      else {\n        headers.emplace(\"X-Log-Range\", \"full\");\n        response->write(SimpleWeb::StatusCode::success_ok, *snapshot->content, headers);\n      }\n    }\n    else {\n      // === Offset-only mode: read from disk, bounded by snapshot->file_size ===\n      if (client_offset > 0 && client_offset < snapshot->file_size) {\n        auto delta_size = snapshot->file_size - client_offset;\n        if (delta_size <= MAX_RESPONSE_SIZE) {\n          auto data = read_file_range(log_path, client_offset, delta_size);\n          if (data) {\n            headers.emplace(\"X-Log-Range\", \"incremental\");\n            response->write(SimpleWeb::StatusCode::success_ok, *data, headers);\n            return;\n          }\n        }\n      }\n      // Delta too large, read failed, or first request: return tail up to snapshot->file_size\n      auto tail_len = std::min(snapshot->file_size, MAX_RESPONSE_SIZE);\n      auto tail_start = snapshot->file_size - tail_len;\n      auto tail = read_file_range(log_path, tail_start, tail_len);\n      if (!tail) {\n        response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Failed to read log file\");\n        return;\n      }\n      headers.emplace(\"X-Log-Range\", \"full\");\n      response->write(SimpleWeb::StatusCode::success_ok, *tail, headers);\n    }\n  }\n\n  void\n  saveApp(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    // Prevent concurrent write to file_apps causing file corruption\n    bool expected = false;\n    if (!apps_writing.compare_exchange_strong(expected, true)) {\n      pt::ptree outputTree;\n      outputTree.put(\"status\", \"false\");\n      outputTree.put(\"error\", \"Another save operation is in progress\");\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(SimpleWeb::StatusCode::client_error_conflict, data.str());\n      return;\n    }\n    auto writing_guard = util::fail_guard([]() { apps_writing = false; });\n\n    std::stringstream ss;\n    ss << request->content.rdbuf();\n\n    pt::ptree outputTree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    pt::ptree inputTree, fileTree;\n\n    try {\n      pt::read_json(ss, inputTree);\n      pt::read_json(config::stream.file_apps, fileTree);\n\n      auto &apps_node = fileTree.get_child(\"apps\"s);\n      auto &input_apps_node = inputTree.get_child(\"apps\"s);\n      auto &input_edit_node = inputTree.get_child(\"editApp\"s);\n\n      // Validate app name when editing a specific app\n      if (!input_edit_node.empty()) {\n        auto app_name = input_edit_node.get<std::string>(\"name\", \"\");\n        if (app_name.empty() || app_name.size() > 256) {\n          outputTree.put(\"status\", \"false\");\n          outputTree.put(\"error\", \"App name must be 1-256 characters\");\n          return;\n        }\n      }\n\n      if (input_edit_node.empty()) {\n        fileTree.erase(\"apps\");\n        fileTree.push_back(std::make_pair(\"apps\", input_apps_node));\n      }\n      else {\n        auto prep_cmd = input_edit_node.get_child_optional(\"prep-cmd\");\n        if (prep_cmd && prep_cmd->empty()) {\n          input_edit_node.erase(\"prep-cmd\");\n        }\n\n        auto detached = input_edit_node.get_child_optional(\"detached\");\n        if (detached && detached->empty()) {\n          input_edit_node.erase(\"detached\");\n        }\n\n        int index = input_edit_node.get<int>(\"index\");\n        input_edit_node.erase(\"index\");\n\n        if (index == -1) {\n          apps_node.push_back(std::make_pair(\"\", input_edit_node));\n        }\n        else {\n          // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick\n          pt::ptree newApps;\n          int i = 0;\n          for (auto &[_, app_node] : input_apps_node) {\n            newApps.push_back(std::make_pair(\"\", i == index ? input_edit_node : app_node));\n            i++;\n          }\n          fileTree.erase(\"apps\");\n          fileTree.push_back(std::make_pair(\"apps\", newApps));\n        }\n      }\n\n      pt::write_json(config::stream.file_apps, fileTree);\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"SaveApp: \"sv << e.what();\n\n      outputTree.put(\"status\", \"false\");\n      outputTree.put(\"error\", \"Invalid Input JSON\");\n      return;\n    }\n\n    BOOST_LOG(info) << \"SaveApp: configuration saved successfully\"sv;\n    outputTree.put(\"status\", \"true\");\n    proc::refresh(config::stream.file_apps);\n  }\n\n  void\n  deleteApp(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    // Prevent concurrent write to file_apps causing file corruption\n    bool expected = false;\n    if (!apps_writing.compare_exchange_strong(expected, true)) {\n      pt::ptree outputTree;\n      outputTree.put(\"status\", \"false\");\n      outputTree.put(\"error\", \"Another operation is in progress\");\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(SimpleWeb::StatusCode::client_error_conflict, data.str());\n      return;\n    }\n    auto writing_guard = util::fail_guard([]() { apps_writing = false; });\n\n    pt::ptree outputTree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n    pt::ptree fileTree;\n    try {\n      pt::read_json(config::stream.file_apps, fileTree);\n      auto &apps_node = fileTree.get_child(\"apps\"s);\n      int index = stoi(request->path_match[1]);\n\n      int apps_count = static_cast<int>(apps_node.size());\n      if (index < 0 || index >= apps_count) {\n        outputTree.put(\"status\", \"false\");\n        outputTree.put(\"error\", \"Invalid Index\");\n        return;\n      }\n      else {\n        // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick\n        pt::ptree newApps;\n        int i = 0;\n        for (const auto &kv : apps_node) {\n          if (i++ != index) {\n            newApps.push_back(std::make_pair(\"\", kv.second));\n          }\n        }\n        fileTree.erase(\"apps\");\n        fileTree.push_back(std::make_pair(\"apps\", newApps));\n      }\n      pt::write_json(config::stream.file_apps, fileTree);\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"DeleteApp: \"sv << e.what();\n      outputTree.put(\"status\", \"false\");\n      outputTree.put(\"error\", \"Invalid File JSON\");\n      return;\n    }\n\n    BOOST_LOG(info) << \"DeleteApp: configuration deleted successfully\"sv;\n    outputTree.put(\"status\", \"true\");\n    proc::refresh(config::stream.file_apps);\n  }\n\n  void\n  uploadCover(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    std::stringstream ss;\n    std::stringstream configStream;\n    ss << request->content.rdbuf();\n    pt::ptree outputTree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      SimpleWeb::StatusCode code = SimpleWeb::StatusCode::success_ok;\n      if (outputTree.get_child_optional(\"error\").has_value()) {\n        code = SimpleWeb::StatusCode::client_error_bad_request;\n      }\n\n      pt::write_json(data, outputTree);\n      response->write(code, data.str());\n    });\n    pt::ptree inputTree;\n    try {\n      pt::read_json(ss, inputTree);\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"UploadCover: \"sv << e.what();\n      outputTree.put(\"status\", \"false\");\n      outputTree.put(\"error\", e.what());\n      return;\n    }\n\n    auto key = inputTree.get(\"key\", \"\");\n    if (key.empty()) {\n      outputTree.put(\"error\", \"Cover key is required\");\n      return;\n    }\n    auto url = inputTree.get(\"url\", \"\");\n\n    const std::string coverdir = platf::appdata().string() + \"/covers/\";\n    file_handler::make_directory(coverdir);\n\n    std::basic_string path = coverdir + http::url_escape(key) + \".png\";\n    if (!url.empty()) {\n      if (http::url_get_host(url) != \"images.igdb.com\") {\n        outputTree.put(\"error\", \"Only images.igdb.com is allowed\");\n        return;\n      }\n      if (!http::download_image_with_magic_check(url, path)) {\n        outputTree.put(\"error\", \"Failed to download cover\");\n        return;\n      }\n    }\n    else {\n      // Limit base64 data size to prevent memory exhaustion\n      // (10MB decoded to about 7.5MB, enough for cover images)\n      constexpr std::size_t MAX_COVER_BASE64_SIZE = 10 * 1024 * 1024;\n      auto base64_str = inputTree.get<std::string>(\"data\");\n      if (base64_str.size() > MAX_COVER_BASE64_SIZE) {\n        outputTree.put(\"error\", \"Cover image too large (max 10MB)\");\n        return;\n      }\n      auto data = SimpleWeb::Crypto::Base64::decode(base64_str);\n\n      std::ofstream imgfile(path, std::ios::binary);\n      if (!imgfile.is_open()) {\n        outputTree.put(\"error\", \"Failed to create file\");\n        return;\n      }\n      imgfile.write(data.data(), (int) data.size());\n      imgfile.close();\n      \n      if (imgfile.fail()) {\n        outputTree.put(\"error\", \"Failed to write file\");\n        return;\n      }\n    }\n    outputTree.put(\"path\", path);\n  }\n\n  void\n  getConfig(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    pt::ptree outputTree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    auto devices { display_device::enum_available_devices() };\n    pt::ptree devices_nodes;\n    for (const auto &[device_id, data] : devices) {\n      pt::ptree devices_node;\n      devices_node.put(\"device_id\"s, device_id);\n      devices_node.put(\"data\"s, to_string(data));\n      devices_nodes.push_back(std::make_pair(\"\"s, devices_node));\n    }\n\n    auto adapters { platf::adapter_names() };\n    pt::ptree adapters_nodes;\n    for (const auto &adapter_name : adapters) {\n      pt::ptree adapters_node;\n      adapters_node.put(\"name\"s, adapter_name);\n      adapters_nodes.push_back(std::make_pair(\"\"s, adapters_node));\n    }\n\n    outputTree.add_child(\"display_devices\", devices_nodes);\n    outputTree.add_child(\"adapters\", adapters_nodes);\n    outputTree.put(\"status\", \"true\");\n    outputTree.put(\"platform\", SUNSHINE_PLATFORM);\n    outputTree.put(\"version\", PROJECT_VER);\n\n    auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str()));\n    for (auto &[name, value] : vars) {\n      outputTree.put(std::move(name), std::move(value));\n    }\n\n    outputTree.put(\"pair_name\", nvhttp::get_pair_name());\n  }\n\n  void\n  getLocale(resp_https_t response, req_https_t request) {\n    // we need to return the locale whether authenticated or not\n\n    print_req(request);\n\n    pt::ptree outputTree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    outputTree.put(\"status\", \"true\");\n    outputTree.put(\"locale\", config::sunshine.locale);\n  }\n\n  std::vector<std::string>\n  split(const std::string &str, char delimiter) {\n    std::vector<std::string> tokens;\n    size_t start = 0, end = 0;\n    while ((end = str.find(delimiter, start)) != std::string::npos) {\n      tokens.push_back(str.substr(start, end - start));\n      start = end + 1;\n    }\n    tokens.push_back(str.substr(start));\n    return tokens;\n  }\n\n  bool\n  saveVddSettings(std::string resArray, std::string fpsArray, std::string gpu_name) {\n    pt::ptree iddOptionTree;\n    pt::ptree global_node;\n    pt::ptree resolutions_nodes;\n\n    // prepare resolutions setting for vdd\n    boost::regex pattern(\"\\\\[|\\\\]|\\\\s+\");\n    char delimiter = ',';\n\n    // 添加全局刷新率到global节点\n    for (const auto &fps : split(boost::regex_replace(fpsArray, pattern, \"\"), delimiter)) {\n      global_node.add(\"g_refresh_rate\", fps);\n    }\n\n    std::string str = boost::regex_replace(resArray, pattern, \"\");\n    boost::algorithm::trim(str);\n    for (const auto &resolution : split(str, delimiter)) {\n      auto index = resolution.find('x');\n      if(index == std::string::npos) {\n        return false;\n      }\n      pt::ptree res_node;\n      res_node.put(\"width\", resolution.substr(0, index));\n      res_node.put(\"height\", resolution.substr(index + 1));\n      resolutions_nodes.push_back(std::make_pair(\"resolution\"s, res_node));\n    }\n\n    // 类似于 config.cpp 中的 path_f 函数逻辑，使用相对路径\n    std::filesystem::path idd_option_path = platf::appdata() / \"vdd_settings.xml\";\n\n    BOOST_LOG(info) << \"VDD配置文件路径: \" << idd_option_path.string();\n\n    if (!fs::exists(idd_option_path)) {\n        return false;\n    }\n\n    // 先读取现有配置文件\n    pt::ptree existing_root;\n    pt::ptree root;\n\n    try {\n      pt::read_xml(idd_option_path.string(), existing_root);\n      // 如果现有配置文件中已有vdd_settings节点\n      if (existing_root.get_child_optional(\"vdd_settings\")) {\n        // 复制现有配置\n        iddOptionTree = existing_root.get_child(\"vdd_settings\");\n\n        // 更新需要更改的部分\n        pt::ptree monitor_node;\n        monitor_node.put(\"count\", 1);\n\n        pt::ptree gpu_node;\n        gpu_node.put(\"friendlyname\", gpu_name.empty() ? \"default\" : gpu_name);\n\n        // 替换配置\n        iddOptionTree.put_child(\"monitors\", monitor_node);\n        iddOptionTree.put_child(\"gpu\", gpu_node);\n        iddOptionTree.put_child(\"global\", global_node);\n        iddOptionTree.put_child(\"resolutions\", resolutions_nodes);\n      } else {\n        // 如果没有vdd_settings节点，创建新的\n        pt::ptree monitor_node;\n        monitor_node.put(\"count\", 1);\n\n        pt::ptree gpu_node;\n        gpu_node.put(\"friendlyname\", gpu_name.empty() ? \"default\" : gpu_name);\n\n        iddOptionTree.add_child(\"monitors\", monitor_node);\n        iddOptionTree.add_child(\"gpu\", gpu_node);\n        iddOptionTree.add_child(\"global\", global_node);\n        iddOptionTree.add_child(\"resolutions\", resolutions_nodes);\n      }\n    } catch(std::exception &e) {\n      // 读取失败，创建新的配置\n      BOOST_LOG(warning) << \"读取现有VDD配置失败，创建新配置: \" << e.what();\n\n      pt::ptree monitor_node;\n      monitor_node.put(\"count\", 1);\n\n      pt::ptree gpu_node;\n      gpu_node.put(\"friendlyname\", gpu_name.empty() ? \"default\" : gpu_name);\n\n      iddOptionTree.add_child(\"monitors\", monitor_node);\n      iddOptionTree.add_child(\"gpu\", gpu_node);\n      iddOptionTree.add_child(\"global\", global_node);\n      iddOptionTree.add_child(\"resolutions\", resolutions_nodes);\n    }\n\n    root.add_child(\"vdd_settings\", iddOptionTree);\n    try {\n      // 使用更紧凑的XML格式设置，减少不必要的空白\n      auto setting = boost::property_tree::xml_writer_make_settings<std::string>(' ', 2);\n      std::ostringstream oss;\n      write_xml(oss, root, setting);\n\n      // 清理多余空行，保持XML格式整洁\n      std::string xml_content = oss.str();\n      boost::regex empty_lines_regex(\"\\\\n\\\\s*\\\\n\");\n      xml_content = boost::regex_replace(xml_content, empty_lines_regex, \"\\n\");\n\n      std::ofstream file(idd_option_path.string());\n      file << xml_content;\n      file.close();\n\n      return true;\n    }\n    catch(std::exception &e) {\n      BOOST_LOG(warning) << \"写入VDD配置失败: \" << e.what();\n      return false;\n    }\n  }\n\n  void\n  saveConfig(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    std::stringstream ss;\n    ss << request->content.rdbuf();\n    pt::ptree outputTree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    pt::ptree inputTree;\n\n    try {\n      pt::read_json(ss, inputTree);\n      std::string resArray = inputTree.get<std::string>(\"resolutions\", \"[]\");\n      std::string fpsArray = inputTree.get<std::string>(\"fps\", \"[]\");\n      std::string gpu_name = inputTree.get<std::string>(\"adapter_name\", \"\");\n\n      // Validate config field lengths to prevent abuse\n      auto sunshine_name = inputTree.get<std::string>(\"sunshine_name\", \"\");\n      if (sunshine_name.size() > 256) {\n        outputTree.put(\"status\", \"false\");\n        outputTree.put(\"error\", \"sunshine_name too long (max 256)\");\n        return;\n      }\n      if (gpu_name.size() > 256) {\n        outputTree.put(\"status\", \"false\");\n        outputTree.put(\"error\", \"adapter_name too long (max 256)\");\n        return;\n      }\n\n      saveVddSettings(resArray, fpsArray, gpu_name);\n\n      // 将 inputTree 转换为 std::map（保证有序）\n      std::map<std::string, std::string> fullConfig;\n      for (const auto &kv : inputTree) {\n        std::string value = inputTree.get<std::string>(kv.first);\n        fullConfig[kv.first] = value;\n      }\n\n      // 更新配置\n      config::update_full_config(fullConfig);\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"SaveConfig: \"sv << e.what();\n      outputTree.put(\"status\", \"false\");\n      outputTree.put(\"error\", e.what());\n      return;\n    }\n\n    outputTree.put(\"status\", \"true\");\n  }\n\n  void\n  restart(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    // We may not return from this call\n    platf::restart();\n  }\n\n  void\n  boom(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n    if (GetConsoleWindow() == NULL) {\n      lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true);\n      return;\n    }\n    lifetime::exit_sunshine(0, false);\n  }\n\n  void\n  resetDisplayDevicePersistence(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    pt::ptree outputTree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    display_device::session_t::get().reset_persistence();\n    outputTree.put(\"status\", true);\n  }\n\n  void\n  savePassword(resp_https_t response, req_https_t request) {\n    if (!config::sunshine.username.empty() && !authenticate(response, request)) return;\n\n    print_req(request);\n\n    std::stringstream ss;\n    std::stringstream configStream;\n    ss << request->content.rdbuf();\n\n    pt::ptree inputTree, outputTree;\n\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    try {\n      pt::read_json(ss, inputTree);\n      auto username = inputTree.count(\"currentUsername\") > 0 ? inputTree.get<std::string>(\"currentUsername\") : \"\";\n      auto newUsername = inputTree.get<std::string>(\"newUsername\");\n      auto password = inputTree.count(\"currentPassword\") > 0 ? inputTree.get<std::string>(\"currentPassword\") : \"\";\n      auto newPassword = inputTree.count(\"newPassword\") > 0 ? inputTree.get<std::string>(\"newPassword\") : \"\";\n      auto confirmPassword = inputTree.count(\"confirmNewPassword\") > 0 ? inputTree.get<std::string>(\"confirmNewPassword\") : \"\";\n\n      // Validate credential lengths\n      if (newUsername.size() > 128) {\n        outputTree.put(\"status\", false);\n        outputTree.put(\"error\", \"Username too long (max 128)\");\n        return;\n      }\n      if (newPassword.size() > 256) {\n        outputTree.put(\"status\", false);\n        outputTree.put(\"error\", \"Password too long (max 256)\");\n        return;\n      }\n\n      if (newUsername.length() == 0) newUsername = username;\n      if (newUsername.length() == 0) {\n        outputTree.put(\"status\", false);\n        outputTree.put(\"error\", \"Invalid Username\");\n      }\n      else {\n        auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();\n        if (config::sunshine.username.empty() || (boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password)) {\n          if (newPassword.empty() || newPassword != confirmPassword) {\n            outputTree.put(\"status\", false);\n            outputTree.put(\"error\", \"Password Mismatch\");\n          }\n          else {\n            http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);\n            http::reload_user_creds(config::sunshine.credentials_file);\n            outputTree.put(\"status\", true);\n          }\n        }\n        else {\n          outputTree.put(\"status\", false);\n          outputTree.put(\"error\", \"Invalid Current Credentials\");\n        }\n      }\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"SavePassword: \"sv << e.what();\n      outputTree.put(\"status\", false);\n      outputTree.put(\"error\", e.what());\n      return;\n    }\n  }\n\n  void\n  savePin(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    std::stringstream ss;\n    ss << request->content.rdbuf();\n\n    pt::ptree inputTree, outputTree;\n\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    try {\n      pt::read_json(ss, inputTree);\n      std::string pin = inputTree.get<std::string>(\"pin\");\n      std::string name = inputTree.get<std::string>(\"name\");\n\n      // Validate PIN: must be numeric digits only, 4-8 characters\n      if (pin.size() < 4 || pin.size() > 8 || !std::all_of(pin.begin(), pin.end(), ::isdigit)) {\n        outputTree.put(\"status\", false);\n        outputTree.put(\"error\", \"PIN must be 4-8 digits\");\n        return;\n      }\n      // Validate client name\n      if (name.empty() || name.size() > 256) {\n        outputTree.put(\"status\", false);\n        outputTree.put(\"error\", \"Client name must be 1-256 characters\");\n        return;\n      }\n\n      bool pin_result = nvhttp::pin(pin, name);\n      outputTree.put(\"status\", pin_result);\n\n      // Send webhook notification\n      webhook::send_event_async(webhook::event_t{\n        .type = pin_result ? webhook::event_type_t::CONFIG_PIN_SUCCESS : webhook::event_type_t::CONFIG_PIN_FAILED,\n        .alert_type = pin_result ? \"config_pair_success\" : \"config_pair_failed\",\n        .timestamp = webhook::get_current_timestamp(),\n        .client_name = name,\n        .client_ip = net::addr_to_normalized_string(request->remote_endpoint().address()),\n        .server_ip = net::addr_to_normalized_string(request->local_endpoint().address()),\n        .app_name = \"\",\n        .app_id = 0,\n        .session_id = \"\",\n        .extra_data = {}\n      });\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"SavePin: \"sv << e.what();\n      outputTree.put(\"status\", false);\n      outputTree.put(\"error\", e.what());\n\n      // Send webhook notification for pairing failure\n      webhook::send_event_async(webhook::event_t{\n        .type = webhook::event_type_t::CONFIG_PIN_FAILED,\n        .alert_type = \"config_pair_failed\",\n        .timestamp = webhook::get_current_timestamp(),\n        .client_name = \"\",\n        .client_ip = net::addr_to_normalized_string(request->remote_endpoint().address()),\n        .server_ip = net::addr_to_normalized_string(request->local_endpoint().address()),\n        .app_name = \"\",\n        .app_id = 0,\n        .session_id = \"\",\n        .extra_data = {{\"error\", e.what()}}\n      });\n      return;\n    }\n  }\n\n  void\n  getQrPairStatus(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    nlohmann::json j;\n    j[\"status\"] = nvhttp::get_qr_pair_status();\n\n    std::string content = j.dump();\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"application/json\");\n    response->write(SimpleWeb::StatusCode::success_ok, content, headers);\n  }\n\n  void\n  generateQrPairInfo(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    pt::ptree outputTree;\n\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    // Generate a random 4-digit PIN using OpenSSL CSPRNG\n    uint16_t random_val;\n    RAND_bytes(reinterpret_cast<unsigned char *>(&random_val), sizeof(random_val));\n    int pin_num = random_val % 10000;\n    char pin_buf[5];\n    std::snprintf(pin_buf, sizeof(pin_buf), \"%04d\", pin_num);\n    std::string pin(pin_buf);\n\n    // Set the preset PIN in nvhttp (valid for 120 seconds)\n    std::string server_name = config::nvhttp.sunshine_name;\n    if (!nvhttp::set_preset_pin(pin, server_name, 120)) {\n      outputTree.put(\"status\", false);\n      outputTree.put(\"error\", \"Failed to set preset PIN\");\n      return;\n    }\n\n    // Get server address info\n    auto local_addr = net::addr_to_normalized_string(request->local_endpoint().address());\n    auto port = net::map_port(nvhttp::PORT_HTTP);\n\n    // Determine the host address for the QR code URL\n    std::string host;\n    if (!config::nvhttp.external_ip.empty()) {\n      // User explicitly configured an external IP (could be WAN for port-forwarded setups)\n      host = config::nvhttp.external_ip;\n    }\n    else {\n      // Detect a usable LAN IP (local_endpoint may be loopback or VPN)\n      host = local_addr;\n      auto host_net_type = net::from_address(host);\n      if (host_net_type != net::LAN) {\n        std::string resolved_host;\n\n        // Method 1: UDP connect trick to find default outgoing interface\n        try {\n          boost::asio::io_context io_ctx;\n          boost::asio::ip::udp::socket socket(io_ctx);\n          socket.connect(boost::asio::ip::udp::endpoint(boost::asio::ip::make_address(\"8.8.8.8\"), 53));\n          auto lan_addr = socket.local_endpoint().address();\n          socket.close();\n          auto candidate = net::addr_to_normalized_string(lan_addr);\n          if (net::from_address(candidate) == net::LAN) {\n            resolved_host = candidate;\n          }\n        }\n        catch (...) {}\n\n#ifdef _WIN32\n        // Method 2 (Windows): enumerate adapters via GetAdaptersAddresses\n        if (resolved_host.empty()) {\n          ULONG bufLen = 15000;\n          std::vector<uint8_t> buf(bufLen);\n          auto pAddresses = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buf.data());\n          if (GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST, nullptr, pAddresses, &bufLen) == NO_ERROR) {\n            for (auto adapter = pAddresses; adapter; adapter = adapter->Next) {\n              if (adapter->OperStatus != IfOperStatusUp) continue;\n              if (adapter->IfType == IF_TYPE_TUNNEL || adapter->IfType == IF_TYPE_PPP) continue;\n              for (auto unicast = adapter->FirstUnicastAddress; unicast; unicast = unicast->Next) {\n                if (unicast->Address.lpSockaddr->sa_family != AF_INET) continue;\n                auto sin = reinterpret_cast<sockaddr_in *>(unicast->Address.lpSockaddr);\n                boost::asio::ip::address_v4 addr(ntohl(sin->sin_addr.s_addr));\n                auto candidate = addr.to_string();\n                if (net::from_address(candidate) == net::LAN) {\n                  resolved_host = candidate;\n                  break;\n                }\n              }\n              if (!resolved_host.empty()) break;\n            }\n          }\n        }\n#endif\n\n        if (!resolved_host.empty()) {\n          host = resolved_host;\n          BOOST_LOG(info) << \"QR pair: resolved to LAN IP \" << host;\n        }\n        else {\n          BOOST_LOG(warning) << \"QR pair: could not find a LAN IP, using \" << host;\n        }\n      }\n    }\n\n    // Build the moonlight:// URL\n    std::string url = \"moonlight://pair?host=\" + host +\n                      \"&port=\" + std::to_string(port) +\n                      \"&pin=\" + pin +\n                      \"&name=\" + server_name;\n\n    outputTree.put(\"status\", true);\n    outputTree.put(\"pin\", pin);\n    outputTree.put(\"host\", host);\n    outputTree.put(\"port\", port);\n    outputTree.put(\"name\", server_name);\n    outputTree.put(\"url\", url);\n    outputTree.put(\"expires_in\", 120);\n  }\n\n  void\n  cancelQrPair(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    nvhttp::clear_preset_pin();\n\n    pt::ptree outputTree;\n    outputTree.put(\"status\", true);\n\n    std::ostringstream data;\n    pt::write_json(data, outputTree);\n    response->write(data.str());\n  }\n\n  void\n  unpairAll(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    pt::ptree outputTree;\n\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n    nvhttp::erase_all_clients();\n    proc::proc.terminate();\n    outputTree.put(\"status\", true);\n  }\n\n  void\n  unpair(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    std::stringstream ss;\n    ss << request->content.rdbuf();\n\n    pt::ptree inputTree, outputTree;\n\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    try {\n      pt::read_json(ss, inputTree);\n      std::string uuid = inputTree.get<std::string>(\"uuid\");\n\n      // Validate UUID format (hex + hyphens, reasonable length)\n      if (uuid.empty() || uuid.size() > 64) {\n        outputTree.put(\"status\", false);\n        outputTree.put(\"error\", \"Invalid client UUID\");\n        return;\n      }\n\n      outputTree.put(\"status\", nvhttp::unpair_client(uuid));\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"Unpair: \"sv << e.what();\n      outputTree.put(\"status\", false);\n      outputTree.put(\"error\", e.what());\n      return;\n    }\n  }\n\n  void\n  renameClient(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    pt::ptree inputTree, outputTree;\n\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    try {\n      std::stringstream ss;\n      ss << request->content.rdbuf();\n      pt::read_json(ss, inputTree);\n\n      std::string uuid = inputTree.get<std::string>(\"uuid\");\n      std::string new_name = inputTree.get<std::string>(\"name\");\n\n      if (new_name.empty()) {\n        outputTree.put(\"status\", false);\n        outputTree.put(\"error\", \"Name cannot be empty\");\n        return;\n      }\n\n      bool result = nvhttp::rename_client(uuid, new_name);\n      outputTree.put(\"status\", result);\n      if (!result) {\n        outputTree.put(\"error\", \"Client not found\");\n      }\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"Rename client: \"sv << e.what();\n      outputTree.put(\"status\", false);\n      outputTree.put(\"error\", e.what());\n    }\n  }\n\n  void\n  listClients(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n    const nlohmann::json named_certs = nvhttp::get_all_clients();\n    nlohmann::json output_tree;\n    output_tree[\"named_certs\"] = named_certs;\n    output_tree[\"status\"] = \"true\";\n    send_response(response, output_tree);\n  }\n\n  void\n  closeApp(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    pt::ptree outputTree;\n\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    proc::proc.terminate();\n    outputTree.put(\"status\", true);\n  }\n\n  void\n  getRuntimeSessions(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    // 限制只允许 localhost 访问（增强安全性）\n    auto client_address = request->remote_endpoint().address();\n    auto address = net::addr_to_normalized_string(client_address);\n    auto ip_type = net::from_address(address);\n    \n    if (ip_type != net::PC) {\n      std::ostringstream msg_stream;\n      msg_stream << \"Access denied when getting runtime sessions. Only localhost requests are allowed. Client IP: \" << client_address.to_string();\n      BOOST_LOG(warning) << msg_stream.str();\n      json error_json;\n      error_json[\"success\"] = false;\n      error_json[\"status_code\"] = 403;\n      error_json[\"status_message\"] = msg_stream.str();\n      \n      response->write(error_json.dump());\n      response->close_connection_after_response = true;\n      return;\n    }\n\n    try {\n      // 获取所有活动会话信息\n      auto sessions_info = stream::session::get_all_sessions_info();\n      \n      json response_json;\n      response_json[\"success\"] = true;\n      response_json[\"status_code\"] = 200;\n      response_json[\"status_message\"] = \"Success\";\n      response_json[\"total_sessions\"] = sessions_info.size();\n      \n      json sessions_array = json::array();\n\n      for (const auto &session_info : sessions_info) {\n        json session_obj;\n        session_obj[\"client_name\"] = session_info.client_name;\n        session_obj[\"client_address\"] = session_info.client_address;\n        session_obj[\"state\"] = session_info.state;\n        session_obj[\"session_id\"] = session_info.session_id;\n        session_obj[\"width\"] = session_info.width;\n        session_obj[\"height\"] = session_info.height;\n        session_obj[\"fps\"] = session_info.fps;\n        session_obj[\"bitrate\"] = session_info.bitrate;\n        session_obj[\"host_audio\"] = session_info.host_audio;\n        session_obj[\"enable_hdr\"] = session_info.enable_hdr;\n        session_obj[\"enable_mic\"] = session_info.enable_mic;\n        session_obj[\"app_name\"] = session_info.app_name;\n        session_obj[\"app_id\"] = session_info.app_id;\n        \n        sessions_array.push_back(session_obj);\n      }\n      \n      response_json[\"sessions\"] = sessions_array;\n      \n      BOOST_LOG(debug) << \"Config API: Runtime sessions info requested, returned \" << sessions_info.size() << \" sessions\";\n      \n      response->write(response_json.dump());\n      response->close_connection_after_response = true;\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(error) << \"getRuntimeSessions: \" << e.what();\n      \n      json error_json;\n      error_json[\"success\"] = false;\n      error_json[\"status_code\"] = 500;\n      error_json[\"status_message\"] = std::string(e.what());\n      \n      response->write(error_json.dump());\n      response->close_connection_after_response = true;\n    }\n    catch (...) {\n      BOOST_LOG(error) << \"getRuntimeSessions: Unknown exception\";\n      \n      json error_json;\n      error_json[\"success\"] = false;\n      error_json[\"status_code\"] = 500;\n      error_json[\"status_message\"] = \"Unknown error\";\n      \n      response->write(error_json.dump());\n      response->close_connection_after_response = true;\n    }\n  }\n\n  void\n  changeRuntimeBitrate(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    print_req(request);\n\n    // 限制只允许 localhost 访问\n    auto client_address = request->remote_endpoint().address();\n    auto address = net::addr_to_normalized_string(client_address);\n    auto ip_type = net::from_address(address);\n    \n    if (ip_type != net::PC) {\n      std::ostringstream msg_stream;\n      msg_stream << \"Access denied. Only localhost requests are allowed. Client IP: \" << client_address.to_string();\n      BOOST_LOG(warning) << msg_stream.str();\n      json error_json;\n      error_json[\"success\"] = false;\n      error_json[\"status_code\"] = 403;\n      error_json[\"status_message\"] = msg_stream.str();\n      \n      response->write(error_json.dump());\n      response->close_connection_after_response = true;\n      return;\n    }\n\n    try {\n      auto args = request->parse_query_string();\n      auto bitrate_param = args.find(\"bitrate\");\n      auto clientname_param = args.find(\"clientname\");\n\n      // 验证参数\n      if (bitrate_param == args.end()) {\n        std::ostringstream msg_stream;\n        msg_stream << \"Missing bitrate parameter when changing bitrate\";\n        BOOST_LOG(warning) << msg_stream.str();\n        json error_json;\n        error_json[\"success\"] = false;\n        error_json[\"status_code\"] = 400;\n        error_json[\"status_message\"] = msg_stream.str();\n        response->write(error_json.dump());\n        response->close_connection_after_response = true;\n        return;\n      }\n\n      if (clientname_param == args.end()) {\n        std::ostringstream msg_stream;\n        msg_stream << \"Missing clientname parameter when changing bitrate\";\n        BOOST_LOG(warning) << msg_stream.str();\n        json error_json;\n        error_json[\"success\"] = false;\n        error_json[\"status_code\"] = 400;\n        error_json[\"status_message\"] = msg_stream.str();\n        response->write(error_json.dump());\n        response->close_connection_after_response = true;\n        return;\n      }\n\n      // 安全地解析码率参数\n      int bitrate = 0;\n      try {\n        bitrate = std::stoi(bitrate_param->second);\n      }\n      catch (...) {\n        json error_json;\n        error_json[\"success\"] = false;\n        error_json[\"status_code\"] = 400;\n        error_json[\"status_message\"] = \"Invalid bitrate parameter format\";\n        response->write(error_json.dump());\n        response->close_connection_after_response = true;\n        return;\n      }\n\n      std::string client_name = clientname_param->second;\n\n      // 验证码率范围\n      if (bitrate <= 0 || bitrate > 800000) {\n        std::ostringstream msg_stream;\n        msg_stream << \"Invalid bitrate value when changing bitrate. Must be between 1 and 800000 Kbps\";\n        BOOST_LOG(warning) << msg_stream.str();\n        json error_json;\n        error_json[\"success\"] = false;\n        error_json[\"status_code\"] = 400;\n        error_json[\"status_message\"] = msg_stream.str();\n        response->write(error_json.dump());\n        response->close_connection_after_response = true;\n        return;\n      }\n\n      // 获取所有活动会话以便调试\n      std::vector<std::string> available_clients;\n      try {\n        auto sessions_info = stream::session::get_all_sessions_info();\n        for (const auto &session_info : sessions_info) {\n          if (session_info.state == \"RUNNING\") {\n            available_clients.push_back(session_info.client_name);\n          }\n        }\n      }\n      catch (...) {\n        // 继续执行，即使获取会话信息失败，仍然尝试修改码率\n      }\n      \n      BOOST_LOG(info) << \"Config API: Attempting to change bitrate for client '\" << client_name \n                      << \"' to \" << bitrate << \" Kbps\";\n      if (!available_clients.empty()) {\n        BOOST_LOG(info) << \"Available RUNNING clients: \" << boost::algorithm::join(available_clients, \", \");\n      }\n      \n      // 调用底层 API 修改码率\n      video::dynamic_param_t param;\n      param.type = video::dynamic_param_type_e::BITRATE;\n      param.value.int_value = bitrate;\n      param.valid = true;\n      \n      bool success = stream::session::change_dynamic_param_for_client(client_name, param);\n\n      json response_json;\n      if (success) {\n        response_json[\"success\"] = true;\n        response_json[\"status_code\"] = 200;\n        response_json[\"status_message\"] = \"Bitrate change request sent to client session\";\n        response_json[\"bitrate\"] = bitrate;\n        response_json[\"client_name\"] = client_name;\n        \n        BOOST_LOG(info) << \"Config API: Dynamic bitrate change requested for client '\" \n                       << client_name << \"': \" << bitrate << \" Kbps\";\n      } else {\n        std::string error_msg = \"No active streaming session found for client: \" + client_name;\n        if (!available_clients.empty()) {\n          error_msg += \". Available clients: \" + boost::algorithm::join(available_clients, \", \");\n        } else {\n          error_msg += \". No RUNNING sessions available.\";\n        }\n        \n        response_json[\"success\"] = false;\n        response_json[\"status_code\"] = 404;\n        response_json[\"status_message\"] = error_msg;\n        \n        BOOST_LOG(warning) << \"Config API: Failed to change bitrate - \" << error_msg;\n      }\n      \n      response->write(response_json.dump());\n      response->close_connection_after_response = true;\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(error) << \"changeRuntimeBitrate: \" << e.what();\n      \n      json error_json;\n      error_json[\"success\"] = false;\n      error_json[\"status_code\"] = 500;\n      error_json[\"status_message\"] = std::string(e.what());\n      \n      response->write(error_json.dump());\n      response->close_connection_after_response = true;\n    }\n    catch (...) {\n      BOOST_LOG(error) << \"changeRuntimeBitrate: Unknown exception\";\n      \n      json error_json;\n      error_json[\"success\"] = false;\n      error_json[\"status_code\"] = 500;\n      error_json[\"status_message\"] = \"Unknown error\";\n      \n      response->write(error_json.dump());\n      response->close_connection_after_response = true;\n    }\n  }\n\n  void\n  proxySteamApi(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n    print_req(request);\n\n    // 提取请求路径，移除/steam-api前缀\n    std::string path = request->path;\n    if (path.find(\"/steam-api\") == 0) {\n      path = path.substr(10); // 移除\"/steam-api\"前缀\n    }\n\n    // 构建目标URL\n    std::string targetUrl = \"https://api.steampowered.com\" + path;\n    \n    // 添加查询参数\n    if (!request->query_string.empty()) {\n      targetUrl += \"?\" + request->query_string;\n    }\n\n    BOOST_LOG(info) << \"Steam API proxy request: \" << targetUrl;\n\n    // 安全检查：防止SSRF，确保目标主机确实是api.steampowered.com\n    if (http::url_get_host(targetUrl) != \"api.steampowered.com\") {\n      BOOST_LOG(warning) << \"Blocked Steam API proxy request to unauthorized host\";\n      response->write(SimpleWeb::StatusCode::client_error_bad_request, \"Invalid Host\");\n      return;\n    }\n\n    // 使用http模块下载数据\n    std::string content;\n    \n    try {\n      if (http::fetch_url(targetUrl, content)) {\n        // 设置响应头\n        SimpleWeb::CaseInsensitiveMultimap headers;\n        headers.emplace(\"Content-Type\", \"application/json\");\n        headers.emplace(\"Access-Control-Allow-Origin\", \"*\");\n        headers.emplace(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, OPTIONS\");\n        headers.emplace(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\");\n        \n        response->write(SimpleWeb::StatusCode::success_ok, content, headers);\n      } else {\n        BOOST_LOG(error) << \"Steam API request failed: \" << targetUrl;\n        response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Steam API request failed\");\n      }\n    } catch (const std::exception& e) {\n      BOOST_LOG(error) << \"Steam API proxy exception: \" << e.what();\n      response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Steam API proxy exception\");\n    }\n  }\n\n  void\n  proxySteamStore(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n    print_req(request);\n\n    // 提取请求路径，移除/steam-store前缀\n    std::string path = request->path;\n    if (path.find(\"/steam-store\") == 0) {\n      path = path.substr(12); // 移除\"/steam-store\"前缀\n    }\n\n    // 构建目标URL\n    std::string targetUrl = \"https://store.steampowered.com\" + path;\n    \n    // 添加查询参数\n    if (!request->query_string.empty()) {\n      targetUrl += \"?\" + request->query_string;\n    }\n\n    BOOST_LOG(info) << \"Steam Store proxy request: \" << targetUrl;\n\n    // 安全检查：防止SSRF，确保目标主机确实是store.steampowered.com\n    if (http::url_get_host(targetUrl) != \"store.steampowered.com\") {\n      BOOST_LOG(warning) << \"Blocked Steam Store proxy request to unauthorized host\";\n      response->write(SimpleWeb::StatusCode::client_error_bad_request, \"Invalid Host\");\n      return;\n    }\n\n    // 使用http模块下载数据\n    std::string content;\n    \n    try {\n      if (http::fetch_url(targetUrl, content)) {\n        // 设置响应头\n        SimpleWeb::CaseInsensitiveMultimap headers;\n        headers.emplace(\"Content-Type\", \"application/json\");\n        headers.emplace(\"Access-Control-Allow-Origin\", \"*\");\n        headers.emplace(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, OPTIONS\");\n        headers.emplace(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\");\n        \n        response->write(SimpleWeb::StatusCode::success_ok, content, headers);\n      } else {\n        BOOST_LOG(error) << \"Steam Store request failed: \" << targetUrl;\n        response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Steam Store request failed\");\n      }\n    } catch (const std::exception& e) {\n      BOOST_LOG(error) << \"Steam Store proxy exception: \" << e.what();\n      response->write(SimpleWeb::StatusCode::server_error_internal_server_error, \"Steam Store proxy exception\");\n    }\n  }\n\n  // ===== AI LLM Proxy =====\n\n  // 内存缓存 AI 配置，避免每次请求都读文件\n  static std::mutex ai_config_mutex;\n  static nlohmann::json ai_config_cache;\n  static bool ai_config_loaded = false;\n\n  /**\n   * @brief 获取 AI 配置文件路径（与 sunshine.conf 同目录）\n   */\n  static std::string\n  getAiConfigPath() {\n    auto config_dir = fs::path(config::sunshine.config_file).parent_path();\n    return (config_dir / \"ai_config.json\").string();\n  }\n\n  /**\n   * @brief 从文件或缓存读取 AI 配置（调用方需持有 ai_config_mutex）\n   */\n  static nlohmann::json\n  loadAiConfigLocked() {\n    if (ai_config_loaded) {\n      return ai_config_cache;\n    }\n\n    auto path = getAiConfigPath();\n    try {\n      std::string content = file_handler::read_file(path.c_str());\n      if (!content.empty()) {\n        ai_config_cache = nlohmann::json::parse(content);\n        ai_config_loaded = true;\n        return ai_config_cache;\n      }\n    } catch (...) {}\n\n    ai_config_cache = nlohmann::json{\n      {\"enabled\", false},\n      {\"provider\", \"openai\"},\n      {\"apiBase\", \"https://api.openai.com/v1\"},\n      {\"apiKey\", \"\"},\n      {\"model\", \"gpt-4o-mini\"}\n    };\n    ai_config_loaded = true;\n    return ai_config_cache;\n  }\n\n  /**\n   * @brief 从文件或缓存读取 AI 配置（线程安全）\n   */\n  static nlohmann::json\n  loadAiConfig() {\n    std::lock_guard<std::mutex> lock(ai_config_mutex);\n    return loadAiConfigLocked();\n  }\n\n  /**\n   * @brief 保存 AI 配置并刷新缓存（调用方需持有 ai_config_mutex）\n   */\n  static bool\n  saveAiConfigLocked(const nlohmann::json &cfg) {\n    auto path = getAiConfigPath();\n    try {\n      std::ofstream file(path);\n      if (file.is_open()) {\n        file << cfg.dump(2);\n        ai_config_cache = cfg;\n        ai_config_loaded = true;\n        return true;\n      }\n    } catch (const std::exception &e) {\n      BOOST_LOG(error) << \"Failed to save AI config: \" << e.what();\n    }\n    return false;\n  }\n\n  /**\n   * @brief 检测 provider 是否为 Anthropic（需要不同的 API 格式）\n   */\n  static bool\n  isAnthropicProvider(const nlohmann::json &cfg) {\n    std::string provider = cfg.value(\"provider\", \"\");\n    std::string apiBase = cfg.value(\"apiBase\", \"\");\n    return provider == \"anthropic\" || apiBase.find(\"anthropic.com\") != std::string::npos;\n  }\n\n  /**\n   * @brief 将 OpenAI 格式的请求转换为 Anthropic 格式\n   */\n  static std::string\n  convertToAnthropicFormat(const std::string &openaiBody, const std::string &model) {\n    try {\n      auto input = nlohmann::json::parse(openaiBody);\n      nlohmann::json anthropic;\n\n      anthropic[\"model\"] = input.value(\"model\", model);\n      anthropic[\"max_tokens\"] = input.value(\"max_tokens\", 4096);\n\n      // 提取 system message 和 user/assistant messages\n      if (input.contains(\"messages\")) {\n        nlohmann::json messages = nlohmann::json::array();\n        for (auto &msg : input[\"messages\"]) {\n          std::string role = msg.value(\"role\", \"\");\n          if (role == \"system\") {\n            anthropic[\"system\"] = msg.value(\"content\", \"\");\n          } else {\n            messages.push_back(msg);\n          }\n        }\n        anthropic[\"messages\"] = messages;\n      }\n\n      // 转换 temperature, top_p 等通用参数\n      if (input.contains(\"temperature\")) anthropic[\"temperature\"] = input[\"temperature\"];\n      if (input.contains(\"top_p\")) anthropic[\"top_p\"] = input[\"top_p\"];\n      if (input.contains(\"stream\")) anthropic[\"stream\"] = input[\"stream\"];\n\n      return anthropic.dump();\n    } catch (...) {\n      return openaiBody;  // 转换失败，原样返回\n    }\n  }\n\n  /**\n   * @brief 将 Anthropic 格式的响应转换回 OpenAI 格式\n   */\n  static std::string\n  convertFromAnthropicFormat(const std::string &anthropicResponse) {\n    try {\n      auto resp = nlohmann::json::parse(anthropicResponse);\n      nlohmann::json openai;\n\n      openai[\"id\"] = resp.value(\"id\", \"\");\n      openai[\"object\"] = \"chat.completion\";\n      openai[\"model\"] = resp.value(\"model\", \"\");\n\n      // 转换 content blocks\n      nlohmann::json choice;\n      choice[\"index\"] = 0;\n      choice[\"finish_reason\"] = resp.value(\"stop_reason\", \"stop\");\n\n      std::string content;\n      if (resp.contains(\"content\") && resp[\"content\"].is_array()) {\n        for (auto &block : resp[\"content\"]) {\n          if (block.value(\"type\", \"\") == \"text\") {\n            content += block.value(\"text\", \"\");\n          }\n        }\n      }\n      choice[\"message\"][\"role\"] = \"assistant\";\n      choice[\"message\"][\"content\"] = content;\n      openai[\"choices\"] = nlohmann::json::array({choice});\n\n      // 转换 usage\n      if (resp.contains(\"usage\")) {\n        openai[\"usage\"][\"prompt_tokens\"] = resp[\"usage\"].value(\"input_tokens\", 0);\n        openai[\"usage\"][\"completion_tokens\"] = resp[\"usage\"].value(\"output_tokens\", 0);\n        openai[\"usage\"][\"total_tokens\"] =\n          resp[\"usage\"].value(\"input_tokens\", 0) + resp[\"usage\"].value(\"output_tokens\", 0);\n      }\n\n      return openai.dump();\n    } catch (...) {\n      return anthropicResponse;\n    }\n  }\n\n  /**\n   * @brief GET /api/ai/config — 获取 AI 配置（不返回完整 API key）\n   */\n  void\n  getAiConfig(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n    print_req(request);\n\n    auto cfg = loadAiConfig();\n\n    // 掩码 API key：仅显示前4+后4字符\n    if (cfg.contains(\"apiKey\") && cfg[\"apiKey\"].is_string()) {\n      std::string key = cfg[\"apiKey\"].get<std::string>();\n      if (key.length() > 8) {\n        cfg[\"apiKey\"] = key.substr(0, 4) + \"****\" + key.substr(key.length() - 4);\n      } else if (!key.empty()) {\n        cfg[\"apiKey\"] = \"****\";\n      }\n    }\n\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"application/json\");\n    response->write(SimpleWeb::StatusCode::success_ok, cfg.dump(), headers);\n  }\n\n  /**\n   * @brief POST /api/ai/config — 保存 AI 配置\n   */\n  void\n  saveAiConfigEndpoint(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n    print_req(request);\n\n    std::stringstream ss;\n    ss << request->content.rdbuf();\n\n    nlohmann::json output;\n    try {\n      auto input = nlohmann::json::parse(ss.str());\n\n      // 用同一把锁包住 load-modify-save，防止并发写入丢失\n      std::lock_guard<std::mutex> lock(ai_config_mutex);\n      auto current = loadAiConfigLocked();\n      if (input.contains(\"enabled\")) current[\"enabled\"] = input[\"enabled\"].get<bool>();\n      if (input.contains(\"provider\")) current[\"provider\"] = input[\"provider\"].get<std::string>();\n      if (input.contains(\"apiBase\")) current[\"apiBase\"] = input[\"apiBase\"].get<std::string>();\n      if (input.contains(\"model\")) current[\"model\"] = input[\"model\"].get<std::string>();\n      if (input.contains(\"apiKey\")) {\n        std::string key = input[\"apiKey\"].get<std::string>();\n        // 如果前端发来的是掩码（包含****），不覆盖\n        if (key.find(\"****\") == std::string::npos) {\n          current[\"apiKey\"] = key;\n        }\n      }\n\n      if (saveAiConfigLocked(current)) {\n        output[\"status\"] = \"ok\";\n      } else {\n        output[\"status\"] = \"error\";\n        output[\"error\"] = \"Failed to write config file\";\n      }\n    } catch (const std::exception &e) {\n      output[\"status\"] = \"error\";\n      output[\"error\"] = std::string(\"Invalid JSON: \") + e.what();\n    }\n\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"application/json\");\n    response->write(SimpleWeb::StatusCode::success_ok, output.dump(), headers);\n  }\n\n  /**\n   * @brief POST /api/ai/chat/completions — OpenAI 兼容的 LLM 代理端点\n   *\n   * 支持普通请求和 SSE 流式请求。\n   * 自动适配 Anthropic API 格式。\n   * 客户端无需知道 API key 或实际后端，Sunshine 作为透明代理。\n   */\n  void\n  proxyAiChat(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n    print_req(request);\n\n    std::stringstream ss;\n    ss << request->content.rdbuf();\n    std::string requestBody = ss.str();\n\n    // 检测是否请求流式输出\n    bool isStream = false;\n    try {\n      auto reqJson = nlohmann::json::parse(requestBody);\n      isStream = reqJson.value(\"stream\", false);\n    } catch (...) {}\n\n    if (isStream) {\n      bool headerSent = false;\n      auto result = processAiChatStream(requestBody, [&](const char *data, size_t len) {\n        if (!headerSent) {\n          *response << \"HTTP/1.1 200 OK\\r\\n\";\n          *response << \"Content-Type: text/event-stream\\r\\n\";\n          *response << \"Cache-Control: no-cache\\r\\n\";\n          *response << \"Connection: keep-alive\\r\\n\";\n          *response << \"\\r\\n\";\n          response->send();\n          headerSent = true;\n        }\n        std::string chunk(data, len);\n        *response << chunk;\n        response->send();\n      });\n\n      if (result.httpCode != 200 && !headerSent) {\n        SimpleWeb::CaseInsensitiveMultimap headers;\n        headers.emplace(\"Content-Type\", \"application/json\");\n        response->write(SimpleWeb::StatusCode::server_error_bad_gateway, result.body, headers);\n      }\n    } else {\n      auto result = processAiChat(requestBody);\n      SimpleWeb::CaseInsensitiveMultimap headers;\n      headers.emplace(\"Content-Type\", result.contentType);\n\n      auto statusCode = (result.httpCode == 200)\n        ? SimpleWeb::StatusCode::success_ok\n        : (result.httpCode == 403)\n          ? SimpleWeb::StatusCode::client_error_forbidden\n          : (result.httpCode == 400)\n            ? SimpleWeb::StatusCode::client_error_bad_request\n            : SimpleWeb::StatusCode::server_error_bad_gateway;\n\n      response->write(statusCode, result.body, headers);\n    }\n  }\n\n  /**\n   * @brief OPTIONS handler for CORS preflight on AI endpoints\n   */\n  void\n  handleAiCors(resp_https_t response, req_https_t request) {\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Access-Control-Allow-Methods\", \"GET, POST, OPTIONS\");\n    headers.emplace(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\");\n    response->write(SimpleWeb::StatusCode::success_no_content, \"\", headers);\n  }\n\n  // ===== AI Proxy shared interface =====\n\n  bool\n  isAiEnabled() {\n    auto cfg = loadAiConfig();\n    return cfg.value(\"enabled\", false) &&\n           !cfg.value(\"apiKey\", \"\").empty() &&\n           !cfg.value(\"apiBase\", \"\").empty();\n  }\n\n  /**\n   * @brief Prepare AI proxy request: validate config, build URL, headers, convert body.\n   * @return empty targetUrl on error (result is filled with error info)\n   */\n  static bool\n  prepareAiRequest(\n    const std::string &requestBody,\n    std::string &targetUrl,\n    std::string &processedBody,\n    std::map<std::string, std::string> &proxyHeaders,\n    bool &isAnthropic,\n    bool &isStream,\n    AiProxyResult &result) {\n\n    auto cfg = loadAiConfig();\n\n    if (!cfg.value(\"enabled\", false)) {\n      result = {403, R\"({\"error\":{\"message\":\"AI proxy is not enabled\",\"type\":\"invalid_request_error\"}})\", \"application/json\"};\n      return false;\n    }\n\n    std::string apiBase = cfg.value(\"apiBase\", \"\");\n    std::string apiKey = cfg.value(\"apiKey\", \"\");\n    std::string defaultModel = cfg.value(\"model\", \"\");\n\n    if (apiBase.empty() || apiKey.empty()) {\n      result = {400, R\"({\"error\":{\"message\":\"AI proxy not configured: missing apiBase or apiKey\",\"type\":\"invalid_request_error\"}})\", \"application/json\"};\n      return false;\n    }\n\n    if (requestBody.empty()) {\n      result = {400, R\"({\"error\":{\"message\":\"Empty request body\",\"type\":\"invalid_request_error\"}})\", \"application/json\"};\n      return false;\n    }\n\n    processedBody = requestBody;\n    isStream = false;\n\n    try {\n      auto reqJson = nlohmann::json::parse(processedBody);\n      isStream = reqJson.value(\"stream\", false);\n      if (!reqJson.contains(\"model\") && !defaultModel.empty()) {\n        reqJson[\"model\"] = defaultModel;\n        processedBody = reqJson.dump();\n      }\n    } catch (...) {}\n\n    isAnthropic = isAnthropicProvider(cfg);\n\n    while (!apiBase.empty() && apiBase.back() == '/') {\n      apiBase.pop_back();\n    }\n    targetUrl = isAnthropic\n      ? apiBase + \"/v1/messages\"\n      : apiBase + \"/chat/completions\";\n\n    if (isAnthropic) {\n      // Anthropic 流式 SSE 格式与 OpenAI 不兼容，强制走非流式以保证响应格式一致\n      if (isStream) {\n        try {\n          auto reqJson = nlohmann::json::parse(processedBody);\n          reqJson[\"stream\"] = false;\n          processedBody = reqJson.dump();\n        } catch (...) {}\n        isStream = false;\n      }\n      processedBody = convertToAnthropicFormat(processedBody, defaultModel);\n      proxyHeaders[\"x-api-key\"] = apiKey;\n      proxyHeaders[\"anthropic-version\"] = \"2023-06-01\";\n    } else {\n      proxyHeaders[\"Authorization\"] = \"Bearer \" + apiKey;\n    }\n\n    BOOST_LOG(info) << \"AI proxy forwarding to: \" << targetUrl << (isStream ? \" (stream)\" : \"\");\n    return true;\n  }\n\n  AiProxyResult\n  processAiChat(const std::string &requestBody) {\n    std::string targetUrl, processedBody;\n    std::map<std::string, std::string> proxyHeaders;\n    bool isAnthropic = false, isStream = false;\n    AiProxyResult result;\n\n    if (!prepareAiRequest(requestBody, targetUrl, processedBody, proxyHeaders, isAnthropic, isStream, result)) {\n      return result;\n    }\n\n    std::string responseBody;\n    long httpCode = 0;\n\n    try {\n      bool ok = http::post_json(targetUrl, processedBody, proxyHeaders, responseBody, httpCode, 120);\n      if (ok) {\n        if (isAnthropic && httpCode >= 200 && httpCode < 300) {\n          responseBody = convertFromAnthropicFormat(responseBody);\n        }\n        int statusCode = (httpCode >= 200 && httpCode < 300) ? 200 : 502;\n        return {statusCode, responseBody, \"application/json\"};\n      } else {\n        return {502, R\"({\"error\":{\"message\":\"Failed to connect to upstream LLM API\",\"type\":\"upstream_error\"}})\", \"application/json\"};\n      }\n    } catch (const std::exception &e) {\n      BOOST_LOG(error) << \"AI proxy exception: \" << e.what();\n      nlohmann::json err;\n      err[\"error\"][\"message\"] = std::string(\"AI proxy exception: \") + e.what();\n      err[\"error\"][\"type\"] = \"internal_error\";\n      return {500, err.dump(), \"application/json\"};\n    }\n  }\n\n  /**\n   * @brief curl write callback for streaming with std::function\n   */\n  struct StreamCallbackContext {\n    std::function<void(const char *, size_t)> callback;\n  };\n\n  static size_t\n  stream_func_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {\n    size_t realsize = size * nmemb;\n    auto *ctx = static_cast<StreamCallbackContext *>(userdata);\n    ctx->callback(ptr, realsize);\n    return realsize;\n  }\n\n  AiProxyResult\n  processAiChatStream(\n    const std::string &requestBody,\n    std::function<void(const char *, size_t)> chunkCallback) {\n\n    std::string targetUrl, processedBody;\n    std::map<std::string, std::string> proxyHeaders;\n    bool isAnthropic = false, isStream = false;\n    AiProxyResult result;\n\n    if (!prepareAiRequest(requestBody, targetUrl, processedBody, proxyHeaders, isAnthropic, isStream, result)) {\n      return result;\n    }\n\n    CURL *curl = curl_easy_init();\n    if (!curl) {\n      return {500, R\"({\"error\":{\"message\":\"Failed to initialize CURL\",\"type\":\"internal_error\"}})\", \"application/json\"};\n    }\n\n    StreamCallbackContext ctx;\n    ctx.callback = std::move(chunkCallback);\n\n    struct curl_slist *header_list = nullptr;\n    header_list = curl_slist_append(header_list, \"Content-Type: application/json\");\n    for (const auto &[key, value] : proxyHeaders) {\n      std::string header_line = key + \": \" + value;\n      header_list = curl_slist_append(header_list, header_line.c_str());\n    }\n\n    curl_easy_setopt(curl, CURLOPT_URL, targetUrl.c_str());\n    curl_easy_setopt(curl, CURLOPT_POST, 1L);\n    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, processedBody.c_str());\n    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(processedBody.size()));\n    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, stream_func_callback);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx);\n    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);\n    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 300L);\n    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);\n    curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);\n\n    CURLcode curlResult = curl_easy_perform(curl);\n    curl_slist_free_all(header_list);\n    curl_easy_cleanup(curl);\n\n    if (curlResult != CURLE_OK) {\n      return {502, R\"({\"error\":{\"message\":\"Failed to connect to upstream LLM API\",\"type\":\"upstream_error\"}})\", \"application/json\"};\n    }\n    return {200, \"\", \"text/event-stream\"};\n  }\n\n  /**\n   * @brief 计算文件的SHA256哈希值\n   * @param filepath 文件路径\n   * @return SHA256哈希字符串，如果失败返回空字符串\n   */\n  std::string\n  calculate_file_hash(const std::string &filepath) {\n    if (filepath.empty() || !boost::filesystem::exists(filepath)) {\n      return \"\";\n    }\n\n    std::ifstream file(filepath, std::ios::binary);\n    if (!file.is_open()) {\n      return \"\";\n    }\n\n    EVP_MD_CTX *mdctx = EVP_MD_CTX_new();\n    if (!mdctx) {\n      return \"\";\n    }\n\n    if (EVP_DigestInit_ex(mdctx, EVP_sha256(), nullptr) != 1) {\n      EVP_MD_CTX_free(mdctx);\n      return \"\";\n    }\n\n    char buf[1024 * 16];\n    while (file.good()) {\n      file.read(buf, sizeof(buf));\n      if (file.gcount() > 0) {\n        if (EVP_DigestUpdate(mdctx, buf, file.gcount()) != 1) {\n          EVP_MD_CTX_free(mdctx);\n          file.close();\n          return \"\";\n        }\n      }\n    }\n    file.close();\n\n    unsigned char hash[SHA256_DIGEST_LENGTH];\n    unsigned int hash_len = 0;\n    if (EVP_DigestFinal_ex(mdctx, hash, &hash_len) != 1) {\n      EVP_MD_CTX_free(mdctx);\n      return \"\";\n    }\n    EVP_MD_CTX_free(mdctx);\n\n    std::stringstream ss;\n    ss << std::hex << std::setfill('0');\n    for (unsigned int i = 0; i < hash_len; i++) {\n      ss << std::setw(2) << static_cast<int>(hash[i]);\n    }\n    return ss.str();\n  }\n\n  /**\n   * @brief 从命令字符串中提取可执行文件路径\n   * @param cmd 完整命令字符串\n   * @return 可执行文件路径\n   */\n  std::string\n  extract_executable_path(const std::string &cmd) {\n    if (cmd.empty()) {\n      return \"\";\n    }\n\n    std::string trimmed = cmd;\n    // 移除前导空格\n    size_t start = trimmed.find_first_not_of(\" \\t\");\n    if (start != std::string::npos) {\n      trimmed = trimmed.substr(start);\n    }\n\n    // 处理引号包裹的路径\n    if (!trimmed.empty() && trimmed[0] == '\"') {\n      size_t end = trimmed.find('\"', 1);\n      if (end != std::string::npos) {\n        return trimmed.substr(1, end - 1);\n      }\n    }\n\n    // 提取第一个空格前的部分（可执行文件路径）\n    size_t space = trimmed.find(' ');\n    if (space != std::string::npos) {\n      return trimmed.substr(0, space);\n    }\n\n    return trimmed;\n  }\n\n  void\n  testMenuCmd(resp_https_t response, req_https_t request) {\n    if (!authenticate(response, request)) return;\n\n    // 安全限制：只允许局域网访问测试命令功能\n    auto address = net::addr_to_normalized_string(request->remote_endpoint().address());\n    auto ip_type = net::from_address(address);\n    \n    if (ip_type != net::PC) {\n      BOOST_LOG(warning) << \"TestMenuCmd: Access denied from non-local network: \" << address;\n      pt::ptree outputTree;\n      outputTree.put(\"status\", false);\n      outputTree.put(\"error\", \"Test command feature is only available from local network\");\n      \n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n      return;\n    }\n\n    print_req(request);\n\n    std::stringstream ss;\n    ss << request->content.rdbuf();\n\n    pt::ptree inputTree, outputTree;\n\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_json(data, outputTree);\n      response->write(data.str());\n    });\n\n    try {\n      pt::read_json(ss, inputTree);\n      auto cmd = inputTree.get<std::string>(\"cmd\");\n      auto working_dir = inputTree.get<std::string>(\"working_dir\", \"\");\n      auto elevated = inputTree.get<bool>(\"elevated\", false);\n\n      // 安全检查：命令不能为空\n      if (cmd.empty()) {\n        BOOST_LOG(warning) << \"TestMenuCmd: Empty command provided\";\n        outputTree.put(\"status\", false);\n        outputTree.put(\"error\", \"Command cannot be empty\");\n        return;\n      }\n\n      // 安全检查：命令长度限制（防止过长的命令）\n      if (cmd.length() > 4096) {\n        BOOST_LOG(warning) << \"TestMenuCmd: Command too long (\" << cmd.length() << \" characters)\";\n        outputTree.put(\"status\", false);\n        outputTree.put(\"error\", \"Command exceeds maximum length\");\n        return;\n      }\n\n      // 提取可执行文件路径并计算SHA256哈希值\n      std::string executable_path = extract_executable_path(cmd);\n      std::string file_hash;\n      \n      if (!executable_path.empty()) {\n        // 如果是相对路径，尝试解析为绝对路径\n        boost::filesystem::path exec_path(executable_path);\n        if (!exec_path.is_absolute()) {\n          // 在PATH中查找或使用工作目录\n          if (!working_dir.empty()) {\n            exec_path = boost::filesystem::path(working_dir) / exec_path;\n          }\n        }\n        \n        file_hash = calculate_file_hash(exec_path.string());\n        \n        if (file_hash.empty() && boost::filesystem::exists(exec_path)) {\n          BOOST_LOG(warning) << \"TestMenuCmd: Failed to calculate hash for executable: \" << exec_path;\n        }\n      }\n\n      // 记录详细信息用于审计（包含文件哈希值）\n      BOOST_LOG(info) << \"Testing menu command from \" << address << \": [\" << cmd << \"]\";\n      if (!file_hash.empty()) {\n        BOOST_LOG(info) << \"Executable SHA256: \" << file_hash << \" (\" << executable_path << \")\";\n      }\n      else if (!executable_path.empty()) {\n        BOOST_LOG(warning) << \"Could not verify executable: \" << executable_path;\n      }\n\n      std::error_code ec;\n      boost::filesystem::path work_dir;\n      \n      if (!working_dir.empty()) {\n        // 验证工作目录是否存在\n        if (!boost::filesystem::exists(working_dir) || !boost::filesystem::is_directory(working_dir)) {\n          BOOST_LOG(warning) << \"TestMenuCmd: Invalid working directory: \" << working_dir;\n          outputTree.put(\"status\", false);\n          outputTree.put(\"error\", \"Invalid working directory\");\n          return;\n        }\n        work_dir = boost::filesystem::path(working_dir);\n      } else {\n        work_dir = boost::filesystem::current_path();\n      }\n\n      // 执行命令\n      auto child = platf::run_command(elevated, true, cmd, work_dir, proc::proc.get_env(), nullptr, ec, nullptr);\n      \n      if (ec) {\n        BOOST_LOG(warning) << \"Failed to run menu command [\" << cmd << \"]: \" << ec.message();\n        outputTree.put(\"status\", false);\n        outputTree.put(\"error\", \"Failed to execute command: \" + ec.message());\n      }\n      else {\n        BOOST_LOG(info) << \"Successfully executed menu command [\" << cmd << \"]\";\n        child.detach();\n        outputTree.put(\"status\", true);\n        outputTree.put(\"message\", \"Command executed successfully\");\n      }\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"TestMenuCmd error: \" << e.what();\n      outputTree.put(\"status\", false);\n      outputTree.put(\"error\", e.what());\n      return;\n    }\n  }\n\n  void\n  start() {\n    auto shutdown_event = mail::man->event<bool>(mail::shutdown);\n\n    auto port_https = net::map_port(PORT_HTTPS);\n    auto address_family = net::af_from_enum_string(config::sunshine.address_family);\n\n    https_server_t server { config::nvhttp.cert, config::nvhttp.pkey };\n    server.default_resource[\"GET\"] = close_connection;\n    server.resource[\"^/$\"][\"GET\"] = getIndexPage;\n    server.resource[\"^/pin/?$\"][\"GET\"] = getPinPage;\n    server.resource[\"^/apps/?$\"][\"GET\"] = getAppsPage;\n    server.resource[\"^/clients/?$\"][\"GET\"] = getClientsPage;\n    server.resource[\"^/config/?$\"][\"GET\"] = getConfigPage;\n    server.resource[\"^/password/?$\"][\"GET\"] = getPasswordPage;\n    server.resource[\"^/welcome/?$\"][\"GET\"] = getWelcomePage;\n    server.resource[\"^/troubleshooting/?$\"][\"GET\"] = getTroubleshootingPage;\n    server.resource[\"^/api/pin$\"][\"POST\"] = savePin;\n    server.resource[\"^/api/qr-pair$\"][\"POST\"] = generateQrPairInfo;\n    server.resource[\"^/api/qr-pair/cancel$\"][\"POST\"] = cancelQrPair;\n    server.resource[\"^/api/qr-pair$\"][\"GET\"] = getQrPairStatus;\n    server.resource[\"^/api/apps$\"][\"GET\"] = getApps;\n    server.resource[\"^/api/logs$\"][\"GET\"] = getLogs;\n    server.resource[\"^/api/apps$\"][\"POST\"] = saveApp;\n    server.resource[\"^/api/config$\"][\"GET\"] = getConfig;\n    server.resource[\"^/api/config$\"][\"POST\"] = saveConfig;\n    server.resource[\"^/api/configLocale$\"][\"GET\"] = getLocale;\n    server.resource[\"^/api/logout$\"][\"GET\"] = handleLogout;\n    server.resource[\"^/api/logout$\"][\"POST\"] = handleLogout;\n    server.resource[\"^/api/restart$\"][\"POST\"] = restart;\n    server.resource[\"^/api/restart$\"][\"GET\"] = restart;\n    server.resource[\"^/api/boom$\"][\"GET\"] = boom;\n    server.resource[\"^/api/reset-display-device-persistence$\"][\"POST\"] = resetDisplayDevicePersistence;\n    server.resource[\"^/api/password$\"][\"POST\"] = savePassword;\n    server.resource[\"^/api/apps/([0-9]+)$\"][\"DELETE\"] = deleteApp;\n    server.resource[\"^/api/clients/unpair-all$\"][\"POST\"] = unpairAll;\n    server.resource[\"^/api/clients/list$\"][\"GET\"] = listClients;\n    server.resource[\"^/api/clients/list$\"][\"POST\"] = saveConfig;\n    server.resource[\"^/api/clients/unpair$\"][\"POST\"] = unpair;\n    server.resource[\"^/api/clients/rename$\"][\"POST\"] = renameClient;\n    server.resource[\"^/api/apps/close$\"][\"POST\"] = closeApp;\n    server.resource[\"^/api/covers/upload$\"][\"POST\"] = uploadCover;\n    server.resource[\"^/api/apps/test-menu-cmd$\"][\"POST\"] = testMenuCmd;\n    server.resource[\"^/api/runtime/sessions$\"][\"GET\"] = getRuntimeSessions;\n    server.resource[\"^/api/runtime/bitrate$\"][\"GET\"] = changeRuntimeBitrate;\n    server.resource[\"^/steam-api/.+$\"][\"GET\"] = proxySteamApi;\n    server.resource[\"^/steam-store/.+$\"][\"GET\"] = proxySteamStore;\n    server.resource[\"^/api/ai/config$\"][\"GET\"] = getAiConfig;\n    server.resource[\"^/api/ai/config$\"][\"POST\"] = saveAiConfigEndpoint;\n    server.resource[\"^/api/ai/chat/completions$\"][\"POST\"] = proxyAiChat;\n    server.resource[\"^/api/ai/chat/completions$\"][\"OPTIONS\"] = handleAiCors;\n    server.resource[\"^/images/sunshine.ico$\"][\"GET\"] = getFaviconImage;\n    server.resource[\"^/images/logo-sunshine-256.png$\"][\"GET\"] = getSunshineLogoImage;\n    server.resource[\"^/boxart/.+$\"][\"GET\"] = getBoxArt;\n    server.resource[\"^/assets\\\\/.+$\"][\"GET\"] = getNodeModules;\n    server.config.reuse_address = true;\n    server.config.address = net::get_bind_address(address_family);\n    server.config.port = port_https;\n\n    auto accept_and_run = [&](https_server_t *server) {\n      try {\n        server->start([](unsigned short port) {\n          BOOST_LOG(debug) << \"Configuration UI available at [https://localhost:\"sv << port << \"]\"sv;\n        });\n      }\n      catch (boost::system::system_error &err) {\n        // It's possible the exception gets thrown after calling server->stop() from a different thread\n        if (shutdown_event->peek()) {\n          return;\n        }\n        BOOST_LOG(fatal) << \"Couldn't start Configuration HTTPS server on port [\"sv << port_https << \"]: \"sv << err.what();\n        shutdown_event->raise(true);\n        return;\n      }\n      catch (std::exception &err) {\n        BOOST_LOG(fatal) << \"Configuration HTTPS server failed to start: \"sv << err.what();\n        shutdown_event->raise(true);\n        return;\n      }\n    };\n    std::thread tcp { accept_and_run, &server };\n\n    // Wait for any event\n    shutdown_event->view();\n\n    server.stop();\n\n    tcp.join();\n  }\n}  // namespace confighttp\n"
  },
  {
    "path": "src/confighttp.h",
    "content": "/**\n * @file src/confighttp.h\n * @brief Declarations for the Web UI Config HTTP server.\n */\n#pragma once\n\n#include <functional>\n#include <string>\n\n#include \"thread_safe.h\"\n\n#define WEB_DIR SUNSHINE_ASSETS_DIR \"/web/\"\n\nnamespace confighttp {\n  constexpr auto PORT_HTTPS = 1;\n  void\n  start();\n\n  bool\n  saveVddSettings(std::string resArray, std::string fpsArray, std::string gpu_name);\n\n  // AI LLM Proxy — shared interface for nvhttp\n  struct AiProxyResult {\n    int httpCode;  // HTTP status code to return (200, 400, 403, 502, 500)\n    std::string body;  // JSON response body\n    std::string contentType;  // \"application/json\" or \"text/event-stream\"\n  };\n\n  /**\n   * @brief Check if AI proxy is enabled and configured.\n   */\n  bool isAiEnabled();\n\n  /**\n   * @brief Process an AI chat completion request (non-streaming).\n   * @param requestBody OpenAI-compatible JSON request body\n   * @return AiProxyResult with status code and response body\n   */\n  AiProxyResult processAiChat(const std::string &requestBody);\n\n  /**\n   * @brief Process an AI chat completion request with streaming (SSE).\n   * Calls chunkCallback for each SSE chunk received from upstream.\n   * @param requestBody OpenAI-compatible JSON request body\n   * @param chunkCallback Called with each data chunk from upstream\n   * @return AiProxyResult (httpCode=200 if streaming started, error otherwise)\n   */\n  AiProxyResult processAiChatStream(\n    const std::string &requestBody,\n    std::function<void(const char *, size_t)> chunkCallback);\n}  // namespace confighttp\n\n// mime types map\nconst std::map<std::string, std::string> mime_types = {\n  { \"css\", \"text/css\" },\n  { \"gif\", \"image/gif\" },\n  { \"htm\", \"text/html\" },\n  { \"html\", \"text/html\" },\n  { \"ico\", \"image/x-icon\" },\n  { \"jpeg\", \"image/jpeg\" },\n  { \"jpg\", \"image/jpeg\" },\n  { \"js\", \"application/javascript\" },\n  { \"json\", \"application/json\" },\n  { \"png\", \"image/png\" },\n  { \"svg\", \"image/svg+xml\" },\n  { \"ttf\", \"font/ttf\" },\n  { \"txt\", \"text/plain\" },\n  { \"woff2\", \"font/woff2\" },\n  { \"xml\", \"text/xml\" },\n};\n"
  },
  {
    "path": "src/crypto.cpp",
    "content": "/**\n * @file src/crypto.cpp\n * @brief Definitions for cryptography functions.\n */\n#include \"crypto.h\"\n#include <openssl/pem.h>\n#include <openssl/rsa.h>\n\nnamespace crypto {\n  using asn1_string_t = util::safe_ptr<ASN1_STRING, ASN1_STRING_free>;\n\n  cert_chain_t::cert_chain_t():\n      _certs {}, _cert_ctx { X509_STORE_CTX_new() } {}\n  void\n  cert_chain_t::add(x509_t &&cert) {\n    x509_store_t x509_store { X509_STORE_new() };\n\n    X509_STORE_add_cert(x509_store.get(), cert.get());\n    _certs.emplace_back(std::make_pair(std::move(cert), std::move(x509_store)));\n  }\n  void\n  cert_chain_t::clear() {\n    _certs.clear();\n  }\n\n  static int\n  openssl_verify_cb(int ok, X509_STORE_CTX *ctx) {\n    int err_code = X509_STORE_CTX_get_error(ctx);\n\n    switch (err_code) {\n      // Expired or not-yet-valid certificates are fine. Sometimes Moonlight is running on embedded devices\n      // that don't have accurate clocks (or haven't yet synchronized by the time Moonlight first runs).\n      // This behavior also matches what GeForce Experience does.\n      // TODO: Checking for X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY is a temporary workaround to get moonlight-embedded to work on the raspberry pi\n      case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:\n      case X509_V_ERR_CERT_NOT_YET_VALID:\n      case X509_V_ERR_CERT_HAS_EXPIRED:\n        return 1;\n\n      default:\n        return ok;\n    }\n  }\n\n  /**\n   * @brief Verify the certificate chain.\n   * When certificates from two or more instances of Moonlight have been added to x509_store_t,\n   * only one of them will be verified by X509_verify_cert, resulting in only a single instance of\n   * Moonlight to be able to use Sunshine\n   *\n   * To circumvent this, x509_store_t instance will be created for each instance of the certificates.\n   * @param cert The certificate to verify.\n   * @return nullptr if the certificate is valid, otherwise an error string.\n   */\n  const char *\n  cert_chain_t::verify(x509_t::element_type *cert) {\n    int err_code = 0;\n    for (auto &[_, x509_store] : _certs) {\n      auto fg = util::fail_guard([this]() {\n        X509_STORE_CTX_cleanup(_cert_ctx.get());\n      });\n\n      X509_STORE_CTX_init(_cert_ctx.get(), x509_store.get(), cert, nullptr);\n      X509_STORE_CTX_set_verify_cb(_cert_ctx.get(), openssl_verify_cb);\n\n      // We don't care to validate the entire chain for the purposes of client auth.\n      // Some versions of clients forked from Moonlight Embedded produce client certs\n      // that OpenSSL doesn't detect as self-signed due to some X509v3 extensions.\n      X509_STORE_CTX_set_flags(_cert_ctx.get(), X509_V_FLAG_PARTIAL_CHAIN);\n\n      auto err = X509_verify_cert(_cert_ctx.get());\n\n      if (err == 1) {\n        return nullptr;\n      }\n\n      err_code = X509_STORE_CTX_get_error(_cert_ctx.get());\n\n      if (err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && err_code != X509_V_ERR_INVALID_CA) {\n        return X509_verify_cert_error_string(err_code);\n      }\n    }\n\n    return X509_verify_cert_error_string(err_code);\n  }\n\n  const char* cert_chain_t::verify_safe(x509_t::element_type* cert) {\n      if (!cert) {\n          return \"Invalid certificate: null pointer\";\n      }\n      if (_certs.empty()) {\n          return \"No certificate stores available\";\n      }\n      int last_err_code = X509_V_ERR_UNSPECIFIED;\n      for (auto& [name, x509_store] : _certs) {\n          auto ctx_deleter = [](X509_STORE_CTX* ctx) {\n              if (ctx) {\n                  X509_STORE_CTX_free(ctx);\n              }\n          };\n          std::unique_ptr<X509_STORE_CTX, decltype(ctx_deleter)> ctx(\n              X509_STORE_CTX_new(), ctx_deleter);\n          \n          if (!ctx) {\n              last_err_code = X509_V_ERR_OUT_OF_MEM;\n              continue;\n          }\n          if (X509_STORE_CTX_init(ctx.get(), x509_store.get(), cert, nullptr) != 1) {\n              last_err_code = X509_V_ERR_STORE_LOOKUP;\n              continue;\n          }\n          X509_STORE_CTX_set_verify_cb(ctx.get(), openssl_verify_cb);\n          int verify_result = X509_verify_cert(ctx.get());\n          if (verify_result == 1) {\n              return nullptr;\n          }\n          int err_code = X509_STORE_CTX_get_error(ctx.get());\n          if (err_code != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) {\n              return X509_verify_cert_error_string(err_code);\n          }\n          last_err_code = err_code;\n      }\n      return X509_verify_cert_error_string(last_err_code);\n  }\n\n  namespace cipher {\n\n    static int\n    init_decrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {\n      ctx.reset(EVP_CIPHER_CTX_new());\n\n      if (!ctx) {\n        return -1;\n      }\n\n      if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {\n        return -1;\n      }\n\n      if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) {\n        return -1;\n      }\n\n      if (EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) {\n        return -1;\n      }\n      EVP_CIPHER_CTX_set_padding(ctx.get(), padding);\n\n      return 0;\n    }\n\n    static int\n    init_encrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {\n      ctx.reset(EVP_CIPHER_CTX_new());\n\n      // Gen 7 servers use 128-bit AES ECB\n      if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {\n        return -1;\n      }\n\n      if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) {\n        return -1;\n      }\n\n      if (EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) {\n        return -1;\n      }\n      EVP_CIPHER_CTX_set_padding(ctx.get(), padding);\n\n      return 0;\n    }\n\n    static int\n    init_encrypt_cbc(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {\n      ctx.reset(EVP_CIPHER_CTX_new());\n\n      // Gen 7 servers use 128-bit AES ECB\n      if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_cbc(), nullptr, key->data(), iv->data()) != 1) {\n        return -1;\n      }\n\n      EVP_CIPHER_CTX_set_padding(ctx.get(), padding);\n\n      return 0;\n    }\n\n    static int\n    init_decrypt_cbc(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {\n      ctx.reset(EVP_CIPHER_CTX_new());\n\n      if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_cbc(), nullptr, key->data(), iv->data()) != 1) {\n        return -1;\n      }\n\n      EVP_CIPHER_CTX_set_padding(ctx.get(), padding);\n\n      return 0;\n    }\n\n    int\n    gcm_t::decrypt(const std::string_view &tagged_cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv) {\n      if (!decrypt_ctx && init_decrypt_gcm(decrypt_ctx, &key, iv, padding)) {\n        return -1;\n      }\n\n      // Calling with cipher == nullptr results in a parameter change\n      // without requiring a reallocation of the internal cipher ctx.\n      if (EVP_DecryptInit_ex(decrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {\n        return false;\n      }\n\n      auto cipher = tagged_cipher.substr(tag_size);\n      auto tag = tagged_cipher.substr(0, tag_size);\n\n      plaintext.resize(round_to_pkcs7_padded(cipher.size()));\n\n      int update_outlen, final_outlen;\n\n      if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) {\n        return -1;\n      }\n\n      if (EVP_CIPHER_CTX_ctrl(decrypt_ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char *>(tag.data())) != 1) {\n        return -1;\n      }\n\n      if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) {\n        return -1;\n      }\n\n      plaintext.resize(update_outlen + final_outlen);\n      return 0;\n    }\n\n    /**\n     * This function encrypts the given plaintext using the AES key in GCM mode. The initialization vector (IV) is also provided.\n     * The function handles the creation and initialization of the encryption context, and manages the encryption process.\n     * The resulting ciphertext and the GCM tag are written into the tagged_cipher buffer.\n     */\n    int\n    gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tag, std::uint8_t *ciphertext, aes_t *iv) {\n      if (!encrypt_ctx && init_encrypt_gcm(encrypt_ctx, &key, iv, padding)) {\n        return -1;\n      }\n\n      // Calling with cipher == nullptr results in a parameter change\n      // without requiring a reallocation of the internal cipher ctx.\n      if (EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {\n        return -1;\n      }\n\n      int update_outlen, final_outlen;\n\n      // Encrypt into the caller's buffer\n      if (EVP_EncryptUpdate(encrypt_ctx.get(), ciphertext, &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) {\n        return -1;\n      }\n\n      // GCM encryption won't ever fill ciphertext here but we have to call it anyway\n      if (EVP_EncryptFinal_ex(encrypt_ctx.get(), ciphertext + update_outlen, &final_outlen) != 1) {\n        return -1;\n      }\n\n      if (EVP_CIPHER_CTX_ctrl(encrypt_ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size, tag) != 1) {\n        return -1;\n      }\n\n      return update_outlen + final_outlen;\n    }\n\n    int\n    gcm_t::encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv) {\n      // This overload handles the common case of [GCM tag][cipher text] buffer layout\n      return encrypt(plaintext, tagged_cipher, tagged_cipher + tag_size, iv);\n    }\n\n    int\n    ecb_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext) {\n      auto fg = util::fail_guard([this]() {\n        EVP_CIPHER_CTX_reset(decrypt_ctx.get());\n      });\n\n      // Gen 7 servers use 128-bit AES ECB\n      if (EVP_DecryptInit_ex(decrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {\n        return -1;\n      }\n\n      EVP_CIPHER_CTX_set_padding(decrypt_ctx.get(), padding);\n      plaintext.resize(round_to_pkcs7_padded(cipher.size()));\n\n      int update_outlen, final_outlen;\n\n      if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) {\n        return -1;\n      }\n\n      if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) {\n        return -1;\n      }\n\n      plaintext.resize(update_outlen + final_outlen);\n      return 0;\n    }\n\n    int\n    ecb_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher) {\n      auto fg = util::fail_guard([this]() {\n        EVP_CIPHER_CTX_reset(encrypt_ctx.get());\n      });\n\n      // Gen 7 servers use 128-bit AES ECB\n      if (EVP_EncryptInit_ex(encrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {\n        return -1;\n      }\n\n      EVP_CIPHER_CTX_set_padding(encrypt_ctx.get(), padding);\n      cipher.resize(round_to_pkcs7_padded(plaintext.size()));\n\n      int update_outlen, final_outlen;\n\n      // Encrypt into the caller's buffer\n      if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) {\n        return -1;\n      }\n\n      if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + update_outlen, &final_outlen) != 1) {\n        return -1;\n      }\n\n      cipher.resize(update_outlen + final_outlen);\n      return 0;\n    }\n\n    /**\n     * This function encrypts the given plaintext using the AES key in CBC mode. The initialization vector (IV) is also provided.\n     * The function handles the creation and initialization of the encryption context, and manages the encryption process.\n     * The resulting ciphertext is written into the cipher buffer.\n     */\n    int\n    cbc_t::encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv) {\n      if (!encrypt_ctx && init_encrypt_cbc(encrypt_ctx, &key, iv, padding)) {\n        return -1;\n      }\n\n      // Calling with cipher == nullptr results in a parameter change\n      // without requiring a reallocation of the internal cipher ctx.\n      if (EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {\n        return false;\n      }\n\n      int update_outlen, final_outlen;\n\n      // Encrypt into the caller's buffer\n      if (EVP_EncryptUpdate(encrypt_ctx.get(), cipher, &update_outlen, (const std::uint8_t *) plaintext.data(), plaintext.size()) != 1) {\n        return -1;\n      }\n\n      if (EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher + update_outlen, &final_outlen) != 1) {\n        return -1;\n      }\n\n      return update_outlen + final_outlen;\n    }\n\n    int\n    cbc_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv) {\n      if (!decrypt_ctx && init_decrypt_cbc(decrypt_ctx, &key, iv, padding)) {\n        return -1;\n      }\n\n      // Calling with cipher == nullptr results in a parameter change\n      // without requiring a reallocation of the internal cipher ctx.\n      if (EVP_DecryptInit_ex(decrypt_ctx.get(), nullptr, nullptr, nullptr, iv->data()) != 1) {\n        return -1;\n      }\n\n      plaintext.resize(round_to_pkcs7_padded(cipher.size()));\n\n      int update_outlen, final_outlen;\n\n      if (EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &update_outlen, (const std::uint8_t *) cipher.data(), cipher.size()) != 1) {\n        return -1;\n      }\n\n      if (EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + update_outlen, &final_outlen) != 1) {\n        return -1;\n      }\n\n      plaintext.resize(update_outlen + final_outlen);\n      return 0;\n    }\n\n    ecb_t::ecb_t(const aes_t &key, bool padding):\n        cipher_t { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_new(), key, padding } {}\n\n    cbc_t::cbc_t(const aes_t &key, bool padding):\n        cipher_t { nullptr, nullptr, key, padding } {}\n\n    gcm_t::gcm_t(const crypto::aes_t &key, bool padding):\n        cipher_t { nullptr, nullptr, key, padding } {}\n\n  }  // namespace cipher\n\n  aes_t\n  gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin) {\n    aes_t key(16);\n\n    std::string salt_pin;\n    salt_pin.reserve(salt.size() + pin.size());\n\n    salt_pin.insert(std::end(salt_pin), std::begin(salt), std::end(salt));\n    salt_pin.insert(std::end(salt_pin), std::begin(pin), std::end(pin));\n\n    auto hsh = hash(salt_pin);\n\n    std::copy(std::begin(hsh), std::begin(hsh) + key.size(), std::begin(key));\n\n    return key;\n  }\n\n  sha256_t\n  hash(const std::string_view &plaintext) {\n    sha256_t hsh;\n    EVP_Digest(plaintext.data(), plaintext.size(), hsh.data(), nullptr, EVP_sha256(), nullptr);\n    return hsh;\n  }\n\n  x509_t\n  x509(const std::string_view &x) {\n    bio_t io { BIO_new(BIO_s_mem()) };\n\n    BIO_write(io.get(), x.data(), x.size());\n\n    x509_t p;\n    PEM_read_bio_X509(io.get(), &p, nullptr, nullptr);\n\n    return p;\n  }\n\n  pkey_t\n  pkey(const std::string_view &k) {\n    bio_t io { BIO_new(BIO_s_mem()) };\n\n    BIO_write(io.get(), k.data(), k.size());\n\n    pkey_t p = nullptr;\n    PEM_read_bio_PrivateKey(io.get(), &p, nullptr, nullptr);\n\n    return p;\n  }\n\n  std::string\n  pem(x509_t &x509) {\n    bio_t bio { BIO_new(BIO_s_mem()) };\n\n    PEM_write_bio_X509(bio.get(), x509.get());\n    BUF_MEM *mem_ptr;\n    BIO_get_mem_ptr(bio.get(), &mem_ptr);\n\n    return { mem_ptr->data, mem_ptr->length };\n  }\n\n  std::string\n  pem(pkey_t &pkey) {\n    bio_t bio { BIO_new(BIO_s_mem()) };\n\n    PEM_write_bio_PrivateKey(bio.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr);\n    BUF_MEM *mem_ptr;\n    BIO_get_mem_ptr(bio.get(), &mem_ptr);\n\n    return { mem_ptr->data, mem_ptr->length };\n  }\n\n  std::string_view\n  signature(const x509_t &x) {\n    // X509_ALGOR *_ = nullptr;\n\n    const ASN1_BIT_STRING *asn1 = nullptr;\n    X509_get0_signature(&asn1, nullptr, x.get());\n\n    return { (const char *) asn1->data, (std::size_t) asn1->length };\n  }\n\n  std::string\n  rand(std::size_t bytes) {\n    std::string r;\n    r.resize(bytes);\n\n    RAND_bytes((uint8_t *) r.data(), r.size());\n\n    return r;\n  }\n\n  std::vector<uint8_t>\n  sign(const pkey_t &pkey, const std::string_view &data, const EVP_MD *md) {\n    md_ctx_t ctx { EVP_MD_CTX_create() };\n\n    if (EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, (EVP_PKEY *) pkey.get()) != 1) {\n      return {};\n    }\n\n    if (EVP_DigestSignUpdate(ctx.get(), data.data(), data.size()) != 1) {\n      return {};\n    }\n\n    std::size_t slen;\n    if (EVP_DigestSignFinal(ctx.get(), nullptr, &slen) != 1) {\n      return {};\n    }\n\n    std::vector<uint8_t> digest(slen);\n    if (EVP_DigestSignFinal(ctx.get(), digest.data(), &slen) != 1) {\n      return {};\n    }\n\n    return digest;\n  }\n\n  creds_t\n  gen_creds(const std::string_view &cn, std::uint32_t key_bits) {\n    x509_t x509 { X509_new() };\n    pkey_ctx_t ctx { EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr) };\n    pkey_t pkey;\n\n    EVP_PKEY_keygen_init(ctx.get());\n    EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), key_bits);\n    EVP_PKEY_keygen(ctx.get(), &pkey);\n\n    X509_set_version(x509.get(), 2);\n\n    // Generate a real serial number to avoid SEC_ERROR_REUSED_ISSUER_AND_SERIAL with Firefox\n    bignum_t serial { BN_new() };\n    BN_rand(serial.get(), 159, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY);  // 159 bits to fit in 20 bytes in DER format\n    BN_set_negative(serial.get(), 0);  // Serial numbers must be positive\n    BN_to_ASN1_INTEGER(serial.get(), X509_get_serialNumber(x509.get()));\n\n    constexpr auto year = 60 * 60 * 24 * 365;\n#if OPENSSL_VERSION_NUMBER < 0x10100000L\n    X509_gmtime_adj(X509_get_notBefore(x509.get()), 0);\n    X509_gmtime_adj(X509_get_notAfter(x509.get()), 20 * year);\n#else\n    asn1_string_t not_before { ASN1_STRING_dup(X509_get0_notBefore(x509.get())) };\n    asn1_string_t not_after { ASN1_STRING_dup(X509_get0_notAfter(x509.get())) };\n\n    X509_gmtime_adj(not_before.get(), 0);\n    X509_gmtime_adj(not_after.get(), 20 * year);\n\n    X509_set1_notBefore(x509.get(), not_before.get());\n    X509_set1_notAfter(x509.get(), not_after.get());\n#endif\n\n    X509_set_pubkey(x509.get(), pkey.get());\n\n    auto name = X509_get_subject_name(x509.get());\n    X509_NAME_add_entry_by_txt(name, \"CN\", MBSTRING_ASC,\n      (const std::uint8_t *) cn.data(), cn.size(),\n      -1, 0);\n\n    X509_set_issuer_name(x509.get(), name);\n    X509_sign(x509.get(), pkey.get(), EVP_sha256());\n\n    return { pem(x509), pem(pkey) };\n  }\n\n  std::vector<uint8_t>\n  sign256(const pkey_t &pkey, const std::string_view &data) {\n    return sign(pkey, data, EVP_sha256());\n  }\n\n  bool\n  verify(const x509_t &x509, const std::string_view &data, const std::string_view &signature, const EVP_MD *md) {\n    auto pkey = X509_get0_pubkey(x509.get());\n\n    md_ctx_t ctx { EVP_MD_CTX_create() };\n\n    if (EVP_DigestVerifyInit(ctx.get(), nullptr, md, nullptr, pkey) != 1) {\n      return false;\n    }\n\n    if (EVP_DigestVerifyUpdate(ctx.get(), data.data(), data.size()) != 1) {\n      return false;\n    }\n\n    if (EVP_DigestVerifyFinal(ctx.get(), (const uint8_t *) signature.data(), signature.size()) != 1) {\n      return false;\n    }\n\n    return true;\n  }\n\n  bool\n  verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature) {\n    return verify(x509, data, signature, EVP_sha256());\n  }\n\n  void\n  md_ctx_destroy(EVP_MD_CTX *ctx) {\n    EVP_MD_CTX_destroy(ctx);\n  }\n\n  std::string\n  rand_alphabet(std::size_t bytes, const std::string_view &alphabet) {\n    auto value = rand(bytes);\n\n    for (std::size_t i = 0; i != value.size(); ++i) {\n      value[i] = alphabet[value[i] % alphabet.length()];\n    }\n    return value;\n  }\n\n}  // namespace crypto\n"
  },
  {
    "path": "src/crypto.h",
    "content": "/**\n * @file src/crypto.h\n * @brief Declarations for cryptography functions.\n */\n#pragma once\n\n#include <array>\n#include <openssl/evp.h>\n#include <openssl/rand.h>\n#include <openssl/sha.h>\n#include <openssl/x509.h>\n\n#include \"utility.h\"\n\nnamespace crypto {\n  struct creds_t {\n    std::string x509;\n    std::string pkey;\n  };\n\n  void\n  md_ctx_destroy(EVP_MD_CTX *);\n\n  using sha256_t = std::array<std::uint8_t, SHA256_DIGEST_LENGTH>;\n\n  using aes_t = std::vector<std::uint8_t>;\n  using x509_t = util::safe_ptr<X509, X509_free>;\n  using x509_store_t = util::safe_ptr<X509_STORE, X509_STORE_free>;\n  using x509_store_ctx_t = util::safe_ptr<X509_STORE_CTX, X509_STORE_CTX_free>;\n  using cipher_ctx_t = util::safe_ptr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;\n  using md_ctx_t = util::safe_ptr<EVP_MD_CTX, md_ctx_destroy>;\n  using bio_t = util::safe_ptr<BIO, BIO_free_all>;\n  using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;\n  using pkey_ctx_t = util::safe_ptr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>;\n  using bignum_t = util::safe_ptr<BIGNUM, BN_free>;\n\n  /**\n   * @brief Hashes the given plaintext using SHA-256.\n   * @param plaintext\n   * @return The SHA-256 hash of the plaintext.\n   */\n  sha256_t\n  hash(const std::string_view &plaintext);\n\n  aes_t\n  gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin);\n\n  x509_t\n  x509(const std::string_view &x);\n  pkey_t\n  pkey(const std::string_view &k);\n  std::string\n  pem(x509_t &x509);\n  std::string\n  pem(pkey_t &pkey);\n\n  std::vector<uint8_t>\n  sign256(const pkey_t &pkey, const std::string_view &data);\n  bool\n  verify256(const x509_t &x509, const std::string_view &data, const std::string_view &signature);\n\n  creds_t\n  gen_creds(const std::string_view &cn, std::uint32_t key_bits);\n\n  std::string_view\n  signature(const x509_t &x);\n\n  std::string\n  rand(std::size_t bytes);\n  std::string\n  rand_alphabet(std::size_t bytes,\n    const std::string_view &alphabet = std::string_view { \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!%&()=-\" });\n\n  class cert_chain_t {\n  public:\n    KITTY_DECL_CONSTR(cert_chain_t)\n\n    void\n    add(x509_t &&cert);\n\n    void\n    clear();\n\n    const char *\n    verify(x509_t::element_type *cert);\n\n    const char *\n    verify_safe(x509_t::element_type *cert);\n\n  private:\n    std::vector<std::pair<x509_t, x509_store_t>> _certs;\n    x509_store_ctx_t _cert_ctx;\n  };\n\n  namespace cipher {\n    constexpr std::size_t tag_size = 16;\n    constexpr std::size_t\n    round_to_pkcs7_padded(std::size_t size) {\n      return ((size + 15) / 16) * 16;\n    }\n\n    class cipher_t {\n    public:\n      cipher_ctx_t decrypt_ctx;\n      cipher_ctx_t encrypt_ctx;\n\n      aes_t key;\n\n      bool padding;\n    };\n\n    class ecb_t: public cipher_t {\n    public:\n      ecb_t() = default;\n      ecb_t(ecb_t &&) noexcept = default;\n      ecb_t &\n      operator=(ecb_t &&) noexcept = default;\n\n      ecb_t(const aes_t &key, bool padding = true);\n\n      int\n      encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);\n      int\n      decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);\n    };\n\n    class gcm_t: public cipher_t {\n    public:\n      gcm_t() = default;\n      gcm_t(gcm_t &&) noexcept = default;\n      gcm_t &\n      operator=(gcm_t &&) noexcept = default;\n\n      gcm_t(const crypto::aes_t &key, bool padding = true);\n\n      /**\n       * @brief Encrypts the plaintext using AES GCM mode.\n       * @param plaintext The plaintext data to be encrypted.\n       * @param tag The buffer where the GCM tag will be written.\n       * @param ciphertext The buffer where the resulting ciphertext will be written.\n       * @param iv The initialization vector to be used for the encryption.\n       * @return The total length of the ciphertext and GCM tag. Returns -1 in case of an error.\n       */\n      int\n      encrypt(const std::string_view &plaintext, std::uint8_t *tag, std::uint8_t *ciphertext, aes_t *iv);\n\n      /**\n       * @brief Encrypts the plaintext using AES GCM mode.\n       * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size()) + crypto::cipher::tag_size\n       * @param plaintext The plaintext data to be encrypted.\n       * @param tagged_cipher The buffer where the resulting ciphertext and GCM tag will be written.\n       * @param iv The initialization vector to be used for the encryption.\n       * @return The total length of the ciphertext and GCM tag written into tagged_cipher. Returns -1 in case of an error.\n       */\n      int\n      encrypt(const std::string_view &plaintext, std::uint8_t *tagged_cipher, aes_t *iv);\n\n      int\n      decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv);\n    };\n\n    class cbc_t: public cipher_t {\n    public:\n      cbc_t() = default;\n      cbc_t(cbc_t &&) noexcept = default;\n      cbc_t &\n      operator=(cbc_t &&) noexcept = default;\n\n      cbc_t(const crypto::aes_t &key, bool padding = true);\n\n      /**\n       * @brief Encrypts the plaintext using AES CBC mode.\n       * length of cipher must be at least: round_to_pkcs7_padded(plaintext.size())\n       * @param plaintext The plaintext data to be encrypted.\n       * @param cipher The buffer where the resulting ciphertext will be written.\n       * @param iv The initialization vector to be used for the encryption.\n       * @return The total length of the ciphertext written into cipher. Returns -1 in case of an error.\n       */\n      int\n      encrypt(const std::string_view &plaintext, std::uint8_t *cipher, aes_t *iv);\n\n      /**\n       * @brief Decrypts the ciphertext using AES CBC mode.\n       * @param cipher The ciphertext data to be decrypted.\n       * @param plaintext The buffer where the resulting plaintext will be written.\n       * @param iv The initialization vector to be used for the decryption.\n       * @return 0 on success, -1 on error.\n       */\n      int\n      decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext, aes_t *iv);\n    };\n  }  // namespace cipher\n}  // namespace crypto\n"
  },
  {
    "path": "src/display_device/display_device.h",
    "content": "#pragma once\n\n// standard includes\n#include <map>\n#include <string>\n#include <unordered_set>\n#include <vector>\n\n// lib includes\n#include <boost/optional.hpp>\n#include <nlohmann/json.hpp>\n\nnamespace display_device {\n\n  /**\n   * @brief The device state in the operating system.\n   * @note On Windows you can have have multiple primary displays when they are duplicated.\n   */\n  enum class device_state_e {\n    inactive,\n    active,\n    primary /**< Primary state is also implicitly active. */\n  };\n\n  /**\n   * @brief The device's HDR state in the operating system.\n   */\n  enum class hdr_state_e {\n    unknown, /**< HDR state could not be retrieved from the OS (even if the display supports it). */\n    disabled,\n    enabled\n  };\n\n  // For JSON serialization for hdr_state_e\n  NLOHMANN_JSON_SERIALIZE_ENUM(hdr_state_e, { { hdr_state_e::unknown, \"unknown\" },\n                                              { hdr_state_e::disabled, \"disabled\" },\n                                              { hdr_state_e::enabled, \"enabled\" } })\n\n  /**\n   * @brief Ordered map of [DEVICE_ID -> hdr_state_e].\n   */\n  using hdr_state_map_t = std::map<std::string, hdr_state_e>;\n\n  /**\n   * @brief The device's HDR state in the operating system.\n   */\n  struct device_info_t {\n    std::string display_name; /**< A name representing the OS display (source) the device is connected to. */\n    std::string friendly_name; /**< A human-readable name for the device. */\n    device_state_e device_state; /**< Device's state. @see device_state_e */\n    hdr_state_e hdr_state; /**< Device's HDR state. @see hdr_state_e */\n  };\n\n  /**\n   * @brief Ordered map of [DEVICE_ID -> device_info_t].\n   * @see device_info_t\n   */\n  using device_info_map_t = std::map<std::string, device_info_t>;\n\n  /**\n   * @brief Display's resolution.\n   */\n  struct resolution_t {\n    unsigned int width;\n    unsigned int height;\n\n    // For JSON serialization\n    NLOHMANN_DEFINE_TYPE_INTRUSIVE(resolution_t, width, height)\n  };\n\n  /**\n   * @brief Display's refresh rate.\n   * @note Floating point is stored in a \"numerator/denominator\" form.\n   */\n  struct refresh_rate_t {\n    unsigned int numerator;\n    unsigned int denominator;\n\n    // For JSON serialization\n    NLOHMANN_DEFINE_TYPE_INTRUSIVE(refresh_rate_t, numerator, denominator)\n  };\n\n  /**\n   * @brief Display's mode (resolution + refresh rate).\n   * @see resolution_t\n   * @see refresh_rate_t\n   */\n  struct display_mode_t {\n    resolution_t resolution;\n    refresh_rate_t refresh_rate;\n\n    // For JSON serialization\n    NLOHMANN_DEFINE_TYPE_INTRUSIVE(display_mode_t, resolution, refresh_rate)\n  };\n\n  /**\n   * @brief Ordered map of [DEVICE_ID -> display_mode_t].\n   * @see display_mode_t\n   */\n  using device_display_mode_map_t = std::map<std::string, display_mode_t>;\n\n  /**\n   * @brief A LIST[LIST[DEVICE_ID]] structure which represents an active topology.\n   *\n   * Single display:\n   *     [[DISPLAY_1]]\n   * 2 extended displays:\n   *     [[DISPLAY_1], [DISPLAY_2]]\n   * 2 duplicated displays:\n   *     [[DISPLAY_1, DISPLAY_2]]\n   * Mixed displays:\n   *     [[EXTENDED_DISPLAY_1], [DUPLICATED_DISPLAY_1, DUPLICATED_DISPLAY_2], [EXTENDED_DISPLAY_2]]\n   *\n   * @note On Windows the order does not matter of both device ids or the inner lists.\n   */\n  using active_topology_t = std::vector<std::vector<std::string>>;\n\n  /**\n   * @brief Enumerate the available (active and inactive) devices.\n   * @returns A map of available devices.\n   *          Empty map can also be returned if an error has occurred.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const auto devices { enum_available_devices() };\n   * ```\n   */\n  device_info_map_t\n  enum_available_devices();\n\n  std::string\n  find_one_of_the_available_devices(const std::string &device_id);\n\n  std::string\n  find_device_by_friendlyname(const std::string &friendly_name);\n\n  /**\n   * @brief Get display name associated with the device.\n   * @param device_id A device to get display name for.\n   * @returns A display name for the device, or an empty string if the device is inactive or not found.\n   *          Empty string can also be returned if an error has occurred.\n   * @see device_info_t\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string device_name { \"MY_DEVICE_ID\" };\n   * const std::string display_name = get_display_name(device_id);\n   * ```\n   */\n  std::string\n  get_display_name(const std::string &device_id);\n\n  std::string\n  get_display_friendly_name(const std::string &device_id);\n\n  /**\n   * @brief Get current display modes for the devices.\n   * @param device_ids A list of devices to get the modes for.\n   * @returns A map of device modes per a device or an empty map if a mode could not be found (e.g. device is inactive).\n   *          Empty map can also be returned if an error has occurred.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::unordered_set<std::string> device_ids { \"DEVICE_ID_1\", \"DEVICE_ID_2\" };\n   * const auto current_modes = get_current_display_modes(device_ids);\n   * ```\n   */\n  device_display_mode_map_t\n  get_current_display_modes(const std::unordered_set<std::string> &device_ids);\n\n  /**\n   * @brief Set new display modes for the devices.\n   * @param modes A map of modes to set.\n   * @returns True if modes were set, false otherwise.\n   * @warning if any of the specified devices are duplicated, modes modes be provided\n   *          for duplicates too!\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string display_a { \"MY_ID_1\" };\n   * const std::string display_b { \"MY_ID_2\" };\n   * const auto success = set_display_modes({ { display_a, { { 1920, 1080 }, { 60, 1 } } },\n   *                                          { display_b, { { 1920, 1080 }, { 120, 1 } } } });\n   * ```\n   */\n  bool\n  set_display_modes(const device_display_mode_map_t &modes);\n\n  /**\n   * @brief Check whether the specified device is primary.\n   * @param device_id A device to perform the check for.\n   * @returns True if the device is primary, false otherwise.\n   * @see device_state_e\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string device_id { \"MY_DEVICE_ID\" };\n   * const bool is_primary = is_primary_device(device_id);\n   * ```\n   */\n  bool\n  is_primary_device(const std::string &device_id);\n\n  /**\n   * @brief Set the device as a primary display.\n   * @param device_id A device to set as primary.\n   * @returns True if the device is or was set as primary, false otherwise.\n   * @note On Windows if the device is duplicated, the other duplicated device(-s) will also become a primary device.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string device_id { \"MY_DEVICE_ID\" };\n   * const bool success = set_as_primary_device(device_id);\n   * ``\n   */\n  bool\n  set_as_primary_device(const std::string &device_id);\n\n  /**\n   * @brief Get HDR state for the devices.\n   * @param device_ids A list of devices to get the HDR states for.\n   * @returns A map of HDR states per a device or an empty map if an error has occurred.\n   * @note On Windows the state cannot be retrieved until the device is active even if it supports it.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::unordered_set<std::string> device_ids { \"DEVICE_ID_1\", \"DEVICE_ID_2\" };\n   * const auto current_hdr_states = get_current_hdr_states(device_ids);\n   * ```\n   */\n  hdr_state_map_t\n  get_current_hdr_states(const std::unordered_set<std::string> &device_ids);\n\n  /**\n   * @brief Set HDR states for the devices.\n   * @param modes A map of HDR states to set.\n   * @returns True if HDR states were set, false otherwise.\n   * @note If `unknown` states are provided, they will be silently ignored\n   *       and current state will not be changed.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string display_a { \"MY_ID_1\" };\n   * const std::string display_b { \"MY_ID_2\" };\n   * const auto success = set_hdr_states({ { display_a, hdr_state_e::enabled },\n   *                                       { display_b, hdr_state_e::disabled } });\n   * ```\n   */\n  bool\n  set_hdr_states(const hdr_state_map_t &states);\n\n  /**\n   * @brief Get the active (current) topology.\n   * @returns A list representing the current topology.\n   *          Empty list can also be returned if an error has occurred.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const auto current_topology { get_current_topology() };\n   * ```\n   */\n  active_topology_t\n  get_current_topology();\n\n  /**\n   * @brief Verify if the active topology is valid.\n   *\n   * This is mostly meant as a sanity check or to verify that it is still valid\n   * after a manual modification to an existing topology.\n   *\n   * @param topology Topology to validated.\n   * @returns True if it is valid, false otherwise.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * auto current_topology { get_current_topology() };\n   * // Modify the current_topology\n   * const bool is_valid = is_topology_valid(current_topology);\n   * ```\n   */\n  bool\n  is_topology_valid(const active_topology_t &topology);\n\n  /**\n   * @brief Check if the topologies are close enough to be considered the same by the OS.\n   * @param topology_a First topology to compare.\n   * @param topology_b Second topology to compare.\n   * @returns True if topologies are close enough, false otherwise.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * auto current_topology { get_current_topology() };\n   * auto new_topology { current_topology };\n   * // Modify the new_topology\n   * const bool is_the_same = is_topology_the_same(current_topology, new_topology);\n   * ```\n   */\n  bool\n  is_topology_the_same(const active_topology_t &topology_a, const active_topology_t &topology_b);\n\n  /**\n   * @brief Set the a new active topology for the OS.\n   * @param new_topology New device topology to set.\n   * @returns True if the new topology has been set, false otherwise.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * auto current_topology { get_current_topology() };\n   * // Modify the current_topology\n   * const bool success = set_topology(current_topology);\n   * ```\n   */\n  bool\n  set_topology(const active_topology_t &new_topology);\n\n  /**\n   * @brief Apply the HDR profile to the specified client.\n   * @param client_name Name of the client to apply the HDR profile to.\n   * @returns True if the HDR profile has been applied, false otherwise.\n   *\n   */\n  bool\n  apply_hdr_profile(const std::string &client_name);\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/display_device/parsed_config.cpp",
    "content": "// lib includes\n#include <boost/algorithm/string.hpp>\n#include <boost/regex.hpp>\n#include <charconv>\n#include <cmath>\n\n// local includes\n#include \"display_device.h\"\n#include \"parsed_config.h\"\n#include \"session.h\"\n#include \"src/config.h\"\n#include \"src/globals.h\"\n#include \"src/logging.h\"\n#include \"src/platform/windows/display_device/windows_utils.h\"\n#include \"src/platform/windows/misc.h\"\n#include \"src/rtsp.h\"\n#include \"to_string.h\"\n\nusing namespace std::literals;\n\nnamespace display_device {\n\n  namespace {\n    /**\n     * @brief Parse resolution value from the string.\n     * @param input String to be parsed.\n     * @param output Reference to output variable.\n     * @returns True on successful parsing (empty string allowed), false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * boost::optional<resolution_t> resolution;\n     * if (parse_resolution_string(\"1920x1080\", resolution)) {\n     *   if (resolution) {\n     *     // Value was specified\n     *   }\n     *   else {\n     *     // Value was empty\n     *   }\n     * }\n     * ```\n     */\n    bool\n    parse_resolution_string(const std::string &input, boost::optional<resolution_t> &output) {\n      const std::string trimmed_input { boost::algorithm::trim_copy(input) };\n      const boost::regex resolution_regex { R\"(^(\\d+)x(\\d+)$)\" };  // std::regex hangs in CTOR for some reason when called in a thread. Problem with MSYS2 packages (UCRT64), maybe?\n\n      boost::smatch match;\n      if (boost::regex_match(trimmed_input, match, resolution_regex)) {\n        try {\n          output = resolution_t {\n            static_cast<unsigned int>(std::stol(match[1])),\n            static_cast<unsigned int>(std::stol(match[2]))\n          };\n        }\n        catch (const std::invalid_argument &err) {\n          BOOST_LOG(error) << \"Failed to parse resolution string \" << trimmed_input << \" (invalid argument):\\n\"\n                           << err.what();\n          return false;\n        }\n        catch (const std::out_of_range &err) {\n          BOOST_LOG(error) << \"Failed to parse resolution string \" << trimmed_input << \" (number out of range):\\n\"\n                           << err.what();\n          return false;\n        }\n        catch (const std::exception &err) {\n          BOOST_LOG(error) << \"Failed to parse resolution string \" << trimmed_input << \":\\n\"\n                           << err.what();\n          return false;\n        }\n      }\n      else {\n        output = boost::none;\n\n        if (!trimmed_input.empty()) {\n          BOOST_LOG(error) << \"Failed to parse resolution string \" << trimmed_input << \". It must match a \\\"1920x1080\\\" pattern!\";\n          return false;\n        }\n      }\n\n      return true;\n    }\n\n    /**\n     * @brief Parse refresh rate value from the string.\n     * @param input String to be parsed.\n     * @param output Reference to output variable.\n     * @param allow_decimal_point Specify whether the decimal point is allowed in the string.\n     * @returns True on successful parsing (empty string allowed), false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * boost::optional<refresh_rate_t> refresh_rate;\n     * if (parse_refresh_rate_string(\"59.95\", refresh_rate)) {\n     *   if (refresh_rate) {\n     *     // Value was specified\n     *   }\n     *   else {\n     *     // Value was empty\n     *   }\n     * }\n     * ```\n     */\n    bool\n    parse_refresh_rate_string(const std::string &input, boost::optional<refresh_rate_t> &output, bool allow_decimal_point = true) {\n      const std::string trimmed_input { boost::algorithm::trim_copy(input) };\n      // std::regex hangs in CTOR for some reason when called in a thread. Problem with MSYS2 packages (UCRT64), maybe?\n      const boost::regex refresh_rate_regex { allow_decimal_point ? R\"(^(\\d+)(?:\\.(\\d+))?$)\" : R\"(^(\\d+)$)\" };\n\n      boost::smatch match;\n      if (boost::regex_match(trimmed_input, match, refresh_rate_regex)) {\n        try {\n          if (allow_decimal_point && match[2].matched) {\n            // We have a decimal point and will have to split it into numerator and denominator.\n            // For example:\n            //   59.995:\n            //     numerator = 59995\n            //     denominator = 1000\n\n            // We are essentially removing the decimal point here: 59.995 -> 59995\n            const std::string numerator_str { match[1].str() + match[2].str() };\n            const auto numerator { static_cast<unsigned int>(std::stol(numerator_str)) };\n\n            // Here we are counting decimal places and calculating denominator: 10^decimal_places\n            const auto denominator { static_cast<unsigned int>(std::pow(10, std::distance(match[2].first, match[2].second))) };\n\n            output = refresh_rate_t { numerator, denominator };\n          }\n          else {\n            // We do not have a decimal point, just a valid number.\n            // For example:\n            //   60:\n            //     numerator = 60\n            //     denominator = 1\n            output = refresh_rate_t { static_cast<unsigned int>(std::stol(match[1])), 1 };\n          }\n        }\n        catch (const std::invalid_argument &err) {\n          BOOST_LOG(error) << \"Failed to parse refresh rate or FPS string \" << trimmed_input << \" (invalid argument):\\n\"\n                           << err.what();\n          return false;\n        }\n        catch (const std::out_of_range &err) {\n          BOOST_LOG(error) << \"Failed to parse refresh rate or FPS string \" << trimmed_input << \" (number out of range):\\n\"\n                           << err.what();\n          return false;\n        }\n        catch (const std::exception &err) {\n          BOOST_LOG(error) << \"Failed to parse refresh rate or FPS string \" << trimmed_input << \":\\n\"\n                           << err.what();\n          return false;\n        }\n      }\n      else {\n        output = boost::none;\n\n        if (!trimmed_input.empty()) {\n          BOOST_LOG(error) << \"Failed to parse refresh rate or FPS string \" << trimmed_input << \". Must have a pattern of \" << (allow_decimal_point ? \"\\\"123\\\" or \\\"123.456\\\"\" : \"\\\"123\\\"\") << \"!\";\n          return false;\n        }\n      }\n\n      return true;\n    }\n\n    /**\n     * @brief Parse resolution option from the user configuration and the session information.\n     * @param config User's video related configuration.\n     * @param session Session information.\n     * @param parsed_config A reference to a config object that will be modified on success.\n     * @returns True on successful parsing, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const std::shared_ptr<rtsp_stream::launch_session_t> launch_session; // Assuming ptr is properly initialized\n     * const config::video_t &video_config { config::video };\n     *\n     * parsed_config_t parsed_config;\n     * const bool success = parse_resolution_option(video_config, *launch_session, parsed_config);\n     * ```\n     */\n    bool\n    parse_resolution_option(const config::video_t &config, const rtsp_stream::launch_session_t &session, parsed_config_t &parsed_config) {\n      const auto resolution_option { static_cast<parsed_config_t::resolution_change_e>(config.resolution_change) };\n      switch (resolution_option) {\n        case parsed_config_t::resolution_change_e::automatic: {\n          if (!session.enable_sops) {\n            BOOST_LOG(warning) << \"Sunshine is configured to change resolution automatically, but the \\\"Optimize game settings\\\" is not set in the client! Resolution will not be changed.\";\n            parsed_config.resolution = boost::none;\n          }\n          else if (session.width > 16384 || session.height > 16384) {\n            BOOST_LOG(warning) << \"奇怪的分辨率增加了...\";\n            parsed_config.resolution = boost::none;\n          }\n          else if (session.width >= 0 && session.height >= 0) {\n            parsed_config.resolution = resolution_t {\n              static_cast<unsigned int>(session.width),\n              static_cast<unsigned int>(session.height)\n            };\n          }\n          else {\n            BOOST_LOG(error) << \"Resolution provided by client session config is invalid: \" << session.width << \"x\" << session.height;\n            return false;\n          }\n          break;\n        }\n        case parsed_config_t::resolution_change_e::manual: {\n          if (!session.enable_sops) {\n            BOOST_LOG(warning) << \"Sunshine is configured to change resolution manually, but the \\\"Optimize game settings\\\" is not set in the client! Resolution will not be changed.\";\n            parsed_config.resolution = boost::none;\n          }\n          else {\n            if (!parse_resolution_string(config.manual_resolution, parsed_config.resolution)) {\n              BOOST_LOG(error) << \"Failed to parse manual resolution string!\";\n              return false;\n            }\n\n            if (!parsed_config.resolution) {\n              BOOST_LOG(error) << \"Manual resolution must be specified!\";\n              return false;\n            }\n          }\n          break;\n        }\n        case parsed_config_t::resolution_change_e::no_operation:\n        default:\n          break;\n      }\n\n      return true;\n    }\n\n    /**\n     * @brief Parse refresh rate option from the user configuration and the session information.\n     * @param config User's video related configuration.\n     * @param session Session information.\n     * @param parsed_config A reference to a config object that will be modified on success.\n     * @returns True on successful parsing, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const std::shared_ptr<rtsp_stream::launch_session_t> launch_session; // Assuming ptr is properly initialized\n     * const config::video_t &video_config { config::video };\n     *\n     * parsed_config_t parsed_config;\n     * const bool success = parse_refresh_rate_option(video_config, *launch_session, parsed_config);\n     * ```\n     */\n    bool\n    parse_refresh_rate_option(const config::video_t &config, const rtsp_stream::launch_session_t &session, parsed_config_t &parsed_config) {\n      const auto refresh_rate_option { static_cast<parsed_config_t::refresh_rate_change_e>(config.refresh_rate_change) };\n      switch (refresh_rate_option) {\n        case parsed_config_t::refresh_rate_change_e::automatic: {\n          if (session.fps >= 0) {\n            parsed_config.refresh_rate = refresh_rate_t { static_cast<unsigned int>(session.fps), 1 };\n          }\n          else {\n            BOOST_LOG(error) << \"FPS value provided by client session config is invalid: \" << session.fps;\n            return false;\n          }\n          break;\n        }\n        case parsed_config_t::refresh_rate_change_e::manual: {\n          if (!parse_refresh_rate_string(config.manual_refresh_rate, parsed_config.refresh_rate)) {\n            BOOST_LOG(error) << \"Failed to parse manual refresh rate string!\";\n            return false;\n          }\n\n          if (!parsed_config.refresh_rate) {\n            BOOST_LOG(error) << \"Manual refresh rate must be specified!\";\n            return false;\n          }\n          break;\n        }\n        case parsed_config_t::refresh_rate_change_e::no_operation:\n        default:\n          break;\n      }\n\n      return true;\n    }\n\n    /**\n     * @brief Remap the already parsed display mode based on the user configuration.\n     * @param config User's video related configuration.\n     * @param parsed_config A reference to a config object that will be modified on success.\n     * @returns True is display mode was remapped or no remapping was needed, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const std::shared_ptr<rtsp_stream::launch_session_t> launch_session; // Assuming ptr is properly initialized\n     * const config::video_t &video_config { config::video };\n     *\n     * parsed_config_t parsed_config;\n     * const bool success = remap_display_modes_if_needed(video_config, *launch_session, parsed_config);\n     * ```\n     */\n    bool\n    remap_display_modes_if_needed(const config::video_t &config, const rtsp_stream::launch_session_t &session, parsed_config_t &parsed_config) {\n      constexpr auto mixed_remapping { \"\" };\n      constexpr auto resolution_only_remapping { \"resolution_only\" };\n      constexpr auto refresh_rate_only_remapping { \"refresh_rate_only\" };\n\n      const auto resolution_option { static_cast<parsed_config_t::resolution_change_e>(config.resolution_change) };\n      const auto refresh_rate_option { static_cast<parsed_config_t::refresh_rate_change_e>(config.refresh_rate_change) };\n\n      // Copy only the remapping values that we can actually use with our configuration options\n      std::vector<config::video_t::display_mode_remapping_t> remapping_values;\n      std::copy_if(std::begin(config.display_mode_remapping), std::end(config.display_mode_remapping), std::back_inserter(remapping_values), [&](const auto &value) {\n        if (resolution_option == parsed_config_t::resolution_change_e::automatic && refresh_rate_option == parsed_config_t::refresh_rate_change_e::automatic) {\n          return value.type == mixed_remapping;  // Comparison instead of empty check to be explicit\n        }\n        else if (resolution_option == parsed_config_t::resolution_change_e::automatic) {\n          return value.type == resolution_only_remapping;\n        }\n        else if (refresh_rate_option == parsed_config_t::refresh_rate_change_e::automatic) {\n          return value.type == refresh_rate_only_remapping;\n        }\n\n        return false;\n      });\n\n      if (remapping_values.empty()) {\n        BOOST_LOG(debug) << \"No values are available for display mode remapping.\";\n        return true;\n      }\n      BOOST_LOG(debug) << \"Trying to remap display modes...\";\n\n      struct parsed_remapping_values_t {\n        boost::optional<resolution_t> received_resolution;\n        boost::optional<refresh_rate_t> received_fps;\n        boost::optional<resolution_t> final_resolution;\n        boost::optional<refresh_rate_t> final_refresh_rate;\n      };\n\n      std::vector<parsed_remapping_values_t> parsed_values;\n      for (const auto &entry : remapping_values) {\n        boost::optional<resolution_t> received_resolution;\n        boost::optional<refresh_rate_t> received_fps;\n        boost::optional<resolution_t> final_resolution;\n        boost::optional<refresh_rate_t> final_refresh_rate;\n\n        if (entry.type == resolution_only_remapping) {\n          if (!parse_resolution_string(entry.received_resolution, received_resolution) ||\n              !parse_resolution_string(entry.final_resolution, final_resolution)) {\n            BOOST_LOG(error) << \"Failed to parse entry value: \" << entry.received_resolution << \" -> \" << entry.final_resolution;\n            return false;\n          }\n\n          if (!received_resolution || !final_resolution) {\n            BOOST_LOG(error) << \"Both values must be set for remapping resolution! Current entry value: \" << entry.received_resolution << \" -> \" << entry.final_resolution;\n            return false;\n          }\n\n          if (!session.enable_sops) {\n            BOOST_LOG(warning) << \"Skipping remapping resolution, because the \\\"Optimize game settings\\\" is not set in the client!\";\n            return true;\n          }\n        }\n        else if (entry.type == refresh_rate_only_remapping) {\n          if (!parse_refresh_rate_string(entry.received_fps, received_fps, false) ||\n              !parse_refresh_rate_string(entry.final_refresh_rate, final_refresh_rate)) {\n            BOOST_LOG(error) << \"Failed to parse entry value: \" << entry.received_fps << \" -> \" << entry.final_refresh_rate;\n            return false;\n          }\n\n          if (!received_fps || !final_refresh_rate) {\n            BOOST_LOG(error) << \"Both values must be set for remapping refresh rate! Current entry value: \" << entry.received_fps << \" -> \" << entry.final_refresh_rate;\n            return false;\n          }\n        }\n        else {\n          if (!parse_resolution_string(entry.received_resolution, received_resolution) ||\n              !parse_refresh_rate_string(entry.received_fps, received_fps, false) ||\n              !parse_resolution_string(entry.final_resolution, final_resolution) ||\n              !parse_refresh_rate_string(entry.final_refresh_rate, final_refresh_rate)) {\n            BOOST_LOG(error) << \"Failed to parse entry value: \"\n                             << \"[\" << entry.received_resolution << \"|\" << entry.received_fps << \"] -> [\" << entry.final_resolution << \"|\" << entry.final_refresh_rate << \"]\";\n            return false;\n          }\n\n          if ((!received_resolution && !received_fps) || (!final_resolution && !final_refresh_rate)) {\n            BOOST_LOG(error) << \"At least one received and final value must be set for remapping display modes! Entry: \"\n                             << \"[\" << entry.received_resolution << \"|\" << entry.received_fps << \"] -> [\" << entry.final_resolution << \"|\" << entry.final_refresh_rate << \"]\";\n            return false;\n          }\n\n          if (!session.enable_sops && (received_resolution || final_resolution)) {\n            BOOST_LOG(warning) << \"Skipping remapping entry, because the \\\"Optimize game settings\\\" is not set in the client! Entry: \"\n                               << \"[\" << entry.received_resolution << \"|\" << entry.received_fps << \"] -> [\" << entry.final_resolution << \"|\" << entry.final_refresh_rate << \"]\";\n            continue;\n          }\n        }\n\n        parsed_values.push_back({ received_resolution, received_fps, final_resolution, final_refresh_rate });\n      }\n\n      const auto compare_resolution { [](const resolution_t &a, const resolution_t &b) {\n        return a.width == b.width && a.height == b.height;\n      } };\n      const auto compare_refresh_rate { [](const refresh_rate_t &a, const refresh_rate_t &b) {\n        return a.numerator == b.numerator && a.denominator == b.denominator;\n      } };\n\n      for (const auto &entry : parsed_values) {\n        bool do_remap { false };\n        if (entry.received_resolution && entry.received_fps) {\n          if (parsed_config.resolution && parsed_config.refresh_rate) {\n            do_remap = compare_resolution(*entry.received_resolution, *parsed_config.resolution) && compare_refresh_rate(*entry.received_fps, *parsed_config.refresh_rate);\n          }\n          else {\n            // Sanity check\n            BOOST_LOG(error) << \"Cannot remap: (parsed_config.resolution && parsed_config.refresh_rate) == false!\";\n            return false;\n          }\n        }\n        else if (entry.received_resolution) {\n          if (parsed_config.resolution) {\n            do_remap = compare_resolution(*entry.received_resolution, *parsed_config.resolution);\n          }\n          else {\n            // Sanity check\n            BOOST_LOG(error) << \"Cannot remap: parsed_config.resolution == false!\";\n            return false;\n          }\n        }\n        else if (entry.received_fps) {\n          if (parsed_config.refresh_rate) {\n            do_remap = compare_refresh_rate(*entry.received_fps, *parsed_config.refresh_rate);\n          }\n          else {\n            // Sanity check\n            BOOST_LOG(error) << \"Cannot remap: parsed_config.refresh_rate == false!\";\n            return false;\n          }\n        }\n        else {\n          // Sanity check\n          BOOST_LOG(error) << \"Cannot remap: (entry.received_resolution || entry.received_fps) == false!\";\n          return false;\n        }\n\n        if (do_remap) {\n          if (!entry.final_resolution && !entry.final_refresh_rate) {\n            // Sanity check\n            BOOST_LOG(error) << \"Cannot remap: (!entry.final_resolution && !entry.final_refresh_rate) == true!\";\n            return false;\n          }\n\n          if (entry.final_resolution) {\n            BOOST_LOG(debug) << \"Remapping resolution to: \" << to_string(*entry.final_resolution);\n            parsed_config.resolution = entry.final_resolution;\n          }\n          if (entry.final_refresh_rate) {\n            BOOST_LOG(debug) << \"Remapping refresh rate to: \" << to_string(*entry.final_refresh_rate);\n            parsed_config.refresh_rate = entry.final_refresh_rate;\n          }\n\n          break;\n        }\n      }\n\n      return true;\n    }\n\n    /**\n     * @brief Parse HDR option from the user configuration and the session information.\n     * @param config User's video related configuration.\n     * @param session Session information.\n     * @returns Parsed HDR state value we need to switch to (true == ON, false == OFF).\n     *          Empty optional if no action is required.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const std::shared_ptr<rtsp_stream::launch_session_t> launch_session; // Assuming ptr is properly initialized\n     * const config::video_t &video_config { config::video };\n     * const auto hdr_option = parse_hdr_option(video_config, *launch_session);\n     * ```\n     */\n    boost::optional<bool>\n    parse_hdr_option(const config::video_t &config, const rtsp_stream::launch_session_t &session) {\n      const auto hdr_prep_option { static_cast<parsed_config_t::hdr_prep_e>(config.hdr_prep) };\n      switch (hdr_prep_option) {\n        case parsed_config_t::hdr_prep_e::automatic:\n          return session.enable_hdr;\n        case parsed_config_t::hdr_prep_e::no_operation:\n        default:\n          return boost::none;\n      }\n    }\n    /**\n     * @brief Parse a numeric string as an enum index with range validation.\n     * @param value String to parse (e.g. \"1\", \"2\").\n     * @param max_val Maximum valid enum value (inclusive).\n     * @param default_val Value to return on parse failure or out-of-range.\n     * @returns Parsed integer if valid and in [0, max_val], otherwise default_val.\n     *\n     * Used as fallback when config stores enum values as numeric strings\n     * instead of named strings (e.g. \"1\" instead of \"automatic\").\n     */\n    int\n    numeric_enum_fallback(std::string_view value, int max_val, int default_val) {\n      int n = 0;\n      auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), n);\n      if (ec == std::errc{} && ptr == value.data() + value.size() && n >= 0 && n <= max_val) {\n        return n;\n      }\n      return default_val;\n    }\n  }  // namespace\n\n  int\n  parsed_config_t::device_prep_from_view(std::string_view value) {\n    using namespace std::string_view_literals;\n#define _CONVERT_(x) \\\n  if (value == #x##sv) return static_cast<int>(parsed_config_t::device_prep_e::x);\n    _CONVERT_(no_operation);\n    _CONVERT_(ensure_active);\n    _CONVERT_(ensure_primary);\n    _CONVERT_(ensure_only_display);\n    _CONVERT_(ensure_secondary);\n#undef _CONVERT_\n    return numeric_enum_fallback(value, 4, static_cast<int>(parsed_config_t::device_prep_e::no_operation));\n  }\n\n  int\n  parsed_config_t::resolution_change_from_view(std::string_view value) {\n    using namespace std::string_view_literals;\n#define _CONVERT_(x) \\\n  if (value == #x##sv) return static_cast<int>(parsed_config_t::resolution_change_e::x);\n    _CONVERT_(no_operation);\n    _CONVERT_(automatic);\n    _CONVERT_(manual);\n#undef _CONVERT_\n    return numeric_enum_fallback(value, 2, static_cast<int>(parsed_config_t::resolution_change_e::no_operation));\n  }\n\n  int\n  parsed_config_t::refresh_rate_change_from_view(std::string_view value) {\n    using namespace std::string_view_literals;\n#define _CONVERT_(x) \\\n  if (value == #x##sv) return static_cast<int>(parsed_config_t::refresh_rate_change_e::x);\n    _CONVERT_(no_operation);\n    _CONVERT_(automatic);\n    _CONVERT_(manual);\n#undef _CONVERT_\n    return numeric_enum_fallback(value, 2, static_cast<int>(parsed_config_t::refresh_rate_change_e::no_operation));\n  }\n\n  int\n  parsed_config_t::hdr_prep_from_view(std::string_view value) {\n    using namespace std::string_view_literals;\n#define _CONVERT_(x) \\\n  if (value == #x##sv) return static_cast<int>(parsed_config_t::hdr_prep_e::x);\n    _CONVERT_(no_operation);\n    _CONVERT_(automatic);\n#undef _CONVERT_\n    return numeric_enum_fallback(value, 1, static_cast<int>(parsed_config_t::hdr_prep_e::no_operation));\n  }\n\n  int\n  parsed_config_t::vdd_prep_from_view(std::string_view value) {\n    using namespace std::string_view_literals;\n#define _CONVERT_(x) \\\n  if (value == #x##sv) return static_cast<int>(parsed_config_t::vdd_prep_e::x);\n    _CONVERT_(no_operation);\n    _CONVERT_(vdd_as_primary);\n    _CONVERT_(vdd_as_secondary);\n    _CONVERT_(display_off);\n#undef _CONVERT_\n    return numeric_enum_fallback(value, 3, static_cast<int>(parsed_config_t::vdd_prep_e::no_operation));\n  }\n\n  parsed_config_t::vdd_prep_e\n  parsed_config_t::to_vdd_prep(device_prep_e unified) {\n    switch (unified) {\n      case device_prep_e::no_operation:\n        return vdd_prep_e::no_operation;\n      case device_prep_e::ensure_active:\n        return vdd_prep_e::no_operation;  // VDD is always active when created\n      case device_prep_e::ensure_primary:\n        return vdd_prep_e::vdd_as_primary;\n      case device_prep_e::ensure_secondary:\n        return vdd_prep_e::vdd_as_secondary;\n      case device_prep_e::ensure_only_display:\n        return vdd_prep_e::display_off;\n      default:\n        return vdd_prep_e::no_operation;\n    }\n  }\n\n  parsed_config_t::device_prep_e\n  parsed_config_t::to_physical_device_prep(device_prep_e unified) {\n    switch (unified) {\n      case device_prep_e::ensure_secondary:\n        return device_prep_e::ensure_active;  // In physical mode, activate as secondary\n      default:\n        return unified;  // All other values map 1:1\n    }\n  }\n\n  boost::optional<parsed_config_t>\n  make_parsed_config(const config::video_t &config, const rtsp_stream::launch_session_t &session, bool is_reconfigure) {\n    parsed_config_t parsed_config;\n    \n    // 优先使用客户端指定的显示器名称，如果没有则使用全局配置\n    std::string device_id_to_use = config.output_name;\n    if (auto it = session.env.find(\"SUNSHINE_CLIENT_DISPLAY_NAME\"); it != session.env.end()) {\n      const std::string client_display_name = it->to_string();\n      if (!client_display_name.empty()) {\n        device_id_to_use = client_display_name;\n        BOOST_LOG(debug) << \"使用客户端指定的显示器: \" << device_id_to_use;\n      }\n    }\n    \n    parsed_config.device_id = device_id_to_use;\n    parsed_config.device_prep = static_cast<parsed_config_t::device_prep_e>(config.display_device_prep);\n    parsed_config.change_hdr_state = parse_hdr_option(config, session);\n\n    const int custom_screen_mode = session.custom_screen_mode;\n    \n    // 客户端自定义屏幕模式（统一覆盖 display_device_prep）\n    if (custom_screen_mode != -1) {\n      BOOST_LOG(debug) << \"客户端自定义屏幕模式: \"sv << custom_screen_mode;\n      if (custom_screen_mode == static_cast<int>(parsed_config_t::device_prep_e::no_operation)) {\n        parsed_config.device_prep = parsed_config_t::device_prep_e::no_operation;\n      }\n      else if (custom_screen_mode == static_cast<int>(parsed_config_t::device_prep_e::ensure_active)) {\n        parsed_config.device_prep = parsed_config_t::device_prep_e::ensure_active;\n      }\n      else if (custom_screen_mode == static_cast<int>(parsed_config_t::device_prep_e::ensure_primary)) {\n        parsed_config.device_prep = parsed_config_t::device_prep_e::ensure_primary;\n      }\n      else if (custom_screen_mode == static_cast<int>(parsed_config_t::device_prep_e::ensure_only_display)) {\n        parsed_config.device_prep = parsed_config_t::device_prep_e::ensure_only_display;\n      }\n      else if (custom_screen_mode == static_cast<int>(parsed_config_t::device_prep_e::ensure_secondary)) {\n        parsed_config.device_prep = parsed_config_t::device_prep_e::ensure_secondary;\n      }\n    }\n\n    // 解析分辨率和刷新率配置\n    if (!parse_resolution_option(config, session, parsed_config) ||\n        !parse_refresh_rate_option(config, session, parsed_config) ||\n        !remap_display_modes_if_needed(config, session, parsed_config)) {\n      // 任何一步失败都返回空值\n      return boost::none;\n    }\n\n    // 记录解析后的配置信息\n    BOOST_LOG(debug) << \"解析后的显示设备配置:\"sv\n                     << \"\\n设备ID: \"sv << parsed_config.device_id\n                     << \"\\n设备准备模式: \"sv << static_cast<int>(parsed_config.device_prep)\n                     << \"\\nHDR状态: \"sv << (parsed_config.change_hdr_state ? (*parsed_config.change_hdr_state ? \"启用\" : \"禁用\") : \"不变\")\n                     << \"\\n分辨率: \"sv << (parsed_config.resolution ? to_string(*parsed_config.resolution) : \"不变\")\n                     << \"\\n刷新率: \"sv << (parsed_config.refresh_rate ? to_string(*parsed_config.refresh_rate) : \"不变\")\n                     << \"\\n\"sv;\n\n    // 检查是否需要使用VDD\n    const auto requested_device_id = display_device::find_one_of_the_available_devices(parsed_config.device_id);\n    const bool is_vdd_device = (display_device::get_display_friendly_name(parsed_config.device_id) == ZAKO_NAME);\n    const bool needs_vdd = session.use_vdd || requested_device_id.empty() || is_vdd_device;\n\n    // 不需要VDD时，使用物理模式映射\n    if (!needs_vdd) {\n      BOOST_LOG(debug) << \"输出设备已存在，跳过VDD准备\"sv;\n      parsed_config.use_vdd = false;\n      parsed_config.device_prep = parsed_config_t::to_physical_device_prep(parsed_config.device_prep);\n      parsed_config.vdd_prep = parsed_config_t::vdd_prep_e::no_operation;\n      return parsed_config;\n    }\n\n    // 标记为VDD模式，从统一的 device_prep 映射到内部 vdd_prep\n    // device_prep 保留原始统一值（用于 apply_config 中的 display_may_change 等判断）\n    parsed_config.use_vdd = true;\n    parsed_config.vdd_prep = parsed_config_t::to_vdd_prep(parsed_config.device_prep);\n    BOOST_LOG(debug) << \"VDD模式：统一值 \" << static_cast<int>(parsed_config.device_prep)\n                     << \" 映射为 vdd_prep=\" << static_cast<int>(parsed_config.vdd_prep);\n\n    // 不是SYSTEM权限且处于RDP中，强制使用RDP虚拟显示器，不创建VDD\n    if (!is_running_as_system_user && display_device::w_utils::is_any_rdp_session_active()) {\n      BOOST_LOG(info) << \"[Display] RDP环境：强制使用RDP虚拟显示器，跳过VDD准备\"sv;\n      return parsed_config;\n    }\n\n    // 准备VDD设备\n    display_device::session_t::get().prepare_vdd(parsed_config, session);\n\n    return parsed_config;\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/display_device/parsed_config.h",
    "content": "#pragma once\n\n// local includes\n#include \"display_device.h\"\n\n// forward declarations\nnamespace config {\n  struct video_t;\n}\nnamespace rtsp_stream {\n  struct launch_session_t;\n}\n\nnamespace display_device {\n\n  /**\n   * @brief Configuration containing parsed information from the user config (video related)\n   *        and the current session.\n   */\n  struct parsed_config_t {\n    /**\n     * @brief Enum detailing how to prepare the display device.\n     */\n    enum class device_prep_e : int {\n      no_operation, /**< User has to make sure the display device is active, we will only verify. */\n      ensure_active, /**< Activate the device if needed. */\n      ensure_primary, /**< Activate the device if needed and make it a primary display. */\n      ensure_only_display, /**< Deactivate other displays and turn on the specified one only. */\n      ensure_secondary /**< Stream on the display as a secondary (extended) display. In VDD mode, physical stays primary+ VDD secondary. */\n    };\n\n    /**\n     * @brief Convert the string to the matching value of device_prep_e.\n     * @param value String value to map to device_prep_e.\n     * @returns A device_prep_e value (converted to int) that matches the string\n     *          or the default value if string does not match anything.\n     * @see device_prep_e\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const int device_prep = device_prep_from_view(\"ensure_only_display\");\n     * ```\n     */\n    static int\n    device_prep_from_view(std::string_view value);\n\n    /**\n     * @brief Enum detailing how to change the display's resolution.\n     */\n    enum class resolution_change_e : int {\n      no_operation, /**< Keep the current resolution. */\n      automatic, /**< Set the resolution to the one received from the client if the \"Optimize game settings\" option is also enabled in the client. */\n      manual /**< User has to specify the resolution (\"Optimize game settings\" option must be enabled in the client). */\n    };\n\n    /**\n     * @brief Convert the string to the matching value of resolution_change_e.\n     * @param value String value to map to resolution_change_e.\n     * @returns A resolution_change_e value (converted to int) that matches the string\n     *          or the default value if string does not match anything.\n     * @see resolution_change_e\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const int resolution_change = resolution_change_from_view(\"manual\");\n     * ```\n     */\n    static int\n    resolution_change_from_view(std::string_view value);\n\n    /**\n     * @brief Enum detailing how to change the display's refresh rate.\n     */\n    enum class refresh_rate_change_e : int {\n      no_operation, /**< Keep the current refresh rate. */\n      automatic, /**< Set the refresh rate to the FPS value received from the client. */\n      manual /**< User has to specify the refresh rate. */\n    };\n\n    /**\n     * @brief Convert the string to the matching value of refresh_rate_change_e.\n     * @param value String value to map to refresh_rate_change_e.\n     * @returns A refresh_rate_change_e value (converted to int) that matches the string\n     *          or the default value if string does not match anything.\n     * @see refresh_rate_change_e\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const int refresh_rate_change = refresh_rate_change_from_view(\"manual\");\n     * ```\n     */\n    static int\n    refresh_rate_change_from_view(std::string_view value);\n\n    /**\n     * @brief Enum detailing how to change the display's HDR state.\n     */\n    enum class hdr_prep_e : int {\n      no_operation, /**< User has to switch the HDR state manually */\n      automatic /**< Switch HDR state based on the session settings and if display supports it. */\n    };\n\n    /**\n     * @brief Convert the string to the matching value of hdr_prep_e.\n     * @param value String value to map to hdr_prep_e.\n     * @returns A hdr_prep_e value (converted to int) that matches the string\n     *          or the default value if string does not match anything.\n     * @see hdr_prep_e\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const int hdr_prep = hdr_prep_from_view(\"automatic\");\n     * ```\n     */\n    static int\n    hdr_prep_from_view(std::string_view value);\n\n    /**\n     * @brief Enum detailing how to prepare physical displays when using VDD (Virtual Display Device).\n     * @note In VDD mode, topology changes are handled by Windows automatically when displays are added/removed,\n     *       so we don't save/restore topology state - just modify it as requested.\n     */\n    enum class vdd_prep_e : int {\n      no_operation, /**< Do nothing to physical displays. */\n      vdd_as_primary, /**< VDD as primary display, physical displays as secondary (extend mode). */\n      vdd_as_secondary, /**< Physical displays as primary, VDD as secondary (extend mode). */\n      display_off /**< Turn off physical displays, only VDD remains active. */\n    };\n\n    /**\n     * @brief Convert the string to the matching value of vdd_prep_e.\n     * @param value String value to map to vdd_prep_e.\n     * @returns A vdd_prep_e value (converted to int) that matches the string\n     *          or the default value if string does not match anything.\n     * @see vdd_prep_e\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const int vdd_prep = vdd_prep_from_view(\"display_off\");\n     * ```\n     */\n    static int\n    vdd_prep_from_view(std::string_view value);\n\n    /**\n     * @brief Map unified device_prep_e to internal vdd_prep_e for VDD mode.\n     */\n    static vdd_prep_e\n    to_vdd_prep(device_prep_e unified);\n\n    /**\n     * @brief Map unified device_prep_e to internal device_prep_e for physical display mode.\n     */\n    static device_prep_e\n    to_physical_device_prep(device_prep_e unified);\n\n    std::string device_id; /**< Device id manually provided by the user via config. */\n    device_prep_e device_prep; /**< The device_prep_e value taken from config. */\n    vdd_prep_e vdd_prep; /**< The vdd_prep_e value mapped from device_prep for VDD mode. */\n    boost::optional<resolution_t> resolution; /**< Parsed resolution value we need to switch to. Empty optional if no action is required. */\n    boost::optional<refresh_rate_t> refresh_rate; /**< Parsed refresh rate value we need to switch to. Empty optional if no action is required. */\n    boost::optional<bool> change_hdr_state; /**< Parsed HDR state value we need to switch to (true == ON, false == OFF). Empty optional if no action is required. */\n    boost::optional<bool> use_vdd; /**< Parsed VDD state value we need to switch to (true == ON, false == OFF). */\n  };\n\n  /**\n   * @brief Parse the user configuration and the session information.\n   * @param config User's video related configuration.\n   * @param session Session information.\n   * @returns Parsed configuration or empty optional if parsing has failed.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::shared_ptr<rtsp_stream::launch_session_t> launch_session; // Assuming ptr is properly initialized\n   * const config::video_t &video_config { config::video };\n   * const auto parsed_config = make_parsed_config(video_config, *launch_session);\n   * ```\n   */\n  boost::optional<parsed_config_t>\n  make_parsed_config(const config::video_t &config, const rtsp_stream::launch_session_t &session, bool is_reconfigure);\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/display_device/session.cpp",
    "content": "// standard includes\n#include <boost/optional/optional_io.hpp>\n#include <boost/process/v1.hpp>\n#include <future>\n#include <thread>\n\n// local includes\n#include \"session.h\"\n#include \"src/confighttp.h\"\n#include \"src/globals.h\"\n#include \"src/platform/common.h\"\n#include \"src/platform/windows/display_device/session_listener.h\"\n#include \"src/platform/windows/display_device/windows_utils.h\"\n#include \"src/rtsp.h\"\n#include \"to_string.h\"\n#include \"vdd_utils.h\"\n\nnamespace display_device {\n\n  class session_t::StateRetryTimer {\n  public:\n    /**\n     * @brief A constructor for the timer.\n     * @param mutex A shared mutex for synchronization.\n     * @warning Because we are keeping references to shared parameters, we MUST ensure they outlive this object!\n     */\n    StateRetryTimer(std::mutex &mutex, std::chrono::seconds timeout = std::chrono::seconds { 5 }):\n        mutex { mutex }, timeout_duration { timeout }, timer_thread {\n          std::thread { [this]() {\n            std::unique_lock<std::mutex> lock { this->mutex };\n            while (keep_alive) {\n              can_wake_up = false;\n              if (next_wake_up_time) {\n                // We're going to sleep forever until manually woken up or the time elapses\n                sleep_cv.wait_until(lock, *next_wake_up_time, [this]() { return can_wake_up; });\n              }\n              else {\n                // We're going to sleep forever until manually woken up\n                sleep_cv.wait(lock, [this]() { return can_wake_up; });\n              }\n\n              if (next_wake_up_time) {\n                // Timer has just been started, or we have waited for the required amount of time.\n                // We can check which case it is by comparing time points.\n\n                const auto now { std::chrono::steady_clock::now() };\n                if (now < *next_wake_up_time) {\n                  // Thread has been woken up manually to synchronize the time points.\n                  // We do nothing and just go back to waiting with a new time point.\n                }\n                else {\n                  next_wake_up_time = boost::none;\n\n                  const auto result { !this->retry_function || this->retry_function() };\n                  if (!result) {\n                    next_wake_up_time = now + this->timeout_duration;\n                  }\n                }\n              }\n              else {\n                // Timer has been stopped.\n                // We do nothing and just go back to waiting until notified (unless we are killing the thread).\n              }\n            }\n          } }\n        } {\n    }\n\n    /**\n     * @brief A destructor for the timer that gracefully shuts down the thread.\n     */\n    ~StateRetryTimer() {\n      {\n        std::lock_guard lock { mutex };\n        keep_alive = false;\n        next_wake_up_time = boost::none;\n        wake_up_thread();\n      }\n\n      timer_thread.join();\n    }\n\n    /**\n     * @brief Start or stop the timer thread.\n     * @param retry_function Function to be executed every X seconds.\n     *                       If the function returns true, the loop is stopped.\n     *                       If the function is of type nullptr_t, the loop is stopped.\n     * @warning This method does NOT acquire the mutex! It is intended to be used from places\n     *          where the mutex has already been locked.\n     */\n    void\n    setup_timer(std::function<bool()> retry_function) {\n      this->retry_function = std::move(retry_function);\n\n      if (this->retry_function) {\n        next_wake_up_time = std::chrono::steady_clock::now() + timeout_duration;\n      }\n      else {\n        if (!next_wake_up_time) {\n          return;\n        }\n\n        next_wake_up_time = boost::none;\n      }\n\n      wake_up_thread();\n    }\n\n  private:\n    /**\n     * @brief Manually wake up the thread.\n     */\n    void\n    wake_up_thread() {\n      can_wake_up = true;\n      sleep_cv.notify_one();\n    }\n\n    std::mutex &mutex; /**< A reference to a shared mutex. */\n    std::chrono::seconds timeout_duration { 5 }; /**< A retry time for the timer. */\n    std::function<bool()> retry_function; /**< Function to be executed until it succeeds. */\n\n    std::thread timer_thread; /**< A timer thread. */\n    std::condition_variable sleep_cv; /**< Condition variable for waking up thread. */\n\n    bool can_wake_up { false }; /**< Safeguard for the condition variable to prevent sporadic thread wake ups. */\n    bool keep_alive { true }; /**< A kill switch for the thread when it has been woken up. */\n    boost::optional<std::chrono::steady_clock::time_point> next_wake_up_time; /**< Next time point for thread to wake up. */\n  };\n\n  session_t::deinit_t::~deinit_t() {\n    // 清理事件监听器\n    SessionEventListener::deinit();\n    \n    // 兜底：退出时如果 VDD 仍存在且 vdd_keep_enabled=false，直接销毁\n    // 使用 nolog 版本，因为析构时 boost::log 可能已被销毁\n    if (!config::video.vdd_keep_enabled) {\n      vdd_utils::destroy_vdd_monitor_nolog();\n    }\n  }\n\n  session_t &\n  session_t::get() {\n    static session_t session;\n    return session;\n  }\n\n  std::unique_ptr<session_t::deinit_t>\n  session_t::init() {\n    session_t::get().settings.set_filepath(platf::appdata() / \"original_display_settings.json\");\n    \n    // 初始化会话事件监听器（用于检测解锁事件）\n    SessionEventListener::init();\n    \n    session_t::get().restore_state();\n    return std::make_unique<deinit_t>();\n  }\n\n  void\n  session_t::clear_vdd_state() {\n    last_vdd_setting.clear();\n    current_device_prep.reset();\n    current_vdd_prep.reset();\n    current_use_vdd.reset();\n    // 恢复原始的 output_name，避免下一个会话使用已销毁的 VDD 设备 ID\n    if (!original_output_name.empty()) {\n      config::video.output_name = original_output_name;\n      original_output_name.clear();\n      BOOST_LOG(debug) << \"已恢复原始 output_name: \" << config::video.output_name;\n    }\n  }\n\n  void\n  session_t::stop_timer_and_clear_vdd_state() {\n    timer->setup_timer(nullptr);\n    clear_vdd_state();\n  }\n\n  namespace {\n    /**\n     * @brief Get client identifier from session.\n     * @details Prioritizes client certificate UUID (stored in env) over client_name as it is more stable.\n     * @param session The launch session containing client information.\n     * @return Client identifier string, or empty string if not available.\n     */\n    std::string\n    get_client_id_from_session(const rtsp_stream::launch_session_t &session) {\n      if (auto cert_uuid_it = session.env.find(\"SUNSHINE_CLIENT_CERT_UUID\");\n        cert_uuid_it != session.env.end()) {\n        if (std::string cert_uuid = cert_uuid_it->to_string(); !cert_uuid.empty()) {\n          return cert_uuid;\n        }\n      }\n\n      if (!session.client_name.empty() && session.client_name != \"unknown\") {\n        return session.client_name;\n      }\n\n      return {};\n    }\n\n    /**\n     * @brief Wait for VDD device to be available (active or inactive).\n     * @param device_zako Output parameter for the device ID.\n     * @param max_attempts Maximum number of retry attempts.\n     * @param initial_delay Initial delay between retries.\n     * @param max_delay Maximum delay between retries.\n     * @return true if device was found (active or inactive), false otherwise.\n     */\n    bool\n    wait_for_vdd_device(std::string &device_zako, int max_attempts,\n      std::chrono::milliseconds initial_delay,\n      std::chrono::milliseconds max_delay) {\n      return vdd_utils::retry_with_backoff(\n        [&device_zako]() {\n          device_zako = display_device::find_device_by_friendlyname(ZAKO_NAME);\n          if (device_zako.empty()) {\n            BOOST_LOG(debug) << \"VDD device not found by friendly name\";\n            return false;\n          }\n\n          // Device found by friendly name - that's all we need\n          // It can be activated later during display configuration\n          BOOST_LOG(debug) << \"VDD device found: \" << device_zako;\n          return true;\n        },\n        { .max_attempts = max_attempts,\n          .initial_delay = initial_delay,\n          .max_delay = max_delay,\n          .context = \"Waiting for VDD device availability\" });\n    }\n\n    /**\n     * @brief Attempt to recover VDD device with retries.\n     * @param client_id Client identifier for the VDD monitor.\n     * @param client_name Client name for getting physical size from config.\n     * @param hdr_brightness hdr_brightness_t.\n     * @param device_zako Output parameter for the device ID.\n     * @return true if recovery succeeded, false otherwise.\n     */\n    bool\n    try_recover_vdd_device(const std::string &client_id, const std::string &client_name, const vdd_utils::hdr_brightness_t &hdr_brightness, std::string &device_zako) {\n      constexpr int max_retries = 3;\n      const vdd_utils::physical_size_t physical_size = vdd_utils::get_client_physical_size(client_name);\n\n      // 复用模式使用固定标识符，否则使用客户端ID\n      const std::string vdd_identifier = config::video.vdd_reuse\n        ? \"shared_vdd\"\n        : client_id;\n\n      for (int retry = 1; retry <= max_retries; ++retry) {\n        BOOST_LOG(info) << \"正在执行第\" << retry << \"次VDD恢复尝试...\";\n\n        if (!vdd_utils::create_vdd_monitor(vdd_identifier, hdr_brightness, physical_size)) {\n          BOOST_LOG(error) << \"创建虚拟显示器失败，尝试\" << retry << \"/\" << max_retries;\n          if (retry < max_retries) {\n            std::this_thread::sleep_for(std::chrono::seconds(1 << retry));\n          }\n          continue;\n        }\n\n        if (wait_for_vdd_device(device_zako, 5, 233ms, 2000ms)) {\n          BOOST_LOG(info) << \"VDD设备恢复成功！\";\n          return true;\n        }\n\n        BOOST_LOG(error) << \"VDD设备检测失败，正在第\" << retry << \"/\" << max_retries << \"次重试...\";\n        if (retry < max_retries) {\n          std::this_thread::sleep_for(std::chrono::seconds(1 << retry));\n        }\n      }\n\n      return false;\n    }\n  }  // namespace\n\n  void\n  session_t::configure_display(const config::video_t &config,\n    const rtsp_stream::launch_session_t &session,\n    bool is_reconfigure) {\n    std::lock_guard lock { mutex };\n\n    // Clean up VDD state if this is a new session with a different client\n    if (!is_reconfigure) {\n      if (const std::string new_client_id = get_client_id_from_session(session);\n        !current_vdd_client_id.empty() && !new_client_id.empty() &&\n        current_vdd_client_id != new_client_id) {\n        BOOST_LOG(info) << \"New session detected with different client ID, cleaning up VDD state\";\n        // Cancel any pending restore from the old session before it can interfere\n        pending_restore_ = false;\n        SessionEventListener::clear_unlock_task();\n        stop_timer_and_clear_vdd_state();\n      }\n    }\n\n    // 在 make_parsed_config 之前保存真实的初始拓扑\n    // 因为 make_parsed_config 内部会调用 prepare_vdd，它会创建VDD并切换到扩展模式，导致原有显示器变成inactive\n    boost::optional<active_topology_t> pre_saved_initial_topology;\n    \n    // 检查是否会使用VDD\n    std::string device_id_to_use = config.output_name;\n    if (auto it = session.env.find(\"SUNSHINE_CLIENT_DISPLAY_NAME\"); it != session.env.end()) {\n      const std::string client_display_name = it->to_string();\n      if (!client_display_name.empty()) {\n        device_id_to_use = client_display_name;\n      }\n    }\n    \n    // 检查VDD是否已存在\n    const auto existing_vdd_id = display_device::find_device_by_friendlyname(ZAKO_NAME);\n    const bool vdd_already_exists = !existing_vdd_id.empty();\n    \n    // 如果会使用VDD且VDD当前不存在，在创建前保存拓扑\n    // 如果VDD已存在，说明拓扑已被破坏，不应该保存当前拓扑\n    const auto requested_device_id = display_device::find_one_of_the_available_devices(device_id_to_use);\n    const bool is_vdd_device = (display_device::get_display_friendly_name(device_id_to_use) == ZAKO_NAME);\n    \n    const bool needs_vdd = session.use_vdd || requested_device_id.empty() || is_vdd_device;\n    \n    // - 如果不需要 VDD：跳过 VDD 相关逻辑\n    // - 如果不是 SYSTEM 权限且处于 RDP 中：使用 RDP 虚拟显示器，不创建 VDD\n    // - 其他情况（包括 SYSTEM 权限）：准备 VDD 设备\n    const bool is_rdp_blocking_vdd = !is_running_as_system_user && display_device::w_utils::is_any_rdp_session_active();\n    const bool will_use_vdd = needs_vdd && !is_rdp_blocking_vdd;\n\n    if (will_use_vdd && !vdd_already_exists) {\n\n      // 如果有待恢复的设置，保留旧的初始拓扑，不要覆盖\n      if (pending_restore_ && settings.has_persistent_data()) {\n        BOOST_LOG(info) << \"有待恢复的设置，保留原有初始拓扑\";\n        // 取消待恢复标志，因为新串流要开始了\n        pending_restore_ = false;\n        SessionEventListener::clear_unlock_task();\n        timer->setup_timer(nullptr);\n        // 不设置 pre_saved_initial_topology，让 apply_config 复用已有的\n      }\n      else {\n        pre_saved_initial_topology = get_current_topology();\n        BOOST_LOG(debug) << \"Pre-saved initial topology before VDD creation: \" << to_string(*pre_saved_initial_topology);\n      }\n    }\n    else if (will_use_vdd && vdd_already_exists) {\n      if (pending_restore_ && settings.has_persistent_data()) {\n        // 有待恢复的设置且 VDD 仍存在（CCD 曾失败），保留原有初始拓扑\n        BOOST_LOG(info) << \"有待恢复的设置且 VDD 仍存在，保留原有初始拓扑\";\n        pending_restore_ = false;\n        SessionEventListener::clear_unlock_task();\n        timer->setup_timer(nullptr);\n      }\n      else {\n        BOOST_LOG(debug) << \"VDD already exists, skipping initial topology save (topology may be corrupted)\";\n      }\n    }\n\n    const auto parsed_config = make_parsed_config(config, session, is_reconfigure);\n    if (!parsed_config) {\n      BOOST_LOG(error) << \"Failed to parse configuration for the display device settings!\";\n      return;\n    }\n\n    // 保存当前会话的配置模式（可能包含客户端的override）\n    current_device_prep = parsed_config->device_prep;\n    current_vdd_prep = parsed_config->vdd_prep;\n    current_use_vdd = parsed_config->use_vdd;\n\n    if (settings.is_changing_settings_going_to_fail()) {\n      timer->setup_timer([this, config_copy = *parsed_config, &session, pre_saved_initial_topology]() {\n        if (settings.is_changing_settings_going_to_fail()) {\n          BOOST_LOG(warning) << \"Applying display settings will fail - retrying later...\";\n          return false;\n        }\n\n        if (!settings.apply_config(config_copy, session, pre_saved_initial_topology)) {\n          BOOST_LOG(warning) << \"Failed to apply display settings - will stop trying, but will allow stream to continue.\";\n          // WARNING! After call to the method below, this lambda function is no longer valid!\n          // DO NOT access anything from the capture list!\n          restore_state_impl(revert_reason_e::config_cleanup);\n        }\n        return true;\n      });\n\n      BOOST_LOG(warning) << \"It is already known that display settings cannot be changed. Allowing stream to start without changing the settings, but will retry changing settings later...\";\n      return;\n    }\n\n    if (settings.apply_config(*parsed_config, session, pre_saved_initial_topology)) {\n      timer->setup_timer(nullptr);\n    }\n    else {\n      restore_state_impl(revert_reason_e::config_cleanup);\n    }\n  }\n\n  bool\n  session_t::create_vdd_monitor(const std::string &client_name) {\n    const vdd_utils::physical_size_t physical_size = vdd_utils::get_client_physical_size(client_name);\n    // 复用模式使用固定标识符，否则使用客户端名称\n    const std::string vdd_identifier = config::video.vdd_reuse\n      ? \"shared_vdd\"\n      : client_name;\n    return vdd_utils::create_vdd_monitor(vdd_identifier, vdd_utils::hdr_brightness_t { 1000.0f, 0.001f, 1000.0f }, physical_size);\n  }\n\n  bool\n  session_t::destroy_vdd_monitor() {\n    current_vdd_client_id.clear();\n    return vdd_utils::destroy_vdd_monitor();\n  }\n\n  bool\n  session_t::is_display_on() {\n    return vdd_utils::is_display_on();\n  }\n\n  bool\n  session_t::toggle_display_power() {\n    return vdd_utils::toggle_display_power();\n  }\n\n  void\n  session_t::update_vdd_resolution(const parsed_config_t &config,\n    const vdd_utils::VddSettings &vdd_settings) {\n    const auto new_setting = to_string(*config.resolution) + \"@\" + to_string(*config.refresh_rate);\n\n    if (last_vdd_setting == new_setting) {\n      BOOST_LOG(debug) << \"VDD配置未变更: \" << new_setting;\n      return;\n    }\n\n    if (!confighttp::saveVddSettings(vdd_settings.resolutions, vdd_settings.fps, config::video.adapter_name)) {\n      BOOST_LOG(error) << \"VDD配置保存失败 [resolutions: \" << vdd_settings.resolutions\n                       << \" fps: \" << vdd_settings.fps << \"]\";\n      return;\n    }\n\n    last_vdd_setting = new_setting;\n    BOOST_LOG(info) << \"VDD配置更新完成: \" << new_setting;\n\n    BOOST_LOG(info) << \"重新加载VDD驱动...\";\n    vdd_utils::reload_driver();\n    std::this_thread::sleep_for(1200ms);\n  }\n\n  void\n  session_t::prepare_vdd(parsed_config_t &config, const rtsp_stream::launch_session_t &session) {\n    const std::string current_client_id = get_client_id_from_session(session);\n    const vdd_utils::hdr_brightness_t hdr_brightness { session.max_nits, session.min_nits, session.max_full_nits };\n    const vdd_utils::physical_size_t physical_size = vdd_utils::get_client_physical_size(session.client_name);\n\n    auto device_zako = display_device::find_device_by_friendlyname(ZAKO_NAME);\n\n    // pre_vdd_devices: 在 VDD 创建前一刻保存的物理显示器快照\n    // 延迟到 VDD 创建前才捕获，确保无论是新建还是重建都能拿到正确状态\n    device_info_map_t pre_vdd_devices;\n\n    // Rebuild VDD device on client switch\n    if (!device_zako.empty() && !current_vdd_client_id.empty() &&\n        !current_client_id.empty() && current_vdd_client_id != current_client_id) {\n      \n      // 是否复用VDD（由独立配置项控制）\n      const bool reuse_vdd = config::video.vdd_reuse;\n\n      if (reuse_vdd) {\n        // 复用VDD：所有客户端共享同一VDD，只更新客户端ID\n        BOOST_LOG(info) << \"共享VDD模式，复用现有VDD（客户端: \" << current_vdd_client_id << \" -> \" << current_client_id << \"）\";\n        current_vdd_client_id = current_client_id;\n      }\n      else {\n        // 不复用：销毁并重建VDD（每个客户端独立VDD）\n        BOOST_LOG(info) << \"独立VDD模式，重建VDD设备（客户端: \" << current_vdd_client_id << \" -> \" << current_client_id << \"）\";\n        \n        const auto old_vdd_id = device_zako;\n        destroy_vdd_monitor();\n        clear_vdd_state();\n        device_zako.clear();\n        \n        // Handle VDD ID in persistent_data\n        if (config::video.vdd_keep_enabled) {\n          // 常驻模式：需要替换ID（保留VDD在persistent_data中）\n          should_replace_vdd_id_ = true;\n          old_vdd_id_ = old_vdd_id;\n          BOOST_LOG(debug) << \"标记需要替换VDD ID: \" << old_vdd_id;\n        }\n        else {\n          // 非常驻模式：从initial中移除VDD\n          BOOST_LOG(debug) << \"从initial拓扑中移除VDD: \" << old_vdd_id;\n          settings.remove_vdd_from_initial_topology(old_vdd_id);\n        }\n        \n        std::this_thread::sleep_for(500ms);\n      }\n    }\n\n    // Update VDD resolution configuration\n    if (auto vdd_settings = vdd_utils::prepare_vdd_settings(config);\n      vdd_settings.needs_update && config.resolution) {\n      update_vdd_resolution(config, vdd_settings);\n    }\n\n    // Create VDD device if not present\n    if (device_zako.empty()) {\n      // 在创建 VDD 之前捕获物理显示器快照\n      // 此时无 VDD 存在（新建 or 重建后已销毁），物理屏应处于正常状态\n      pre_vdd_devices = display_device::enum_available_devices();\n      BOOST_LOG(info) << \"已保存pre-VDD设备列表: \" << display_device::to_string(pre_vdd_devices);\n\n      BOOST_LOG(info) << \"创建虚拟显示器...\";\n      // 复用模式使用固定标识符，否则使用客户端ID生成唯一GUID\n      const std::string vdd_identifier = config::video.vdd_reuse\n        ? \"shared_vdd\"  // 固定标识符，所有客户端共用同一GUID\n        : current_client_id;  // 为每个客户端生成不同GUID\n      vdd_utils::create_vdd_monitor(vdd_identifier, hdr_brightness, physical_size);\n      std::this_thread::sleep_for(200ms);\n    }\n\n    // Wait for device to be ready\n    if (!wait_for_vdd_device(device_zako, 5, 200ms, 1000ms)) {\n      BOOST_LOG(error) << \"VDD设备初始化失败，尝试恢复\";\n      vdd_utils::disable_enable_vdd();\n      std::this_thread::sleep_for(2s);\n\n      if (!try_recover_vdd_device(current_client_id, session.client_name, hdr_brightness, device_zako)) {\n        BOOST_LOG(error) << \"VDD设备最终初始化失败\";\n        vdd_utils::disable_enable_vdd();\n        return;\n      }\n    }\n\n    if (device_zako.empty()) {\n      return;\n    }\n\n    if (original_output_name.empty()) {\n      original_output_name = config::video.output_name;\n      BOOST_LOG(debug) << \"保存原始 output_name: \" << original_output_name;\n    }\n\n    // Replace VDD ID if needed (after client switch in keep_enabled mode)\n    if (should_replace_vdd_id_ && !old_vdd_id_.empty()) {\n      BOOST_LOG(info) << \"替换persistent_data中的VDD ID: \" << old_vdd_id_ << \" -> \" << device_zako;\n      settings.replace_vdd_id(old_vdd_id_, device_zako);\n      should_replace_vdd_id_ = false;\n      old_vdd_id_.clear();\n    }\n    \n    // Update configuration and state\n    config.device_id = device_zako;\n    config::video.output_name = device_zako;\n    current_vdd_client_id = current_client_id;\n    BOOST_LOG(info) << \"成功配置VDD设备: \" << device_zako;\n\n    // Apply VDD prep settings to handle display topology\n    // This determines how VDD interacts with physical displays\n    // VDD模式下的拓扑控制与普通模式分开处理\n    if (config.vdd_prep != parsed_config_t::vdd_prep_e::no_operation) {\n      // User has specified a display configuration, apply it\n      if (vdd_utils::apply_vdd_prep(device_zako, config.vdd_prep, pre_vdd_devices)) {\n        BOOST_LOG(info) << \"已应用VDD屏幕布局设置\";\n        std::this_thread::sleep_for(200ms);\n      }\n    }\n    else {\n      // No specific configuration, ensure VDD is in extended mode (default behavior)\n      if (vdd_utils::ensure_vdd_extended_mode(device_zako)) {\n        BOOST_LOG(info) << \"已将VDD切换到扩展模式\";\n        std::this_thread::sleep_for(500ms);\n      }\n    }\n\n    // Set HDR state with retry\n    if (!vdd_utils::set_hdr_state(false)) {\n      BOOST_LOG(debug) << \"首次设置HDR状态失败，等待设备稳定后重试\";\n      std::this_thread::sleep_for(500ms);\n      vdd_utils::set_hdr_state(false);\n    }\n  }\n\n  void\n  session_t::restore_state() {\n    std::lock_guard lock { mutex };\n    restore_state_impl();\n  }\n\n  void\n  session_t::reset_persistence() {\n    std::lock_guard lock { mutex };\n    settings.reset_persistence();\n    pending_restore_ = false;\n    SessionEventListener::clear_unlock_task();\n    stop_timer_and_clear_vdd_state();\n    current_vdd_client_id.clear();\n  }\n\n  void\n  session_t::restore_state_impl(revert_reason_e reason) {\n    // 统一的VDD清理逻辑（在恢复拓扑之前执行，不需要CCD API，锁屏时也可以执行）\n    const auto vdd_id = display_device::find_device_by_friendlyname(ZAKO_NAME);\n\n    // 常驻模式：只影响 VDD 是否销毁，不影响拓扑恢复\n    const bool is_keep_enabled = config::video.vdd_keep_enabled;\n\n    // 如果没有会话配置过（current_use_vdd 为 nullopt），说明：\n    // 1. 程序刚启动进行崩溃恢复（init() 调用）\n    // 2. 或者上一次会话已经正常结束且清理了状态\n    // 此时不需要恢复拓扑（没有拓扑被修改过），只需要清理可能残留的 VDD\n    if (!current_use_vdd.has_value()) {\n      BOOST_LOG(debug) << \" 无会话配置（current_use_vdd=nullopt），仅执行 VDD 清理\";\n      \n      if (!vdd_id.empty() && !is_keep_enabled) {\n        if (settings.has_persistent_data()) {\n          BOOST_LOG(info) << \"非常驻模式，销毁残留 VDD\";\n        }\n        else {\n          BOOST_LOG(info) << \"检测到异常残留的 VDD（无 persistent_data），清理 VDD\";\n        }\n        destroy_vdd_monitor();\n        std::this_thread::sleep_for(1000ms);\n      }\n\n      // 无头主机自动创建检查\n      if (reason == revert_reason_e::stream_ended && config::video.vdd_headless_create_enabled) {\n        auto devices = display_device::enum_available_devices();\n        if (devices.empty()) {\n          BOOST_LOG(info) << \"无头主机检测：未找到显示设备，自动创建基地显示器\";\n          create_vdd_monitor(\"\");\n          constexpr int max_attempts = 5;\n          constexpr auto wait_time = std::chrono::milliseconds(233);\n          for (int i = 0; i < max_attempts && !is_display_on(); ++i) {\n            std::this_thread::sleep_for(wait_time);\n          }\n        }\n      }\n\n      stop_timer_and_clear_vdd_state();\n      return;\n    }\n\n    // 以下逻辑仅在有会话配置时执行（current_use_vdd 有值）\n    const bool is_vdd_mode = *current_use_vdd;\n\n    // 获取当前有效的配置模式\n    // VDD模式：从统一值映射到 vdd_prep\n    // 普通模式：从统一值映射到 device_prep\n    const auto display_prep = current_device_prep.value_or(\n      static_cast<parsed_config_t::device_prep_e>(config::video.display_device_prep)\n    );\n    const auto vdd_prep = current_vdd_prep.value_or(\n      parsed_config_t::to_vdd_prep(display_prep)\n    );\n    const auto device_prep = is_vdd_mode\n      ? display_prep\n      : parsed_config_t::to_physical_device_prep(display_prep);\n    \n    // 判断是否是无操作模式（会话配置了 no_operation，意味着拓扑从未被修改过）\n    // VDD模式看 vdd_prep，普通模式看 device_prep\n    const bool is_no_operation = is_vdd_mode \n      ? (vdd_prep == parsed_config_t::vdd_prep_e::no_operation)\n      : (device_prep == parsed_config_t::device_prep_e::no_operation);\n\n    BOOST_LOG(debug) << \"restore_state_impl 决策参数:\"\n                     << \" is_vdd_mode=\" << is_vdd_mode\n                     << \" vdd_prep=\" << static_cast<int>(vdd_prep)\n                     << \" device_prep=\" << static_cast<int>(device_prep)\n                     << \" is_no_operation=\" << is_no_operation;\n\n    // 检查 apply_config 是否曾成功执行（persistent_data 是否存在）\n    const bool has_persistent = settings.has_persistent_data();\n\n    // 立即执行完整 restore\n    // VDD 销毁逻辑\n    if (!vdd_id.empty()) {\n      bool should_destroy = false;\n      \n      // 判断1：常驻模式 - 保留VDD\n      if (is_keep_enabled) {\n        BOOST_LOG(debug) << \"常驻模式，保留VDD\";\n      }\n      // 判断2：非常驻模式 - 销毁VDD（无论是否是无操作模式）\n      else if (has_persistent) {\n        BOOST_LOG(info) << \"非常驻模式，销毁VDD\";\n        should_destroy = true;\n      }\n      // 判断3：无persistent_data - apply_config 从未执行成功（如锁屏中退出串流）\n      else {\n        BOOST_LOG(info) << \"apply_config 未执行（无persistent_data），销毁VDD并跳过拓扑恢复\";\n        should_destroy = true;\n      }\n\n      // 无头主机保护：如果销毁后会变成无头（VDD 是唯一显示设备），跳过销毁\n      // 这避免了无意义的销毁+重建循环（device ID 变化导致 persistent_data 失效）\n      if (should_destroy) {\n        auto devices = display_device::enum_available_devices();\n        bool only_vdd = (devices.size() == 1 && devices.count(vdd_id));\n        if (only_vdd || devices.empty()) {\n          BOOST_LOG(info) << \"无头主机检测：VDD 是唯一显示设备，跳过销毁\";\n          should_destroy = false;\n        }\n      }\n\n      if (should_destroy) {\n        destroy_vdd_monitor();\n        std::this_thread::sleep_for(1000ms);\n      }\n    }\n\n    // 如果 apply_config 从未执行成功，拓扑从未被修改过，不需要恢复\n    if (!has_persistent) {\n      BOOST_LOG(info) << \"apply_config 从未执行成功，跳过拓扑恢复\";\n      stop_timer_and_clear_vdd_state();\n      return;\n    }\n\n    // 添加诊断日志\n    const bool settings_will_fail = settings.is_changing_settings_going_to_fail();\n    BOOST_LOG(debug) << \"Checking if reverting settings will fail: \" << settings_will_fail;\n    \n    // VDD生命周期已在上面的逻辑中决定（销毁或保留），通知revert_settings不要再处理VDD销毁\n    const bool vdd_already_handled = true;\n    \n    if (!settings_will_fail && settings.revert_settings(reason, vdd_already_handled)) {\n      stop_timer_and_clear_vdd_state();\n    }\n    else {\n      // 无法立即恢复，添加任务到解锁队列\n      BOOST_LOG(warning) << \"无法立即恢复显示设置\";\n      \n      // 设置待恢复标志\n      pending_restore_ = true;\n      \n      // 添加恢复任务（自动处理锁屏检查和立即执行）\n      SessionEventListener::add_unlock_task([this, reason]() {\n        // 快速检查是否还需要恢复（最小化锁持有时间）\n        {\n          std::lock_guard lock { mutex };\n          if (!pending_restore_) {\n            BOOST_LOG(info) << \"恢复操作已取消，跳过\";\n            return;\n          }\n        }\n        \n        // 在锁外执行CCD检查和恢复操作（避免阻塞托盘等其他操作）\n        if (settings.is_changing_settings_going_to_fail()) {\n          BOOST_LOG(warning) << \"CCD API仍不可用，启动轮询机制\";\n          std::lock_guard lock { mutex };\n          this->start_polling_restore(reason);\n          return;\n        }\n        \n        // 执行恢复\n        auto result = settings.revert_settings(reason, true);\n        BOOST_LOG(info) << \"恢复显示设置\" << (result ? \"成功\" : \"失败\");\n        \n        // 恢复完成后清除标志和状态\n        {\n          std::lock_guard lock { mutex };\n          pending_restore_ = false;\n          stop_timer_and_clear_vdd_state();\n        }\n      });\n    }\n  }\n\n  void\n  session_t::start_polling_restore(revert_reason_e reason) {\n    polling_retry_count_.store(0, boost::memory_order_relaxed);  // 重置计数器\n    const int max_retries = 20;\n\n    timer->setup_timer([this, reason, max_retries]() {\n      // 检查是否还需要恢复\n      if (!pending_restore_) {\n        BOOST_LOG(debug) << \"恢复操作已取消，跳过\";\n        return true;\n      }\n      \n      if (settings.is_changing_settings_going_to_fail()) {\n        const int current_count = polling_retry_count_.fetch_add(1, boost::memory_order_relaxed) + 1;\n        if (current_count >= max_retries) {\n          BOOST_LOG(warning) << \"已达到最大重试次数，停止尝试恢复显示设置\";\n          pending_restore_ = false;\n          clear_vdd_state();\n          return true;\n        }\n        BOOST_LOG(warning) << \"Timer: 仍在等待CCD恢复... (Count: \" << current_count << \"/\" << max_retries << \")\";\n        return false;\n      }\n\n      // VDD生命周期已由restore_state_impl决定，跳过revert_settings中的VDD销毁\n      auto result = settings.revert_settings(reason, true);\n      BOOST_LOG(info) << \"轮询恢复显示设置\" << (result ? \"成功\" : \"失败\") << \"，不再重试\";\n      pending_restore_ = false;\n      clear_vdd_state();\n      return true;\n    });\n  }\n\n  session_t::session_t():\n      timer { std::make_unique<StateRetryTimer>(mutex) } {\n  }\n}  // namespace display_device\n"
  },
  {
    "path": "src/display_device/session.h",
    "content": "#pragma once\n\n// standard includes\n#include <mutex>\n// lib includes\n#include <boost/atomic.hpp>\n// local includes\n#include \"settings.h\"\n#include \"vdd_utils.h\"\n\nnamespace display_device {\n\n  /**\n   * @brief A singleton class for managing the display device configuration for the whole Sunshine session.\n   *\n   * This class is meant to be an entry point for applying the configuration and reverting it later\n   * from within the various places in the Sunshine's source code.\n   *\n   * It is similar to settings_t and is more or less a wrapper around it.\n   * However, this class ensures thread-safe usage for the methods and additionally\n   * performs automatic cleanups.\n   *\n   * @note A lazy-evaluated, correctly-destroyed, thread-safe singleton pattern is used here (https://stackoverflow.com/a/1008289).\n   */\n  class session_t {\n  public:\n    /**\n     * @brief A class that uses RAII to perform cleanup when it's destroyed.\n     * @note The deinit_t usage pattern is used here instead of the session_t destructor\n     *       to expedite the cleanup process in case of Sunshine termination.\n     * @see session_t::init()\n     */\n    class deinit_t {\n    public:\n      /**\n       * @brief A destructor that restores (or tries to) the initial state.\n       */\n      virtual ~deinit_t();\n    };\n\n    /**\n     * @brief Get the singleton instance.\n     * @returns Singleton instance for the class.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * session_t& session { session_t::get() };\n     * ```\n     */\n    static session_t &\n    get();\n\n    /**\n     * @brief Initialize the singleton and perform the initial state recovery (if needed).\n     * @returns A deinit_t instance that performs cleanup when destroyed.\n     * @see deinit_t\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const auto session_guard { session_t::init() };\n     * ```\n     */\n    static std::unique_ptr<deinit_t>\n    init();\n\n    /**\n     * @brief Configure the display device based on the user configuration and the session information.\n     *\n     * Upon failing to completely apply configuration, the applied settings will be reverted.\n     * Or, in some cases, we will keep retrying even when the stream has already started as there\n     * is no possibility to apply settings before the stream start.\n     *\n     * @param config User's video related configuration.\n     * @param session Session information.\n     * @note There is no return value as we still want to continue with the stream, so that\n     *       users can do something about it once they are connected. Otherwise, we might\n     *       prevent users from logging in at all...\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const std::shared_ptr<rtsp_stream::launch_session_t> launch_session; // Assuming ptr is properly initialized\n     * const config::video_t &video_config { config::video };\n     *\n     * session_t::get().configure_display(video_config, *launch_session);\n     * ```\n     */\n    void\n    configure_display(const config::video_t &config, const rtsp_stream::launch_session_t &session, bool is_reconfigure = false);\n\n    /**\n     * @brief Revert the display configuration and restore the previous state.\n     * @note This method automatically loads the persistence (if any) from the previous Sunshine session.\n     * @note In case the state could not be restored, it will be retried again in X seconds\n     *       (repeating indefinitely until success or until persistence is reset).\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const std::shared_ptr<rtsp_stream::launch_session_t> launch_session; // Assuming ptr is properly initialized\n     * const config::video_t &video_config { config::video };\n     *\n     * const auto result = session_t::get().configure_display(video_config, *launch_session);\n     * if (result) {\n     *   // Wait for some time\n     *   session_t::get().restore_state();\n     * }\n     * ```\n     */\n    void\n    restore_state();\n\n    /**\n     * @brief Reset the persistence and currently held initial display state.\n     *\n     * This is normally used to get out of the \"broken\" state where the algorithm wants\n     * to restore the initial display state and refuses start the stream in most cases.\n     *\n     * This could happen if the display is no longer available or the hardware was changed\n     * and the device ids no longer match.\n     *\n     * The user then accepts that Sunshine is not able to restore the state and \"agrees\" to\n     * do it manually.\n     *\n     * @note This also stops the retry timer.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const std::shared_ptr<rtsp_stream::launch_session_t> launch_session; // Assuming ptr is properly initialized\n     * const config::video_t &video_config { config::video };\n     *\n     * const auto result = session_t::get().configure_display(video_config, *launch_session);\n     * if (!result) {\n     *   // Wait for user to decide what to do\n     *   const bool user_wants_reset { true };\n     *   if (user_wants_reset) {\n     *     session_t::get().reset_persistence();\n     *   }\n     * }\n     * ```\n     */\n    void\n    reset_persistence();\n\n    /**\n     * @brief Create VDD monitor\n     * @param client_name 客户端名称，用于驱动识别客户端并启动对应的显示器\n     */\n    bool\n    create_vdd_monitor(const std::string &client_name = \"\");\n\n    /**\n     * @brief Destroy VDD monitor\n     */\n    bool\n    destroy_vdd_monitor();\n\n    /**\n     * @brief Enable VDD driver\n     */\n    void\n    enable_vdd();\n\n    /**\n     * @brief Disable VDD driver\n     */\n    void\n    disable_vdd();\n\n    /**\n     * @brief Disable and enable VDD driver\n     */\n    void\n    disable_enable_vdd();\n\n    /**\n     * @brief Toggle display power\n     */\n    bool\n    toggle_display_power();\n\n    /**\n     * @brief Check if display is on\n     */\n    bool\n    is_display_on();\n\n    /**\n     * @brief Prepares VDD for use\n     */\n    void\n    prepare_vdd(parsed_config_t &config, const rtsp_stream::launch_session_t &session);\n\n    /**\n     * @brief A deleted copy constructor for singleton pattern.\n     * @note Public to ensure better error message.\n     */\n    session_t(session_t const &) = delete;\n\n    /**\n     * @brief A deleted assignment operator for singleton pattern.\n     * @note Public to ensure better error message.\n     */\n    void\n    operator=(session_t const &) = delete;\n\n  private:\n    /**\n     * @brief A class for retrying to set/reset state.\n     *\n     * This timer class spins a thread which is mostly sleeping all the time, but can be\n     * configured to wake up every X seconds.\n     *\n     * It is tightly synchronized with the session_t class via a shared mutex to ensure\n     * that stupid race conditions do not happen where we successfully apply settings\n     * for them to be reset by the timer thread immediately.\n     */\n    class StateRetryTimer;\n\n    /**\n     * @brief A private constructor to ensure the singleton pattern.\n     * @note Cannot be defaulted in declaration because of forward declared StateRetryTimer.\n     */\n    explicit session_t();\n\n    /**\n     * @brief An implementation of `restore_state` without a mutex lock.\n     * @param reason The reason for reverting settings, used to determine appropriate cleanup behavior.\n     * @see restore_state for the description.\n     */\n    void\n    restore_state_impl(revert_reason_e reason = revert_reason_e::stream_ended);\n\n    /**\n     * @brief Start polling mechanism as fallback when CCD API is temporarily unavailable.\n     * @param reason The reason for reverting settings.\n     */\n    void\n    start_polling_restore(revert_reason_e reason);\n\n    settings_t settings; /**< A class for managing display device settings. */\n    std::mutex mutex; /**< A mutex for ensuring thread-safety. */\n    std::string last_vdd_setting; /**< Last VDD resolution and refresh rate setting. */\n    std::string current_vdd_client_id; /**< Current client ID associated with VDD monitor. */\n    std::string original_output_name; /**< Original output_name value before VDD device ID was set. */\n    boost::optional<parsed_config_t::device_prep_e> current_device_prep; /**< Current device preparation mode, respecting client overrides. */\n    boost::optional<parsed_config_t::vdd_prep_e> current_vdd_prep; /**< Current VDD preparation mode for VDD mode sessions. */\n    boost::optional<bool> current_use_vdd; /**< Whether current session is using VDD mode. */\n    bool pending_restore_ = false; /**< Flag indicating if there is a pending restore settings operation waiting for unlock. */\n    bool should_replace_vdd_id_ = false; /**< Flag indicating if VDD ID needs to be replaced after client switch. */\n    std::string old_vdd_id_; /**< Old VDD ID that needs to be replaced. */\n    boost::atomic<int> polling_retry_count_ {0}; /**< Retry counter for polling restore mechanism. */\n\n    /**\n     * @brief An instance of StateRetryTimer.\n     * @warning MUST BE declared after the settings and mutex members to ensure proper destruction order!.\n     */\n    std::unique_ptr<StateRetryTimer> timer;\n\n    void\n    update_vdd_resolution(const parsed_config_t &config, const vdd_utils::VddSettings &vdd_settings);\n\n    /**\n     * @brief Clear VDD state (client ID and last setting)\n     * @note This method does NOT acquire the mutex! It is intended to be used from places\n     *       where the mutex has already been locked.\n     */\n    void\n    clear_vdd_state();\n\n    /**\n     * @brief Stop timer and clear VDD state\n     * @note This method does NOT acquire the mutex! It is intended to be used from places\n     *       where the mutex has already been locked.\n     */\n    void\n    stop_timer_and_clear_vdd_state();\n  };\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/display_device/settings.cpp",
    "content": "// local includes\n#include \"settings.h\"\n#include \"src/logging.h\"\n\nnamespace display_device {\n\n  settings_t::apply_result_t::operator bool() const {\n    return result == result_e::success;\n  }\n\n  std::string\n  settings_t::apply_result_t::get_error_message() const {\n    switch (result) {\n      case result_e::success:\n        return \"Success\";\n      case result_e::topology_fail:\n        return \"Failed to change or validate the display topology\";\n      case result_e::primary_display_fail:\n        return \"Failed to change primary display\";\n      case result_e::modes_fail:\n        return \"Failed to set new display modes (resolution + refresh rate)\";\n      case result_e::hdr_states_fail:\n        return \"Failed to set new HDR states\";\n      case result_e::file_save_fail:\n        return \"Failed to save the original settings to persistent file\";\n      case result_e::revert_fail:\n        return \"Failed to revert back to the original display settings\";\n      default:\n        BOOST_LOG(fatal) << \"result_e conversion not implemented!\";\n        return \"FATAL\";\n    }\n  }\n\n  void\n  settings_t::set_filepath(std::filesystem::path filepath) {\n    this->filepath = std::move(filepath);\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/display_device/settings.h",
    "content": "#pragma once\n\n// standard includes\n#include <filesystem>\n#include <memory>\n\n// local includes\n#include \"parsed_config.h\"\n\nnamespace display_device {\n\n  /**\n   * @brief Reason for reverting display settings.\n   * @note Used to distinguish different scenarios when reverting settings.\n   */\n  enum class revert_reason_e {\n    stream_ended,      /**< Reverting after stream ended (normal cleanup). */\n    topology_switch,   /**< Reverting before topology switch during config application. */\n    config_cleanup,    /**< Cleaning up when no modifications are needed. */\n    persistence_reset  /**< Resetting persistence data. */\n  };\n\n  /**\n   * @brief A platform specific class that can apply configuration to the display device and later revert it.\n   *\n   * Main goals of this class:\n   *   - Apply the configuration to the display device.\n   *   - Revert the applied configuration to get back to the initial state.\n   *   - Save and load the previous state to/from a file.\n   */\n  class settings_t {\n  public:\n    /**\n     * @brief Platform specific persistent data.\n     */\n    struct persistent_data_t;\n\n    /**\n     * @brief Platform specific non-persistent audio data in case we need to manipulate\n     *        audio session and keep some temporary data around.\n     */\n    struct audio_data_t;\n\n    /**\n     * @brief The result value of the apply_config with additional metadata.\n     * @note Metadata is used when generating an XML status report to the client.\n     * @see apply_config\n     */\n    struct apply_result_t {\n      /**\n       * @brief Possible result values/reasons from apply_config.\n       * @note There is no deeper meaning behind the values. They simply represent\n       *       the stage where the method has failed to give some hints to the user.\n       * @note The value of 700 has no special meaning and is just arbitrary.\n       * @see apply_config\n       */\n      enum class result_e : int {\n        success,\n        topology_fail,\n        primary_display_fail,\n        modes_fail,\n        hdr_states_fail,\n        file_save_fail,\n        revert_fail\n      };\n\n      /**\n       * @brief Convert the result to boolean equivalent.\n       * @returns True if result means success, false otherwise.\n       *\n       * EXAMPLES:\n       * ```cpp\n       * const apply_result_t result { result_e::topology_fail };\n       * if (result) {\n       *   // Handle good result\n       * }\n       * else {\n       *   // Handle bad result\n       * }\n       * ```\n       */\n      explicit\n      operator bool() const;\n\n      /**\n       * @brief Get a string message with better explanation for the result.\n       * @returns String message for the result.\n       *\n       * EXAMPLES:\n       * ```cpp\n       * const apply_result_t result { result_e::topology_fail };\n       * if (!result) {\n       *   const int error_message = result.get_error_message();\n       * }\n       * ```\n       */\n      [[nodiscard]] std::string\n      get_error_message() const;\n\n      result_e result; /**< The result value. */\n    };\n\n    /**\n     * @brief A platform specific default constructor.\n     * @note Needed due to forwarding declarations used by the class.\n     */\n    explicit settings_t();\n\n    /**\n     * @brief A platform specific destructor.\n     * @note Needed due to forwarding declarations used by the class.\n     */\n    virtual ~settings_t();\n\n    /**\n     * @brief Check whether it is already known that changing settings will fail due to various reasons.\n     * @returns True if it's definitely known that changing settings will fail, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * settings_t settings;\n     * const bool will_fail { settings.is_changing_settings_going_to_fail() };\n     * ```\n     */\n    bool\n    is_changing_settings_going_to_fail() const;\n\n    /**\n     * @brief Set the file path for persistent data.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * settings_t settings;\n     * settings.set_filepath(\"/foo/bar.json\");\n     * ```\n     */\n    void\n    set_filepath(std::filesystem::path filepath);\n\n    /**\n     * @brief Apply the parsed configuration.\n     * @param config A parsed and validated configuration.\n     * @returns The apply result value.\n     * @see apply_result_t\n     * @see parsed_config_t\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const parsed_config_t config;\n     *\n     * settings_t settings;\n     * const auto result = settings.apply_config(config);\n     * ```\n     */\n    apply_result_t\n    apply_config(\n      const parsed_config_t &config,\n      const rtsp_stream::launch_session_t &session,\n      const boost::optional<active_topology_t> &pre_saved_initial_topology = boost::none);\n\n    /**\n     * @brief Revert the applied configuration and restore the previous settings.\n     * @param reason The reason for reverting settings, used to determine appropriate cleanup behavior.\n     * @note It automatically loads the settings from persistence file if cached settings do not exist.\n     * @returns True if settings were reverted or there was nothing to revert, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const std::shared_ptr<rtsp_stream::launch_session_t> launch_session; // Assuming ptr is properly initialized\n     * const config::video_t &video_config { config::video };\n     *\n     * settings_t settings;\n     * const auto result = settings.apply_config(video_config, *launch_session);\n     * if (result) {\n     *   // Wait for some time\n     *   settings.revert_settings(revert_reason_e::stream_ended);\n     * }\n     * ```\n     */\n    bool\n    revert_settings(revert_reason_e reason = revert_reason_e::stream_ended, bool skip_vdd_destroy = false);\n\n    /**\n     * @brief Reset the persistence and currently held initial display state.\n     * @see session_t::reset_persistence for more details.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const std::shared_ptr<rtsp_stream::launch_session_t> launch_session; // Assuming ptr is properly initialized\n     * const config::video_t &video_config { config::video };\n     *\n     * settings_t settings;\n     * const auto result = settings.apply_config(video_config, *launch_session);\n     * if (result) {\n     *   // Wait for some time\n     *   if (settings.revert_settings()) {\n     *     // Wait for user input\n     *     const bool user_wants_reset { true };\n     *     if (user_wants_reset) {\n     *       settings.reset_persistence();\n     *     }\n     *   }\n     * }\n     * ```\n     */\n   void\n    reset_persistence();\n\n    /**\n     * @brief Check if there is saved persistent data.\n     * @returns True if persistent data exists, false otherwise.\n     */\n    bool\n    has_persistent_data() const;\n\n    /**\n     * @brief Check if VDD is in the initial topology.\n     * @returns True if VDD is in the initial topology, false otherwise.\n     */\n    bool\n    is_vdd_in_initial_topology() const;\n\n    /**\n     * @brief Remove VDD from initial and modified topology.\n     * @param vdd_id The VDD device ID to remove.\n     */\n    void\n    remove_vdd_from_initial_topology(const std::string& vdd_id);\n\n    /**\n     * @brief Replace VDD ID in initial and modified topology.\n     * @param old_id The old VDD device ID.\n     * @param new_id The new VDD device ID.\n     */\n    void\n    replace_vdd_id(const std::string& old_id, const std::string& new_id);\n\n  private:\n    std::unique_ptr<persistent_data_t> persistent_data; /**< Platform specific persistent data. */\n    std::unique_ptr<audio_data_t> audio_data; /**< Platform specific temporary audio data. */\n    std::filesystem::path filepath; /**< Filepath for persistent file. */\n  };\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/display_device/to_string.cpp",
    "content": "// local includes\n#include \"to_string.h\"\n#include \"src/logging.h\"\n\nnamespace display_device {\n\n  std::string\n  to_string(device_state_e value) {\n    switch (value) {\n      case device_state_e::inactive:\n        return \"INACTIVE\";\n      case device_state_e::active:\n        return \"ACTIVE\";\n      case device_state_e::primary:\n        return \"PRIMARY\";\n      default:\n        BOOST_LOG(fatal) << \"device_state_e conversion not implemented!\";\n        return {};\n    }\n  }\n\n  std::string\n  to_string(hdr_state_e value) {\n    switch (value) {\n      case hdr_state_e::unknown:\n        return \"UNKNOWN\";\n      case hdr_state_e::disabled:\n        return \"DISABLED\";\n      case hdr_state_e::enabled:\n        return \"ENABLED\";\n      default:\n        BOOST_LOG(fatal) << \"hdr_state_e conversion not implemented!\";\n        return {};\n    }\n  }\n\n  std::string\n  to_string(const hdr_state_map_t &value) {\n    std::stringstream output;\n    for (const auto &item : value) {\n      output << std::endl\n             << item.first << \" -> \" << to_string(item.second);\n    }\n    return output.str();\n  }\n\n  std::string\n  to_string(const device_info_t &value) {\n    std::stringstream output;\n    output << \"DISPLAY NAME: \" << (value.display_name.empty() ? \"NOT AVAILABLE\" : value.display_name) << std::endl;\n    output << \"FRIENDLY NAME: \" << (value.friendly_name.empty() ? \"通用/内建显示器\" : value.friendly_name) << std::endl;\n    output << \"DEVICE STATE: \" << to_string(value.device_state) << std::endl;\n    output << \"HDR STATE: \" << to_string(value.hdr_state);\n    return output.str();\n  }\n\n  std::string\n  to_string(const device_info_map_t &value) {\n    std::stringstream output;\n    bool output_is_empty { true };\n    for (const auto &item : value) {\n      output << std::endl;\n      if (!output_is_empty) {\n        output << \"-----------------------\" << std::endl;\n      }\n\n      output << \"DEVICE ID: \" << item.first << std::endl;\n      output << to_string(item.second);\n      output_is_empty = false;\n    }\n    return output.str();\n  }\n\n  std::string\n  to_string(const resolution_t &value) {\n    std::stringstream output;\n    output << value.width << \"x\" << value.height;\n    return output.str();\n  }\n\n  std::string\n  to_string(const refresh_rate_t &value) {\n    std::stringstream output;\n    if (value.denominator > 0) {\n      output << (static_cast<float>(value.numerator) / value.denominator);\n    }\n    else {\n      output << \"INF\";\n    }\n    return output.str();\n  }\n\n  std::string\n  to_string(const display_mode_t &value) {\n    std::stringstream output;\n    output << to_string(value.resolution) << \"x\" << to_string(value.refresh_rate);\n    return output.str();\n  }\n\n  std::string\n  to_string(const device_display_mode_map_t &value) {\n    std::stringstream output;\n    for (const auto &item : value) {\n      output << std::endl\n             << item.first << \" -> \" << to_string(item.second);\n    }\n    return output.str();\n  }\n\n  std::string\n  to_string(const active_topology_t &value) {\n    std::stringstream output;\n    bool first_group { true };\n\n    output << \"[\";\n    for (const auto &group : value) {\n      if (!first_group) {\n        output << \", \";\n      }\n      first_group = false;\n\n      output << \"[\";\n      bool first_group_item { true };\n      for (const auto &group_item : group) {\n        if (!first_group_item) {\n          output << \", \";\n        }\n        first_group_item = false;\n\n        output << group_item;\n      }\n      output << \"]\";\n    }\n    output << \"]\";\n\n    return output.str();\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/display_device/to_string.h",
    "content": "#pragma once\n\n// local includes\n#include \"display_device.h\"\n\nnamespace display_device {\n\n  /**\n   * @brief Stringify a device_state_e value.\n   * @param value Value to be stringified.\n   * @return A string representation of device_state_e value.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string string_value = to_string(device_state_e { });\n   * ```\n   */\n  std::string\n  to_string(device_state_e value);\n\n  /**\n   * @brief Stringify a hdr_state_e value.\n   * @param value Value to be stringified.\n   * @return A string representation of hdr_state_e value.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string string_value = to_string(hdr_state_e { });\n   * ```\n   */\n  std::string\n  to_string(hdr_state_e value);\n\n  /**\n   * @brief Stringify a hdr_state_map_t value.\n   * @param value Value to be stringified.\n   * @return A string representation of hdr_state_map_t value.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string string_value = to_string(hdr_state_map_t { });\n   * ```\n   */\n  std::string\n  to_string(const hdr_state_map_t &value);\n\n  /**\n   * @brief Stringify a device_info_t value.\n   * @param value Value to be stringified.\n   * @return A string representation of device_info_t value.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string string_value = to_string(device_info_t { });\n   * ```\n   */\n  std::string\n  to_string(const device_info_t &value);\n\n  /**\n   * @brief Stringify a device_info_map_t value.\n   * @param value Value to be stringified.\n   * @return A string representation of device_info_map_t value.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string string_value = to_string(device_info_map_t { });\n   * ```\n   */\n  std::string\n  to_string(const device_info_map_t &value);\n\n  /**\n   * @brief Stringify a resolution_t value.\n   * @param value Value to be stringified.\n   * @return A string representation of resolution_t value.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string string_value = to_string(resolution_t { });\n   * ```\n   */\n  std::string\n  to_string(const resolution_t &value);\n\n  /**\n   * @brief Stringify a refresh_rate_t value.\n   * @param value Value to be stringified.\n   * @return A string representation of refresh_rate_t value.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string string_value = to_string(refresh_rate_t { });\n   * ```\n   */\n  std::string\n  to_string(const refresh_rate_t &value);\n\n  /**\n   * @brief Stringify a display_mode_t value.\n   * @param value Value to be stringified.\n   * @return A string representation of display_mode_t value.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string string_value = to_string(display_mode_t { });\n   * ```\n   */\n  std::string\n  to_string(const display_mode_t &value);\n\n  /**\n   * @brief Stringify a device_display_mode_map_t value.\n   * @param value Value to be stringified.\n   * @return A string representation of device_display_mode_map_t value.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string string_value = to_string(device_display_mode_map_t { });\n   * ```\n   */\n  std::string\n  to_string(const device_display_mode_map_t &value);\n\n  /**\n   * @brief Stringify a active_topology_t value.\n   * @param value Value to be stringified.\n   * @return A string representation of active_topology_t value.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string string_value = to_string(active_topology_t { });\n   * ```\n   */\n  std::string\n  to_string(const active_topology_t &value);\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/display_device/vdd_utils.cpp",
    "content": "#define WIN32_LEAN_AND_MEAN\n\n#include \"vdd_utils.h\"\n\n#include <algorithm>\n#include <boost/filesystem.hpp>\n#include <boost/process/v1.hpp>\n#include <boost/property_tree/json_parser.hpp>\n#include <boost/property_tree/ptree.hpp>\n#include <boost/uuid/name_generator_sha1.hpp>\n#include <boost/uuid/uuid.hpp>\n#include <boost/uuid/uuid_io.hpp>\n#include <filesystem>\n#include <future>\n#include <sstream>\n#include <thread>\n#include <unordered_set>\n#include <vector>\n\n#include \"src/confighttp.h\"\n#include \"src/globals.h\"\n#include \"src/platform/common.h\"\n#include \"src/platform/run_command.h\"\n#include \"src/platform/windows/display_device/windows_utils.h\"\n#include \"src/rtsp.h\"\n#include \"src/system_tray.h\"\n#include \"src/system_tray_i18n.h\"\n#include \"to_string.h\"\n\nnamespace pt = boost::property_tree;\n\nnamespace display_device {\n  namespace vdd_utils {\n\n    const wchar_t *kVddPipeName = L\"\\\\\\\\.\\\\pipe\\\\ZakoVDDPipe\";\n    const DWORD kPipeTimeoutMs = 3000;\n    const DWORD kPipeBufferSize = 4096;\n    const std::chrono::milliseconds kDefaultDebounceInterval { 2000 };\n\n    // 上次切换显示器的时间点\n    static std::chrono::steady_clock::time_point last_toggle_time { std::chrono::steady_clock::now() };\n    // 防抖间隔\n    static std::chrono::milliseconds debounce_interval { kDefaultDebounceInterval };\n    // 上一次使用的客户端UUID，用于在没有提供UUID时使用\n    static std::string last_used_client_uuid;\n\n    std::chrono::milliseconds\n    calculate_exponential_backoff(int attempt) {\n      auto delay = kInitialRetryDelay * (1 << attempt);\n      return std::min(delay, kMaxRetryDelay);\n    }\n\n    /**\n     * @brief Allowed DevManView actions for VDD driver management.\n     */\n    enum class vdd_action_e {\n      enable,\n      disable,\n      disable_enable\n    };\n\n    /**\n     * @brief Get the command-line argument string for a VDD action.\n     */\n    const char *\n    vdd_action_to_string(vdd_action_e action) {\n      switch (action) {\n        case vdd_action_e::enable: return \"enable\";\n        case vdd_action_e::disable: return \"disable\";\n        case vdd_action_e::disable_enable: return \"disable_enable\";\n        default: return nullptr;\n      }\n    }\n\n    bool\n    execute_vdd_command(vdd_action_e action) {\n      static const std::string kDevManPath = (std::filesystem::path(SUNSHINE_ASSETS_DIR).parent_path() / \"tools\" / \"DevManView.exe\").string();\n      static const std::string kDriverName = \"Zako Display Adapter\";\n\n      const char *action_str = vdd_action_to_string(action);\n      if (!action_str) {\n        BOOST_LOG(error) << \"未知的VDD命令操作\";\n        return false;\n      }\n\n      boost::process::v1::environment _env = boost::this_process::environment();\n      auto working_dir = boost::filesystem::path();\n      std::error_code ec;\n\n      std::string cmd = kDevManPath + \" /\" + action_str + \" \\\"\" + kDriverName + \"\\\"\";\n\n      for (int attempt = 0; attempt < kMaxRetryCount; ++attempt) {\n        auto child = platf::run_command(true, true, cmd, working_dir, _env, nullptr, ec, nullptr);\n        if (!ec) {\n          BOOST_LOG(info) << \"成功执行VDD \" << action_str << \" 命令\";\n          child.detach();\n          return true;\n        }\n\n        auto delay = calculate_exponential_backoff(attempt);\n        BOOST_LOG(warning) << \"执行VDD \" << action_str << \" 命令失败 (尝试 \"\n                           << (attempt + 1) << \"/\" << kMaxRetryCount\n                           << \"): \" << ec.message() << \". 将在 \"\n                           << delay.count() << \"ms 后重试\";\n        std::this_thread::sleep_for(delay);\n      }\n\n      BOOST_LOG(error) << \"执行VDD \" << action_str << \" 命令失败，已达到最大重试次数\";\n      return false;\n    }\n\n    HANDLE\n    connect_to_pipe_with_retry(const wchar_t *pipe_name, int max_retries) {\n      HANDLE hPipe = INVALID_HANDLE_VALUE;\n      int attempt = 0;\n      auto retry_delay = kInitialRetryDelay;\n\n      while (attempt < max_retries) {\n        hPipe = CreateFileW(\n          pipe_name,\n          GENERIC_READ | GENERIC_WRITE,\n          0,\n          NULL,\n          OPEN_EXISTING,\n          FILE_FLAG_OVERLAPPED,  // 使用异步IO\n          NULL);\n\n        if (hPipe != INVALID_HANDLE_VALUE) {\n          DWORD mode = PIPE_READMODE_MESSAGE;\n          if (SetNamedPipeHandleState(hPipe, &mode, NULL, NULL)) {\n            return hPipe;\n          }\n          CloseHandle(hPipe);\n        }\n\n        ++attempt;\n        retry_delay = calculate_exponential_backoff(attempt);\n        std::this_thread::sleep_for(retry_delay);\n      }\n      return INVALID_HANDLE_VALUE;\n    }\n\n    bool\n    execute_pipe_command(const wchar_t *pipe_name, const wchar_t *command, std::string *response, bool *timed_out) {\n      auto hPipe = connect_to_pipe_with_retry(pipe_name);\n      if (hPipe == INVALID_HANDLE_VALUE) {\n        BOOST_LOG(error) << \"连接MTT虚拟显示管道失败，已重试多次\";\n        return false;\n      }\n\n      // RAII guard for pipe handle to prevent handle leak\n      struct HandleGuard {\n        HANDLE handle;\n        ~HandleGuard() {\n          if (handle && handle != INVALID_HANDLE_VALUE) CloseHandle(handle);\n        }\n      } pipe_guard { hPipe };\n\n      // 异步IO结构体\n      OVERLAPPED overlapped = { 0 };\n      overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);\n\n      HandleGuard event_guard { overlapped.hEvent };\n\n      // 发送命令（使用宽字符版本）\n      DWORD bytesWritten;\n      size_t cmd_len = (wcslen(command) + 1) * sizeof(wchar_t);  // 包含终止符\n      if (!WriteFile(hPipe, command, (DWORD) cmd_len, &bytesWritten, &overlapped)) {\n        if (GetLastError() != ERROR_IO_PENDING) {\n          BOOST_LOG(error) << L\"发送\" << command << L\"命令失败，错误代码: \" << GetLastError();\n          return false;\n        }\n\n        // 等待写入完成\n        DWORD waitResult = WaitForSingleObject(overlapped.hEvent, kPipeTimeoutMs);\n        if (waitResult != WAIT_OBJECT_0) {\n          BOOST_LOG(error) << L\"发送\" << command << L\"命令超时\";\n          return false;\n        }\n      }\n\n      // 读取响应\n      bool read_timed_out = false;\n      if (response) {\n        char buffer[kPipeBufferSize];\n        DWORD bytesRead = 0;\n        if (!ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, &overlapped)) {\n          if (GetLastError() != ERROR_IO_PENDING) {\n            BOOST_LOG(warning) << \"读取响应失败，错误代码: \" << GetLastError();\n            return false;\n          }\n\n          DWORD waitResult = WaitForSingleObject(overlapped.hEvent, kPipeTimeoutMs);\n          if (waitResult == WAIT_OBJECT_0 && GetOverlappedResult(hPipe, &overlapped, &bytesRead, FALSE)) {\n            buffer[bytesRead] = '\\0';\n            *response = std::string(buffer, bytesRead);\n          }\n          else {\n            read_timed_out = true;\n            CancelIo(hPipe);\n          }\n        }\n        else {\n          // ReadFile completed synchronously\n          buffer[bytesRead] = '\\0';\n          *response = std::string(buffer, bytesRead);\n        }\n      }\n\n      if (timed_out) {\n        *timed_out = read_timed_out;\n      }\n      return true;\n    }\n\n    bool\n    reload_driver() {\n      std::string response;\n      return execute_pipe_command(kVddPipeName, L\"RELOAD_DRIVER\", &response);\n    }\n\n    std::string\n    generate_client_guid(const std::string &identifier) {\n      if (identifier.empty()) {\n        return \"\";\n      }\n\n      // 使用SHA1 name generator确保相同标识符生成相同GUID\n      static constexpr boost::uuids::uuid ns_id {};\n      const auto boost_uuid = boost::uuids::name_generator_sha1 { ns_id }(\n        reinterpret_cast<const unsigned char *>(identifier.c_str()),\n        identifier.size());\n\n      return \"{\" + boost::uuids::to_string(boost_uuid) + \"}\";\n    }\n\n    /**\n     * @brief 从客户端配置中获取物理尺寸\n     * @param client_name 客户端名称\n     * @return 物理尺寸结构，如果未找到则返回默认值（0,0）\n     */\n    physical_size_t\n    get_client_physical_size(const std::string &client_name) {\n      if (client_name.empty()) {\n        return {};\n      }\n\n      // 预定义尺寸映射表\n      static const std::unordered_map<std::string, physical_size_t> size_map = {\n        { \"small\", { 13.3f, 7.5f } },  // 小型设备：约6英寸，16:9比例\n        { \"medium\", { 34.5f, 19.4f } },  // 中型设备：约15.6英寸，16:9比例\n        { \"large\", { 70.8f, 39.8f } }  // 大型设备：约32英寸，16:9比例\n      };\n\n      try {\n        pt::ptree clientArray;\n        std::stringstream ss(config::nvhttp.clients);\n        pt::read_json(ss, clientArray);\n\n        for (const auto &client : clientArray) {\n          if (client.second.get<std::string>(\"name\", \"\") == client_name) {\n            const std::string device_size = client.second.get<std::string>(\"deviceSize\", \"medium\");\n            auto it = size_map.find(device_size);\n            return (it != size_map.end()) ? it->second : size_map.at(\"medium\");\n          }\n        }\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(debug) << \"获取客户端物理尺寸失败: \" << e.what();\n      }\n\n      return {};\n    }\n\n    bool\n    create_vdd_monitor(const std::string &client_identifier, const hdr_brightness_t &hdr_brightness, const physical_size_t &physical_size) {\n      std::string response;\n      std::wstring command = L\"CREATEMONITOR\";\n\n      // 如果没有提供UUID，使用上一次的UUID\n      std::string identifier_to_use = client_identifier.empty() && !last_used_client_uuid.empty() ? last_used_client_uuid : client_identifier;\n\n      if (identifier_to_use != client_identifier && !identifier_to_use.empty()) {\n        BOOST_LOG(info) << \"未提供客户端标识符，使用上一次的UUID: \" << identifier_to_use;\n      }\n\n      // 生成GUID并构建命令\n      std::string guid_str = generate_client_guid(identifier_to_use);\n      if (!guid_str.empty()) {\n        // 构建完整参数: {GUID}:[max_nits,min_nits,maxFALL][widthCm,heightCm]\n        std::ostringstream param_stream;\n        param_stream << guid_str << \":[\" << hdr_brightness.max_nits << \",\" << hdr_brightness.min_nits << \",\" << hdr_brightness.max_full_nits << \"]\";\n\n        // 如果提供了物理尺寸，添加到参数中\n        if (physical_size.width_cm > 0.0f && physical_size.height_cm > 0.0f) {\n          param_stream << \"[\" << physical_size.width_cm << \",\" << physical_size.height_cm << \"]\";\n        }\n\n        std::string param_str = param_stream.str();\n\n        // 转换为宽字符并添加到命令\n        int size_needed = MultiByteToWideChar(CP_UTF8, 0, param_str.c_str(), -1, NULL, 0);\n        if (size_needed > 0) {\n          std::vector<wchar_t> param_wide(size_needed);\n          MultiByteToWideChar(CP_UTF8, 0, param_str.c_str(), -1, param_wide.data(), size_needed);\n          command += L\" \" + std::wstring(param_wide.data());\n        }\n\n        std::ostringstream log_stream;\n        log_stream << \"创建虚拟显示器，客户端标识符: \" << identifier_to_use\n                   << \", GUID: \" << guid_str\n                   << \", HDR亮度范围: [\" << hdr_brightness.max_nits << \", \" << hdr_brightness.min_nits << \", \" << hdr_brightness.max_full_nits << \"]\";\n        if (physical_size.width_cm > 0.0f && physical_size.height_cm > 0.0f) {\n          log_stream << \", 物理尺寸: [\" << physical_size.width_cm << \"cm, \" << physical_size.height_cm << \"cm]\";\n        }\n        BOOST_LOG(info) << log_stream.str();\n      }\n\n      // 如果使用了有效的UUID，更新上一次使用的UUID\n      if (!identifier_to_use.empty()) {\n        last_used_client_uuid = identifier_to_use;\n      }\n\n      // 尝试发送命令（带GUID或不带GUID）\n      bool read_timed_out = false;\n      bool success = execute_pipe_command(kVddPipeName, command.c_str(), &response, &read_timed_out);\n\n      // 如果带GUID的命令失败，降级为不带GUID的命令（兼容旧版驱动）\n      if (!success && !guid_str.empty()) {\n        BOOST_LOG(warning) << \"带GUID的命令失败，尝试降级为不带GUID的命令\";\n        read_timed_out = false;\n        success = execute_pipe_command(kVddPipeName, L\"CREATEMONITOR\", &response, &read_timed_out);\n      }\n\n      if (!success) {\n        BOOST_LOG(error) << \"创建虚拟显示器失败\";\n        return false;\n      }\n\n#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1\n      system_tray::update_vdd_menu();\n#endif\n      BOOST_LOG(info) << \"创建虚拟显示器完成，响应: \" << response << \" [return=\" << (read_timed_out ? 1 : 0) << \"]\";\n      return true;\n    }\n\n    bool\n    destroy_vdd_monitor() {\n      // 如果VDD已不存在，直接返回成功\n      if (find_device_by_friendlyname(ZAKO_NAME).empty()) {\n        BOOST_LOG(debug) << \"VDD设备已不存在，跳过销毁\";\n        return true;\n      }\n\n      std::string response;\n      if (!execute_pipe_command(kVddPipeName, L\"DESTROYMONITOR\", &response)) {\n        BOOST_LOG(error) << \"销毁虚拟显示器失败\";\n        return false;\n      }\n\n      BOOST_LOG(info) << \"销毁虚拟显示器完成，响应: \" << response;\n\n      // 等待驱动程序完全卸载，避免WUDFHost.exe崩溃\n      // 这是必要的，因为驱动程序卸载是异步的\n      std::this_thread::sleep_for(std::chrono::milliseconds(500));\n\n#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1\n      system_tray::update_vdd_menu();\n#endif\n      return true;\n    }\n\n    void\n    destroy_vdd_monitor_nolog() {\n      HANDLE hPipe = CreateFileW(\n        kVddPipeName,\n        GENERIC_READ | GENERIC_WRITE,\n        0, NULL, OPEN_EXISTING, 0, NULL);\n      if (hPipe != INVALID_HANDLE_VALUE) {\n        DWORD mode = PIPE_READMODE_MESSAGE;\n        SetNamedPipeHandleState(hPipe, &mode, NULL, NULL);\n        const wchar_t cmd[] = L\"DESTROYMONITOR\";\n        DWORD bytesWritten;\n        WriteFile(hPipe, cmd, sizeof(cmd), &bytesWritten, NULL);\n        CloseHandle(hPipe);\n      }\n    }\n\n    void\n    enable_vdd() {\n      execute_vdd_command(vdd_action_e::enable);\n    }\n\n    void\n    disable_vdd() {\n      execute_vdd_command(vdd_action_e::disable);\n    }\n\n    void\n    disable_enable_vdd() {\n      execute_vdd_command(vdd_action_e::disable_enable);\n    }\n\n    bool\n    is_display_on() {\n      return !find_device_by_friendlyname(ZAKO_NAME).empty();\n    }\n\n    bool\n    toggle_display_power() {\n      auto now = std::chrono::steady_clock::now();\n\n      if (now - last_toggle_time < debounce_interval) {\n        BOOST_LOG(debug) << \"忽略快速重复的显示器开关请求，请等待\"\n                         << std::chrono::duration_cast<std::chrono::seconds>(\n                              debounce_interval - (now - last_toggle_time))\n                              .count()\n                         << \"秒\";\n        return false;\n      }\n\n      last_toggle_time = now;\n\n      if (is_display_on()) {\n        destroy_vdd_monitor();\n        return true;\n      }\n\n      // 创建前先确认\n      std::wstring confirm_title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CONFIRM_CREATE_TITLE));\n      std::wstring confirm_message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CONFIRM_CREATE_MSG));\n\n      if (MessageBoxW(NULL, confirm_message.c_str(), confirm_title.c_str(), MB_OKCANCEL | MB_ICONQUESTION) == IDCANCEL) {\n        BOOST_LOG(info) << system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CANCEL_CREATE_LOG);\n        return false;\n      }\n\n      if (!create_vdd_monitor(\"\", vdd_utils::hdr_brightness_t {}, vdd_utils::physical_size_t {})) {\n        return false;\n      }\n\n      // 保存创建虚拟显示器前的物理设备列表\n      // 同时从所有可用设备中查找物理显示器（包括可能被禁用的）\n      std::unordered_set<std::string> physical_devices_before;\n      auto topology_before = get_current_topology();\n      auto all_devices_before = enum_available_devices();\n\n      // 从当前拓扑中获取活动的物理设备\n      for (const auto &group : topology_before) {\n        for (const auto &device_id : group) {\n          if (get_display_friendly_name(device_id) != ZAKO_NAME) {\n            physical_devices_before.insert(device_id);\n          }\n        }\n      }\n\n      // 如果拓扑中没有物理设备，尝试从所有设备中查找（可能被禁用了）\n      if (physical_devices_before.empty()) {\n        for (const auto &[device_id, device_info] : all_devices_before) {\n          if (get_display_friendly_name(device_id) != ZAKO_NAME) {\n            physical_devices_before.insert(device_id);\n            BOOST_LOG(debug) << \"从所有设备中找到物理显示器: \" << device_id;\n          }\n        }\n      }\n\n      // 后台线程确保VDD处于扩展模式，并进行二次确认\n      std::thread([vdd_device_id = find_device_by_friendlyname(ZAKO_NAME), physical_devices_before]() mutable {\n        if (vdd_device_id.empty()) {\n          std::this_thread::sleep_for(std::chrono::seconds(2));\n          vdd_device_id = find_device_by_friendlyname(ZAKO_NAME);\n        }\n\n        if (vdd_device_id.empty()) {\n          BOOST_LOG(warning) << \"无法找到基地显示器设备，跳过配置\";\n        }\n        else {\n          BOOST_LOG(info) << \"找到基地显示器设备: \" << vdd_device_id;\n\n          if (ensure_vdd_extended_mode(vdd_device_id, physical_devices_before)) {\n            BOOST_LOG(info) << \"已确保基地显示器处于扩展模式\";\n          }\n        }\n\n        // 创建后二次确认，20秒超时\n        constexpr auto timeout = std::chrono::seconds(20);\n        std::wstring dialog_title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CONFIRM_KEEP_TITLE));\n        std::wstring confirm_message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CONFIRM_KEEP_MSG));\n\n        auto future = std::async(std::launch::async, [&]() {\n          return MessageBoxW(nullptr, confirm_message.c_str(), dialog_title.c_str(), MB_YESNO | MB_ICONQUESTION) == IDYES;\n        });\n\n        if (future.wait_for(timeout) == std::future_status::ready && future.get()) {\n          BOOST_LOG(info) << \"用户确认保留基地显示器\";\n          return;\n        }\n\n        BOOST_LOG(info) << \"用户未确认或超时，自动销毁基地显示器\";\n\n        std::wstring w_dialog_title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CONFIRM_KEEP_TITLE));\n        if (HWND hwnd = FindWindowW(L\"#32770\", w_dialog_title.c_str()); hwnd && IsWindow(hwnd)) {\n          PostMessage(hwnd, WM_COMMAND, MAKEWPARAM(IDNO, BN_CLICKED), 0);\n          PostMessage(hwnd, WM_CLOSE, 0, 0);\n\n          for (int i = 0; i < 5 && IsWindow(hwnd); ++i) {\n            std::this_thread::sleep_for(std::chrono::milliseconds(200));\n          }\n\n          if (IsWindow(hwnd)) {\n            BOOST_LOG(warning) << \"无法正常关闭确认窗口，尝试终止窗口进程\";\n            EndDialog(hwnd, IDNO);\n          }\n        }\n\n        destroy_vdd_monitor();\n      }).detach();\n\n      return true;\n    }\n\n    VddSettings\n    prepare_vdd_settings(const parsed_config_t &config) {\n      auto is_res_cached = false;\n      auto is_fps_cached = false;\n      std::ostringstream res_stream, fps_stream;\n\n      res_stream << '[';\n      fps_stream << '[';\n\n      // 检查分辨率是否已缓存\n      for (const auto &res : config::nvhttp.resolutions) {\n        res_stream << res << ',';\n        if (config.resolution && res == to_string(*config.resolution)) {\n          is_res_cached = true;\n        }\n      }\n\n      // 检查帧率是否已缓存\n      for (const auto &fps : config::nvhttp.fps) {\n        fps_stream << fps << ',';\n        if (config.refresh_rate && fps == to_string(*config.refresh_rate)) {\n          is_fps_cached = true;\n        }\n      }\n\n      // 如果需要更新设置\n      bool needs_update = (!is_res_cached || !is_fps_cached) && config.resolution;\n      if (needs_update) {\n        if (!is_res_cached) {\n          res_stream << to_string(*config.resolution);\n        }\n        if (!is_fps_cached && config.refresh_rate) {\n          fps_stream << to_string(*config.refresh_rate);\n        }\n      }\n\n      // 移除最后的逗号并添加结束括号\n      auto res_str = res_stream.str();\n      auto fps_str = fps_stream.str();\n      if (res_str.back() == ',') res_str.pop_back();\n      if (fps_str.back() == ',') fps_str.pop_back();\n      res_str += ']';\n      fps_str += ']';\n\n      return { res_str, fps_str, needs_update };\n    }\n\n    bool\n    ensure_vdd_extended_mode(const std::string &device_id, const std::unordered_set<std::string> &physical_devices_to_preserve) {\n      if (device_id.empty()) {\n        return false;\n      }\n\n      auto current_topology = get_current_topology();\n      if (current_topology.empty()) {\n        BOOST_LOG(warning) << \"无法获取当前显示器拓扑\";\n        return false;\n      }\n\n      // 查找VDD所在的拓扑组\n      std::size_t vdd_group_index = SIZE_MAX;\n      for (std::size_t i = 0; i < current_topology.size(); ++i) {\n        if (std::find(current_topology[i].begin(), current_topology[i].end(), device_id) != current_topology[i].end()) {\n          vdd_group_index = i;\n          break;\n        }\n      }\n\n      // 检查是否需要切换\n      bool is_duplicated = (vdd_group_index != SIZE_MAX && current_topology[vdd_group_index].size() > 1);\n      bool is_vdd_only = (current_topology.size() == 1 && current_topology[0].size() == 1 && current_topology[0][0] == device_id);\n\n      if (!is_duplicated && !is_vdd_only) {\n        BOOST_LOG(debug) << \"VDD已经是扩展模式\";\n        return false;\n      }\n\n      BOOST_LOG(info) << \"检测到VDD处于\" << (is_vdd_only ? \"仅启用\" : \"复制\") << \"模式，切换到扩展模式\";\n\n      // 构建新拓扑：分离VDD，保留其他设备\n      active_topology_t new_topology;\n      std::unordered_set<std::string> included;\n\n      for (std::size_t i = 0; i < current_topology.size(); ++i) {\n        const auto &group = current_topology[i];\n\n        if (i == vdd_group_index) {\n          // 分离VDD到独立组\n          for (const auto &id : group) {\n            new_topology.push_back({ id });\n            included.insert(id);\n          }\n        }\n        else {\n          for (const auto &id : group) {\n            included.insert(id);\n          }\n          new_topology.push_back(group);\n        }\n      }\n\n      // 添加缺失的物理显示器\n      auto all_devices = enum_available_devices();\n      for (const auto &physical_id : physical_devices_to_preserve) {\n        if (included.count(physical_id) == 0 && all_devices.find(physical_id) != all_devices.end()) {\n          new_topology.push_back({ physical_id });\n          BOOST_LOG(info) << \"添加物理显示器到拓扑: \" << physical_id;\n        }\n      }\n\n      if (!is_topology_valid(new_topology) || !set_topology(new_topology)) {\n        BOOST_LOG(error) << \"设置拓扑失败\";\n        return false;\n      }\n\n      BOOST_LOG(info) << \"成功切换到扩展模式\";\n      return true;\n    }\n\n    bool\n    set_hdr_state(bool enable_hdr) {\n      auto vdd_device_id = find_device_by_friendlyname(ZAKO_NAME);\n      if (vdd_device_id.empty()) {\n        BOOST_LOG(info) << \"未找到虚拟显示器设备，跳过HDR状态设置\";\n        return true;\n      }\n\n      std::unordered_set<std::string> vdd_device_ids = { vdd_device_id };\n      auto current_hdr_states = get_current_hdr_states(vdd_device_ids);\n\n      auto hdr_state_it = current_hdr_states.find(vdd_device_id);\n      if (hdr_state_it == current_hdr_states.end()) {\n        BOOST_LOG(info) << \"虚拟显示器不支持HDR或状态未知\";\n        return true;\n      }\n\n      hdr_state_e target_state = enable_hdr ? hdr_state_e::enabled : hdr_state_e::disabled;\n      if (hdr_state_it->second == target_state) {\n        BOOST_LOG(info) << \"虚拟显示器HDR状态已是目标状态\";\n        return true;\n      }\n\n      hdr_state_map_t new_hdr_states;\n      new_hdr_states[vdd_device_id] = target_state;\n\n      const std::string action = enable_hdr ? \"启用\" : \"关闭\";\n      BOOST_LOG(info) << \"正在\" << action << \"虚拟显示器HDR...\";\n\n      if (set_hdr_states(new_hdr_states)) {\n        BOOST_LOG(info) << \"成功\" << action << \"虚拟显示器HDR\";\n        return true;\n      }\n\n      BOOST_LOG(warning) << action << \"虚拟显示器HDR失败\";\n      return false;\n    }\n\n    bool\n    apply_vdd_prep(const std::string &vdd_device_id, parsed_config_t::vdd_prep_e vdd_prep,\n      const device_info_map_t &pre_vdd_devices) {\n      if (vdd_device_id.empty()) {\n        BOOST_LOG(info) << \"VDD设备ID为空，跳过vdd_prep处理\";\n        return true;\n      }\n\n      if (vdd_prep == parsed_config_t::vdd_prep_e::no_operation) {\n        BOOST_LOG(info) << \"vdd_prep设置为无操作，跳过物理显示器处理\";\n        return true;\n      }\n\n      // 从 pre_vdd_devices（VDD创建前保存的设备列表）中获取物理显示器，\n      // 确保即使 VDD 创建后物理屏变 inactive 也能正确识别\n      std::vector<std::string> physical_devices;\n      std::string original_primary_id;\n\n      if (!pre_vdd_devices.empty()) {\n        // 使用 VDD 创建前保存的设备信息（可靠）\n        for (const auto &[device_id, info] : pre_vdd_devices) {\n          if (info.friendly_name != ZAKO_NAME) {\n            physical_devices.push_back(device_id);\n            if (info.device_state == device_state_e::primary) {\n              original_primary_id = device_id;\n            }\n          }\n        }\n        BOOST_LOG(info) << \"使用pre-VDD设备列表: \" << physical_devices.size() << \"个物理显示器\"\n                        << (original_primary_id.empty() ? \"\" : \", 原主屏: \" + original_primary_id);\n      }\n      else {\n        // 回退：从当前设备枚举中获取（VDD创建前未保存时的兜底逻辑）\n        BOOST_LOG(warning) << \"未提供pre-VDD设备列表，从当前设备枚举中查找物理显示器\";\n        const auto all_devices = enum_available_devices();\n        for (const auto &[device_id, info] : all_devices) {\n          if (device_id != vdd_device_id && info.friendly_name != ZAKO_NAME) {\n            physical_devices.push_back(device_id);\n            if (info.device_state == device_state_e::primary) {\n              original_primary_id = device_id;\n            }\n          }\n        }\n      }\n\n      // 确保原主屏在列表最前面（set_topology 中第一组拥有主屏优先权）\n      if (!original_primary_id.empty()) {\n        auto it = std::find(physical_devices.begin(), physical_devices.end(), original_primary_id);\n        if (it != physical_devices.begin() && it != physical_devices.end()) {\n          std::rotate(physical_devices.begin(), it, it + 1);\n        }\n      }\n\n      if (physical_devices.empty()) {\n        BOOST_LOG(debug) << \"没有物理显示器需要处理\";\n        return true;\n      }\n\n      active_topology_t new_topology;\n\n      switch (vdd_prep) {\n        case parsed_config_t::vdd_prep_e::vdd_as_primary: {\n          // VDD为主屏模式：VDD放在第一位（主屏），物理显示器作为扩展显示器\n          BOOST_LOG(info) << \"应用vdd_prep: VDD为主屏，物理显示器为副屏\";\n          // VDD单独一组（放在第一位作为主显示器）\n          new_topology.push_back({ vdd_device_id });\n          // 每个物理显示器单独一组（扩展模式）\n          for (const auto &physical_id : physical_devices) {\n            new_topology.push_back({ physical_id });\n          }\n          break;\n        }\n\n        case parsed_config_t::vdd_prep_e::vdd_as_secondary: {\n          // VDD为副屏模式：物理显示器为主屏，VDD作为扩展显示器\n          BOOST_LOG(info) << \"应用vdd_prep: 物理显示器为主屏，VDD为副屏\";\n          // 物理显示器放在前面（第一个为主显示器）\n          for (const auto &physical_id : physical_devices) {\n            new_topology.push_back({ physical_id });\n          }\n          // VDD单独一组（作为副显示器）\n          new_topology.push_back({ vdd_device_id });\n          break;\n        }\n\n        case parsed_config_t::vdd_prep_e::display_off: {\n          // 熄屏模式：只保留VDD，关闭所有物理显示器\n          BOOST_LOG(info) << \"应用vdd_prep: 关闭物理显示器\";\n          new_topology.push_back({ vdd_device_id });\n          // 不添加物理显示器，它们将被禁用\n          break;\n        }\n\n        default:\n          return true;\n      }\n\n      if (!is_topology_valid(new_topology)) {\n        BOOST_LOG(error) << \"新拓扑无效\";\n        return false;\n      }\n\n      if (!set_topology(new_topology)) {\n        BOOST_LOG(error) << \"设置拓扑失败\";\n        return false;\n      }\n\n      BOOST_LOG(info) << \"成功应用vdd_prep设置\";\n      return true;\n    }\n  }  // namespace vdd_utils\n}  // namespace display_device"
  },
  {
    "path": "src/display_device/vdd_utils.h",
    "content": "#pragma once\n\n#define WIN32_LEAN_AND_MEAN\n\n#include <chrono>\n#include <functional>\n#include <string>\n#include <string_view>\n#include <thread>\n#include <unordered_set>\n#include <windows.h>\n\n#include \"parsed_config.h\"\n\nnamespace display_device::vdd_utils {\n\n  using namespace std::chrono_literals;\n\n  // 常量定义\n  inline constexpr int kMaxRetryCount = 3;\n  inline constexpr auto kInitialRetryDelay = 500ms;\n  inline constexpr auto kMaxRetryDelay = 3000ms;\n\n  extern const wchar_t *kVddPipeName;\n  extern const DWORD kPipeTimeoutMs;\n  extern const DWORD kPipeBufferSize;\n  extern const std::chrono::milliseconds kDefaultDebounceInterval;\n\n  // HDR亮度范围结构\n  struct hdr_brightness_t {\n    float max_nits = 1000.0f;\n    float min_nits = 0.001f;\n    float max_full_nits = 1000.0f;\n  };\n\n  // 物理尺寸结构（厘米）\n  struct physical_size_t {\n    float width_cm = 0.0f;   // 宽度（厘米），0表示未指定\n    float height_cm = 0.0f;  // 高度（厘米），0表示未指定\n  };\n\n  // 重试配置结构\n  struct RetryConfig {\n    int max_attempts = kMaxRetryCount;\n    std::chrono::milliseconds initial_delay = kInitialRetryDelay;\n    std::chrono::milliseconds max_delay = kMaxRetryDelay;\n    std::string_view context;\n  };\n\n  // VDD设置结构\n  struct VddSettings {\n    std::string resolutions;\n    std::string fps;\n    bool needs_update = false;\n  };\n\n  // 指数退避计算\n  std::chrono::milliseconds\n  calculate_exponential_backoff(int attempt);\n\n  // VDD命令执行\n  bool\n  execute_vdd_command(const std::string &action);\n\n  // 管道相关函数\n  HANDLE\n  connect_to_pipe_with_retry(const wchar_t *pipe_name, int max_retries = 3);\n\n  bool\n  execute_pipe_command(const wchar_t *pipe_name, const wchar_t *command, std::string *response = nullptr, bool *timed_out = nullptr);\n\n  // 驱动重载函数\n  bool\n  reload_driver();\n\n  /**\n   * @brief 从客户端标识符生成GUID字符串（用于驱动识别）\n   * @param identifier 客户端标识符，如果为空则返回空字符串\n   * @return GUID格式字符串: {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}，如果identifier为空则返回空字符串\n   */\n  std::string\n  generate_client_guid(const std::string &identifier);\n\n  /**\n   * @brief 从客户端配置中获取物理尺寸\n   * @param client_name 客户端名称\n   * @return 物理尺寸结构，如果未找到则返回默认值（0,0）\n   */\n  physical_size_t\n  get_client_physical_size(const std::string &client_name);\n\n  /**\n   * @brief 创建VDD监视器\n   * @param client_identifier 客户端标识符（可选），用于驱动识别客户端并启动对应的显示器\n   * @param hdr_brightness HDR亮度配置\n   * @param physical_size 物理尺寸配置（厘米），可选\n   * @return 创建是否成功\n   */\n  bool\n  create_vdd_monitor(const std::string &client_identifier = \"\", const hdr_brightness_t &hdr_brightness = {}, const physical_size_t &physical_size = {});\n\n  bool\n  destroy_vdd_monitor();\n\n  /**\n   * @brief Shutdown-safe VDD destroy. Uses raw Win32 pipe API without BOOST_LOG.\n   * Safe to call from destructors where boost::log may already be destroyed.\n   */\n  void\n  destroy_vdd_monitor_nolog();\n\n  void\n  enable_vdd();\n\n  void\n  disable_vdd();\n\n  void\n  disable_enable_vdd();\n\n  bool\n  toggle_display_power();\n\n  bool\n  is_display_on();\n\n  bool\n  set_hdr_state(bool enable_hdr);\n\n  bool\n  ensure_vdd_extended_mode(const std::string &device_id, const std::unordered_set<std::string> &physical_devices_to_preserve = {});\n\n  /**\n   * @brief Apply VDD prep settings to handle physical displays.\n   * @param vdd_device_id The VDD device ID.\n   * @param vdd_prep The vdd_prep_e value specifying how to handle physical displays.\n   * @param pre_vdd_devices Physical device info captured BEFORE VDD creation.\n   *        Used to reliably identify physical displays even if VDD creation\n   *        caused them to become inactive. If empty, falls back to current device enumeration.\n   * @returns True if the operation succeeded.\n   * @note This operation modifies topology without saving/restoring state,\n   *       as Windows automatically handles topology memory when displays change.\n   */\n  bool\n  apply_vdd_prep(const std::string &vdd_device_id, parsed_config_t::vdd_prep_e vdd_prep,\n    const device_info_map_t &pre_vdd_devices = {});\n\n  VddSettings\n  prepare_vdd_settings(const parsed_config_t &config);\n\n  // 重试函数模板\n  template <typename Func>\n  bool\n  retry_with_backoff(Func &&check_func, const RetryConfig &config) {\n    auto delay = config.initial_delay;\n\n    for (int attempt = 0; attempt < config.max_attempts; ++attempt) {\n      if (check_func()) {\n        return true;\n      }\n\n      if (attempt + 1 < config.max_attempts) {\n        std::this_thread::sleep_for(delay);\n        delay = std::min(config.max_delay, delay * 2);\n      }\n    }\n    return false;\n  }\n\n}  // namespace display_device::vdd_utils"
  },
  {
    "path": "src/entry_handler.cpp",
    "content": "/**\n * @file entry_handler.cpp\n * @brief Definitions for entry handling functions.\n */\n// standard includes\n#include <csignal>\n#include <iostream>\n#include <thread>\n\n#include <boost/process/v1.hpp>\n\n// local includes\n#include \"config.h\"\n#include \"confighttp.h\"\n#include \"entry_handler.h\"\n#include \"globals.h\"\n#include \"httpcommon.h\"\n#include \"logging.h\"\n#include \"network.h\"\n#include \"platform/common.h\"\n#include \"src/display_device/display_device.h\"\n#include \"src/platform/windows/display_device/windows_utils.h\"\n#include \"version.h\"\n\nextern \"C\" {\n#ifdef _WIN32\n  #include <iphlpapi.h>\n#endif\n}\n\nusing namespace std::literals;\n\nvoid\nlaunch_ui() {\n  std::string url = \"https://localhost:\" + std::to_string(net::map_port(confighttp::PORT_HTTPS));\n  platf::open_url(url);\n}\n\nvoid\nlaunch_ui_with_path(std::string path) {\n  std::string url = \"https://localhost:\" + std::to_string(net::map_port(confighttp::PORT_HTTPS)) + path;\n  platf::open_url(url);\n}\n\nnamespace args {\n  int\n  creds(const char *name, int argc, char *argv[]) {\n    if (argc < 2 || argv[0] == \"help\"sv || argv[1] == \"help\"sv) {\n      help(name);\n    }\n\n    http::save_user_creds(config::sunshine.credentials_file, argv[0], argv[1]);\n\n    return 0;\n  }\n\n  int\n  help(const char *name) {\n    logging::print_help(name);\n    return 0;\n  }\n\n  int\n  version() {\n    // version was already logged at startup\n    return 0;\n  }\n\n#ifdef _WIN32\n  int\n  restore_nvprefs_undo() {\n    if (nvprefs_instance.load()) {\n      nvprefs_instance.restore_from_and_delete_undo_file_if_exists();\n      nvprefs_instance.unload();\n    }\n    return 0;\n  }\n#endif\n}  // namespace args\n\nnamespace lifetime {\n  char **argv;\n  std::atomic_int desired_exit_code;\n\n  void\n  exit_sunshine(int exit_code, bool async) {\n    // Store the exit code of the first exit_sunshine() call\n    int zero = 0;\n    desired_exit_code.compare_exchange_strong(zero, exit_code);\n\n    mail::man->event<bool>(mail::shutdown)->raise(true);\n\n    // Termination will happen asynchronously, but the caller may\n    // have wanted synchronous behavior.\n    while (!async) {\n      std::this_thread::sleep_for(1s);\n    }\n  }\n\n  void\n  debug_trap() {\n#ifdef _WIN32\n    DebugBreak();\n#else\n    std::raise(SIGTRAP);\n#endif\n  }\n\n  char **\n  get_argv() {\n    return argv;\n  }\n}  // namespace lifetime\n\nvoid\nlog_publisher_data() {\n  BOOST_LOG(info) << \"Package Publisher: \"sv << SUNSHINE_PUBLISHER_NAME;\n  BOOST_LOG(info) << \"Publisher Website: \"sv << SUNSHINE_PUBLISHER_WEBSITE;\n  BOOST_LOG(info) << \"Get support: \"sv << SUNSHINE_PUBLISHER_ISSUE_URL;\n}\n\n#ifdef _WIN32\nbool\nis_gamestream_enabled() {\n  DWORD enabled;\n  DWORD size = sizeof(enabled);\n  return RegGetValueW(\n           HKEY_LOCAL_MACHINE,\n           L\"SOFTWARE\\\\NVIDIA Corporation\\\\NvStream\",\n           L\"EnableStreaming\",\n           RRF_RT_REG_DWORD,\n           nullptr,\n           &enabled,\n           &size) == ERROR_SUCCESS &&\n         enabled != 0;\n}\n\nnamespace service_ctrl {\n  class service_controller {\n  public:\n    /**\n     * @brief Constructor for service_controller class.\n     * @param service_desired_access SERVICE_* desired access flags.\n     */\n    service_controller(DWORD service_desired_access) {\n      scm_handle = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT);\n      if (!scm_handle) {\n        auto winerr = GetLastError();\n        BOOST_LOG(error) << \"OpenSCManager() failed: \"sv << winerr;\n        return;\n      }\n\n      service_handle = OpenServiceA(scm_handle, \"SunshineService\", service_desired_access);\n      if (!service_handle) {\n        auto winerr = GetLastError();\n        BOOST_LOG(error) << \"OpenService() failed: \"sv << winerr;\n        return;\n      }\n    }\n\n    ~service_controller() {\n      if (service_handle) {\n        CloseServiceHandle(service_handle);\n      }\n\n      if (scm_handle) {\n        CloseServiceHandle(scm_handle);\n      }\n    }\n\n    /**\n     * @brief Asynchronously starts the Sunshine service.\n     */\n    bool\n    start_service() {\n      if (!service_handle) {\n        return false;\n      }\n\n      if (!StartServiceA(service_handle, 0, nullptr)) {\n        auto winerr = GetLastError();\n        if (winerr != ERROR_SERVICE_ALREADY_RUNNING) {\n          BOOST_LOG(error) << \"StartService() failed: \"sv << winerr;\n          return false;\n        }\n      }\n\n      return true;\n    }\n\n    /**\n     * @brief Query the service status.\n     * @param status The SERVICE_STATUS struct to populate.\n     */\n    bool\n    query_service_status(SERVICE_STATUS &status) {\n      if (!service_handle) {\n        return false;\n      }\n\n      if (!QueryServiceStatus(service_handle, &status)) {\n        auto winerr = GetLastError();\n        BOOST_LOG(error) << \"QueryServiceStatus() failed: \"sv << winerr;\n        return false;\n      }\n\n      return true;\n    }\n\n  private:\n    SC_HANDLE scm_handle = NULL;\n    SC_HANDLE service_handle = NULL;\n  };\n\n  bool\n  is_service_running() {\n    service_controller sc { SERVICE_QUERY_STATUS };\n\n    SERVICE_STATUS status;\n    if (!sc.query_service_status(status)) {\n      return false;\n    }\n\n    return status.dwCurrentState == SERVICE_RUNNING;\n  }\n\n  bool\n  start_service() {\n    service_controller sc { SERVICE_QUERY_STATUS | SERVICE_START };\n\n    std::cout << \"Starting Sunshine...\"sv;\n\n    // This operation is asynchronous, so we must wait for it to complete\n    if (!sc.start_service()) {\n      return false;\n    }\n\n    SERVICE_STATUS status;\n    do {\n      Sleep(1000);\n      std::cout << '.';\n    } while (sc.query_service_status(status) && status.dwCurrentState == SERVICE_START_PENDING);\n\n    if (status.dwCurrentState != SERVICE_RUNNING) {\n      BOOST_LOG(error) << SERVICE_NAME \" failed to start: \"sv << status.dwWin32ExitCode;\n      return false;\n    }\n\n    std::cout << std::endl;\n    return true;\n  }\n\n  bool\n  wait_for_ui_ready() {\n    std::cout << \"Waiting for Web UI to be ready...\";\n\n    // Wait up to 30 seconds for the web UI to start\n    for (int i = 0; i < 30; i++) {\n      PMIB_TCPTABLE tcp_table = nullptr;\n      ULONG table_size = 0;\n      ULONG err;\n\n      auto fg = util::fail_guard([&tcp_table]() {\n        free(tcp_table);\n      });\n\n      do {\n        // Query all open TCP sockets to look for our web UI port\n        err = GetTcpTable(tcp_table, &table_size, false);\n        if (err == ERROR_INSUFFICIENT_BUFFER) {\n          free(tcp_table);\n          tcp_table = (PMIB_TCPTABLE) malloc(table_size);\n        }\n      } while (err == ERROR_INSUFFICIENT_BUFFER);\n\n      if (err != NO_ERROR) {\n        BOOST_LOG(error) << \"Failed to query TCP table: \"sv << err;\n        return false;\n      }\n\n      uint16_t port_nbo = htons(net::map_port(confighttp::PORT_HTTPS));\n      for (DWORD i = 0; i < tcp_table->dwNumEntries; i++) {\n        auto &entry = tcp_table->table[i];\n\n        // Look for our port in the listening state\n        if (entry.dwLocalPort == port_nbo && entry.dwState == MIB_TCP_STATE_LISTEN) {\n          std::cout << std::endl;\n          return true;\n        }\n      }\n\n      Sleep(1000);\n      std::cout << '.';\n    }\n\n    std::cout << \"timed out\"sv << std::endl;\n    return false;\n  }\n}  // namespace service_ctrl\n#endif\n"
  },
  {
    "path": "src/entry_handler.h",
    "content": "/**\n * @file entry_handler.h\n * @brief Declarations for entry handling functions.\n */\n#pragma once\n\n// standard includes\n#include <atomic>\n#include <string_view>\n\n// local includes\n#include \"thread_pool.h\"\n#include \"thread_safe.h\"\n\n/**\n * @brief Launch the Web UI.\n * @examples\n * launch_ui();\n * @examples_end\n */\nvoid\nlaunch_ui();\n\n/**\n * @brief Launch the Web UI at a specific endpoint.\n * @examples\n * launch_ui_with_path(\"/pin\");\n * @examples_end\n */\nvoid\nlaunch_ui_with_path(std::string path);\n\n/**\n * @brief Functions for handling command line arguments.\n */\nnamespace args {\n  /**\n   * @brief Reset the user credentials.\n   * @param name The name of the program.\n   * @param argc The number of arguments.\n   * @param argv The arguments.\n   * @examples\n   * creds(\"sunshine\", 2, {\"new_username\", \"new_password\"});\n   * @examples_end\n   */\n  int\n  creds(const char *name, int argc, char *argv[]);\n\n  /**\n   * @brief Print help to stdout, then exit.\n   * @param name The name of the program.\n   * @examples\n   * help(\"sunshine\");\n   * @examples_end\n   */\n  int\n  help(const char *name);\n\n  /**\n   * @brief Print the version to stdout, then exit.\n   * @examples\n   * version();\n   * @examples_end\n   */\n  int\n  version();\n\n#ifdef _WIN32\n  /**\n   * @brief Restore global NVIDIA control panel settings.\n   * If Sunshine was improperly terminated, this function restores\n   * the global NVIDIA control panel settings to the undo file left\n   * by Sunshine. This function is typically called by the uninstaller.\n   * @examples\n   * restore_nvprefs_undo();\n   * @examples_end\n   */\n  int\n  restore_nvprefs_undo();\n#endif\n}  // namespace args\n\n/**\n * @brief Functions for handling the lifetime of Sunshine.\n */\nnamespace lifetime {\n  extern char **argv;\n  extern std::atomic_int desired_exit_code;\n\n  /**\n   * @brief Terminates Sunshine gracefully with the provided exit code.\n   * @param exit_code The exit code to return from main().\n   * @param async Specifies whether our termination will be non-blocking.\n   */\n  void\n  exit_sunshine(int exit_code, bool async);\n\n  /**\n   * @brief Breaks into the debugger or terminates Sunshine if no debugger is attached.\n   */\n  void\n  debug_trap();\n\n  /**\n   * @brief Get the argv array passed to main().\n   */\n  char **\n  get_argv();\n}  // namespace lifetime\n\n/**\n * @brief Log the publisher metadata provided from CMake.\n */\nvoid\nlog_publisher_data();\n\n#ifdef _WIN32\n/**\n * @brief Check if NVIDIA's GameStream software is running.\n * @return `true` if GameStream is enabled, `false` otherwise.\n */\nbool\nis_gamestream_enabled();\n\n/**\n * @brief Namespace for controlling the Sunshine service model on Windows.\n */\nnamespace service_ctrl {\n  /**\n   * @brief Check if the service is running.\n   * @examples\n   * is_service_running();\n   * @examples_end\n   */\n  bool\n  is_service_running();\n\n  /**\n   * @brief Start the service and wait for startup to complete.\n   * @examples\n   * start_service();\n   * @examples_end\n   */\n  bool\n  start_service();\n\n  /**\n   * @brief Wait for the UI to be ready after Sunshine startup.\n   * @examples\n   * wait_for_ui_ready();\n   * @examples_end\n   */\n  bool\n  wait_for_ui_ready();\n}  // namespace service_ctrl\n#endif\n"
  },
  {
    "path": "src/file_handler.cpp",
    "content": "/**\n * @file file_handler.cpp\n * @brief Definitions for file handling functions.\n */\n\n// standard includes\n#include <filesystem>\n#include <fstream>\n\n// local includes\n#include \"file_handler.h\"\n#include \"logging.h\"\n\nnamespace file_handler {\n  std::string\n  get_parent_directory(const std::string &path) {\n    // remove any trailing path separators\n    std::string trimmed_path = path;\n    while (!trimmed_path.empty() && trimmed_path.back() == '/') {\n      trimmed_path.pop_back();\n    }\n\n    std::filesystem::path p(trimmed_path);\n    return p.parent_path().string();\n  }\n\n  bool\n  make_directory(const std::string &path) {\n    // first, check if the directory already exists\n    if (std::filesystem::exists(path)) {\n      return true;\n    }\n\n    return std::filesystem::create_directories(path);\n  }\n\n  std::string\n  read_file(const char *path) {\n    if (!std::filesystem::exists(path)) {\n      BOOST_LOG(debug) << \"Missing file: \" << path;\n      return {};\n    }\n\n    std::ifstream in(path);\n    return std::string { (std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>() };\n  }\n\n  int\n  write_file(const char *path, const std::string_view &contents) {\n    std::ofstream out(path);\n\n    if (!out.is_open()) {\n      return -1;\n    }\n\n    out << contents;\n\n    return 0;\n  }\n}  // namespace file_handler\n"
  },
  {
    "path": "src/file_handler.h",
    "content": "/**\n * @file file_handler.h\n * @brief Declarations for file handling functions.\n */\n#pragma once\n\n#include <string>\n\n/**\n * @brief Responsible for file handling functions.\n */\nnamespace file_handler {\n  /**\n   * @brief Get the parent directory of a file or directory.\n   * @param path The path of the file or directory.\n   * @return The parent directory.\n   * @examples\n   * std::string parent_dir = get_parent_directory(\"path/to/file\");\n   * @examples_end\n   */\n  std::string\n  get_parent_directory(const std::string &path);\n\n  /**\n   * @brief Make a directory.\n   * @param path The path of the directory.\n   * @return `true` on success, `false` on failure.\n   * @examples\n   * bool dir_created = make_directory(\"path/to/directory\");\n   * @examples_end\n   */\n  bool\n  make_directory(const std::string &path);\n\n  /**\n   * @brief Read a file to string.\n   * @param path The path of the file.\n   * @return The contents of the file.\n   * @examples\n   * std::string contents = read_file(\"path/to/file\");\n   * @examples_end\n   */\n  std::string\n  read_file(const char *path);\n\n  /**\n   * @brief Writes a file.\n   * @param path The path of the file.\n   * @param contents The contents to write.\n   * @return ``0`` on success, ``-1`` on failure.\n   * @examples\n   * int write_status = write_file(\"path/to/file\", \"file contents\");\n   * @examples_end\n   */\n  int\n  write_file(const char *path, const std::string_view &contents);\n}  // namespace file_handler\n"
  },
  {
    "path": "src/globals.cpp",
    "content": "/**\n * @file globals.cpp\n * @brief Definitions for globally accessible variables and functions.\n */\n#include \"globals.h\"\n\nsafe::mail_t mail::man;\nthread_pool_util::ThreadPool task_pool;\nbool display_cursor = true;\n\n#ifdef _WIN32\nnvprefs::nvprefs_interface nvprefs_instance;\nconst std::string VDD_NAME = \"ZakoHDR\";\nconst std::string ZAKO_NAME = \"Zako HDR\";\nstd::string zako_device_id;\nbool is_running_as_system_user = false;\n#endif\n"
  },
  {
    "path": "src/globals.h",
    "content": "/**\n * @file globals.h\n * @brief Declarations for globally accessible variables and functions.\n */\n#pragma once\n\n#include \"entry_handler.h\"\n#include \"thread_pool.h\"\n/**\n * @brief The encryption flag for microphone data.\n */\n#define SS_ENC_MIC 0x08\n\n/**\n * @brief A thread pool for processing tasks.\n */\nextern thread_pool_util::ThreadPool task_pool;\n\n/**\n * @brief A boolean flag to indicate whether the cursor should be displayed.\n */\nextern bool display_cursor;\n\n#ifdef _WIN32\n  // Declare global singleton used for NVIDIA control panel modifications\n  #include \"platform/windows/nvprefs/nvprefs_interface.h\"\n\n/**\n * @brief A global singleton used for NVIDIA control panel modifications.\n */\nextern nvprefs::nvprefs_interface nvprefs_instance;\n\nextern const std::string VDD_NAME;\nextern const std::string ZAKO_NAME;\nextern std::string zako_device_id;\n\n/**\n * @brief Cached result of is_running_as_system() check.\n * @details This is set once at program startup and never changes during runtime.\n */\nextern bool is_running_as_system_user;\n#endif\n\n/**\n * @brief Handles process-wide communication.\n */\nnamespace mail {\n#define MAIL(x)                         \\\n  constexpr auto x = std::string_view { \\\n    #x                                  \\\n  }\n\n  /**\n   * @brief A process-wide communication mechanism.\n   */\n  extern safe::mail_t man;\n\n  // Global mail\n  MAIL(shutdown);\n  MAIL(broadcast_shutdown);\n  MAIL(video_packets);\n  MAIL(audio_packets);\n  MAIL(switch_display);\n\n  // Local mail\n  MAIL(touch_port);\n  MAIL(idr);\n  MAIL(invalidate_ref_frames);\n  MAIL(gamepad_feedback);\n  MAIL(hdr);\n  MAIL(dynamic_param_change);\n  MAIL(resolution_change);\n#undef MAIL\n\n}  // namespace mail\n"
  },
  {
    "path": "src/httpcommon.cpp",
    "content": "/**\n * @file src/httpcommon.cpp\n * @brief Definitions for common HTTP.\n */\n#define BOOST_BIND_GLOBAL_PLACEHOLDERS\n\n#include \"process.h\"\n\n#include <filesystem>\n#include <utility>\n\n#include <boost/property_tree/json_parser.hpp>\n#include <boost/property_tree/ptree.hpp>\n#include <boost/property_tree/xml_parser.hpp>\n\n#include <cstring>\n\n#include <boost/asio/ssl/context.hpp>\n\n#include <Simple-Web-Server/server_http.hpp>\n#include <Simple-Web-Server/server_https.hpp>\n#include <boost/asio/ssl/context_base.hpp>\n#include <curl/curl.h>\n\n#include \"config.h\"\n#include \"crypto.h\"\n#include \"file_handler.h\"\n#include \"httpcommon.h\"\n#include \"logging.h\"\n#include \"network.h\"\n#include \"nvhttp.h\"\n#include \"platform/common.h\"\n#include \"rtsp.h\"\n#include \"utility.h\"\n#include \"uuid.h\"\n\nnamespace http {\n  using namespace std::literals;\n  namespace fs = std::filesystem;\n  namespace pt = boost::property_tree;\n\n  int\n  reload_user_creds(const std::string &file);\n  bool\n  user_creds_exist(const std::string &file);\n\n  std::string unique_id;\n  net::net_e origin_web_ui_allowed;\n\n  int\n  init() {\n    bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];\n    origin_web_ui_allowed = net::from_enum_string(config::nvhttp.origin_web_ui_allowed);\n\n    if (clean_slate) {\n      unique_id = uuid_util::uuid_t::generate().string();\n      auto dir = std::filesystem::temp_directory_path() / \"Sunshine\"sv;\n      config::nvhttp.cert = (dir / (\"cert-\"s + unique_id)).string();\n      config::nvhttp.pkey = (dir / (\"pkey-\"s + unique_id)).string();\n    }\n\n    if ((!fs::exists(config::nvhttp.pkey) || !fs::exists(config::nvhttp.cert)) &&\n        create_creds(config::nvhttp.pkey, config::nvhttp.cert)) {\n      return -1;\n    }\n    if (!user_creds_exist(config::sunshine.credentials_file)) {\n      BOOST_LOG(info) << \"Open the Web UI to set your new username and password and getting started\";\n    } else if (reload_user_creds(config::sunshine.credentials_file)) {\n      return -1;\n    }\n    return 0;\n  }\n\n  int\n  save_user_creds(const std::string &file, const std::string &username, const std::string &password, bool run_our_mouth) {\n    pt::ptree outputTree;\n\n    if (fs::exists(file)) {\n      try {\n        pt::read_json(file, outputTree);\n      }\n      catch (std::exception &e) {\n        BOOST_LOG(error) << \"Couldn't read user credentials: \"sv << e.what();\n        return -1;\n      }\n    }\n\n    auto salt = crypto::rand_alphabet(16);\n    outputTree.put(\"username\", username);\n    outputTree.put(\"salt\", salt);\n    outputTree.put(\"password\", util::hex(crypto::hash(password + salt)).to_string());\n    try {\n      pt::write_json(file, outputTree);\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(error) << \"error writing to the credentials file, perhaps try this again as an administrator? Details: \"sv << e.what();\n      return -1;\n    }\n\n    BOOST_LOG(info) << \"New credentials have been created\"sv;\n    return 0;\n  }\n\n  bool\n  user_creds_exist(const std::string &file) {\n    if (!fs::exists(file)) {\n      return false;\n    }\n\n    pt::ptree inputTree;\n    try {\n      pt::read_json(file, inputTree);\n      return inputTree.find(\"username\") != inputTree.not_found() &&\n             inputTree.find(\"password\") != inputTree.not_found() &&\n             inputTree.find(\"salt\") != inputTree.not_found();\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(error) << \"validating user credentials: \"sv << e.what();\n    }\n\n    return false;\n  }\n\n  int\n  reload_user_creds(const std::string &file) {\n    pt::ptree inputTree;\n    try {\n      pt::read_json(file, inputTree);\n      config::sunshine.username = inputTree.get<std::string>(\"username\");\n      config::sunshine.password = inputTree.get<std::string>(\"password\");\n      config::sunshine.salt = inputTree.get<std::string>(\"salt\");\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(error) << \"loading user credentials: \"sv << e.what();\n      return -1;\n    }\n    return 0;\n  }\n\n  int\n  create_creds(const std::string &pkey, const std::string &cert) {\n    fs::path pkey_path = pkey;\n    fs::path cert_path = cert;\n\n    auto creds = crypto::gen_creds(\"Sunshine Gamestream Host\"sv, 2048);\n\n    auto pkey_dir = pkey_path;\n    auto cert_dir = cert_path;\n    pkey_dir.remove_filename();\n    cert_dir.remove_filename();\n\n    std::error_code err_code {};\n    fs::create_directories(pkey_dir, err_code);\n    if (err_code) {\n      BOOST_LOG(error) << \"Couldn't create directory [\"sv << pkey_dir << \"] :\"sv << err_code.message();\n      return -1;\n    }\n\n    fs::create_directories(cert_dir, err_code);\n    if (err_code) {\n      BOOST_LOG(error) << \"Couldn't create directory [\"sv << cert_dir << \"] :\"sv << err_code.message();\n      return -1;\n    }\n\n    if (file_handler::write_file(pkey.c_str(), creds.pkey)) {\n      BOOST_LOG(error) << \"Couldn't open [\"sv << config::nvhttp.pkey << ']';\n      return -1;\n    }\n\n    if (file_handler::write_file(cert.c_str(), creds.x509)) {\n      BOOST_LOG(error) << \"Couldn't open [\"sv << config::nvhttp.cert << ']';\n      return -1;\n    }\n\n    fs::permissions(pkey_path,\n      fs::perms::owner_read | fs::perms::owner_write,\n      fs::perm_options::replace, err_code);\n\n    if (err_code) {\n      BOOST_LOG(error) << \"Couldn't change permissions of [\"sv << config::nvhttp.pkey << \"] :\"sv << err_code.message();\n      return -1;\n    }\n\n    fs::permissions(cert_path,\n      fs::perms::owner_read | fs::perms::group_read | fs::perms::others_read | fs::perms::owner_write,\n      fs::perm_options::replace, err_code);\n\n    if (err_code) {\n      BOOST_LOG(error) << \"Couldn't change permissions of [\"sv << config::nvhttp.cert << \"] :\"sv << err_code.message();\n      return -1;\n    }\n\n    return 0;\n  }\n\n  bool download_file(const std::string &url, const std::string &file, long ssl_version) {\n    BOOST_LOG(info) << \"Downloading external resource: \" << url;\n    // sonar complains about weak ssl and tls versions; however sonar cannot detect the fix\n    CURL *curl = curl_easy_init();  // NOSONAR\n    if (!curl) {\n      BOOST_LOG(error) << \"Couldn't create CURL instance [\"sv << url << ']';\n      return false;\n    }\n\n    if (std::string file_dir = file_handler::get_parent_directory(file); !file_handler::make_directory(file_dir)) {\n      BOOST_LOG(error) << \"Couldn't create directory [\"sv << file_dir << \"] for [\"sv << url << ']';\n      curl_easy_cleanup(curl);\n      return false;\n    }\n\n    FILE *fp = fopen(file.c_str(), \"wb\");\n    if (!fp) {\n      BOOST_LOG(error) << \"Couldn't open [\"sv << file << \"] for [\"sv << url << ']';\n      curl_easy_cleanup(curl);\n      return false;\n    }\n\n    curl_easy_setopt(curl, CURLOPT_SSLVERSION, ssl_version);  // NOSONAR\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);\n\n    // Security limits\n    curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)10 * 1024 * 1024); // 10MB limit\n    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);\n    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);\n    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); // Disable 302 redirects\n\n    long response_code = 0;\n    CURLcode result = curl_easy_perform(curl);\n    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);\n\n    if (result != CURLE_OK || response_code != 200) {\n      if (result != CURLE_OK) {\n        BOOST_LOG(error) << \"Couldn't download [\"sv << url << \", code:\" << result << ']';\n      } else {\n        BOOST_LOG(error) << \"Download failed: HTTP \" << response_code << \" [\" << url << \"]\";\n      }\n      // Force result to fail state if we got a non-200 response code\n      result = (result == CURLE_OK) ? CURLE_HTTP_RETURNED_ERROR : result;\n    }\n\n    curl_easy_cleanup(curl);\n    fclose(fp);\n    if (result != CURLE_OK) {\n        // Cleanup partial file\n        if (fs::exists(file)) {\n            boost::system::error_code ec;\n            fs::remove(file, ec); // Don't crash if delete fails\n        }\n    }\n    return result == CURLE_OK;\n  }\n\n  size_t string_write_callback(void *contents, size_t size, size_t nmemb, void *userp) {\n    size_t realsize = size * nmemb;\n    auto *str = static_cast<std::string*>(userp);\n    \n    // Safety check: Don't allow string to grow beyond strict limits\n    if (str->size() + realsize > 10 * 1024 * 1024) {\n      BOOST_LOG(error) << \"Fetch URL: memory limit exceeded\";\n      return 0;\n    }\n    \n    str->append(static_cast<char*>(contents), realsize);\n    return realsize;\n  }\n\n  bool fetch_url(const std::string &url, std::string &content, long ssl_version) {\n    BOOST_LOG(info) << \"Fetching external resource: \" << url;\n    CURL *curl = curl_easy_init();\n    if (!curl) {\n      BOOST_LOG(error) << \"Couldn't create CURL instance [\"sv << url << ']';\n      return false;\n    }\n\n    content.clear();\n    // Reserve some memory to reduce reallocations\n    content.reserve(4096);\n\n    curl_easy_setopt(curl, CURLOPT_SSLVERSION, ssl_version);\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, string_write_callback);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &content);\n\n    // Security limits\n    curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)10 * 1024 * 1024); // 10MB limit\n    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);\n    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);\n    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);\n\n    long response_code = 0;\n    CURLcode result = curl_easy_perform(curl);\n    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);\n\n    curl_easy_cleanup(curl);\n\n    if (result != CURLE_OK || response_code != 200) {\n      if (result != CURLE_OK) {\n        BOOST_LOG(error) << \"Couldn't fetch [\"sv << url << \", code:\" << result << ']';\n      } else {\n        BOOST_LOG(error) << \"Fetch failed: HTTP \" << response_code << \" [\" << url << \"]\";\n      }\n      return false;\n    }\n\n    return true;\n  }\n\n  bool post_json(const std::string &url, const std::string &body, const std::map<std::string, std::string> &headers, std::string &response_body, long &http_code, long timeout_seconds) {\n    BOOST_LOG(info) << \"POST JSON to: \" << url;\n    CURL *curl = curl_easy_init();\n    if (!curl) {\n      BOOST_LOG(error) << \"Couldn't create CURL instance for POST [\"sv << url << ']';\n      return false;\n    }\n\n    response_body.clear();\n    response_body.reserve(4096);\n\n    // Build custom headers\n    struct curl_slist *header_list = nullptr;\n    header_list = curl_slist_append(header_list, \"Content-Type: application/json\");\n    for (const auto &[key, value] : headers) {\n      std::string header_line = key + \": \" + value;\n      header_list = curl_slist_append(header_list, header_line.c_str());\n    }\n\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    curl_easy_setopt(curl, CURLOPT_POST, 1L);\n    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());\n    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(body.size()));\n    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, string_write_callback);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body);\n\n    // Security & timeout\n    curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)10 * 1024 * 1024);\n    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);\n    curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_seconds);\n    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);\n    curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);\n\n    CURLcode result = curl_easy_perform(curl);\n    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);\n\n    curl_slist_free_all(header_list);\n    curl_easy_cleanup(curl);\n\n    if (result != CURLE_OK) {\n      BOOST_LOG(error) << \"POST failed [\"sv << url << \", curl code:\" << result << ']';\n      return false;\n    }\n\n    return true;\n  }\n\n  std::string url_escape(const std::string &url) {\n    char *string = curl_easy_escape(nullptr, url.c_str(), static_cast<int>(url.length()));\n    std::string result(string);\n    curl_free(string);\n    return result;\n  }\n\n  std::string\n  url_get_host(const std::string &url) {\n    CURLU *curlu = curl_url();\n    curl_url_set(curlu, CURLUPART_URL, url.c_str(), static_cast<unsigned int>(url.length()));\n    char *host;\n    if (curl_url_get(curlu, CURLUPART_HOST, &host, 0) != CURLUE_OK) {\n      curl_url_cleanup(curlu);\n      return \"\";\n    }\n    std::string result(host);\n    curl_free(host);\n    curl_url_cleanup(curlu);\n    return result;\n  }\n}  // namespace http\n\nnamespace {\n  struct ImageCheckContext {\n    std::string filename;\n    std::string url;\n    FILE *fp = nullptr;\n    unsigned char buffer[12]; // Buffer for magic bytes\n    size_t buffer_len = 0;\n    bool checked = false;\n    bool valid = false;\n\n    // Ensure buffer is large enough for our checks to avoid overflow\n    static_assert(sizeof(buffer) >= 12, \"Image check buffer too small\");\n  };\n\n  size_t image_write_callback(void *ptr, size_t size, size_t nmemb, void *userdata) {\n    try {\n      if (!ptr || !userdata) {\n        return 0;\n      }\n\n      // Check for overflow in size calculation\n      if (size > 0 && nmemb > SIZE_MAX / size) {\n        auto *ctx = static_cast<ImageCheckContext *>(userdata);\n        BOOST_LOG(error) << \"Image check size overflow [\"sv << (ctx ? ctx->url : \"unknown\") << ']';\n        return 0;\n      }\n\n      auto *ctx = static_cast<ImageCheckContext *>(userdata);\n      size_t total_size = size * nmemb;\n      if (total_size == 0) {\n        return 0;\n      }\n      const unsigned char *data = static_cast<const unsigned char *>(ptr);\n\n      // If not yet checked, accumulating bytes\n      if (!ctx->checked) {\n        size_t needed = sizeof(ctx->buffer) - ctx->buffer_len;\n        size_t to_copy = std::min(needed, total_size);\n\n        memcpy(ctx->buffer + ctx->buffer_len, data, to_copy);\n        ctx->buffer_len += to_copy;\n\n        // Have we accumulated enough?\n        if (ctx->buffer_len == sizeof(ctx->buffer)) {\n          ctx->checked = true;\n          unsigned char *magic = ctx->buffer;\n\n          // Perform Magic Byte Check\n          // PNG: 89 50 4E 47\n          if (magic[0] == 0x89 && magic[1] == 0x50 && magic[2] == 0x4E && magic[3] == 0x47) ctx->valid = true;\n          // JPG: FF D8 FF\n          else if (magic[0] == 0xFF && magic[1] == 0xD8 && magic[2] == 0xFF) ctx->valid = true;\n          // BMP: 42 4D\n          else if (magic[0] == 0x42 && magic[1] == 0x4D) ctx->valid = true;\n          // WEBP: RIFF ... WEBP\n          else if (memcmp(magic, \"RIFF\", 4) == 0 && memcmp(magic + 8, \"WEBP\", 4) == 0) ctx->valid = true;\n          // ICO: 00 00 01 00\n          else if (magic[0] == 0x00 && magic[1] == 0x00 && magic[2] == 0x01 && magic[3] == 0x00) ctx->valid = true;\n\n          if (!ctx->valid) {\n            BOOST_LOG(warning) << \"Streaming validation failed: Invalid magic bytes [\"sv << ctx->url << ']';\n            return 0; // Stop download\n          }\n\n          // Check passed, open file\n          ctx->fp = fopen(ctx->filename.c_str(), \"wb\");\n          if (!ctx->fp) {\n            BOOST_LOG(error) << \"Couldn't open [\"sv << ctx->filename << \"] for [\"sv << ctx->url << ']';\n            return 0;\n          }\n\n          // Flush buffer to file\n          fwrite(ctx->buffer, 1, ctx->buffer_len, ctx->fp);\n        }\n        \n        // If we have leftovers in this chunk that weren't part of the buffer fill\n        if (total_size > to_copy) {\n          if (ctx->valid && ctx->fp) {\n            fwrite(data + to_copy, 1, total_size - to_copy, ctx->fp);\n          } else if (!ctx->valid && ctx->checked) {\n             // Should have returned 0 above, but just in case logic flows here\n             return 0;\n          }\n        }\n      } else {\n        // Already checked and valid, just write\n        if (ctx->valid && ctx->fp) {\n          fwrite(ptr, size, nmemb, ctx->fp);\n        } else {\n          return 0;\n        }\n      }\n\n      return total_size;\n    } catch (...) {\n      BOOST_LOG(error) << \"Exception in image_write_callback\";\n      return 0;\n    }\n  }\n}\n\nnamespace http {\n  bool download_image_with_magic_check(const std::string &url, const std::string &file, long ssl_version) {\n    BOOST_LOG(info) << \"Downloading external image with magic check: \" << url;\n    CURL *curl = curl_easy_init();\n    if (!curl) {\n      BOOST_LOG(error) << \"Couldn't create CURL instance [\"sv << url << ']';\n      return false;\n    }\n\n    if (std::string file_dir = file_handler::get_parent_directory(file); !file_handler::make_directory(file_dir)) {\n      BOOST_LOG(error) << \"Couldn't create directory [\"sv << file_dir << \"] for [\"sv << url << ']';\n      curl_easy_cleanup(curl);\n      return false;\n    }\n\n    ImageCheckContext ctx;\n    ctx.filename = file;\n    ctx.url = url;\n\n    curl_easy_setopt(curl, CURLOPT_SSLVERSION, ssl_version);\n    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, image_write_callback);\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx);\n    \n    // Security limits\n    curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)10 * 1024 * 1024); // 10MB limit\n\n    // Disable redirects\n    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);\n    // Timeouts\n    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);\n    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);\n\n    long response_code = 0;\n    CURLcode result = curl_easy_perform(curl);\n    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);\n    \n    if (ctx.fp) {\n      fclose(ctx.fp);\n    }\n\n    curl_easy_cleanup(curl);\n\n    bool http_ok = (response_code == 200);\n\n    if (result != CURLE_OK || !http_ok) {\n      if (result != CURLE_OK) {\n        BOOST_LOG(error) << \"Download failed or rejected [\"sv << url << \", code:\" << result << ']';\n      } else {\n        BOOST_LOG(error) << \"Download failed: HTTP \" << response_code << \" [\" << url << \"]\";\n      }\n      \n      // Cleanup partial file if it exists (though usually it shouldn't be much)\n      if (boost::filesystem::exists(file)) {\n        boost::system::error_code ec;\n        boost::filesystem::remove(file, ec);\n      }\n      return false;\n    }\n\n    // Double check: if download finished but we never got enough bytes to check?\n    // Treat as failure (empty or too small file)\n    if (!ctx.checked) {\n       BOOST_LOG(warning) << \"Download too small to validate magic bytes [\"sv << url << ']';\n       // Cleanup if file was created\n       if (boost::filesystem::exists(file)) {\n         boost::system::error_code ec;\n         boost::filesystem::remove(file, ec);\n       }\n       return false;\n    }\n\n    return true;\n  }\n}\n\n"
  },
  {
    "path": "src/httpcommon.h",
    "content": "/**\n * @file src/httpcommon.h\n * @brief Declarations for common HTTP.\n */\n#pragma once\n\n// lib includes\n#include <curl/curl.h>\n#include <map>\n#include <string>\n\n// local includes\n#include \"network.h\"\n#include \"thread_safe.h\"\n\nnamespace http {\n\n  int\n  init();\n  int\n  create_creds(const std::string &pkey, const std::string &cert);\n  int\n  save_user_creds(\n    const std::string &file,\n    const std::string &username,\n    const std::string &password,\n    bool run_our_mouth = false);\n\n  int reload_user_creds(const std::string &file);\n  bool download_file(const std::string &url, const std::string &file, long ssl_version = CURL_SSLVERSION_TLSv1_2);\n  bool fetch_url(const std::string &url, std::string &content, long ssl_version = CURL_SSLVERSION_TLSv1_2);\n  bool post_json(const std::string &url, const std::string &body, const std::map<std::string, std::string> &headers, std::string &response_body, long &http_code, long timeout_seconds = 120);\n  bool download_image_with_magic_check(const std::string &url, const std::string &file, long ssl_version = CURL_SSLVERSION_TLSv1_2);\n  std::string url_escape(const std::string &url);\n  std::string url_get_host(const std::string &url);\n\n  extern std::string unique_id;\n  extern net::net_e origin_web_ui_allowed;\n\n}  // namespace http\n"
  },
  {
    "path": "src/input.cpp",
    "content": "/**\n * @file src/input.cpp\n * @brief Definitions for gamepad, keyboard, and mouse input handling.\n */\n// define uint32_t for <moonlight-common-c/src/Input.h>\n#include <cstdint>\nextern \"C\" {\n#include <moonlight-common-c/src/Input.h>\n#include <moonlight-common-c/src/Limelight.h>\n}\n\n#include <bitset>\n#include <chrono>\n#include <cmath>\n#include <list>\n#include <thread>\n#include <unordered_map>\n\n#include \"config.h\"\n#include \"globals.h\"\n#include \"input.h\"\n#include \"logging.h\"\n#include \"platform/common.h\"\n#include \"display_device/session.h\"\n#include \"thread_pool.h\"\n#include \"utility.h\"\n\n#include <boost/endian/buffers.hpp>\n\n// Win32 WHEEL_DELTA constant\n#ifndef WHEEL_DELTA\n  #define WHEEL_DELTA 120\n#endif\n\nusing namespace std::literals;\nnamespace input {\n\n  constexpr auto MAX_GAMEPADS = std::min((std::size_t) platf::MAX_GAMEPADS, sizeof(std::int16_t) * 8);\n#define DISABLE_LEFT_BUTTON_DELAY ((thread_pool_util::ThreadPool::task_id_t) 0x01)\n#define ENABLE_LEFT_BUTTON_DELAY nullptr\n\n  constexpr auto VKEY_SHIFT = 0x10;\n  constexpr auto VKEY_LSHIFT = 0xA0;\n  constexpr auto VKEY_RSHIFT = 0xA1;\n  constexpr auto VKEY_CONTROL = 0x11;\n  constexpr auto VKEY_LCONTROL = 0xA2;\n  constexpr auto VKEY_RCONTROL = 0xA3;\n  constexpr auto VKEY_MENU = 0x12;\n  constexpr auto VKEY_LMENU = 0xA4;\n  constexpr auto VKEY_RMENU = 0xA5;\n\n  enum class button_state_e {\n    NONE,  ///< No button state\n    DOWN,  ///< Button is down\n    UP  ///< Button is up\n  };\n\n  template <std::size_t N>\n  int\n  alloc_id(std::bitset<N> &gamepad_mask) {\n    for (int x = 0; x < gamepad_mask.size(); ++x) {\n      if (!gamepad_mask[x]) {\n        gamepad_mask[x] = true;\n        return x;\n      }\n    }\n\n    return -1;\n  }\n\n  template <std::size_t N>\n  void\n  free_id(std::bitset<N> &gamepad_mask, int id) {\n    gamepad_mask[id] = false;\n  }\n\n  typedef uint32_t key_press_id_t;\n  key_press_id_t\n  make_kpid(uint16_t vk, uint8_t flags) {\n    return (key_press_id_t) vk << 8 | flags;\n  }\n  uint16_t\n  vk_from_kpid(key_press_id_t kpid) {\n    return kpid >> 8;\n  }\n  uint8_t\n  flags_from_kpid(key_press_id_t kpid) {\n    return kpid & 0xFF;\n  }\n\n  /**\n   * @brief Convert a little-endian netfloat to a native endianness float.\n   * @param f Netfloat value.\n   * @return The native endianness float value.\n   */\n  float\n  from_netfloat(netfloat f) {\n    return boost::endian::endian_load<float, sizeof(float), boost::endian::order::little>(f);\n  }\n\n  /**\n   * @brief Convert a little-endian netfloat to a native endianness float and clamps it.\n   * @param f Netfloat value.\n   * @param min The minimium value for clamping.\n   * @param max The maximum value for clamping.\n   * @return Clamped native endianess float value.\n   */\n  float\n  from_clamped_netfloat(netfloat f, float min, float max) {\n    return std::clamp(from_netfloat(f), min, max);\n  }\n\n  static task_pool_util::TaskPool::task_id_t key_press_repeat_id {};\n  static std::unordered_map<key_press_id_t, bool> key_press {};\n  static std::array<std::uint8_t, 5> mouse_press {};\n\n  static platf::input_t platf_input;\n  static std::bitset<platf::MAX_GAMEPADS> gamepadMask {};\n\n  void\n  free_gamepad(platf::input_t &platf_input, int id) {\n    platf::gamepad_update(platf_input, id, platf::gamepad_state_t {});\n    platf::free_gamepad(platf_input, id);\n\n    free_id(gamepadMask, id);\n  }\n  struct gamepad_t {\n    gamepad_t():\n        gamepad_state {}, back_timeout_id {}, id { -1 }, back_button_state { button_state_e::NONE } {}\n    ~gamepad_t() {\n      if (id >= 0) {\n        task_pool.push([id = this->id]() {\n          free_gamepad(platf_input, id);\n        });\n      }\n    }\n\n    platf::gamepad_state_t gamepad_state;\n\n    thread_pool_util::ThreadPool::task_id_t back_timeout_id;\n\n    int id;\n\n    // When emulating the HOME button, we may need to artificially release the back button.\n    // Afterwards, the gamepad state on sunshine won't match the state on Moonlight.\n    // To prevent Sunshine from sending erroneous input data to the active application,\n    // Sunshine forces the button to be in a specific state until the gamepad state matches that of\n    // Moonlight once more.\n    button_state_e back_button_state;\n  };\n\n  struct input_t {\n    enum shortkey_e {\n      CTRL = 0x1,  ///< Control key\n      ALT = 0x2,  ///< Alt key\n      SHIFT = 0x4,  ///< Shift key\n      SHORTCUT = CTRL | ALT | SHIFT  ///< Shortcut combination\n    };\n\n    input_t(\n      safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event,\n      platf::feedback_queue_t feedback_queue):\n        shortcutFlags {},\n        gamepads(MAX_GAMEPADS),\n        client_context { platf::allocate_client_input_context(platf_input) },\n        touch_port_event { std::move(touch_port_event) },\n        feedback_queue { std::move(feedback_queue) },\n        mouse_left_button_timeout {},\n        touch_port { { 0, 0, 0, 0 }, 0, 0, 1.0f },\n        accumulated_vscroll_delta {},\n        accumulated_hscroll_delta {} {}\n\n    // Keep track of alt+ctrl+shift key combo\n    int shortcutFlags;\n\n    std::vector<gamepad_t> gamepads;\n    std::unique_ptr<platf::client_input_t> client_context;\n\n    safe::mail_raw_t::event_t<input::touch_port_t> touch_port_event;\n    platf::feedback_queue_t feedback_queue;\n\n    std::list<std::vector<uint8_t>> input_queue;\n    std::mutex input_queue_lock;\n\n    thread_pool_util::ThreadPool::task_id_t mouse_left_button_timeout;\n\n    input::touch_port_t touch_port;\n\n    int32_t accumulated_vscroll_delta;\n    int32_t accumulated_hscroll_delta;\n  };\n\n  /**\n   * @brief Apply shortcut based on VKEY\n   * @param keyCode The VKEY code\n   * @return 0 if no shortcut applied, > 0 if shortcut applied.\n   */\n  inline int\n  apply_shortcut(short keyCode) {\n    constexpr auto SUNSHINE_VK_F1 = 0x70;\n    constexpr auto SUNSHINE_VK_F13 = 0x7C;\n\n    BOOST_LOG(debug) << \"Apply Shortcut: 0x\"sv << util::hex((std::uint8_t) keyCode).to_string_view();\n\n    if (keyCode >= SUNSHINE_VK_F1 && keyCode <= SUNSHINE_VK_F13) {\n      mail::man->event<int>(mail::switch_display)->raise(keyCode - SUNSHINE_VK_F1);\n      return 1;\n    }\n\n    switch (keyCode) {\n      case 0x4E /* VKEY_N */:\n        display_cursor = !display_cursor;\n        return 1;\n      case 0x56 /* VKEY_V */:\n        display_device::session_t::get().toggle_display_power();\n        return 1;\n    }\n\n    return 0;\n  }\n\n  void\n  print(PNV_REL_MOUSE_MOVE_PACKET packet) {\n    BOOST_LOG(debug)\n      << \"--begin relative mouse move packet--\"sv << std::endl\n      << \"deltaX [\"sv << util::endian::big(packet->deltaX) << ']' << std::endl\n      << \"deltaY [\"sv << util::endian::big(packet->deltaY) << ']' << std::endl\n      << \"--end relative mouse move packet--\"sv;\n  }\n\n  void\n  print(PNV_ABS_MOUSE_MOVE_PACKET packet) {\n    BOOST_LOG(debug)\n      << \"--begin absolute mouse move packet--\"sv << std::endl\n      << \"x      [\"sv << util::endian::big(packet->x) << ']' << std::endl\n      << \"y      [\"sv << util::endian::big(packet->y) << ']' << std::endl\n      << \"width  [\"sv << util::endian::big(packet->width) << ']' << std::endl\n      << \"height [\"sv << util::endian::big(packet->height) << ']' << std::endl\n      << \"--end absolute mouse move packet--\"sv;\n  }\n\n  void\n  print(PNV_MOUSE_BUTTON_PACKET packet) {\n    BOOST_LOG(debug)\n      << \"--begin mouse button packet--\"sv << std::endl\n      << \"action [\"sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl\n      << \"button [\"sv << util::hex(packet->button).to_string_view() << ']' << std::endl\n      << \"--end mouse button packet--\"sv;\n  }\n\n  void\n  print(PNV_SCROLL_PACKET packet) {\n    BOOST_LOG(debug)\n      << \"--begin mouse scroll packet--\"sv << std::endl\n      << \"scrollAmt1 [\"sv << util::endian::big(packet->scrollAmt1) << ']' << std::endl\n      << \"--end mouse scroll packet--\"sv;\n  }\n\n  void\n  print(PSS_HSCROLL_PACKET packet) {\n    BOOST_LOG(debug)\n      << \"--begin mouse hscroll packet--\"sv << std::endl\n      << \"scrollAmount [\"sv << util::endian::big(packet->scrollAmount) << ']' << std::endl\n      << \"--end mouse hscroll packet--\"sv;\n  }\n\n  void\n  print(PNV_KEYBOARD_PACKET packet) {\n    BOOST_LOG(debug)\n      << \"--begin keyboard packet--\"sv << std::endl\n      << \"keyAction [\"sv << util::hex(packet->header.magic).to_string_view() << ']' << std::endl\n      << \"keyCode [\"sv << util::hex(packet->keyCode).to_string_view() << ']' << std::endl\n      << \"modifiers [\"sv << util::hex(packet->modifiers).to_string_view() << ']' << std::endl\n      << \"flags [\"sv << util::hex(packet->flags).to_string_view() << ']' << std::endl\n      << \"--end keyboard packet--\"sv;\n  }\n\n  void\n  print(PNV_UNICODE_PACKET packet) {\n    std::string text(packet->text, util::endian::big(packet->header.size) - sizeof(packet->header.magic));\n    BOOST_LOG(debug)\n      << \"--begin unicode packet--\"sv << std::endl\n      << \"text [\"sv << text << ']' << std::endl\n      << \"--end unicode packet--\"sv;\n  }\n\n  void\n  print(PNV_MULTI_CONTROLLER_PACKET packet) {\n    // Moonlight spams controller packet even when not necessary\n    BOOST_LOG(verbose)\n      << \"--begin controller packet--\"sv << std::endl\n      << \"controllerNumber [\"sv << packet->controllerNumber << ']' << std::endl\n      << \"activeGamepadMask [\"sv << util::hex(packet->activeGamepadMask).to_string_view() << ']' << std::endl\n      << \"buttonFlags [\"sv << util::hex((uint32_t) packet->buttonFlags | (packet->buttonFlags2 << 16)).to_string_view() << ']' << std::endl\n      << \"leftTrigger [\"sv << util::hex(packet->leftTrigger).to_string_view() << ']' << std::endl\n      << \"rightTrigger [\"sv << util::hex(packet->rightTrigger).to_string_view() << ']' << std::endl\n      << \"leftStickX [\"sv << packet->leftStickX << ']' << std::endl\n      << \"leftStickY [\"sv << packet->leftStickY << ']' << std::endl\n      << \"rightStickX [\"sv << packet->rightStickX << ']' << std::endl\n      << \"rightStickY [\"sv << packet->rightStickY << ']' << std::endl\n      << \"--end controller packet--\"sv;\n  }\n\n  /**\n   * @brief Prints a touch packet.\n   * @param packet The touch packet.\n   */\n  void\n  print(PSS_TOUCH_PACKET packet) {\n    BOOST_LOG(debug)\n      << \"--begin touch packet--\"sv << std::endl\n      << \"eventType [\"sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl\n      << \"pointerId [\"sv << util::hex(packet->pointerId).to_string_view() << ']' << std::endl\n      << \"x [\"sv << from_netfloat(packet->x) << ']' << std::endl\n      << \"y [\"sv << from_netfloat(packet->y) << ']' << std::endl\n      << \"pressureOrDistance [\"sv << from_netfloat(packet->pressureOrDistance) << ']' << std::endl\n      << \"contactAreaMajor [\"sv << from_netfloat(packet->contactAreaMajor) << ']' << std::endl\n      << \"contactAreaMinor [\"sv << from_netfloat(packet->contactAreaMinor) << ']' << std::endl\n      << \"rotation [\"sv << (uint32_t) packet->rotation << ']' << std::endl\n      << \"--end touch packet--\"sv;\n  }\n\n  /**\n   * @brief Prints a pen packet.\n   * @param packet The pen packet.\n   */\n  void\n  print(PSS_PEN_PACKET packet) {\n    BOOST_LOG(debug)\n      << \"--begin pen packet--\"sv << std::endl\n      << \"eventType [\"sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl\n      << \"toolType [\"sv << util::hex(packet->toolType).to_string_view() << ']' << std::endl\n      << \"penButtons [\"sv << util::hex(packet->penButtons).to_string_view() << ']' << std::endl\n      << \"x [\"sv << from_netfloat(packet->x) << ']' << std::endl\n      << \"y [\"sv << from_netfloat(packet->y) << ']' << std::endl\n      << \"pressureOrDistance [\"sv << from_netfloat(packet->pressureOrDistance) << ']' << std::endl\n      << \"contactAreaMajor [\"sv << from_netfloat(packet->contactAreaMajor) << ']' << std::endl\n      << \"contactAreaMinor [\"sv << from_netfloat(packet->contactAreaMinor) << ']' << std::endl\n      << \"rotation [\"sv << (uint32_t) packet->rotation << ']' << std::endl\n      << \"tilt [\"sv << (uint32_t) packet->tilt << ']' << std::endl\n      << \"--end pen packet--\"sv;\n  }\n\n  /**\n   * @brief Prints a controller arrival packet.\n   * @param packet The controller arrival packet.\n   */\n  void\n  print(PSS_CONTROLLER_ARRIVAL_PACKET packet) {\n    BOOST_LOG(debug)\n      << \"--begin controller arrival packet--\"sv << std::endl\n      << \"controllerNumber [\"sv << (uint32_t) packet->controllerNumber << ']' << std::endl\n      << \"type [\"sv << util::hex(packet->type).to_string_view() << ']' << std::endl\n      << \"capabilities [\"sv << util::hex(packet->capabilities).to_string_view() << ']' << std::endl\n      << \"supportedButtonFlags [\"sv << util::hex(packet->supportedButtonFlags).to_string_view() << ']' << std::endl\n      << \"--end controller arrival packet--\"sv;\n  }\n\n  /**\n   * @brief Prints a controller touch packet.\n   * @param packet The controller touch packet.\n   */\n  void\n  print(PSS_CONTROLLER_TOUCH_PACKET packet) {\n    BOOST_LOG(debug)\n      << \"--begin controller touch packet--\"sv << std::endl\n      << \"controllerNumber [\"sv << (uint32_t) packet->controllerNumber << ']' << std::endl\n      << \"eventType [\"sv << util::hex(packet->eventType).to_string_view() << ']' << std::endl\n      << \"pointerId [\"sv << util::hex(packet->pointerId).to_string_view() << ']' << std::endl\n      << \"x [\"sv << from_netfloat(packet->x) << ']' << std::endl\n      << \"y [\"sv << from_netfloat(packet->y) << ']' << std::endl\n      << \"pressure [\"sv << from_netfloat(packet->pressure) << ']' << std::endl\n      << \"--end controller touch packet--\"sv;\n  }\n\n  /**\n   * @brief Prints a controller motion packet.\n   * @param packet The controller motion packet.\n   */\n  void\n  print(PSS_CONTROLLER_MOTION_PACKET packet) {\n    BOOST_LOG(verbose)\n      << \"--begin controller motion packet--\"sv << std::endl\n      << \"controllerNumber [\"sv << util::hex(packet->controllerNumber).to_string_view() << ']' << std::endl\n      << \"motionType [\"sv << util::hex(packet->motionType).to_string_view() << ']' << std::endl\n      << \"x [\"sv << from_netfloat(packet->x) << ']' << std::endl\n      << \"y [\"sv << from_netfloat(packet->y) << ']' << std::endl\n      << \"z [\"sv << from_netfloat(packet->z) << ']' << std::endl\n      << \"--end controller motion packet--\"sv;\n  }\n\n  /**\n   * @brief Prints a controller battery packet.\n   * @param packet The controller battery packet.\n   */\n  void\n  print(PSS_CONTROLLER_BATTERY_PACKET packet) {\n    BOOST_LOG(verbose)\n      << \"--begin controller battery packet--\"sv << std::endl\n      << \"controllerNumber [\"sv << util::hex(packet->controllerNumber).to_string_view() << ']' << std::endl\n      << \"batteryState [\"sv << util::hex(packet->batteryState).to_string_view() << ']' << std::endl\n      << \"batteryPercentage [\"sv << util::hex(packet->batteryPercentage).to_string_view() << ']' << std::endl\n      << \"--end controller battery packet--\"sv;\n  }\n\n  void\n  print(void *payload) {\n    auto header = (PNV_INPUT_HEADER) payload;\n\n    switch (util::endian::little(header->magic)) {\n      case MOUSE_MOVE_REL_MAGIC_GEN5:\n        print((PNV_REL_MOUSE_MOVE_PACKET) payload);\n        break;\n      case MOUSE_MOVE_ABS_MAGIC:\n        print((PNV_ABS_MOUSE_MOVE_PACKET) payload);\n        break;\n      case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5:\n      case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5:\n        print((PNV_MOUSE_BUTTON_PACKET) payload);\n        break;\n      case SCROLL_MAGIC_GEN5:\n        print((PNV_SCROLL_PACKET) payload);\n        break;\n      case SS_HSCROLL_MAGIC:\n        print((PSS_HSCROLL_PACKET) payload);\n        break;\n      case KEY_DOWN_EVENT_MAGIC:\n      case KEY_UP_EVENT_MAGIC:\n        print((PNV_KEYBOARD_PACKET) payload);\n        break;\n      case UTF8_TEXT_EVENT_MAGIC:\n        print((PNV_UNICODE_PACKET) payload);\n        break;\n      case MULTI_CONTROLLER_MAGIC_GEN5:\n        print((PNV_MULTI_CONTROLLER_PACKET) payload);\n        break;\n      case SS_TOUCH_MAGIC:\n        print((PSS_TOUCH_PACKET) payload);\n        break;\n      case SS_PEN_MAGIC:\n        print((PSS_PEN_PACKET) payload);\n        break;\n      case SS_CONTROLLER_ARRIVAL_MAGIC:\n        print((PSS_CONTROLLER_ARRIVAL_PACKET) payload);\n        break;\n      case SS_CONTROLLER_TOUCH_MAGIC:\n        print((PSS_CONTROLLER_TOUCH_PACKET) payload);\n        break;\n      case SS_CONTROLLER_MOTION_MAGIC:\n        print((PSS_CONTROLLER_MOTION_PACKET) payload);\n        break;\n      case SS_CONTROLLER_BATTERY_MAGIC:\n        print((PSS_CONTROLLER_BATTERY_PACKET) payload);\n        break;\n    }\n  }\n\n  void\n  passthrough(std::shared_ptr<input_t> &input, PNV_REL_MOUSE_MOVE_PACKET packet) {\n    if (!config::input.mouse) {\n      return;\n    }\n\n    input->mouse_left_button_timeout = DISABLE_LEFT_BUTTON_DELAY;\n    platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY));\n  }\n\n  /**\n   * @brief Converts client coordinates on the specified surface into screen coordinates.\n   * @param input The input context.\n   * @param val The cartesian coordinate pair to convert.\n   * @param size The size of the client's surface containing the value.\n   * @return The host-relative coordinate pair if a touchport is available.\n   */\n  std::optional<std::pair<float, float>>\n  client_to_touchport(std::shared_ptr<input_t> &input, const std::pair<float, float> &val, const std::pair<float, float> &size) {\n    auto &touch_port_event = input->touch_port_event;\n    auto &touch_port = input->touch_port;\n    if (touch_port_event->peek()) {\n      touch_port = *touch_port_event->pop();\n    }\n    if (!touch_port) {\n      BOOST_LOG(verbose) << \"Ignoring early absolute input without a touch port\"sv;\n      return std::nullopt;\n    }\n\n    auto scalarX = touch_port.width / size.first;\n    auto scalarY = touch_port.height / size.second;\n\n    float x = std::clamp(val.first, 0.0f, size.first) * scalarX;\n    float y = std::clamp(val.second, 0.0f, size.second) * scalarY;\n\n    auto offsetX = touch_port.client_offsetX;\n    auto offsetY = touch_port.client_offsetY;\n\n    x = std::clamp(x, offsetX, (size.first * scalarX) - offsetX);\n    y = std::clamp(y, offsetY, (size.second * scalarY) - offsetY);\n\n    return std::pair { (x - offsetX) * touch_port.scalar_inv, (y - offsetY) * touch_port.scalar_inv };\n  }\n\n  /**\n   * @brief Multiply a polar coordinate pair by a cartesian scaling factor.\n   * @param r The radial coordinate.\n   * @param angle The angular coordinate (radians).\n   * @param scalar The scalar cartesian coordinate pair.\n   * @return The scaled radial coordinate.\n   */\n  float\n  multiply_polar_by_cartesian_scalar(float r, float angle, const std::pair<float, float> &scalar) {\n    // Convert polar to cartesian coordinates\n    float x = r * std::cos(angle);\n    float y = r * std::sin(angle);\n\n    // Scale the values\n    x *= scalar.first;\n    y *= scalar.second;\n\n    // Convert the result back to a polar radial coordinate\n    return std::sqrt(std::pow(x, 2) + std::pow(y, 2));\n  }\n\n  std::pair<float, float>\n  scale_client_contact_area(const std::pair<float, float> &val, uint16_t rotation, const std::pair<float, float> &scalar) {\n    // If the rotation is unknown, we'll just scale both axes equally by using\n    // a 45-degree angle for our scaling calculations\n    float angle = rotation == LI_ROT_UNKNOWN ? (M_PI / 4) : (rotation * (M_PI / 180));\n\n    // If we have a major but not a minor axis, treat the touch as circular\n    float major = val.first;\n    float minor = val.second != 0.0f ? val.second : val.first;\n\n    // The minor axis is perpendicular to major axis so the angle must be rotated by 90 degrees\n    return { multiply_polar_by_cartesian_scalar(major, angle, scalar), multiply_polar_by_cartesian_scalar(minor, angle + (M_PI / 2), scalar) };\n  }\n\n  void\n  passthrough(std::shared_ptr<input_t> &input, PNV_ABS_MOUSE_MOVE_PACKET packet) {\n    if (!config::input.mouse) {\n      return;\n    }\n\n    if (input->mouse_left_button_timeout == DISABLE_LEFT_BUTTON_DELAY) {\n      input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY;\n    }\n\n    float x = util::endian::big(packet->x);\n    float y = util::endian::big(packet->y);\n\n    // Prevent divide by zero\n    // Don't expect it to happen, but just in case\n    if (!packet->width || !packet->height) {\n      BOOST_LOG(warning) << \"Moonlight passed invalid dimensions\"sv;\n\n      return;\n    }\n\n    auto width = (float) util::endian::big(packet->width);\n    auto height = (float) util::endian::big(packet->height);\n\n    auto tpcoords = client_to_touchport(input, { x, y }, { width, height });\n    if (!tpcoords) {\n      return;\n    }\n\n    auto &touch_port = input->touch_port;\n    platf::touch_port_t abs_port {\n      touch_port.offset_x, touch_port.offset_y,\n      touch_port.env_width, touch_port.env_height\n    };\n\n    platf::abs_mouse(platf_input, abs_port, tpcoords->first, tpcoords->second);\n  }\n\n  void\n  passthrough(std::shared_ptr<input_t> &input, PNV_MOUSE_BUTTON_PACKET packet) {\n    if (!config::input.mouse) {\n      return;\n    }\n\n    auto release = util::endian::little(packet->header.magic) == MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5;\n    auto button = util::endian::big(packet->button);\n    if (button > 0 && button < mouse_press.size()) {\n      if (mouse_press[button] != release) {\n        // button state is already what we want\n        return;\n      }\n\n      mouse_press[button] = !release;\n    }\n    /**\n     * When Moonlight sends mouse input through absolute coordinates,\n     * it's possible that BUTTON_RIGHT is pressed down immediately after releasing BUTTON_LEFT.\n     * As a result, Sunshine will left-click on hyperlinks in the browser before right-clicking\n     *\n     * This can be solved by delaying BUTTON_LEFT, however, any delay on input is undesirable during gaming\n     * As a compromise, Sunshine will only put delays on BUTTON_LEFT when\n     * absolute mouse coordinates have been sent.\n     *\n     * Try to make sure BUTTON_RIGHT gets called before BUTTON_LEFT is released.\n     *\n     * input->mouse_left_button_timeout can only be nullptr\n     * when the last mouse coordinates were absolute\n     */\n    if (button == BUTTON_LEFT && release && !input->mouse_left_button_timeout) {\n      auto f = [=]() {\n        auto left_released = mouse_press[BUTTON_LEFT];\n        if (left_released) {\n          // Already released left button\n          return;\n        }\n        platf::button_mouse(platf_input, BUTTON_LEFT, release);\n\n        mouse_press[BUTTON_LEFT] = false;\n        input->mouse_left_button_timeout = nullptr;\n      };\n\n      input->mouse_left_button_timeout = task_pool.pushDelayed(std::move(f), 10ms).task_id;\n\n      return;\n    }\n    if (\n      button == BUTTON_RIGHT && !release &&\n      input->mouse_left_button_timeout > DISABLE_LEFT_BUTTON_DELAY) {\n      platf::button_mouse(platf_input, BUTTON_RIGHT, false);\n      platf::button_mouse(platf_input, BUTTON_RIGHT, true);\n\n      mouse_press[BUTTON_RIGHT] = false;\n\n      return;\n    }\n\n    platf::button_mouse(platf_input, button, release);\n  }\n\n  short\n  map_keycode(short keycode) {\n    auto it = config::input.keybindings.find(keycode);\n    if (it != std::end(config::input.keybindings)) {\n      return it->second;\n    }\n\n    return keycode;\n  }\n\n  /**\n   * @brief Update flags for keyboard shortcut combo's\n   */\n  inline void\n  update_shortcutFlags(int *flags, short keyCode, bool release) {\n    switch (keyCode) {\n      case VKEY_SHIFT:\n      case VKEY_LSHIFT:\n      case VKEY_RSHIFT:\n        if (release) {\n          *flags &= ~input_t::SHIFT;\n        }\n        else {\n          *flags |= input_t::SHIFT;\n        }\n        break;\n      case VKEY_CONTROL:\n      case VKEY_LCONTROL:\n      case VKEY_RCONTROL:\n        if (release) {\n          *flags &= ~input_t::CTRL;\n        }\n        else {\n          *flags |= input_t::CTRL;\n        }\n        break;\n      case VKEY_MENU:\n      case VKEY_LMENU:\n      case VKEY_RMENU:\n        if (release) {\n          *flags &= ~input_t::ALT;\n        }\n        else {\n          *flags |= input_t::ALT;\n        }\n        break;\n    }\n  }\n\n  bool\n  is_modifier(uint16_t keyCode) {\n    switch (keyCode) {\n      case VKEY_SHIFT:\n      case VKEY_LSHIFT:\n      case VKEY_RSHIFT:\n      case VKEY_CONTROL:\n      case VKEY_LCONTROL:\n      case VKEY_RCONTROL:\n      case VKEY_MENU:\n      case VKEY_LMENU:\n      case VKEY_RMENU:\n        return true;\n      default:\n        return false;\n    }\n  }\n\n  void\n  send_key_and_modifiers(uint16_t key_code, bool release, uint8_t flags, uint8_t synthetic_modifiers) {\n    if (!release) {\n      // Press any synthetic modifiers required for this key\n      if (synthetic_modifiers & MODIFIER_SHIFT) {\n        platf::keyboard_update(platf_input, VKEY_SHIFT, false, flags);\n      }\n      if (synthetic_modifiers & MODIFIER_CTRL) {\n        platf::keyboard_update(platf_input, VKEY_CONTROL, false, flags);\n      }\n      if (synthetic_modifiers & MODIFIER_ALT) {\n        platf::keyboard_update(platf_input, VKEY_MENU, false, flags);\n      }\n    }\n\n    platf::keyboard_update(platf_input, map_keycode(key_code), release, flags);\n\n    if (!release) {\n      // Raise any synthetic modifier keys we pressed\n      if (synthetic_modifiers & MODIFIER_SHIFT) {\n        platf::keyboard_update(platf_input, VKEY_SHIFT, true, flags);\n      }\n      if (synthetic_modifiers & MODIFIER_CTRL) {\n        platf::keyboard_update(platf_input, VKEY_CONTROL, true, flags);\n      }\n      if (synthetic_modifiers & MODIFIER_ALT) {\n        platf::keyboard_update(platf_input, VKEY_MENU, true, flags);\n      }\n    }\n  }\n\n  void\n  repeat_key(uint16_t key_code, uint8_t flags, uint8_t synthetic_modifiers) {\n    // If key no longer pressed, stop repeating\n    if (!key_press[make_kpid(key_code, flags)]) {\n      key_press_repeat_id = nullptr;\n      return;\n    }\n\n    send_key_and_modifiers(key_code, false, flags, synthetic_modifiers);\n\n    key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_period, key_code, flags, synthetic_modifiers).task_id;\n  }\n\n  void\n  passthrough(std::shared_ptr<input_t> &input, PNV_KEYBOARD_PACKET packet) {\n    if (!config::input.keyboard) {\n      return;\n    }\n\n    auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC;\n    auto keyCode = packet->keyCode & 0x00FF;\n\n    // Set synthetic modifier flags if the keyboard packet is requesting modifier\n    // keys that are not current pressed.\n    uint8_t synthetic_modifiers = 0;\n    if (!release && !is_modifier(keyCode)) {\n      if (!(input->shortcutFlags & input_t::SHIFT) && (packet->modifiers & MODIFIER_SHIFT)) {\n        synthetic_modifiers |= MODIFIER_SHIFT;\n      }\n      if (!(input->shortcutFlags & input_t::CTRL) && (packet->modifiers & MODIFIER_CTRL)) {\n        synthetic_modifiers |= MODIFIER_CTRL;\n      }\n      if (!(input->shortcutFlags & input_t::ALT) && (packet->modifiers & MODIFIER_ALT)) {\n        synthetic_modifiers |= MODIFIER_ALT;\n      }\n    }\n\n    auto &pressed = key_press[make_kpid(keyCode, packet->flags)];\n    if (!pressed) {\n      if (!release) {\n        // A new key has been pressed down, we need to check for key combo's\n        // If a key-combo has been pressed down, don't pass it through\n        if (input->shortcutFlags == input_t::SHORTCUT && apply_shortcut(keyCode) > 0) {\n          return;\n        }\n\n        if (key_press_repeat_id) {\n          task_pool.cancel(key_press_repeat_id);\n        }\n\n        if (config::input.key_repeat_delay.count() > 0) {\n          key_press_repeat_id = task_pool.pushDelayed(repeat_key, config::input.key_repeat_delay, keyCode, packet->flags, synthetic_modifiers).task_id;\n        }\n      }\n      else {\n        // Already released\n        return;\n      }\n    }\n    else if (!release) {\n      // Already pressed down key\n      return;\n    }\n\n    pressed = !release;\n\n    send_key_and_modifiers(keyCode, release, packet->flags, synthetic_modifiers);\n\n    update_shortcutFlags(&input->shortcutFlags, map_keycode(keyCode), release);\n  }\n\n  /**\n   * @brief Called to pass a vertical scroll message the platform backend.\n   * @param input The input context pointer.\n   * @param packet The scroll packet.\n   */\n  void\n  passthrough(std::shared_ptr<input_t> &input, PNV_SCROLL_PACKET packet) {\n    if (!config::input.mouse) {\n      return;\n    }\n\n    if (config::input.high_resolution_scrolling) {\n      platf::scroll(platf_input, util::endian::big(packet->scrollAmt1));\n    }\n    else {\n      input->accumulated_vscroll_delta += util::endian::big(packet->scrollAmt1);\n      auto full_ticks = input->accumulated_vscroll_delta / WHEEL_DELTA;\n      if (full_ticks) {\n        // Send any full ticks that have accumulated and store the rest\n        platf::scroll(platf_input, full_ticks * WHEEL_DELTA);\n        input->accumulated_vscroll_delta -= full_ticks * WHEEL_DELTA;\n      }\n    }\n  }\n\n  /**\n   * @brief Called to pass a horizontal scroll message the platform backend.\n   * @param input The input context pointer.\n   * @param packet The scroll packet.\n   */\n  void\n  passthrough(std::shared_ptr<input_t> &input, PSS_HSCROLL_PACKET packet) {\n    if (!config::input.mouse) {\n      return;\n    }\n\n    if (config::input.high_resolution_scrolling) {\n      platf::hscroll(platf_input, util::endian::big(packet->scrollAmount));\n    }\n    else {\n      input->accumulated_hscroll_delta += util::endian::big(packet->scrollAmount);\n      auto full_ticks = input->accumulated_hscroll_delta / WHEEL_DELTA;\n      if (full_ticks) {\n        // Send any full ticks that have accumulated and store the rest\n        platf::hscroll(platf_input, full_ticks * WHEEL_DELTA);\n        input->accumulated_hscroll_delta -= full_ticks * WHEEL_DELTA;\n      }\n    }\n  }\n\n  void\n  passthrough(PNV_UNICODE_PACKET packet) {\n    if (!config::input.keyboard) {\n      return;\n    }\n\n    auto size = util::endian::big(packet->header.size) - sizeof(packet->header.magic);\n    platf::unicode(platf_input, packet->text, size);\n  }\n\n  /**\n   * @brief Called to pass a controller arrival message to the platform backend.\n   * @param input The input context pointer.\n   * @param packet The controller arrival packet.\n   */\n  void\n  passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_ARRIVAL_PACKET packet) {\n    if (!config::input.controller) {\n      return;\n    }\n\n    if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {\n      BOOST_LOG(warning) << \"ControllerNumber out of range [\"sv << packet->controllerNumber << ']';\n      return;\n    }\n\n    if (input->gamepads[packet->controllerNumber].id >= 0) {\n      BOOST_LOG(warning) << \"ControllerNumber already allocated [\"sv << packet->controllerNumber << ']';\n      return;\n    }\n\n    platf::gamepad_arrival_t arrival {\n      packet->type,\n      util::endian::little(packet->capabilities),\n      util::endian::little(packet->supportedButtonFlags),\n    };\n\n    auto id = alloc_id(gamepadMask);\n    if (id < 0) {\n      return;\n    }\n\n    // Allocate a new gamepad\n    if (platf::alloc_gamepad(platf_input, { id, packet->controllerNumber }, arrival, input->feedback_queue)) {\n      free_id(gamepadMask, id);\n      return;\n    }\n\n    input->gamepads[packet->controllerNumber].id = id;\n  }\n\n  /**\n   * @brief Called to pass a touch message to the platform backend.\n   * @param input The input context pointer.\n   * @param packet The touch packet.\n   */\n  void\n  passthrough(std::shared_ptr<input_t> &input, PSS_TOUCH_PACKET packet) {\n    if (!config::input.mouse) {\n      return;\n    }\n\n    // Convert the client normalized coordinates to touchport coordinates\n    auto coords = client_to_touchport(input,\n      { from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f,\n        from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f },\n      { 65535.f, 65535.f });\n    if (!coords) {\n      return;\n    }\n\n    auto &touch_port = input->touch_port;\n    platf::touch_port_t abs_port {\n      touch_port.offset_x, touch_port.offset_y,\n      touch_port.env_width, touch_port.env_height\n    };\n\n    // Renormalize the coordinates\n    coords->first /= abs_port.width;\n    coords->second /= abs_port.height;\n\n    // Normalize rotation value to 0-359 degree range\n    auto rotation = util::endian::little(packet->rotation);\n    if (rotation != LI_ROT_UNKNOWN) {\n      rotation %= 360;\n    }\n\n    // Normalize the contact area based on the touchport\n    auto contact_area = scale_client_contact_area(\n      { from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f,\n        from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f },\n      rotation,\n      { abs_port.width / 65535.f, abs_port.height / 65535.f });\n\n    platf::touch_input_t touch {\n      packet->eventType,\n      rotation,\n      util::endian::little(packet->pointerId),\n      coords->first,\n      coords->second,\n      from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f),\n      contact_area.first,\n      contact_area.second,\n    };\n\n    platf::touch_update(input->client_context.get(), abs_port, touch);\n  }\n\n  /**\n   * @brief Called to pass a pen message to the platform backend.\n   * @param input The input context pointer.\n   * @param packet The pen packet.\n   */\n  void\n  passthrough(std::shared_ptr<input_t> &input, PSS_PEN_PACKET packet) {\n    if (!config::input.mouse) {\n      return;\n    }\n\n    // Convert the client normalized coordinates to touchport coordinates\n    auto coords = client_to_touchport(input,\n      { from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f,\n        from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f },\n      { 65535.f, 65535.f });\n    if (!coords) {\n      return;\n    }\n\n    auto &touch_port = input->touch_port;\n    platf::touch_port_t abs_port {\n      touch_port.offset_x, touch_port.offset_y,\n      touch_port.env_width, touch_port.env_height\n    };\n\n    // Renormalize the coordinates\n    coords->first /= abs_port.width;\n    coords->second /= abs_port.height;\n\n    // Normalize rotation value to 0-359 degree range\n    auto rotation = util::endian::little(packet->rotation);\n    if (rotation != LI_ROT_UNKNOWN) {\n      rotation %= 360;\n    }\n\n    // Normalize the contact area based on the touchport\n    auto contact_area = scale_client_contact_area(\n      { from_clamped_netfloat(packet->contactAreaMajor, 0.0f, 1.0f) * 65535.f,\n        from_clamped_netfloat(packet->contactAreaMinor, 0.0f, 1.0f) * 65535.f },\n      rotation,\n      { abs_port.width / 65535.f, abs_port.height / 65535.f });\n\n    platf::pen_input_t pen {\n      packet->eventType,\n      packet->toolType,\n      packet->penButtons,\n      packet->tilt,\n      rotation,\n      coords->first,\n      coords->second,\n      from_clamped_netfloat(packet->pressureOrDistance, 0.0f, 1.0f),\n      contact_area.first,\n      contact_area.second,\n    };\n\n    platf::pen_update(input->client_context.get(), abs_port, pen);\n  }\n\n  /**\n   * @brief Called to pass a controller touch message to the platform backend.\n   * @param input The input context pointer.\n   * @param packet The controller touch packet.\n   */\n  void\n  passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_TOUCH_PACKET packet) {\n    if (!config::input.controller) {\n      return;\n    }\n\n    if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {\n      BOOST_LOG(warning) << \"ControllerNumber out of range [\"sv << packet->controllerNumber << ']';\n      return;\n    }\n\n    auto &gamepad = input->gamepads[packet->controllerNumber];\n    if (gamepad.id < 0) {\n      BOOST_LOG(warning) << \"ControllerNumber [\"sv << packet->controllerNumber << \"] not allocated\"sv;\n      return;\n    }\n\n    platf::gamepad_touch_t touch {\n      { gamepad.id, packet->controllerNumber },\n      packet->eventType,\n      util::endian::little(packet->pointerId),\n      from_clamped_netfloat(packet->x, 0.0f, 1.0f),\n      from_clamped_netfloat(packet->y, 0.0f, 1.0f),\n      from_clamped_netfloat(packet->pressure, 0.0f, 1.0f),\n    };\n\n    platf::gamepad_touch(platf_input, touch);\n  }\n\n  /**\n   * @brief Called to pass a controller motion message to the platform backend.\n   * @param input The input context pointer.\n   * @param packet The controller motion packet.\n   */\n  void\n  passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_MOTION_PACKET packet) {\n    if (!config::input.controller) {\n      return;\n    }\n\n    if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {\n      BOOST_LOG(warning) << \"ControllerNumber out of range [\"sv << packet->controllerNumber << ']';\n      return;\n    }\n\n    auto &gamepad = input->gamepads[packet->controllerNumber];\n    if (gamepad.id < 0) {\n      BOOST_LOG(warning) << \"ControllerNumber [\"sv << packet->controllerNumber << \"] not allocated\"sv;\n      return;\n    }\n\n    platf::gamepad_motion_t motion {\n      { gamepad.id, packet->controllerNumber },\n      packet->motionType,\n      from_netfloat(packet->x),\n      from_netfloat(packet->y),\n      from_netfloat(packet->z),\n    };\n\n    platf::gamepad_motion(platf_input, motion);\n  }\n\n  /**\n   * @brief Called to pass a controller battery message to the platform backend.\n   * @param input The input context pointer.\n   * @param packet The controller battery packet.\n   */\n  void\n  passthrough(std::shared_ptr<input_t> &input, PSS_CONTROLLER_BATTERY_PACKET packet) {\n    if (!config::input.controller) {\n      return;\n    }\n\n    if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {\n      BOOST_LOG(warning) << \"ControllerNumber out of range [\"sv << packet->controllerNumber << ']';\n      return;\n    }\n\n    auto &gamepad = input->gamepads[packet->controllerNumber];\n    if (gamepad.id < 0) {\n      BOOST_LOG(warning) << \"ControllerNumber [\"sv << packet->controllerNumber << \"] not allocated\"sv;\n      return;\n    }\n\n    platf::gamepad_battery_t battery {\n      { gamepad.id, packet->controllerNumber },\n      packet->batteryState,\n      packet->batteryPercentage\n    };\n\n    platf::gamepad_battery(platf_input, battery);\n  }\n\n  void\n  passthrough(std::shared_ptr<input_t> &input, PNV_MULTI_CONTROLLER_PACKET packet) {\n    if (!config::input.controller) {\n      return;\n    }\n\n    if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) {\n      BOOST_LOG(warning) << \"ControllerNumber out of range [\"sv << packet->controllerNumber << ']';\n\n      return;\n    }\n\n    auto &gamepad = input->gamepads[packet->controllerNumber];\n\n    // If this is an event for a new gamepad, create the gamepad now. Ideally, the client would\n    // send a controller arrival instead of this but it's still supported for legacy clients.\n    if ((packet->activeGamepadMask & (1 << packet->controllerNumber)) && gamepad.id < 0) {\n      auto id = alloc_id(gamepadMask);\n      if (id < 0) {\n        return;\n      }\n\n      if (platf::alloc_gamepad(platf_input, { id, (uint8_t) packet->controllerNumber }, {}, input->feedback_queue)) {\n        free_id(gamepadMask, id);\n        return;\n      }\n\n      gamepad.id = id;\n    }\n    else if (!(packet->activeGamepadMask & (1 << packet->controllerNumber)) && gamepad.id >= 0) {\n      // If this is the final event for a gamepad being removed, free the gamepad and return.\n      free_gamepad(platf_input, gamepad.id);\n      gamepad.id = -1;\n      return;\n    }\n\n    // If this gamepad has not been initialized, ignore it.\n    // This could happen when platf::alloc_gamepad fails\n    if (gamepad.id < 0) {\n      BOOST_LOG(warning) << \"ControllerNumber [\"sv << packet->controllerNumber << \"] not allocated\"sv;\n      return;\n    }\n\n    std::uint16_t bf = packet->buttonFlags;\n    std::uint32_t bf2 = packet->buttonFlags2;\n    platf::gamepad_state_t gamepad_state {\n      bf | (bf2 << 16),\n      packet->leftTrigger,\n      packet->rightTrigger,\n      packet->leftStickX,\n      packet->leftStickY,\n      packet->rightStickX,\n      packet->rightStickY\n    };\n\n    auto bf_new = gamepad_state.buttonFlags;\n    switch (gamepad.back_button_state) {\n      case button_state_e::UP:\n        if (!(platf::BACK & bf_new)) {\n          gamepad.back_button_state = button_state_e::NONE;\n        }\n        gamepad_state.buttonFlags &= ~platf::BACK;\n        break;\n      case button_state_e::DOWN:\n        if (platf::BACK & bf_new) {\n          gamepad.back_button_state = button_state_e::NONE;\n        }\n        gamepad_state.buttonFlags |= platf::BACK;\n        break;\n      case button_state_e::NONE:\n        break;\n    }\n\n    bf = gamepad_state.buttonFlags ^ gamepad.gamepad_state.buttonFlags;\n    bf_new = gamepad_state.buttonFlags;\n\n    if (platf::BACK & bf) {\n      if (platf::BACK & bf_new) {\n        // Don't emulate home button if timeout < 0\n        if (config::input.back_button_timeout >= 0ms) {\n          auto f = [input, controller = packet->controllerNumber]() {\n            auto &gamepad = input->gamepads[controller];\n\n            auto &state = gamepad.gamepad_state;\n\n            // Force the back button up\n            gamepad.back_button_state = button_state_e::UP;\n            state.buttonFlags &= ~platf::BACK;\n            platf::gamepad_update(platf_input, gamepad.id, state);\n\n            // Press Home button\n            state.buttonFlags |= platf::HOME;\n            platf::gamepad_update(platf_input, gamepad.id, state);\n\n            // Sleep for a short time to allow the input to be detected\n            std::this_thread::sleep_for(std::chrono::milliseconds(100));\n\n            // Release Home button\n            state.buttonFlags &= ~platf::HOME;\n            platf::gamepad_update(platf_input, gamepad.id, state);\n\n            gamepad.back_timeout_id = nullptr;\n          };\n\n          gamepad.back_timeout_id = task_pool.pushDelayed(std::move(f), config::input.back_button_timeout).task_id;\n        }\n      }\n      else if (gamepad.back_timeout_id) {\n        task_pool.cancel(gamepad.back_timeout_id);\n        gamepad.back_timeout_id = nullptr;\n      }\n    }\n\n    platf::gamepad_update(platf_input, gamepad.id, gamepad_state);\n\n    gamepad.gamepad_state = gamepad_state;\n  }\n\n  enum class batch_result_e {\n    batched,  ///< This entry was batched with the source entry\n    not_batchable,  ///< Not eligible to batch but continue attempts to batch\n    terminate_batch,  ///< Stop trying to batch with this entry\n  };\n\n  /**\n   * @brief Batch two relative mouse messages.\n   * @param dest The original packet to batch into.\n   * @param src A later packet to attempt to batch.\n   * @return The status of the batching operation.\n   */\n  batch_result_e\n  batch(PNV_REL_MOUSE_MOVE_PACKET dest, PNV_REL_MOUSE_MOVE_PACKET src) {\n    short deltaX, deltaY;\n\n    // Batching is safe as long as the result doesn't overflow a 16-bit integer\n    if (!__builtin_add_overflow(util::endian::big(dest->deltaX), util::endian::big(src->deltaX), &deltaX)) {\n      return batch_result_e::terminate_batch;\n    }\n    if (!__builtin_add_overflow(util::endian::big(dest->deltaY), util::endian::big(src->deltaY), &deltaY)) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Take the sum of deltas\n    dest->deltaX = util::endian::big(deltaX);\n    dest->deltaY = util::endian::big(deltaY);\n    return batch_result_e::batched;\n  }\n\n  /**\n   * @brief Batch two absolute mouse messages.\n   * @param dest The original packet to batch into.\n   * @param src A later packet to attempt to batch.\n   * @return The status of the batching operation.\n   */\n  batch_result_e\n  batch(PNV_ABS_MOUSE_MOVE_PACKET dest, PNV_ABS_MOUSE_MOVE_PACKET src) {\n    // Batching must only happen if the reference width and height don't change\n    if (dest->width != src->width || dest->height != src->height) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Take the latest absolute position\n    *dest = *src;\n    return batch_result_e::batched;\n  }\n\n  /**\n   * @brief Batch two vertical scroll messages.\n   * @param dest The original packet to batch into.\n   * @param src A later packet to attempt to batch.\n   * @return The status of the batching operation.\n   */\n  batch_result_e\n  batch(PNV_SCROLL_PACKET dest, PNV_SCROLL_PACKET src) {\n    short scrollAmt;\n\n    // Batching is safe as long as the result doesn't overflow a 16-bit integer\n    if (!__builtin_add_overflow(util::endian::big(dest->scrollAmt1), util::endian::big(src->scrollAmt1), &scrollAmt)) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Take the sum of delta\n    dest->scrollAmt1 = util::endian::big(scrollAmt);\n    dest->scrollAmt2 = util::endian::big(scrollAmt);\n    return batch_result_e::batched;\n  }\n\n  /**\n   * @brief Batch two horizontal scroll messages.\n   * @param dest The original packet to batch into.\n   * @param src A later packet to attempt to batch.\n   * @return The status of the batching operation.\n   */\n  batch_result_e\n  batch(PSS_HSCROLL_PACKET dest, PSS_HSCROLL_PACKET src) {\n    short scrollAmt;\n\n    // Batching is safe as long as the result doesn't overflow a 16-bit integer\n    if (!__builtin_add_overflow(util::endian::big(dest->scrollAmount), util::endian::big(src->scrollAmount), &scrollAmt)) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Take the sum of delta\n    dest->scrollAmount = util::endian::big(scrollAmt);\n    return batch_result_e::batched;\n  }\n\n  /**\n   * @brief Batch two controller state messages.\n   * @param dest The original packet to batch into.\n   * @param src A later packet to attempt to batch.\n   * @return The status of the batching operation.\n   */\n  batch_result_e\n  batch(PNV_MULTI_CONTROLLER_PACKET dest, PNV_MULTI_CONTROLLER_PACKET src) {\n    // Do not allow batching if the active controllers change\n    if (dest->activeGamepadMask != src->activeGamepadMask) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // We can only batch entries for the same controller, but allow batching attempts to continue\n    // in case we have more packets for this controller later in the queue.\n    if (dest->controllerNumber != src->controllerNumber) {\n      return batch_result_e::not_batchable;\n    }\n\n    // Do not allow batching if the button state changes on this controller\n    if (dest->buttonFlags != src->buttonFlags || dest->buttonFlags2 != src->buttonFlags2) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Take the latest state\n    *dest = *src;\n    return batch_result_e::batched;\n  }\n\n  /**\n   * @brief Batch two touch messages.\n   * @param dest The original packet to batch into.\n   * @param src A later packet to attempt to batch.\n   * @return The status of the batching operation.\n   */\n  batch_result_e\n  batch(PSS_TOUCH_PACKET dest, PSS_TOUCH_PACKET src) {\n    // Only batch hover or move events\n    if (dest->eventType != LI_TOUCH_EVENT_MOVE &&\n        dest->eventType != LI_TOUCH_EVENT_HOVER) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Don't batch beyond state changing events\n    if (src->eventType != LI_TOUCH_EVENT_MOVE &&\n        src->eventType != LI_TOUCH_EVENT_HOVER) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Batched events must be the same pointer ID\n    if (dest->pointerId != src->pointerId) {\n      return batch_result_e::not_batchable;\n    }\n\n    // The pointer must be in the same state\n    if (dest->eventType != src->eventType) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Take the latest state\n    *dest = *src;\n    return batch_result_e::batched;\n  }\n\n  /**\n   * @brief Batch two pen messages.\n   * @param dest The original packet to batch into.\n   * @param src A later packet to attempt to batch.\n   * @return The status of the batching operation.\n   */\n  batch_result_e\n  batch(PSS_PEN_PACKET dest, PSS_PEN_PACKET src) {\n    // Only batch hover or move events\n    if (dest->eventType != LI_TOUCH_EVENT_MOVE &&\n        dest->eventType != LI_TOUCH_EVENT_HOVER) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Batched events must be the same type\n    if (dest->eventType != src->eventType) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Do not allow batching if the button state changes\n    if (dest->penButtons != src->penButtons) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Do not batch beyond tool changes\n    if (dest->toolType != src->toolType) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Take the latest state\n    *dest = *src;\n    return batch_result_e::batched;\n  }\n\n  /**\n   * @brief Batch two controller touch messages.\n   * @param dest The original packet to batch into.\n   * @param src A later packet to attempt to batch.\n   * @return The status of the batching operation.\n   */\n  batch_result_e\n  batch(PSS_CONTROLLER_TOUCH_PACKET dest, PSS_CONTROLLER_TOUCH_PACKET src) {\n    // Only batch hover or move events\n    if (dest->eventType != LI_TOUCH_EVENT_MOVE &&\n        dest->eventType != LI_TOUCH_EVENT_HOVER) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // We can only batch entries for the same controller, but allow batching attempts to continue\n    // in case we have more packets for this controller later in the queue.\n    if (dest->controllerNumber != src->controllerNumber) {\n      return batch_result_e::not_batchable;\n    }\n\n    // Don't batch beyond state changing events\n    if (src->eventType != LI_TOUCH_EVENT_MOVE &&\n        src->eventType != LI_TOUCH_EVENT_HOVER) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Batched events must be the same pointer ID\n    if (dest->pointerId != src->pointerId) {\n      return batch_result_e::not_batchable;\n    }\n\n    // The pointer must be in the same state\n    if (dest->eventType != src->eventType) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // Take the latest state\n    *dest = *src;\n    return batch_result_e::batched;\n  }\n\n  /**\n   * @brief Batch two controller motion messages.\n   * @param dest The original packet to batch into.\n   * @param src A later packet to attempt to batch.\n   * @return The status of the batching operation.\n   */\n  batch_result_e\n  batch(PSS_CONTROLLER_MOTION_PACKET dest, PSS_CONTROLLER_MOTION_PACKET src) {\n    // We can only batch entries for the same controller, but allow batching attempts to continue\n    // in case we have more packets for this controller later in the queue.\n    if (dest->controllerNumber != src->controllerNumber) {\n      return batch_result_e::not_batchable;\n    }\n\n    // Batched events must be the same sensor\n    if (dest->motionType != src->motionType) {\n      return batch_result_e::not_batchable;\n    }\n\n    // Take the latest state\n    *dest = *src;\n    return batch_result_e::batched;\n  }\n\n  /**\n   * @brief Batch two input messages.\n   * @param dest The original packet to batch into.\n   * @param src A later packet to attempt to batch.\n   * @return The status of the batching operation.\n   */\n  batch_result_e\n  batch(PNV_INPUT_HEADER dest, PNV_INPUT_HEADER src) {\n    // We can only batch if the packet types are the same\n    if (dest->magic != src->magic) {\n      return batch_result_e::terminate_batch;\n    }\n\n    // We can only batch certain message types\n    switch (util::endian::little(dest->magic)) {\n      case MOUSE_MOVE_REL_MAGIC_GEN5:\n        return batch((PNV_REL_MOUSE_MOVE_PACKET) dest, (PNV_REL_MOUSE_MOVE_PACKET) src);\n      case MOUSE_MOVE_ABS_MAGIC:\n        return batch((PNV_ABS_MOUSE_MOVE_PACKET) dest, (PNV_ABS_MOUSE_MOVE_PACKET) src);\n      case SCROLL_MAGIC_GEN5:\n        return batch((PNV_SCROLL_PACKET) dest, (PNV_SCROLL_PACKET) src);\n      case SS_HSCROLL_MAGIC:\n        return batch((PSS_HSCROLL_PACKET) dest, (PSS_HSCROLL_PACKET) src);\n      case MULTI_CONTROLLER_MAGIC_GEN5:\n        return batch((PNV_MULTI_CONTROLLER_PACKET) dest, (PNV_MULTI_CONTROLLER_PACKET) src);\n      case SS_TOUCH_MAGIC:\n        return batch((PSS_TOUCH_PACKET) dest, (PSS_TOUCH_PACKET) src);\n      case SS_PEN_MAGIC:\n        return batch((PSS_PEN_PACKET) dest, (PSS_PEN_PACKET) src);\n      case SS_CONTROLLER_TOUCH_MAGIC:\n        return batch((PSS_CONTROLLER_TOUCH_PACKET) dest, (PSS_CONTROLLER_TOUCH_PACKET) src);\n      case SS_CONTROLLER_MOTION_MAGIC:\n        return batch((PSS_CONTROLLER_MOTION_PACKET) dest, (PSS_CONTROLLER_MOTION_PACKET) src);\n      default:\n        // Not a batchable message type\n        return batch_result_e::terminate_batch;\n    }\n  }\n\n  /**\n   * @brief Called on a thread pool thread to process an input message.\n   * @param input The input context pointer.\n   */\n  void\n  passthrough_next_message(std::shared_ptr<input_t> input) {\n    // 'entry' backs the 'payload' pointer, so they must remain in scope together\n    std::vector<uint8_t> entry;\n    PNV_INPUT_HEADER payload;\n\n    // Lock the input queue while batching, but release it before sending\n    // the input to the OS. This avoids potentially lengthy lock contention\n    // in the control stream thread while input is being processed by the OS.\n    {\n      std::lock_guard<std::mutex> lg(input->input_queue_lock);\n\n      // If all entries have already been processed, nothing to do\n      if (input->input_queue.empty()) {\n        return;\n      }\n\n      // Pop off the first entry, which we will send\n      entry = input->input_queue.front();\n      payload = (PNV_INPUT_HEADER) entry.data();\n      input->input_queue.pop_front();\n\n      // Try to batch with remaining items on the queue\n      auto i = input->input_queue.begin();\n      while (i != input->input_queue.end()) {\n        auto batchable_entry = *i;\n        auto batchable_payload = (PNV_INPUT_HEADER) batchable_entry.data();\n\n        auto batch_result = batch(payload, batchable_payload);\n        if (batch_result == batch_result_e::terminate_batch) {\n          // Stop batching\n          break;\n        }\n        else if (batch_result == batch_result_e::batched) {\n          // Erase this entry since it was batched\n          i = input->input_queue.erase(i);\n        }\n        else {\n          // We couldn't batch this entry, but try to batch later entries.\n          i++;\n        }\n      }\n    }\n\n    // Print the final input packet\n    input::print((void *) payload);\n\n    // Send the batched input to the OS\n    switch (util::endian::little(payload->magic)) {\n      case MOUSE_MOVE_REL_MAGIC_GEN5:\n        passthrough(input, (PNV_REL_MOUSE_MOVE_PACKET) payload);\n        break;\n      case MOUSE_MOVE_ABS_MAGIC:\n        passthrough(input, (PNV_ABS_MOUSE_MOVE_PACKET) payload);\n        break;\n      case MOUSE_BUTTON_DOWN_EVENT_MAGIC_GEN5:\n      case MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5:\n        passthrough(input, (PNV_MOUSE_BUTTON_PACKET) payload);\n        break;\n      case SCROLL_MAGIC_GEN5:\n        passthrough(input, (PNV_SCROLL_PACKET) payload);\n        break;\n      case SS_HSCROLL_MAGIC:\n        passthrough(input, (PSS_HSCROLL_PACKET) payload);\n        break;\n      case KEY_DOWN_EVENT_MAGIC:\n      case KEY_UP_EVENT_MAGIC:\n        passthrough(input, (PNV_KEYBOARD_PACKET) payload);\n        break;\n      case UTF8_TEXT_EVENT_MAGIC:\n        passthrough((PNV_UNICODE_PACKET) payload);\n        break;\n      case MULTI_CONTROLLER_MAGIC_GEN5:\n        passthrough(input, (PNV_MULTI_CONTROLLER_PACKET) payload);\n        break;\n      case SS_TOUCH_MAGIC:\n        passthrough(input, (PSS_TOUCH_PACKET) payload);\n        break;\n      case SS_PEN_MAGIC:\n        passthrough(input, (PSS_PEN_PACKET) payload);\n        break;\n      case SS_CONTROLLER_ARRIVAL_MAGIC:\n        passthrough(input, (PSS_CONTROLLER_ARRIVAL_PACKET) payload);\n        break;\n      case SS_CONTROLLER_TOUCH_MAGIC:\n        passthrough(input, (PSS_CONTROLLER_TOUCH_PACKET) payload);\n        break;\n      case SS_CONTROLLER_MOTION_MAGIC:\n        passthrough(input, (PSS_CONTROLLER_MOTION_PACKET) payload);\n        break;\n      case SS_CONTROLLER_BATTERY_MAGIC:\n        passthrough(input, (PSS_CONTROLLER_BATTERY_PACKET) payload);\n        break;\n    }\n  }\n\n  /**\n   * @brief Called on the control stream thread to queue an input message.\n   * @param input The input context pointer.\n   * @param input_data The input message.\n   */\n  void\n  passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data) {\n    {\n      std::lock_guard<std::mutex> lg(input->input_queue_lock);\n      input->input_queue.push_back(std::move(input_data));\n    }\n    task_pool.push(passthrough_next_message, input);\n  }\n\n  void\n  reset(std::shared_ptr<input_t> &input) {\n    task_pool.cancel(key_press_repeat_id);\n    task_pool.cancel(input->mouse_left_button_timeout);\n\n    // Ensure input is synchronous, by using the task_pool\n    task_pool.push([]() {\n      for (int x = 0; x < mouse_press.size(); ++x) {\n        if (mouse_press[x]) {\n          platf::button_mouse(platf_input, x, true);\n          mouse_press[x] = false;\n        }\n      }\n\n      for (auto &kp : key_press) {\n        if (!kp.second) {\n          // already released\n          continue;\n        }\n        platf::keyboard_update(platf_input, vk_from_kpid(kp.first) & 0x00FF, true, flags_from_kpid(kp.first));\n        key_press[kp.first] = false;\n      }\n    });\n  }\n\n  class deinit_t: public platf::deinit_t {\n  public:\n    ~deinit_t() override {\n      platf_input.reset();\n    }\n  };\n\n  [[nodiscard]] std::unique_ptr<platf::deinit_t>\n  init() {\n    platf_input = platf::input();\n\n    return std::make_unique<deinit_t>();\n  }\n\n  bool\n  probe_gamepads() {\n    auto input = static_cast<platf::input_t *>(platf_input.get());\n    const auto gamepads = platf::supported_gamepads(input);\n    for (auto &gamepad : gamepads) {\n      if (gamepad.is_enabled && gamepad.name != \"auto\") {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  std::shared_ptr<input_t>\n  alloc(safe::mail_t mail) {\n    auto input = std::make_shared<input_t>(\n      mail->event<input::touch_port_t>(mail::touch_port),\n      mail->queue<platf::gamepad_feedback_msg_t>(mail::gamepad_feedback));\n\n    // Workaround to ensure new frames will be captured when a client connects\n    task_pool.pushDelayed([]() {\n      platf::move_mouse(platf_input, 1, 1);\n      platf::move_mouse(platf_input, -1, -1);\n    },\n      100ms);\n\n    return input;\n  }\n}  // namespace input\n"
  },
  {
    "path": "src/input.h",
    "content": "/**\n * @file src/input.h\n * @brief Declarations for gamepad, keyboard, and mouse input handling.\n */\n#pragma once\n\n#include <functional>\n\n#include \"platform/common.h\"\n#include \"thread_safe.h\"\n\nnamespace input {\n  struct input_t;\n\n  void\n  print(void *input);\n  void\n  reset(std::shared_ptr<input_t> &input);\n  void\n  passthrough(std::shared_ptr<input_t> &input, std::vector<std::uint8_t> &&input_data);\n\n  [[nodiscard]] std::unique_ptr<platf::deinit_t>\n  init();\n\n  bool\n  probe_gamepads();\n\n  std::shared_ptr<input_t>\n  alloc(safe::mail_t mail);\n\n  struct touch_port_t: public platf::touch_port_t {\n    int env_width, env_height;\n\n    // Offset x and y coordinates of the client\n    float client_offsetX, client_offsetY;\n\n    float scalar_inv;\n\n    explicit\n    operator bool() const {\n      return width != 0 && height != 0 && env_width != 0 && env_height != 0;\n    }\n  };\n\n  /**\n   * @brief Scale the ellipse axes according to the provided size.\n   * @param val The major and minor axis pair.\n   * @param rotation The rotation value from the touch/pen event.\n   * @param scalar The scalar cartesian coordinate pair.\n   * @return The major and minor axis pair.\n   */\n  std::pair<float, float>\n  scale_client_contact_area(const std::pair<float, float> &val, uint16_t rotation, const std::pair<float, float> &scalar);\n}  // namespace input\n"
  },
  {
    "path": "src/logging.cpp",
    "content": "/**\n * @file src/logging.cpp\n * @brief Definitions for logging related functions.\n */\n// standard includes\n#include <fstream>\n#include <iomanip>\n#include <iostream>\n#include <filesystem>\n#include <ctime>\n\n// lib includes\n#include <boost/core/null_deleter.hpp>\n#include <boost/format.hpp>\n#include <boost/log/attributes/clock.hpp>\n#include <boost/log/common.hpp>\n#include <boost/log/expressions.hpp>\n#include <boost/log/sinks.hpp>\n#include <boost/log/sinks/text_file_backend.hpp>\n#include <boost/log/sources/severity_logger.hpp>\n#include <boost/log/utility/exception_handler.hpp>\n#include <boost/log/utility/setup/file.hpp>\n\n// local includes\n#include \"logging.h\"\n\nextern \"C\" {\n#include <libavutil/log.h>\n}\n\nusing namespace std::literals;\n\nnamespace bl = boost::log;\n\nboost::shared_ptr<boost::log::sinks::asynchronous_sink<boost::log::sinks::text_ostream_backend>> console_sink;\nboost::shared_ptr<boost::log::sinks::synchronous_sink<boost::log::sinks::text_file_backend>> file_sink_ptr;\nboost::shared_ptr<boost::log::sinks::asynchronous_sink<boost::log::sinks::text_ostream_backend>> file_ostream_sink;\n\nbl::sources::severity_logger<int> verbose(0);  // Dominating output\nbl::sources::severity_logger<int> debug(1);  // Follow what is happening\nbl::sources::severity_logger<int> info(2);  // Should be informed about\nbl::sources::severity_logger<int> warning(3);  // Strange events\nbl::sources::severity_logger<int> error(4);  // Recoverable errors\nbl::sources::severity_logger<int> fatal(5);  // Unrecoverable errors\n#ifdef SUNSHINE_TESTS\nbl::sources::severity_logger<int> tests(10);  // Automatic tests output\n#endif\n\nBOOST_LOG_ATTRIBUTE_KEYWORD(severity, \"Severity\", int)\n\nnamespace logging {\n  deinit_t::~deinit_t() {\n    deinit();\n  }\n\n  void\n  deinit() {\n    log_flush();\n    if (console_sink) {\n      bl::core::get()->remove_sink(console_sink);\n      console_sink.reset();\n    }\n    if (file_sink_ptr) {\n      bl::core::get()->remove_sink(file_sink_ptr);\n      file_sink_ptr.reset();\n    }\n    if (file_ostream_sink) {\n      bl::core::get()->remove_sink(file_ostream_sink);\n      file_ostream_sink.reset();\n    }\n  }\n\n  /**\n   * @brief 将现有日志文件转写到带日期的备份文件中\n   * @param log_file 当前日志文件路径\n   */\n  void\n  archive_existing_log(const std::string &log_file) {\n    namespace fs = std::filesystem;\n    \n    // 检查日志文件是否存在\n    if (!fs::exists(log_file)) {\n      return;\n    }\n    \n    try {\n      // 获取当前时间\n      auto now = std::chrono::system_clock::now();\n      auto time_t = std::chrono::system_clock::to_time_t(now);\n      auto tm = *std::localtime(&time_t);\n      \n      // 生成带日期的备份文件名（只精确到日期）\n      std::ostringstream backup_name;\n      backup_name << \"sunshine_\" \n                  << std::put_time(&tm, \"%Y%m%d\") \n                  << \".log\";\n      \n      // 构建备份文件路径\n      fs::path log_path(log_file);\n      fs::path backup_path = log_path.parent_path() / backup_name.str();\n      \n      // 如果备份文件已存在，则追加到文件尾部\n      if (fs::exists(backup_path)) {\n        std::ifstream source(log_file, std::ios::binary);\n        std::ofstream dest(backup_path, std::ios::binary | std::ios::app);\n        \n        if (source && dest) {\n          dest << source.rdbuf();\n          dest.close();\n          source.close();\n          \n          // 删除原日志文件\n          fs::remove(log_file);\n          \n          BOOST_LOG(info) << \"Appended log file to: \" << backup_path.string();\n        }\n        else {\n          BOOST_LOG(warning) << \"Failed to open files for append operation\";\n        }\n      }\n      else {\n        // 备份文件不存在，直接重命名\n        fs::rename(log_file, backup_path);\n        BOOST_LOG(info) << \"Archived log file to: \" << backup_path.string();\n      }\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(warning) << \"Failed to archive log file: \" << e.what();\n    }\n  }\n\n  void\n  formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os) {\n    constexpr const char *message = \"Message\";\n    constexpr const char *severity = \"Severity\";\n\n    auto log_level = view.attribute_values()[severity].extract<int>().get();\n\n    std::string_view log_type;\n    switch (log_level) {\n      case 0:\n        log_type = \"Verbose: \"sv;\n        break;\n      case 1:\n        log_type = \"Debug: \"sv;\n        break;\n      case 2:\n        log_type = \"Info: \"sv;\n        break;\n      case 3:\n        log_type = \"Warning: \"sv;\n        break;\n      case 4:\n        log_type = \"Error: \"sv;\n        break;\n      case 5:\n        log_type = \"Fatal: \"sv;\n        break;\n#ifdef SUNSHINE_TESTS\n      case 10:\n        log_type = \"Tests: \"sv;\n        break;\n#endif\n    };\n\n    auto now = std::chrono::system_clock::now();\n    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(\n      now - std::chrono::time_point_cast<std::chrono::seconds>(now));\n\n    auto t = std::chrono::system_clock::to_time_t(now);\n    auto lt = *std::localtime(&t);\n\n    os << \"[\"sv << std::put_time(&lt, \"%Y-%m-%d %H:%M:%S.\") << boost::format(\"%03u\") % ms.count() << \"]: \"sv\n       << log_type << view.attribute_values()[message].extract<std::string>();\n  }\n\n  /**\n   * @brief Initialize the logging system.\n   * @param min_log_level The minimum log level to output.\n   * @param log_file The log file to write to.\n   * @param restore_log Whether to restore existing log file (true=restore, false=overwrite).\n   * @return An object that will deinitialize the logging system when it goes out of scope.\n   * @examples\n   * log_init(2, \"sunshine.log\", true);\n   * @examples_end\n   */\n  [[nodiscard]] std::unique_ptr<deinit_t>\n  init(int min_log_level, const std::string &log_file, bool restore_log) {\n    if (console_sink || file_sink_ptr || file_ostream_sink) {\n      // Deinitialize the logging system before reinitializing it. This can probably only ever be hit in tests.\n      deinit();\n    }\n\n    setup_av_logging(min_log_level);\n\n    // Console sink (async, stdout)\n#ifndef SUNSHINE_TESTS\n    console_sink = boost::make_shared<text_sink>();\n    boost::shared_ptr<std::ostream> stream { &std::cout, boost::null_deleter() };\n    console_sink->locked_backend()->add_stream(stream);\n    console_sink->locked_backend()->auto_flush(true);\n    console_sink->set_filter(severity >= min_log_level);\n    console_sink->set_formatter(&formatter);\n    console_sink->set_exception_handler(bl::make_exception_suppressor());\n    bl::core::get()->add_sink(console_sink);\n#endif\n\n    // 转写现有日志文件\n    if (config::sunshine.restore_log) {\n      archive_existing_log(log_file);\n    }\n\n    // File sink with rotation support\n    namespace fs = std::filesystem;\n    const auto max_log_size_mb = config::sunshine.max_log_size_mb;\n    const auto log_dir = fs::path(log_file).parent_path();\n    const auto log_filename = fs::path(log_file).filename().string();\n\n    if (max_log_size_mb > 0) {\n      // When restore_log is false, truncate the existing log file before starting rotation\n      if (!config::sunshine.restore_log && fs::exists(log_file)) {\n        std::error_code ec;\n        fs::remove(log_file, ec);\n      }\n\n      // Use text_file_backend with automatic size-based rotation\n      auto file_backend = boost::make_shared<bl::sinks::text_file_backend>(\n        bl::keywords::file_name = log_file,\n        bl::keywords::rotation_size = static_cast<uintmax_t>(max_log_size_mb) * 1024 * 1024,\n        bl::keywords::open_mode = std::ios_base::out | std::ios_base::app\n      );\n\n      // Set up file collector to manage rotated log files\n      file_backend->set_file_collector(bl::sinks::file::make_collector(\n        bl::keywords::target = (log_dir / \"logs_archive\").string(),\n        bl::keywords::max_size = static_cast<uintmax_t>(max_log_size_mb) * 1024 * 1024 * 5,  // Keep up to 5x max_size total\n        bl::keywords::max_files = 10\n      ));\n\n      file_backend->auto_flush(true);\n      // Scan for any previously rotated files to properly manage the archive\n      file_backend->scan_for_files();\n\n      file_sink_ptr = boost::make_shared<file_sink>(file_backend);\n      file_sink_ptr->set_filter(severity >= min_log_level);\n      file_sink_ptr->set_formatter(&formatter);\n      file_sink_ptr->set_exception_handler(bl::make_exception_suppressor());\n      bl::core::get()->add_sink(file_sink_ptr);\n    }\n    else {\n      // No rotation: use simple text_ostream_backend (original behavior)\n      file_ostream_sink = boost::make_shared<text_sink>();\n      file_ostream_sink->locked_backend()->add_stream(boost::make_shared<std::ofstream>(log_file, std::ios_base::out));\n      file_ostream_sink->locked_backend()->auto_flush(true);\n      file_ostream_sink->set_filter(severity >= min_log_level);\n      file_ostream_sink->set_formatter(&formatter);\n      file_ostream_sink->set_exception_handler(bl::make_exception_suppressor());\n      bl::core::get()->add_sink(file_ostream_sink);\n    }\n\n    return std::make_unique<deinit_t>();\n  }\n\n  void\n  setup_av_logging(int min_log_level) {\n    if (min_log_level >= 1) {\n      av_log_set_level(AV_LOG_QUIET);\n    }\n    else {\n      av_log_set_level(AV_LOG_DEBUG);\n    }\n    av_log_set_callback([](void *ptr, int level, const char *fmt, va_list vl) {\n      static int print_prefix = 1;\n      char buffer[1024];\n\n      av_log_format_line(ptr, level, fmt, vl, buffer, sizeof(buffer), &print_prefix);\n      if (level <= AV_LOG_ERROR) {\n        // We print AV_LOG_FATAL at the error level. FFmpeg prints things as fatal that\n        // are expected in some cases, such as lack of codec support or similar things.\n        BOOST_LOG(error) << buffer;\n      }\n      else if (level <= AV_LOG_WARNING) {\n        BOOST_LOG(warning) << buffer;\n      }\n      else if (level <= AV_LOG_INFO) {\n        BOOST_LOG(info) << buffer;\n      }\n      else if (level <= AV_LOG_VERBOSE) {\n        // AV_LOG_VERBOSE is less verbose than AV_LOG_DEBUG\n        BOOST_LOG(debug) << buffer;\n      }\n      else {\n        BOOST_LOG(verbose) << buffer;\n      }\n    });\n  }\n\n  void\n  log_flush() {\n    if (console_sink) {\n      console_sink->flush();\n    }\n    if (file_sink_ptr) {\n      file_sink_ptr->flush();\n    }\n    if (file_ostream_sink) {\n      file_ostream_sink->flush();\n    }\n  }\n\n  void\n  print_help(const char *name) {\n    std::cout\n      << \"Usage: \"sv << name << \" [options] [/path/to/configuration_file] [--cmd]\"sv << std::endl\n      << \"    Any configurable option can be overwritten with: \\\"name=value\\\"\"sv << std::endl\n      << std::endl\n      << \"    Note: The configuration will be created if it doesn't exist.\"sv << std::endl\n      << std::endl\n      << \"    --help                    | print help\"sv << std::endl\n      << \"    --creds username password | set user credentials for the Web manager\"sv << std::endl\n      << \"    --version                 | print the version of sunshine\"sv << std::endl\n      << std::endl\n      << \"    flags\"sv << std::endl\n      << \"        -0 | Read PIN from stdin\"sv << std::endl\n      << \"        -1 | Do not load previously saved state and do retain any state after shutdown\"sv << std::endl\n      << \"           | Effectively starting as if for the first time without overwriting any pairings with your devices\"sv << std::endl\n      << \"        -2 | Force replacement of headers in video stream\"sv << std::endl\n      << \"        -p | Enable/Disable UPnP\"sv << std::endl\n      << std::endl;\n  }\n\n  std::string\n  bracket(const std::string &input) {\n    return \"[\"s + input + \"]\"s;\n  }\n\n  std::wstring\n  bracket(const std::wstring &input) {\n    return L\"[\"s + input + L\"]\"s;\n  }\n\n}  // namespace logging\n"
  },
  {
    "path": "src/logging.h",
    "content": "/**\n * @file src/logging.h\n * @brief Declarations for logging related functions.\n */\n#pragma once\n\n// lib includes\n#include <boost/log/common.hpp>\n#include <boost/log/sinks.hpp>\n#include <boost/log/sinks/text_file_backend.hpp>\n\nusing text_sink = boost::log::sinks::asynchronous_sink<boost::log::sinks::text_ostream_backend>;\nusing file_sink = boost::log::sinks::synchronous_sink<boost::log::sinks::text_file_backend>;\n\nextern boost::log::sources::severity_logger<int> verbose;\nextern boost::log::sources::severity_logger<int> debug;\nextern boost::log::sources::severity_logger<int> info;\nextern boost::log::sources::severity_logger<int> warning;\nextern boost::log::sources::severity_logger<int> error;\nextern boost::log::sources::severity_logger<int> fatal;\n#ifdef SUNSHINE_TESTS\nextern boost::log::sources::severity_logger<int> tests;\n#endif\n\n#include \"config.h\"\n#include \"stat_trackers.h\"\n\n/**\n * @brief Handles the initialization and deinitialization of the logging system.\n */\nnamespace logging {\n  class deinit_t {\n  public:\n    /**\n     * @brief A destructor that restores the initial state.\n     */\n    ~deinit_t();\n  };\n\n  /**\n   * @brief Deinitialize the logging system.\n   * @examples\n   * deinit();\n   * @examples_end\n   */\n  void\n  deinit();\n\n  void\n  formatter(const boost::log::record_view &view, boost::log::formatting_ostream &os);\n\n  /**\n   * @brief Initialize the logging system.\n   * @param min_log_level The minimum log level to output.\n   * @param log_file The log file to write to.\n   * @param restore_log Whether to restore existing log file (true=restore, false=overwrite).\n   * @return An object that will deinitialize the logging system when it goes out of scope.\n   * @examples\n   * log_init(2, \"sunshine.log\", true);\n   * @examples_end\n   */\n  [[nodiscard]] std::unique_ptr<deinit_t>\n  init(int min_log_level, const std::string &log_file, bool restore_log);\n\n  /**\n   * @brief Setup AV logging.\n   * @param min_log_level The log level.\n   */\n  void\n  setup_av_logging(int min_log_level);\n\n  /**\n   * @brief Flush the log.\n   * @examples\n   * log_flush();\n   * @examples_end\n   */\n  void\n  log_flush();\n\n  /**\n   * @brief Print help to stdout.\n   * @param name The name of the program.\n   * @examples\n   * print_help(\"sunshine\");\n   * @examples_end\n   */\n  void\n  print_help(const char *name);\n\n  /**\n   * @brief A helper class for tracking and logging numerical values across a period of time\n   * @examples\n   * min_max_avg_periodic_logger<int> logger(debug, \"Test time value\", \"ms\", 5s);\n   * logger.collect_and_log(1);\n   * // ...\n   * logger.collect_and_log(2);\n   * // after 5 seconds\n   * logger.collect_and_log(3);\n   * // In the log:\n   * // [2024:01:01:12:00:00]: Debug: Test time value (min/max/avg): 1ms/3ms/2.00ms\n   * @examples_end\n   */\n  template <typename T>\n  class min_max_avg_periodic_logger {\n  public:\n    min_max_avg_periodic_logger(boost::log::sources::severity_logger<int> &severity,\n      std::string_view message,\n      std::string_view units,\n      std::chrono::seconds interval_in_seconds = std::chrono::seconds(20)):\n        severity(severity),\n        message(message),\n        units(units),\n        interval(interval_in_seconds),\n        enabled(config::sunshine.min_log_level <= severity.default_severity()) {}\n\n    void\n    collect_and_log(const T &value) {\n      if (enabled) {\n        auto print_info = [&](const T &min_value, const T &max_value, double avg_value) {\n          auto f = stat_trackers::two_digits_after_decimal();\n          if constexpr (std::is_floating_point_v<T>) {\n            BOOST_LOG(severity.get()) << message << \" (min/max/avg): \" << f % min_value << units << \"/\" << f % max_value << units << \"/\" << f % avg_value << units;\n          }\n          else {\n            BOOST_LOG(severity.get()) << message << \" (min/max/avg): \" << min_value << units << \"/\" << max_value << units << \"/\" << f % avg_value << units;\n          }\n        };\n        tracker.collect_and_callback_on_interval(value, print_info, interval);\n      }\n    }\n\n    void\n    collect_and_log(std::function<T()> func) {\n      if (enabled) collect_and_log(func());\n    }\n\n    void\n    reset() {\n      if (enabled) tracker.reset();\n    }\n\n    bool\n    is_enabled() const {\n      return enabled;\n    }\n\n  private:\n    std::reference_wrapper<boost::log::sources::severity_logger<int>> severity;\n    std::string message;\n    std::string units;\n    std::chrono::seconds interval;\n    bool enabled;\n    stat_trackers::min_max_avg_tracker<T> tracker;\n  };\n\n  /**\n   * @brief A helper class for tracking and logging short time intervals across a period of time\n   * @examples\n   * time_delta_periodic_logger logger(debug, \"Test duration\", 5s);\n   * logger.first_point_now();\n   * // ...\n   * logger.second_point_now_and_log();\n   * // after 5 seconds\n   * logger.first_point_now();\n   * // ...\n   * logger.second_point_now_and_log();\n   * // In the log:\n   * // [2024:01:01:12:00:00]: Debug: Test duration (min/max/avg): 1.23ms/3.21ms/2.31ms\n   * @examples_end\n   */\n  class time_delta_periodic_logger {\n  public:\n    time_delta_periodic_logger(boost::log::sources::severity_logger<int> &severity,\n      std::string_view message,\n      std::chrono::seconds interval_in_seconds = std::chrono::seconds(20)):\n        logger(severity, message, \"ms\", interval_in_seconds) {}\n\n    void\n    first_point(const std::chrono::steady_clock::time_point &point) {\n      if (logger.is_enabled()) point1 = point;\n    }\n\n    void\n    first_point_now() {\n      if (logger.is_enabled()) first_point(std::chrono::steady_clock::now());\n    }\n\n    void\n    second_point_and_log(const std::chrono::steady_clock::time_point &point) {\n      if (logger.is_enabled()) {\n        logger.collect_and_log(std::chrono::duration<double, std::milli>(point - point1).count());\n      }\n    }\n\n    void\n    second_point_now_and_log() {\n      if (logger.is_enabled()) second_point_and_log(std::chrono::steady_clock::now());\n    }\n\n    void\n    reset() {\n      if (logger.is_enabled()) logger.reset();\n    }\n\n    bool\n    is_enabled() const {\n      return logger.is_enabled();\n    }\n\n  private:\n    std::chrono::steady_clock::time_point point1 = std::chrono::steady_clock::now();\n    min_max_avg_periodic_logger<double> logger;\n  };\n\n  /**\n   * @brief Enclose string in square brackets.\n   * @param input Input string.\n   * @return Enclosed string.\n   */\n  std::string\n  bracket(const std::string &input);\n\n  /**\n   * @brief Enclose string in square brackets.\n   * @param input Input string.\n   * @return Enclosed string.\n   */\n  std::wstring\n  bracket(const std::wstring &input);\n\n}  // namespace logging\n"
  },
  {
    "path": "src/main.cpp",
    "content": "/**\n * @file src/main.cpp\n * @brief Definitions for the main entry point for Sunshine.\n */\n// standard includes\n#include <codecvt>\n#include <csignal>\n#include <fstream>\n#include <iostream>\n\n// local includes\n#include \"confighttp.h\"\n#include \"display_device/session.h\"\n#include \"entry_handler.h\"\n#include \"globals.h\"\n#include \"httpcommon.h\"\n#include \"logging.h\"\n#include \"main.h\"\n#include \"nvhttp.h\"\n#include \"process.h\"\n#include \"system_tray.h\"\n#include \"upnp.h\"\n#include \"version.h\"\n#include \"video.h\"\n\n#ifdef _WIN32\n  #include \"platform/windows/misc.h\"\n  #include \"platform/windows/win_dark_mode.h\"\n#endif\n\nextern \"C\" {\n#include \"rswrapper.h\"\n}\n\nusing namespace std::literals;\n\nstd::map<int, std::function<void()>> signal_handlers;\nvoid\non_signal_forwarder(int sig) {\n  signal_handlers.at(sig)();\n}\n\ntemplate <class FN>\nvoid\non_signal(int sig, FN &&fn) {\n  signal_handlers.emplace(sig, std::forward<FN>(fn));\n\n  std::signal(sig, on_signal_forwarder);\n}\n\nstd::map<std::string_view, std::function<int(const char *name, int argc, char **argv)>> cmd_to_func {\n  { \"creds\"sv, [](const char *name, int argc, char **argv) { return args::creds(name, argc, argv); } },\n  { \"help\"sv, [](const char *name, int argc, char **argv) { return args::help(name); } },\n  { \"version\"sv, [](const char *name, int argc, char **argv) { return args::version(); } },\n#ifdef _WIN32\n  { \"restore-nvprefs-undo\"sv, [](const char *name, int argc, char **argv) { return args::restore_nvprefs_undo(); } },\n#endif\n};\n\n#ifdef _WIN32\nLRESULT CALLBACK\nSessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {\n  switch (uMsg) {\n    case WM_CLOSE:\n      DestroyWindow(hwnd);\n      return 0;\n    case WM_DESTROY:\n      PostQuitMessage(0);\n      return 0;\n    case WM_ENDSESSION: {\n      // Terminate ourselves with a blocking exit call\n      std::cout << \"Received WM_ENDSESSION\"sv << std::endl;\n      lifetime::exit_sunshine(0, false);\n      return 0;\n    }\n    default:\n      return DefWindowProc(hwnd, uMsg, wParam, lParam);\n  }\n}\n\nWINAPI BOOL\nConsoleCtrlHandler(DWORD type) {\n  if (type == CTRL_CLOSE_EVENT) {\n    BOOST_LOG(info) << \"Console closed handler called\";\n    lifetime::exit_sunshine(0, false);\n  }\n  return FALSE;\n}\n#endif\n\n#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1\nconstexpr bool tray_is_enabled = true;\n#else\nconstexpr bool tray_is_enabled = false;\n#endif\n\nvoid\nmainThreadLoop(const std::shared_ptr<safe::event_t<bool>> &shutdown_event) {\n  bool run_loop = false;\n\n  // Conditions that would require the main thread event loop\n#ifndef _WIN32\n  run_loop = tray_is_enabled && config::sunshine.system_tray;  // On Windows, tray runs in separate thread, so no main loop needed for tray\n#endif\n\n  if (!run_loop) {\n    BOOST_LOG(info) << \"No main thread features enabled, skipping event loop\"sv;\n    // Wait for shutdown\n    shutdown_event->view();\n    return;\n  }\n\n  // Main thread event loop\n  BOOST_LOG(info) << \"Starting main loop\"sv;\n  while (system_tray::process_tray_events() == 0);\n  BOOST_LOG(info) << \"Main loop has exited\"sv;\n}\n\nint\nmain(int argc, char *argv[]) {\n  lifetime::argv = argv;\n\n  task_pool_util::TaskPool::task_id_t force_shutdown = nullptr;\n\n#ifdef _WIN32\n  // Avoid searching the PATH in case a user has configured their system insecurely\n  // by placing a user-writable directory in the system-wide PATH variable.\n  SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32);\n\n  // Enable dark mode for the entire process before creating any windows\n  // This must be called early, before any windows or system tray icons are created\n  win_dark_mode::enable_process_dark_mode();\n\n  // Set locale to UTF-8 instead of C locale\n  setlocale(LC_ALL, \".UTF-8\");\n#endif\n\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wdeprecated-declarations\"\n  // Use UTF-8 conversion for the default C++ locale (used by boost::log)\n  std::locale::global(std::locale(std::locale(), new std::codecvt_utf8<wchar_t>));\n#pragma GCC diagnostic pop\n\n  mail::man = std::make_shared<safe::mail_raw_t>();\n  for (int i = 1; i < argc; ++i) {\n    if (std::string_view(argv[i]) == \"--version\"sv) {\n      std::cout << PROJECT_NAME << \" version: \" << PROJECT_VER << std::endl;\n      return 0;\n    }\n  }\n\n  // parse config file\n  if (config::parse(argc, argv)) {\n    return 0;\n  }\n\n  auto log_deinit_guard = logging::init(config::sunshine.min_log_level, config::sunshine.log_file, config::sunshine.restore_log);\n  if (!log_deinit_guard) {\n    BOOST_LOG(error) << \"Logging failed to initialize\"sv;\n  }\n\n  // logging can begin at this point\n  // if anything is logged prior to this point, it will appear in stdout, but not in the log viewer in the UI\n  // the version should be printed to the log before anything else\n  BOOST_LOG(info) << PROJECT_NAME << \" version: \" << PROJECT_VER;\n\n#ifdef _WIN32\n  // Cache the result of is_running_as_system() check once at startup\n  is_running_as_system_user = platf::is_running_as_system();\n  if (is_running_as_system_user) {\n    BOOST_LOG(info) << \"Running as SYSTEM user (service mode)\";\n  }\n#endif\n\n  // Log publisher metadata\n  log_publisher_data();\n\n  // Log modified_config_settings as JSON\n  if (!config::modified_config_settings.empty()) {\n    std::ostringstream config_json;\n    config_json << \"Modified config settings: {\";\n    bool first = true;\n    for (auto &[name, val] : config::modified_config_settings) {\n      if (!first) config_json << \", \";\n      config_json << \"\\\"\" << name << \"\\\": \\\"\" << val << \"\\\"\";\n      first = false;\n    }\n    config_json << \"}\";\n    BOOST_LOG(info) << config_json.str();\n  }\n  config::modified_config_settings.clear();\n\n  if (!config::sunshine.cmd.name.empty()) {\n    auto fn = cmd_to_func.find(config::sunshine.cmd.name);\n    if (fn == std::end(cmd_to_func)) {\n      BOOST_LOG(fatal) << \"Unknown command: \"sv << config::sunshine.cmd.name;\n\n      BOOST_LOG(info) << \"Possible commands:\"sv;\n      for (auto &[key, _] : cmd_to_func) {\n        BOOST_LOG(info) << '\\t' << key;\n      }\n\n      return 7;\n    }\n\n    return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.argv);\n  }\n\n  // Adding this guard here first as it also performs recovery after crash,\n  // otherwise people could theoretically end up without display output.\n  // It also should be run be destroyed before forced shutdown.\n  auto display_device_deinit_guard = display_device::session_t::init();\n  if (!display_device_deinit_guard) {\n    BOOST_LOG(error) << \"Display device session failed to initialize\"sv;\n  }\n\n#ifdef WIN32\n  // Modify relevant NVIDIA control panel settings if the system has corresponding gpu\n  if (nvprefs_instance.load()) {\n    // Restore global settings to the undo file left by improper termination of sunshine.exe\n    nvprefs_instance.restore_from_and_delete_undo_file_if_exists();\n    // Modify application settings for sunshine.exe\n    nvprefs_instance.modify_application_profile();\n    // Modify global settings, undo file is produced in the process to restore after improper termination\n    nvprefs_instance.modify_global_profile();\n    // Unload dynamic library to survive driver re-installation\n    nvprefs_instance.unload();\n  }\n\n  // Wait as long as possible to terminate Sunshine.exe during logoff/shutdown\n  SetProcessShutdownParameters(0x100, SHUTDOWN_NORETRY);\n\n  // We must create a hidden window to receive shutdown notifications since we load gdi32.dll\n  std::promise<HWND> session_monitor_hwnd_promise;\n  auto session_monitor_hwnd_future = session_monitor_hwnd_promise.get_future();\n  std::promise<void> session_monitor_join_thread_promise;\n  auto session_monitor_join_thread_future = session_monitor_join_thread_promise.get_future();\n\n  std::thread session_monitor_thread([&]() {\n    session_monitor_join_thread_promise.set_value_at_thread_exit();\n\n    WNDCLASSA wnd_class {};\n    wnd_class.lpszClassName = \"SunshineSessionMonitorClass\";\n    wnd_class.lpfnWndProc = SessionMonitorWindowProc;\n    if (!RegisterClassA(&wnd_class)) {\n      session_monitor_hwnd_promise.set_value(NULL);\n      BOOST_LOG(error) << \"Failed to register session monitor window class\"sv << std::endl;\n      return;\n    }\n\n    auto wnd = CreateWindowExA(\n      0,\n      wnd_class.lpszClassName,\n      \"Sunshine Session Monitor Window\",\n      0,\n      CW_USEDEFAULT,\n      CW_USEDEFAULT,\n      CW_USEDEFAULT,\n      CW_USEDEFAULT,\n      nullptr,\n      nullptr,\n      nullptr,\n      nullptr);\n\n    session_monitor_hwnd_promise.set_value(wnd);\n\n    if (!wnd) {\n      BOOST_LOG(error) << \"Failed to create session monitor window\"sv << std::endl;\n      return;\n    }\n\n    ShowWindow(wnd, SW_HIDE);\n\n    // Run the message loop for our window\n    MSG msg {};\n    while (GetMessage(&msg, nullptr, 0, 0) > 0) {\n      TranslateMessage(&msg);\n      DispatchMessage(&msg);\n    }\n  });\n\n  auto session_monitor_join_thread_guard = util::fail_guard([&]() {\n    if (session_monitor_hwnd_future.wait_for(1s) == std::future_status::ready) {\n      if (HWND session_monitor_hwnd = session_monitor_hwnd_future.get()) {\n        PostMessage(session_monitor_hwnd, WM_CLOSE, 0, 0);\n      }\n\n      if (session_monitor_join_thread_future.wait_for(1s) == std::future_status::ready) {\n        session_monitor_thread.join();\n        return;\n      }\n      else {\n        BOOST_LOG(warning) << \"session_monitor_join_thread_future reached timeout\";\n      }\n    }\n    else {\n      BOOST_LOG(warning) << \"session_monitor_hwnd_future reached timeout\";\n    }\n\n    session_monitor_thread.detach();\n  });\n\n#endif\n\n  task_pool.start(1);\n\n  // Create signal handler after logging has been initialized\n  auto shutdown_event = mail::man->event<bool>(mail::shutdown);\n  on_signal(SIGINT, [&force_shutdown, shutdown_event]() {\n    BOOST_LOG(info) << \"Interrupt handler called\"sv;\n\n    auto task = []() {\n      BOOST_LOG(fatal) << \"10 seconds passed, yet Sunshine's still running: Forcing shutdown\"sv;\n      logging::log_flush();\n      lifetime::debug_trap();\n    };\n    force_shutdown = task_pool.pushDelayed(task, 10s).task_id;\n\n    shutdown_event->raise(true);\n  });\n\n  on_signal(SIGTERM, [&force_shutdown, shutdown_event]() {\n    BOOST_LOG(info) << \"Terminate handler called\"sv;\n\n    auto task = []() {\n      BOOST_LOG(fatal) << \"10 seconds passed, yet Sunshine's still running: Forcing shutdown\"sv;\n      logging::log_flush();\n      lifetime::debug_trap();\n    };\n    force_shutdown = task_pool.pushDelayed(task, 10s).task_id;\n\n    shutdown_event->raise(true);\n  });\n\n#ifdef _WIN32\n  // Terminate gracefully on Windows when console window is closed\n  SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE);\n#endif\n\n  proc::refresh(config::stream.file_apps);\n\n  // If any of the following fail, we log an error and continue event though sunshine will not function correctly.\n  // This allows access to the UI to fix configuration problems or view the logs.\n\n  auto platf_deinit_guard = platf::init();\n  if (!platf_deinit_guard) {\n    BOOST_LOG(error) << \"Platform failed to initialize\"sv;\n  }\n\n  auto proc_deinit_guard = proc::init();\n  if (!proc_deinit_guard) {\n    BOOST_LOG(error) << \"Proc failed to initialize\"sv;\n  }\n\n  reed_solomon_init();\n  auto input_deinit_guard = input::init();\n\n  if (input::probe_gamepads()) {\n    BOOST_LOG(warning) << \"No gamepad input is available\"sv;\n  }\n\n  if (video::probe_encoders()) {\n    BOOST_LOG(error) << \"Video failed to find working encoder\"sv;\n  }\n\n  if (http::init()) {\n    BOOST_LOG(fatal) << \"HTTP interface failed to initialize\"sv;\n\n#ifdef _WIN32\n    BOOST_LOG(fatal) << \"To relaunch Sunshine successfully, use the shortcut in the Start Menu. Do not run Sunshine.exe manually.\"sv;\n    std::this_thread::sleep_for(10s);\n#endif\n\n    return -1;\n  }\n\n  std::unique_ptr<platf::deinit_t> mDNS;\n  std::future<void> sync_mDNS;\n  if (config::sunshine.flags[config::flag::MDNS_BROADCAST]) {\n    BOOST_LOG(info) << \"mDNS broadcast enabled\"sv;\n    sync_mDNS = std::async(std::launch::async, [&mDNS]() {\n      mDNS = platf::publish::start();\n    });\n  }\n\n  std::unique_ptr<platf::deinit_t> upnp_unmap;\n  auto sync_upnp = std::async(std::launch::async, [&upnp_unmap]() {\n    upnp_unmap = upnp::start();\n  });\n\n  // FIXME: Temporary workaround: Simple-Web_server needs to be updated or replaced\n  if (shutdown_event->peek()) {\n    return lifetime::desired_exit_code;\n  }\n\n  std::thread httpThread { nvhttp::start };\n  std::thread configThread { confighttp::start };\n  std::thread rtspThread { rtsp_stream::start };\n\n#ifdef _WIN32\n  // If we're using the default port and GameStream is enabled, warn the user\n  if (config::sunshine.port == 47989 && is_gamestream_enabled()) {\n    BOOST_LOG(fatal) << \"GameStream is still enabled in GeForce Experience! This *will* cause streaming problems with Sunshine!\"sv;\n    BOOST_LOG(fatal) << \"GeForce Experience 中仍然启用了 GameStream！这将导致流媒体问题与 Sunshine！\"sv;\n    BOOST_LOG(fatal) << \"Disable GameStream on the SHIELD tab in GeForce Experience or change the Port setting on the Advanced tab in the Sunshine Web UI.\"sv;\n    BOOST_LOG(fatal) << \"在 GeForce Experience 的 SHIELD 标签中禁用 GameStream，或在 Sunshine Web UI 的 Advanced 标签中更改端口设置。\"sv;\n  }\n#endif\n\n  if (tray_is_enabled && config::sunshine.system_tray) {\n    BOOST_LOG(info) << \"Starting system tray\"sv;\n#ifdef _WIN32\n    // TODO: Windows has a weird bug where when running as a service and on the first Windows boot,\n    // he tray icon would not appear even though Sunshine is running correctly otherwise.\n    // Restarting the service would allow the icon to appear normally.\n    // For now we will keep the Windows tray icon on a separate thread.\n    // Ideally, we would run the system tray on the main thread for all platforms.\n    system_tray::init_tray_threaded();\n#else\n    system_tray::init_tray();\n#endif\n  }\n\n  mainThreadLoop(shutdown_event);\n\n  system_tray::end_tray();\n  try {\n    display_device::session_t::get().restore_state();\n  }\n  catch (...) {\n  }\n  display_device_deinit_guard = nullptr;\n\n  httpThread.join();\n  configThread.join();\n  rtspThread.join();\n\n  task_pool.stop();\n  task_pool.join();\n\n  // stop system tray\n#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1\n  system_tray::end_tray();\n#endif\n\n#ifdef WIN32\n  // Restore global NVIDIA control panel settings\n  if (nvprefs_instance.owning_undo_file() && nvprefs_instance.load()) {\n    nvprefs_instance.restore_global_profile();\n    nvprefs_instance.unload();\n  }\n#endif\n\n  return lifetime::desired_exit_code;\n}\n"
  },
  {
    "path": "src/main.h",
    "content": "/**\n * @file src/main.h\n * @brief Declarations for the main entry point for Sunshine.\n */\n#pragma once\n\n/**\n * @brief Main application entry point.\n * @param argc The number of arguments.\n * @param argv The arguments.\n * @examples\n * main(1, const char* args[] = {\"sunshine\", nullptr});\n * @examples_end\n */\nint\nmain(int argc, char *argv[]);\n"
  },
  {
    "path": "src/move_by_copy.h",
    "content": "/**\n * @file src/move_by_copy.h\n * @brief Declarations for the MoveByCopy utility class.\n */\n#pragma once\n\n#include <utility>\n\n/**\n * @brief Contains utilities for moving objects by copying them.\n */\nnamespace move_by_copy_util {\n  /**\n   * When a copy is made, it moves the object\n   * This allows you to move an object when a move can't be done.\n   */\n  template <class T>\n  class MoveByCopy {\n  public:\n    typedef T move_type;\n\n  private:\n    move_type _to_move;\n\n  public:\n    explicit MoveByCopy(move_type &&to_move):\n        _to_move(std::move(to_move)) {}\n\n    MoveByCopy(MoveByCopy &&other) = default;\n\n    MoveByCopy(const MoveByCopy &other) {\n      *this = other;\n    }\n\n    MoveByCopy &\n    operator=(MoveByCopy &&other) = default;\n\n    MoveByCopy &\n    operator=(const MoveByCopy &other) {\n      this->_to_move = std::move(const_cast<MoveByCopy &>(other)._to_move);\n\n      return *this;\n    }\n\n    operator move_type() {\n      return std::move(_to_move);\n    }\n  };\n\n  template <class T>\n  MoveByCopy<T>\n  cmove(T &movable) {\n    return MoveByCopy<T>(std::move(movable));\n  }\n\n  // Do NOT use this unless you are absolutely certain the object to be moved is no longer used by the caller\n  template <class T>\n  MoveByCopy<T>\n  const_cmove(const T &movable) {\n    return MoveByCopy<T>(std::move(const_cast<T &>(movable)));\n  }\n}  // namespace move_by_copy_util\n"
  },
  {
    "path": "src/network.cpp",
    "content": "/**\n * @file src/network.cpp\n * @brief Definitions for networking related functions.\n */\n#include \"network.h\"\n#include \"config.h\"\n#include \"logging.h\"\n#include \"utility.h\"\n#include <algorithm>\n#include <sstream>\n\nusing namespace std::literals;\n\nnamespace ip = boost::asio::ip;\n\nnamespace net {\n  std::vector<ip::network_v4> pc_ips_v4 {\n    ip::make_network_v4(\"127.0.0.0/8\"sv),\n  };\n  std::vector<ip::network_v4> lan_ips_v4 {\n    ip::make_network_v4(\"192.168.0.0/16\"sv),\n    ip::make_network_v4(\"172.16.0.0/12\"sv),\n    ip::make_network_v4(\"10.0.0.0/8\"sv),\n    ip::make_network_v4(\"100.64.0.0/10\"sv),\n    ip::make_network_v4(\"169.254.0.0/16\"sv),\n  };\n\n  std::vector<ip::network_v6> pc_ips_v6 {\n    ip::make_network_v6(\"::1/128\"sv),\n  };\n  std::vector<ip::network_v6> lan_ips_v6 {\n    ip::make_network_v6(\"fc00::/7\"sv),\n    ip::make_network_v6(\"fe80::/64\"sv),\n  };\n\n  net_e\n  from_enum_string(const std::string_view &view) {\n    if (view == \"wan\") {\n      return WAN;\n    }\n    if (view == \"lan\") {\n      return LAN;\n    }\n\n    return PC;\n  }\n\n  net_e\n  from_address(const std::string_view &view) {\n    auto addr = normalize_address(ip::make_address(view));\n\n    if (addr.is_v6()) {\n      for (auto &range : pc_ips_v6) {\n        if (range.hosts().find(addr.to_v6()) != range.hosts().end()) {\n          return PC;\n        }\n      }\n\n      for (auto &range : lan_ips_v6) {\n        if (range.hosts().find(addr.to_v6()) != range.hosts().end()) {\n          return LAN;\n        }\n      }\n    }\n    else {\n      for (auto &range : pc_ips_v4) {\n        if (range.hosts().find(addr.to_v4()) != range.hosts().end()) {\n          return PC;\n        }\n      }\n\n      for (auto &range : lan_ips_v4) {\n        if (range.hosts().find(addr.to_v4()) != range.hosts().end()) {\n          return LAN;\n        }\n      }\n    }\n\n    return WAN;\n  }\n\n  std::string_view\n  to_enum_string(net_e net) {\n    switch (net) {\n      case PC:\n        return \"pc\"sv;\n      case LAN:\n        return \"lan\"sv;\n      case WAN:\n        return \"wan\"sv;\n    }\n\n    // avoid warning\n    return \"wan\"sv;\n  }\n\n  af_e\n  af_from_enum_string(const std::string_view &view) {\n    if (view == \"ipv4\") {\n      return IPV4;\n    }\n    if (view == \"both\") {\n      return BOTH;\n    }\n\n    // avoid warning\n    return BOTH;\n  }\n\n  std::string_view af_to_any_address_string(const af_e af) {\n    switch (af) {\n      case IPV4:\n        return \"0.0.0.0\"sv;\n      case BOTH:\n        return \"::\"sv;\n    }\n\n    // avoid warning\n    return \"::\"sv;\n  }\n\n  std::string get_bind_address(const af_e af) {\n    // If bind_address is configured, use it\n    if (!config::sunshine.bind_address.empty()) {\n      return config::sunshine.bind_address;\n    }\n\n    // Otherwise use the wildcard address for the given address family\n    return std::string(af_to_any_address_string(af));\n  }\n\n  boost::asio::ip::address\n  normalize_address(boost::asio::ip::address address) {\n    // Convert IPv6-mapped IPv4 addresses into regular IPv4 addresses\n    if (address.is_v6()) {\n      auto v6 = address.to_v6();\n      if (v6.is_v4_mapped()) {\n        return boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, v6);\n      }\n    }\n\n    return address;\n  }\n\n  std::string\n  addr_to_normalized_string(boost::asio::ip::address address) {\n    return normalize_address(address).to_string();\n  }\n\n  std::string\n  addr_to_url_escaped_string(boost::asio::ip::address address) {\n    address = normalize_address(address);\n    if (address.is_v6()) {\n      std::stringstream ss;\n      ss << '[' << address.to_string() << ']';\n      return ss.str();\n    }\n    else {\n      return address.to_string();\n    }\n  }\n\n  int\n  encryption_mode_for_address(boost::asio::ip::address address) {\n    auto nettype = net::from_address(address.to_string());\n    if (nettype == net::net_e::PC || nettype == net::net_e::LAN) {\n      return config::stream.lan_encryption_mode;\n    }\n    else {\n      return config::stream.wan_encryption_mode;\n    }\n  }\n\n  host_t\n  host_create(af_e af, ENetAddress &addr, std::uint16_t port) {\n    static std::once_flag enet_init_flag;\n    std::call_once(enet_init_flag, []() {\n      enet_initialize();\n    });\n\n    const auto bind_addr = net::get_bind_address(af);\n    enet_address_set_host(&addr, bind_addr.c_str());\n    enet_address_set_port(&addr, port);\n\n    // Maximum of 128 clients, which should be enough for anyone\n    auto host = host_t { enet_host_create(af == IPV4 ? AF_INET : AF_INET6, &addr, 128, 0, 0, 0) };\n\n    // Enable opportunistic QoS tagging (automatically disables if the network appears to drop tagged packets)\n    enet_socket_set_option(host->socket, ENET_SOCKOPT_QOS, 1);\n\n    return host;\n  }\n\n  void\n  free_host(ENetHost *host) {\n    std::for_each(host->peers, host->peers + host->peerCount, [](ENetPeer &peer_ref) {\n      ENetPeer *peer = &peer_ref;\n\n      if (peer) {\n        enet_peer_disconnect_now(peer, 0);\n      }\n    });\n\n    enet_host_destroy(host);\n  }\n\n  std::uint16_t\n  map_port(int port) {\n    // calculate the port from the config port\n    auto mapped_port = (std::uint16_t)((int) config::sunshine.port + port);\n\n    // Ensure port is in the range of 1024-65535\n    if (mapped_port < 1024 || mapped_port > 65535) {\n      BOOST_LOG(warning) << \"Port out of range: \"sv << mapped_port;\n    }\n\n    return mapped_port;\n  }\n\n  /**\n   * @brief Returns a string for use as the instance name for mDNS.\n   * @param hostname The hostname to use for instance name generation.\n   * @return Hostname-based instance name or \"Sunshine\" if hostname is invalid.\n   */\n  std::string\n  mdns_instance_name(const std::string_view &hostname) {\n    // Start with the unmodified hostname\n    std::string instancename { hostname.data(), hostname.size() };\n\n    // Truncate to 63 characters per RFC 6763 section 7.2.\n    if (instancename.size() > 63) {\n      instancename.resize(63);\n    }\n\n    for (auto i = 0; i < instancename.size(); i++) {\n      // Replace any spaces with dashes\n      if (instancename[i] == ' ') {\n        instancename[i] = '-';\n      }\n      else if (!std::isalnum(instancename[i]) && instancename[i] != '-') {\n        // Stop at the first invalid character\n        instancename.resize(i);\n        break;\n      }\n    }\n\n    return !instancename.empty() ? instancename : \"Sunshine\";\n  }\n}  // namespace net\n"
  },
  {
    "path": "src/network.h",
    "content": "/**\n * @file src/network.h\n * @brief Declarations for networking related functions.\n */\n#pragma once\n\n#include <tuple>\n#include <utility>\n\n#include <boost/asio.hpp>\n\n#include <enet/enet.h>\n\n#include \"utility.h\"\n\nnamespace net {\n  void\n  free_host(ENetHost *host);\n\n  /**\n   * @brief Map a specified port based on the base port.\n   * @param port The port to map as a difference from the base port.\n   * @return The mapped port number.\n   * @examples\n   * std::uint16_t mapped_port = net::map_port(1);\n   * @examples_end\n   * @todo Ensure port is not already in use by another application.\n   */\n  std::uint16_t\n  map_port(int port);\n\n  using host_t = util::safe_ptr<ENetHost, free_host>;\n  using peer_t = ENetPeer *;\n  using packet_t = util::safe_ptr<ENetPacket, enet_packet_destroy>;\n\n  enum net_e : int {\n    PC,  ///< PC\n    LAN,  ///< LAN\n    WAN  ///< WAN\n  };\n\n  enum af_e : int {\n    IPV4,  ///< IPv4 only\n    BOTH  ///< IPv4 and IPv6\n  };\n\n  net_e\n  from_enum_string(const std::string_view &view);\n  std::string_view\n  to_enum_string(net_e net);\n\n  net_e\n  from_address(const std::string_view &view);\n\n  host_t\n  host_create(af_e af, ENetAddress &addr, std::uint16_t port);\n\n  /**\n   * @brief Get the address family enum value from a string.\n   * @param view The config option value.\n   * @return The address family enum value.\n   */\n  af_e\n  af_from_enum_string(const std::string_view &view);\n\n  /**\n   * @brief Get the wildcard binding address for a given address family.\n   * @param af Address family.\n   * @return Normalized address.\n   */\n  std::string_view\n  af_to_any_address_string(af_e af);\n\n  /**\n   * @brief Get the binding address to use based on config.\n   * @param af Address family.\n   * @return The configured bind address or wildcard if not configured.\n   */\n  std::string get_bind_address(af_e af);\n\n  /**\n   * @brief Convert an address to a normalized form.\n   * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses.\n   * @param address The address to normalize.\n   * @return Normalized address.\n   */\n  boost::asio::ip::address\n  normalize_address(boost::asio::ip::address address);\n\n  /**\n   * @brief Get the given address in normalized string form.\n   * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses.\n   * @param address The address to normalize.\n   * @return Normalized address in string form.\n   */\n  std::string\n  addr_to_normalized_string(boost::asio::ip::address address);\n\n  /**\n   * @brief Get the given address in a normalized form for the host portion of a URL.\n   * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses.\n   * @param address The address to normalize and escape.\n   * @return Normalized address in URL-escaped string.\n   */\n  std::string\n  addr_to_url_escaped_string(boost::asio::ip::address address);\n\n  /**\n   * @brief Get the encryption mode for the given remote endpoint address.\n   * @param address The address used to look up the desired encryption mode.\n   * @return The WAN or LAN encryption mode, based on the provided address.\n   */\n  int\n  encryption_mode_for_address(boost::asio::ip::address address);\n\n  /**\n   * @brief Returns a string for use as the instance name for mDNS.\n   * @param hostname The hostname to use for instance name generation.\n   * @return Hostname-based instance name or \"Sunshine\" if hostname is invalid.\n   */\n  std::string\n  mdns_instance_name(const std::string_view &hostname);\n}  // namespace net\n"
  },
  {
    "path": "src/nvenc/common_impl/nvenc_base.cpp",
    "content": "/**\n * @file src/nvenc/common_impl/nvenc_base.cpp\n * @brief Definitions for abstract platform-agnostic base of standalone NVENC encoder.\n */\n#include \"nvenc_base.h\"\n\n#include \"nvenc_utils.h\"\n\n#include \"src/utility.h\"\n\n#include <algorithm>\n#include <cmath>\n\n#define NVENC_INT_VERSION (NVENCAPI_MAJOR_VERSION * 100 + NVENCAPI_MINOR_VERSION)\n\nnamespace {\n\n#ifdef NVENC_NAMESPACE\n  using namespace NVENC_NAMESPACE;\n#endif\n\n  GUID\n  quality_preset_guid_from_number(unsigned number) {\n    if (number > 7) number = 7;\n\n    switch (number) {\n      case 1:\n      default:\n        return NV_ENC_PRESET_P1_GUID;\n\n      case 2:\n        return NV_ENC_PRESET_P2_GUID;\n\n      case 3:\n        return NV_ENC_PRESET_P3_GUID;\n\n      case 4:\n        return NV_ENC_PRESET_P4_GUID;\n\n      case 5:\n        return NV_ENC_PRESET_P5_GUID;\n\n      case 6:\n        return NV_ENC_PRESET_P6_GUID;\n\n      case 7:\n        return NV_ENC_PRESET_P7_GUID;\n    }\n  };\n\n  bool\n  equal_guids(const GUID &guid1, const GUID &guid2) {\n    return std::memcmp(&guid1, &guid2, sizeof(GUID)) == 0;\n  }\n\n  auto\n  quality_preset_string_from_guid(const GUID &guid) {\n    if (equal_guids(guid, NV_ENC_PRESET_P1_GUID)) {\n      return \"P1\";\n    }\n    if (equal_guids(guid, NV_ENC_PRESET_P2_GUID)) {\n      return \"P2\";\n    }\n    if (equal_guids(guid, NV_ENC_PRESET_P3_GUID)) {\n      return \"P3\";\n    }\n    if (equal_guids(guid, NV_ENC_PRESET_P4_GUID)) {\n      return \"P4\";\n    }\n    if (equal_guids(guid, NV_ENC_PRESET_P5_GUID)) {\n      return \"P5\";\n    }\n    if (equal_guids(guid, NV_ENC_PRESET_P6_GUID)) {\n      return \"P6\";\n    }\n    if (equal_guids(guid, NV_ENC_PRESET_P7_GUID)) {\n      return \"P7\";\n    }\n    return \"Unknown\";\n  }\n\n}  // namespace\n\n#ifdef NVENC_NAMESPACE\nnamespace NVENC_NAMESPACE {\n#else\nnamespace nvenc {\n#endif\n\n  nvenc_base::nvenc_base(NV_ENC_DEVICE_TYPE device_type):\n      device_type(device_type) {\n  }\n\n  nvenc_base::~nvenc_base() {\n    // Use destroy_encoder() instead\n  }\n\n  bool\n  nvenc_base::create_encoder(\n    const nvenc_config &config,\n    const video::config_t &client_config,\n    const video::sunshine_colorspace_t &sunshine_colorspace,\n    platf::pix_fmt_e sunshine_buffer_format) {\n    if (!nvenc && !init_library()) return false;\n\n    if (encoder) destroy_encoder();\n    auto fail_guard = util::fail_guard([this] { destroy_encoder(); });\n\n    auto colorspace = nvenc_colorspace_from_sunshine_colorspace(sunshine_colorspace);\n    auto buffer_format = nvenc_format_from_sunshine_format(sunshine_buffer_format);\n\n    encoder_params.width = client_config.width;\n    encoder_params.height = client_config.height;\n    encoder_params.buffer_format = buffer_format;\n\n    // YUV 4:2:0 formats (NV12/P010) require even dimensions because\n    // the chroma plane is half the size of the luma plane in both dimensions.\n    if (buffer_format == NV_ENC_BUFFER_FORMAT_NV12 || buffer_format == NV_ENC_BUFFER_FORMAT_YUV420_10BIT) {\n      encoder_params.width = (encoder_params.width + 1) & ~1;\n      encoder_params.height = (encoder_params.height + 1) & ~1;\n    }\n    encoder_params.rfi = true;\n\n    NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = { NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER };\n    session_params.device = device;\n    session_params.deviceType = device_type;\n    session_params.apiVersion = NVENCAPI_VERSION;\n    if (nvenc_failed(nvenc->nvEncOpenEncodeSessionEx(&session_params, &encoder))) {\n      BOOST_LOG(error) << \"NvEnc: NvEncOpenEncodeSessionEx() failed: \" << last_nvenc_error_string;\n      return false;\n    }\n\n    uint32_t encode_guid_count = 0;\n    if (nvenc_failed(nvenc->nvEncGetEncodeGUIDCount(encoder, &encode_guid_count))) {\n      BOOST_LOG(error) << \"NvEnc: NvEncGetEncodeGUIDCount() failed: \" << last_nvenc_error_string;\n      return false;\n    };\n\n    std::vector<GUID> encode_guids(encode_guid_count);\n    if (nvenc_failed(nvenc->nvEncGetEncodeGUIDs(encoder, encode_guids.data(), encode_guids.size(), &encode_guid_count))) {\n      BOOST_LOG(error) << \"NvEnc: NvEncGetEncodeGUIDs() failed: \" << last_nvenc_error_string;\n      return false;\n    }\n\n    NV_ENC_INITIALIZE_PARAMS init_params = { NV_ENC_INITIALIZE_PARAMS_VER };\n    std::string encode_guid_support = \"\";\n    video_format = client_config.videoFormat;  // Save video format for HDR metadata handling\n    switch (client_config.videoFormat) {\n      case 0:\n        // H.264\n        init_params.encodeGUID = NV_ENC_CODEC_H264_GUID;\n        encode_guid_support += \"H.264\";\n        break;\n\n      case 1:\n        // HEVC\n        init_params.encodeGUID = NV_ENC_CODEC_HEVC_GUID;\n        encode_guid_support += \"HEVC\";\n        break;\n\n#if NVENC_INT_VERSION >= 1200\n      case 2:\n        // AV1\n        init_params.encodeGUID = NV_ENC_CODEC_AV1_GUID;\n        encode_guid_support += \"AV1\";\n        break;\n#endif\n\n      default:\n        BOOST_LOG(error) << \"NvEnc: unknown video format \" << client_config.videoFormat;\n        return false;\n    }\n\n    {\n      auto search_predicate = [&](const GUID &guid) {\n        return equal_guids(init_params.encodeGUID, guid);\n      };\n      if (std::find_if(encode_guids.begin(), encode_guids.end(), search_predicate) == encode_guids.end()) {\n        BOOST_LOG(error) << \"NvEnc: encoding format is not supported by the gpu, NVENCAPI_VERSION: \" << NVENCAPI_VERSION;\n        return false;\n      }\n    }\n\n    auto get_encoder_cap = [&](NV_ENC_CAPS cap) {\n      NV_ENC_CAPS_PARAM param = { NV_ENC_CAPS_PARAM_VER };\n      param.capsToQuery = cap;\n      int value = 0;\n      nvenc->nvEncGetEncodeCaps(encoder, init_params.encodeGUID, &param, &value);\n      return value;\n    };\n\n    auto buffer_is_10bit = [&]() {\n      return buffer_format == NV_ENC_BUFFER_FORMAT_YUV420_10BIT || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT;\n    };\n\n    auto buffer_is_yuv444 = [&]() {\n      return buffer_format == NV_ENC_BUFFER_FORMAT_AYUV || buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT;\n    };\n\n    {\n      auto supported_width = get_encoder_cap(NV_ENC_CAPS_WIDTH_MAX);\n      auto supported_height = get_encoder_cap(NV_ENC_CAPS_HEIGHT_MAX);\n      if (encoder_params.width > supported_width || encoder_params.height > supported_height) {\n        BOOST_LOG(error) << \"NvEnc: gpu max encode resolution \" << supported_width << \"x\" << supported_height\n                         << \", requested \" << encoder_params.width << \"x\" << encoder_params.height;\n        return false;\n      }\n    }\n\n    if (buffer_is_10bit() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_10BIT_ENCODE)) {\n      BOOST_LOG(warning) << \"NvEnc: gpu doesn't support 10-bit encode, format: \" << buffer_format << \", encode_guid_support: \" << encode_guid_support << \", NVENCAPI_VERSION: \" << NVENCAPI_VERSION;\n      return false;\n    }\n\n    if (buffer_is_yuv444() && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_YUV444_ENCODE)) {\n      BOOST_LOG(warning) << \"NvEnc: gpu doesn't support YUV444 encode, format: \" << buffer_format << \", encode_guid_support: \" << encode_guid_support << \", NVENCAPI_VERSION: \" << NVENCAPI_VERSION;\n      if (async_event_handle) {\n        CloseHandle(async_event_handle);\n        async_event_handle = nullptr;\n      }\n      return false;\n    }\n\n    if (async_event_handle && !get_encoder_cap(NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT)) {\n      BOOST_LOG(warning) << \"NvEnc: gpu doesn't support async encode, NVENCAPI_VERSION: \" << NVENCAPI_VERSION;\n      async_event_handle = nullptr;\n    }\n\n    encoder_params.rfi = get_encoder_cap(NV_ENC_CAPS_SUPPORT_REF_PIC_INVALIDATION);\n\n    init_params.presetGUID = quality_preset_guid_from_number(config.quality_preset);\n    init_params.tuningInfo = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY;\n    init_params.enablePTD = 1;\n    init_params.enableEncodeAsync = async_event_handle ? 1 : 0;\n    init_params.enableWeightedPrediction = config.weighted_prediction && get_encoder_cap(NV_ENC_CAPS_SUPPORT_WEIGHTED_PREDICTION);\n\n    init_params.encodeWidth = encoder_params.width;\n    init_params.darWidth = encoder_params.width;\n    init_params.encodeHeight = encoder_params.height;\n    init_params.darHeight = encoder_params.height;\n\n    // Use fractional framerate if available (for NTSC support)\n    if (client_config.frameRateNum > 0 && client_config.frameRateDen > 0) {\n      init_params.frameRateNum = client_config.frameRateNum;\n      init_params.frameRateDen = client_config.frameRateDen;\n      BOOST_LOG(debug) << \"NvEnc: Using fractional framerate: \" << client_config.frameRateNum << \"/\"\n                       << client_config.frameRateDen << \" (\" << client_config.get_effective_framerate() << \"fps)\";\n    }\n    else {\n      init_params.frameRateNum = client_config.framerate;\n      init_params.frameRateDen = 1;\n    }\n\n#if NVENC_INT_VERSION >= 1202\n    {\n      using enum nvenc_split_frame_encoding;\n      switch (config.split_frame_encoding) {\n        case disabled:\n          init_params.splitEncodeMode = NV_ENC_SPLIT_DISABLE_MODE;\n          break;\n        case driver_decides:\n          init_params.splitEncodeMode = NV_ENC_SPLIT_AUTO_MODE;\n          break;\n        case force_enabled:\n          init_params.splitEncodeMode = NV_ENC_SPLIT_AUTO_FORCED_MODE;\n          break;\n        case two_strips:\n          init_params.splitEncodeMode = NV_ENC_SPLIT_TWO_FORCED_MODE;\n          break;\n        case three_strips:\n          init_params.splitEncodeMode = NV_ENC_SPLIT_THREE_FORCED_MODE;\n          break;\n        case four_strips:\n          init_params.splitEncodeMode = NV_ENC_SPLIT_FOUR_FORCED_MODE;\n          break;\n        default:\n          init_params.splitEncodeMode = NV_ENC_SPLIT_AUTO_MODE;\n          break;\n      }\n    }\n#endif\n\n    NV_ENC_PRESET_CONFIG preset_config = { NV_ENC_PRESET_CONFIG_VER };\n    preset_config.presetCfg.version = NV_ENC_CONFIG_VER;\n    if (nvenc_failed(nvenc->nvEncGetEncodePresetConfigEx(encoder, init_params.encodeGUID, init_params.presetGUID, init_params.tuningInfo, &preset_config))) {\n      BOOST_LOG(error) << \"NvEnc: NvEncGetEncodePresetConfigEx() failed: \" << last_nvenc_error_string;\n      return false;\n    }\n\n    NV_ENC_CONFIG enc_config = preset_config.presetCfg;\n    enc_config.profileGUID = NV_ENC_CODEC_PROFILE_AUTOSELECT_GUID;\n    enc_config.gopLength = NVENC_INFINITE_GOPLENGTH;\n    enc_config.frameIntervalP = 1;\n    \n    // Configure rate control mode (CBR or VBR)\n    auto supported_rc_modes = get_encoder_cap(NV_ENC_CAPS_SUPPORTED_RATECONTROL_MODES);\n    bool vbr_supported = (supported_rc_modes & NV_ENC_PARAMS_RC_VBR) != 0;\n    \n    if (config.rate_control_mode == nvenc_rate_control_mode::vbr && vbr_supported) {\n      enc_config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_VBR;\n      // Set max bitrate for VBR (typically 1.5x average bitrate for better quality)\n      enc_config.rcParams.maxBitRate = static_cast<uint32_t>(client_config.bitrate * 1500);\n      \n      // Set target quality for VBR mode (0 = automatic)\n      if (config.target_quality > 0) {\n        // Clamp target quality based on codec\n        unsigned max_quality = 51;  // H.264/HEVC\n        if (client_config.videoFormat == 2) {  // AV1\n          max_quality = 63;\n        }\n        if (config.target_quality > static_cast<int>(max_quality)) {\n          enc_config.rcParams.targetQuality = static_cast<uint8_t>(max_quality);\n          BOOST_LOG(warning) << \"NvEnc: target_quality clamped to \" << max_quality;\n        }\n        else {\n          enc_config.rcParams.targetQuality = static_cast<uint8_t>(config.target_quality);\n        }\n        BOOST_LOG(info) << \"NvEnc: VBR mode with target quality \" << enc_config.rcParams.targetQuality;\n      }\n      else {\n        enc_config.rcParams.targetQuality = 0;  // Automatic\n        BOOST_LOG(info) << \"NvEnc: VBR mode with automatic target quality\";\n      }\n    }\n    else {\n      enc_config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR;\n      if (config.rate_control_mode == nvenc_rate_control_mode::vbr && !vbr_supported) {\n        BOOST_LOG(warning) << \"NvEnc: VBR mode requested but not supported by GPU, using CBR\";\n      }\n    }\n    \n    enc_config.rcParams.zeroReorderDelay = 1;\n    enc_config.rcParams.lowDelayKeyFrameScale = 1;\n    enc_config.rcParams.multiPass = config.two_pass == nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION :\n                                    config.two_pass == nvenc_two_pass::full_resolution    ? NV_ENC_TWO_PASS_FULL_RESOLUTION :\n                                                                                            NV_ENC_MULTI_PASS_DISABLED;\n\n    // Configure lookahead\n    bool lookahead_supported = get_encoder_cap(NV_ENC_CAPS_SUPPORT_LOOKAHEAD) != 0;\n    bool lookahead_enabled = config.lookahead_depth > 0 && lookahead_supported;\n    enc_config.rcParams.enableLookahead = lookahead_enabled ? 1 : 0;\n    \n    if (lookahead_enabled) {\n      enc_config.rcParams.lookaheadDepth = config.lookahead_depth;\n      // Clamp lookahead depth to reasonable range (0-32)\n      if (enc_config.rcParams.lookaheadDepth > 32) {\n        enc_config.rcParams.lookaheadDepth = 32;\n        BOOST_LOG(warning) << \"NvEnc: lookahead_depth clamped to 32\";\n      }\n      \n      // Set lookahead level if supported (NVENC SDK 13.0+)\n#if NVENC_INT_VERSION >= 1202\n      if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_LOOKAHEAD_LEVEL) != 0) {\n        switch (config.lookahead_level) {\n          case nvenc_lookahead_level::disabled:\n            enc_config.rcParams.lookaheadLevel = NV_ENC_LOOKAHEAD_LEVEL_0;\n            break;\n          case nvenc_lookahead_level::level_1:\n            enc_config.rcParams.lookaheadLevel = NV_ENC_LOOKAHEAD_LEVEL_1;\n            break;\n          case nvenc_lookahead_level::level_2:\n            enc_config.rcParams.lookaheadLevel = NV_ENC_LOOKAHEAD_LEVEL_2;\n            break;\n          case nvenc_lookahead_level::level_3:\n            enc_config.rcParams.lookaheadLevel = NV_ENC_LOOKAHEAD_LEVEL_3;\n            break;\n          case nvenc_lookahead_level::autoselect:\n            enc_config.rcParams.lookaheadLevel = NV_ENC_LOOKAHEAD_LEVEL_AUTOSELECT;\n            break;\n          default:\n            enc_config.rcParams.lookaheadLevel = NV_ENC_LOOKAHEAD_LEVEL_0;\n            break;\n        }\n      }\n      else {\n        enc_config.rcParams.lookaheadLevel = NV_ENC_LOOKAHEAD_LEVEL_0;\n      }\n#else\n      // Lookahead level not supported in older SDK versions, skip setting it\n#endif\n    }\n    else {\n      enc_config.rcParams.lookaheadDepth = 0;\n#if NVENC_INT_VERSION >= 1202\n      enc_config.rcParams.lookaheadLevel = NV_ENC_LOOKAHEAD_LEVEL_0;\n#endif\n      if (config.lookahead_depth > 0 && !lookahead_supported) {\n        BOOST_LOG(warning) << \"NvEnc: lookahead requested but not supported by GPU\";\n      }\n    }\n\n    enc_config.rcParams.enableAQ = config.adaptive_quantization;\n    \n    // Enable temporal AQ if supported and lookahead is enabled\n    if (config.enable_temporal_aq && lookahead_enabled) {\n      if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_TEMPORAL_AQ) != 0) {\n        // Temporal AQ is enabled through enableAQ when lookahead is active\n        // The encoder will use temporal AQ automatically if supported\n        BOOST_LOG(debug) << \"NvEnc: Temporal AQ enabled (requires lookahead)\";\n      }\n      else {\n        BOOST_LOG(warning) << \"NvEnc: Temporal AQ requested but not supported by GPU\";\n      }\n    }\n    enc_config.rcParams.averageBitRate = client_config.bitrate * 1000;\n\n    if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) {\n      // Use effective framerate for VBV buffer calculation (supports NTSC fractional framerates)\n      double effective_fps = client_config.get_effective_framerate();\n      enc_config.rcParams.vbvBufferSize = static_cast<uint32_t>(client_config.bitrate * 1000 / effective_fps);\n      if (config.vbv_percentage_increase > 0) {\n        enc_config.rcParams.vbvBufferSize += enc_config.rcParams.vbvBufferSize * config.vbv_percentage_increase / 100;\n      }\n    }\n\n    auto set_h264_hevc_common_format_config = [&](auto &format_config) {\n      format_config.repeatSPSPPS = 1;\n      format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH;\n      if (client_config.slicesPerFrame > 1 ||\n          NVENC_INT_VERSION < 1202 ||\n          config.split_frame_encoding == nvenc_split_frame_encoding::disabled) {\n        format_config.sliceMode = 3;\n        format_config.sliceModeData = client_config.slicesPerFrame;\n      }\n      if (buffer_is_yuv444()) {\n        format_config.chromaFormatIDC = 3;\n      }\n      format_config.enableFillerDataInsertion = config.insert_filler_data;\n    };\n\n    auto set_ref_frames = [&](uint32_t &ref_frames_option, NV_ENC_NUM_REF_FRAMES &L0_option, uint32_t ref_frames_default) {\n      if (client_config.numRefFrames > 0) {\n        ref_frames_option = client_config.numRefFrames;\n      }\n      else {\n        ref_frames_option = ref_frames_default;\n      }\n      if (ref_frames_option > 0 && !get_encoder_cap(NV_ENC_CAPS_SUPPORT_MULTIPLE_REF_FRAMES)) {\n        ref_frames_option = 1;\n        encoder_params.rfi = false;\n      }\n      encoder_params.ref_frames_in_dpb = ref_frames_option;\n      // This limits ref frames any frame can use to 1, but allows larger buffer size for fallback if some frames are invalidated through rfi\n      L0_option = NV_ENC_NUM_REF_FRAMES_1;\n    };\n\n    auto set_minqp_if_enabled = [&](int value) {\n      if (config.enable_min_qp) {\n        enc_config.rcParams.enableMinQP = 1;\n        enc_config.rcParams.minQP.qpInterP = value;\n        enc_config.rcParams.minQP.qpIntra = value;\n      }\n    };\n\n    auto fill_h264_hevc_vui = [&](auto &vui_config) {\n      vui_config.videoSignalTypePresentFlag = 1;\n      vui_config.videoFormat = NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED;\n      vui_config.videoFullRangeFlag = colorspace.full_range;\n      vui_config.colourDescriptionPresentFlag = 1;\n      vui_config.colourPrimaries = colorspace.primaries;\n      vui_config.transferCharacteristics = colorspace.tranfer_function;\n      vui_config.colourMatrix = colorspace.matrix;\n      vui_config.chromaSampleLocationFlag = buffer_is_yuv444() ? 0 : 1;\n      vui_config.chromaSampleLocationTop = 0;\n      vui_config.chromaSampleLocationBot = 0;\n\n      // This is critical for low decoding latency on certain devices\n      vui_config.bitstreamRestrictionFlag = 1;\n    };\n\n    switch (client_config.videoFormat) {\n      case 0: {\n        // H.264\n        enc_config.profileGUID = buffer_is_yuv444() ? NV_ENC_H264_PROFILE_HIGH_444_GUID : NV_ENC_H264_PROFILE_HIGH_GUID;\n        auto &format_config = enc_config.encodeCodecConfig.h264Config;\n        set_h264_hevc_common_format_config(format_config);\n        if (config.h264_cavlc || !get_encoder_cap(NV_ENC_CAPS_SUPPORT_CABAC)) {\n          format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC;\n        }\n        else {\n          format_config.entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CABAC;\n        }\n        set_ref_frames(format_config.maxNumRefFrames, format_config.numRefL0, 5);\n        set_minqp_if_enabled(config.min_qp_h264);\n        fill_h264_hevc_vui(format_config.h264VUIParameters);\n        \n        // Configure temporal filter for H.264 (NVENC SDK 13.0+)\n#if NVENC_INT_VERSION >= 1202\n        if (config.temporal_filter_level != nvenc_temporal_filter_level::disabled) {\n          if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_TEMPORAL_FILTER) != 0) {\n            if (enc_config.frameIntervalP >= 5) {\n              switch (config.temporal_filter_level) {\n                case nvenc_temporal_filter_level::level_4:\n                  format_config.tfLevel = NV_ENC_TEMPORAL_FILTER_LEVEL_4;\n                  break;\n                case nvenc_temporal_filter_level::disabled:\n                default:\n                  format_config.tfLevel = NV_ENC_TEMPORAL_FILTER_LEVEL_0;\n                  break;\n              }\n            }\n            else {\n              BOOST_LOG(warning) << \"NvEnc: Temporal filter requires frameIntervalP >= 5, but current value is \" << enc_config.frameIntervalP << \". Disabling temporal filter.\";\n            }\n          }\n          else {\n            BOOST_LOG(warning) << \"NvEnc: Temporal filter requested but not supported by GPU\";\n          }\n        }\n#endif\n        break;\n      }\n\n      case 1: {\n        // HEVC\n        auto &format_config = enc_config.encodeCodecConfig.hevcConfig;\n        set_h264_hevc_common_format_config(format_config);\n        if (buffer_is_10bit()) {\n#if NVENC_INT_VERSION >= 1202\n          format_config.inputBitDepth = NV_ENC_BIT_DEPTH_10;\n          format_config.outputBitDepth = NV_ENC_BIT_DEPTH_10;\n#else\n          format_config.pixelBitDepthMinus8 = 2;\n#endif\n        }\n        set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numRefL0, 5);\n        set_minqp_if_enabled(config.min_qp_hevc);\n        fill_h264_hevc_vui(format_config.hevcVUIParameters);\n\n#if NVENC_INT_VERSION >= 1202\n        // Enable HDR metadata output (mastering display and content light level SEI)\n        // Available in NVENC SDK 12.2+\n        format_config.outputMaxCll = 1;\n        format_config.outputMasteringDisplay = 1;\n#endif\n        \n        // Configure temporal filter for HEVC (NVENC SDK 13.0+)\n#if NVENC_INT_VERSION >= 1202\n        if (config.temporal_filter_level != nvenc_temporal_filter_level::disabled) {\n          if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_TEMPORAL_FILTER) != 0) {\n            if (enc_config.frameIntervalP >= 5) {\n              switch (config.temporal_filter_level) {\n                case nvenc_temporal_filter_level::level_4:\n                  format_config.tfLevel = NV_ENC_TEMPORAL_FILTER_LEVEL_4;\n                  break;\n                case nvenc_temporal_filter_level::disabled:\n                default:\n                  format_config.tfLevel = NV_ENC_TEMPORAL_FILTER_LEVEL_0;\n                  break;\n              }\n            }\n            else {\n              BOOST_LOG(warning) << \"NvEnc: Temporal filter requires frameIntervalP >= 5, but current value is \" << enc_config.frameIntervalP << \". Disabling temporal filter.\";\n            }\n          }\n          else {\n            BOOST_LOG(warning) << \"NvEnc: Temporal filter requested but not supported by GPU\";\n          }\n        }\n#endif\n        \n        if (client_config.enableIntraRefresh == 1) {\n          if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_INTRA_REFRESH)) {\n            format_config.enableIntraRefresh = 1;\n            format_config.intraRefreshPeriod = 300;\n            format_config.intraRefreshCnt = 299;\n#if NVENC_INT_VERSION >= 1200\n            if (get_encoder_cap(NV_ENC_CAPS_SINGLE_SLICE_INTRA_REFRESH)) {\n              format_config.singleSliceIntraRefresh = 1;\n            }\n            else {\n              BOOST_LOG(warning) << \"NvEnc: Single Slice Intra Refresh not supported\";\n            }\n#endif\n          }\n          else {\n            BOOST_LOG(error) << \"NvEnc: Client asked for intra-refresh but the encoder does not support intra-refresh\";\n          }\n        }\n        break;\n      }\n\n#if NVENC_INT_VERSION >= 1200\n      case 2: {\n        // AV1\n        auto &format_config = enc_config.encodeCodecConfig.av1Config;\n        format_config.repeatSeqHdr = 1;\n        format_config.idrPeriod = NVENC_INFINITE_GOPLENGTH;\n        if (buffer_is_yuv444()) {\n          format_config.chromaFormatIDC = 3;\n        }\n        format_config.enableBitstreamPadding = config.insert_filler_data;\n        if (buffer_is_10bit()) {\n  #if NVENC_INT_VERSION >= 1202\n          format_config.inputBitDepth = NV_ENC_BIT_DEPTH_10;\n          format_config.outputBitDepth = NV_ENC_BIT_DEPTH_10;\n  #else\n          format_config.inputPixelBitDepthMinus8 = 2;\n          format_config.pixelBitDepthMinus8 = 2;\n  #endif\n        }\n        format_config.colorPrimaries = colorspace.primaries;\n        format_config.transferCharacteristics = colorspace.tranfer_function;\n        format_config.matrixCoefficients = colorspace.matrix;\n        format_config.colorRange = colorspace.full_range;\n        format_config.chromaSamplePosition = buffer_is_yuv444() ? 0 : 1;\n        set_ref_frames(format_config.maxNumRefFramesInDPB, format_config.numFwdRefs, 8);\n        set_minqp_if_enabled(config.min_qp_av1);\n\n#if NVENC_INT_VERSION >= 1202\n        // Enable HDR metadata output (mastering display and content light level)\n        // Available in NVENC SDK 12.2+\n        format_config.outputMaxCll = 1;\n        format_config.outputMasteringDisplay = 1;\n#endif\n        \n        // Configure temporal filter for AV1 (NVENC SDK 13.0+)\n#if NVENC_INT_VERSION >= 1202\n        if (config.temporal_filter_level != nvenc_temporal_filter_level::disabled) {\n          if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_TEMPORAL_FILTER) != 0) {\n            if (enc_config.frameIntervalP >= 5) {\n              switch (config.temporal_filter_level) {\n                case nvenc_temporal_filter_level::level_4:\n                  format_config.tfLevel = NV_ENC_TEMPORAL_FILTER_LEVEL_4;\n                  break;\n                case nvenc_temporal_filter_level::disabled:\n                default:\n                  format_config.tfLevel = NV_ENC_TEMPORAL_FILTER_LEVEL_0;\n                  break;\n              }\n            }\n            else {\n              BOOST_LOG(warning) << \"NvEnc: Temporal filter requires frameIntervalP >= 5, but current value is \" << enc_config.frameIntervalP << \". Disabling temporal filter.\";\n            }\n          }\n          else {\n            BOOST_LOG(warning) << \"NvEnc: Temporal filter requested but not supported by GPU\";\n          }\n        }\n#endif\n\n        if (client_config.slicesPerFrame > 1) {\n          // NVENC only supports slice counts that are powers of two, so we'll pick powers of two\n          // with bias to rows due to hopefully more similar macroblocks with a row vs a column.\n          format_config.numTileRows = std::pow(2, std::ceil(std::log2(client_config.slicesPerFrame) / 2));\n          format_config.numTileColumns = std::pow(2, std::floor(std::log2(client_config.slicesPerFrame) / 2));\n        }\n        break;\n      }\n#endif\n    }\n\n    init_params.encodeConfig = &enc_config;\n\n    if (nvenc_failed(nvenc->nvEncInitializeEncoder(encoder, &init_params))) {\n      BOOST_LOG(error) << \"NvEnc: NvEncInitializeEncoder() failed: \" << last_nvenc_error_string;\n      return false;\n    }\n\n    if (async_event_handle) {\n      NV_ENC_EVENT_PARAMS event_params = { NV_ENC_EVENT_PARAMS_VER };\n      event_params.completionEvent = async_event_handle;\n      if (nvenc_failed(nvenc->nvEncRegisterAsyncEvent(encoder, &event_params))) {\n        BOOST_LOG(error) << \"NvEnc: NvEncRegisterAsyncEvent() failed: \" << last_nvenc_error_string;\n        return false;\n      }\n    }\n\n    NV_ENC_CREATE_BITSTREAM_BUFFER create_bitstream_buffer = { NV_ENC_CREATE_BITSTREAM_BUFFER_VER };\n    if (nvenc_failed(nvenc->nvEncCreateBitstreamBuffer(encoder, &create_bitstream_buffer))) {\n      BOOST_LOG(error) << \"NvEnc: NvEncCreateBitstreamBuffer() failed: \" << last_nvenc_error_string;\n      return false;\n    }\n    output_bitstream = create_bitstream_buffer.bitstreamBuffer;\n\n    if (!create_and_register_input_buffer()) {\n      return false;\n    }\n\n    {\n      auto f = stat_trackers::two_digits_after_decimal();\n      BOOST_LOG(debug) << \"NvEnc: requested encoded frame size \" << f % (client_config.bitrate / 8. / client_config.framerate) << \" kB\";\n    }\n\n    {\n      auto video_format_string = client_config.videoFormat == 0 ? \"H.264 \" :\n                                 client_config.videoFormat == 1 ? \"HEVC \" :\n                                 client_config.videoFormat == 2 ? \"AV1 \" :\n                                                                  \" \";\n      std::string extra;\n      if (init_params.enableEncodeAsync) extra += \" async\";\n      if (buffer_is_yuv444()) extra += \" yuv444\";\n      if (buffer_is_10bit()) extra += \" 10-bit\";\n      if (enc_config.rcParams.rateControlMode == NV_ENC_PARAMS_RC_VBR) {\n        extra += \" vbr\";\n        if (enc_config.rcParams.targetQuality > 0) {\n          extra += \" quality=\" + std::to_string(enc_config.rcParams.targetQuality);\n        }\n        else {\n          extra += \" quality=auto\";\n        }\n      }\n      else {\n        extra += \" cbr\";\n      }\n      if (enc_config.rcParams.multiPass != NV_ENC_MULTI_PASS_DISABLED) extra += \" two-pass\";\n      if (config.vbv_percentage_increase > 0 && get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) {\n        extra += \" vbv+\" + std::to_string(config.vbv_percentage_increase);\n      }\n      if (encoder_params.rfi) extra += \" rfi\";\n      if (init_params.enableWeightedPrediction) extra += \" weighted-prediction\";\n      if (enc_config.rcParams.enableAQ) extra += \" spatial-aq\";\n      if (enc_config.rcParams.enableMinQP) extra += \" qpmin=\" + std::to_string(enc_config.rcParams.minQP.qpInterP);\n      if (config.insert_filler_data) extra += \" filler-data\";\n\n      BOOST_LOG(info) << \"NvEnc: created encoder v\" << NVENC_INT_VERSION << \" \"\n                      << video_format_string << quality_preset_string_from_guid(init_params.presetGUID) << extra;\n    }\n\n    // 保存当前编码器配置和初始化参数，用于后续动态调整\n    saved_init_params = init_params;\n    current_enc_config = enc_config;\n    saved_init_params.encodeConfig = &current_enc_config;  // 确保指针指向我们的成员变量\n\n    encoder_state = {};\n    fail_guard.disable();\n    return true;\n  }\n\n  void\n  nvenc_base::destroy_encoder() {\n    if (output_bitstream) {\n      if (nvenc_failed(nvenc->nvEncDestroyBitstreamBuffer(encoder, output_bitstream))) {\n        BOOST_LOG(error) << \"NvEnc: NvEncDestroyBitstreamBuffer() failed: \" << last_nvenc_error_string;\n      }\n      output_bitstream = nullptr;\n    }\n    if (encoder && async_event_handle) {\n      NV_ENC_EVENT_PARAMS event_params = { NV_ENC_EVENT_PARAMS_VER };\n      event_params.completionEvent = async_event_handle;\n      if (nvenc_failed(nvenc->nvEncUnregisterAsyncEvent(encoder, &event_params))) {\n        BOOST_LOG(error) << \"NvEnc: NvEncUnregisterAsyncEvent() failed: \" << last_nvenc_error_string;\n      }\n    }\n    if (registered_input_buffer) {\n      if (nvenc_failed(nvenc->nvEncUnregisterResource(encoder, registered_input_buffer))) {\n        BOOST_LOG(error) << \"NvEnc: NvEncUnregisterResource() failed: \" << last_nvenc_error_string;\n      }\n      registered_input_buffer = nullptr;\n    }\n    if (encoder) {\n      if (nvenc_failed(nvenc->nvEncDestroyEncoder(encoder))) {\n        BOOST_LOG(error) << \"NvEnc: NvEncDestroyEncoder() failed: \" << last_nvenc_error_string;\n      }\n      encoder = nullptr;\n    }\n\n    encoder_state = {};\n    encoder_params = {};\n  }\n\n  nvenc_encoded_frame\n  nvenc_base::encode_frame(uint64_t frame_index, bool force_idr) {\n    if (!encoder) {\n      return {};\n    }\n\n    assert(registered_input_buffer);\n    assert(output_bitstream);\n\n    if (!synchronize_input_buffer()) {\n      BOOST_LOG(error) << \"NvEnc: failed to synchronize input buffer\";\n      return {};\n    }\n\n    NV_ENC_MAP_INPUT_RESOURCE mapped_input_buffer = { NV_ENC_MAP_INPUT_RESOURCE_VER };\n    mapped_input_buffer.registeredResource = registered_input_buffer;\n\n    if (nvenc_failed(nvenc->nvEncMapInputResource(encoder, &mapped_input_buffer))) {\n      BOOST_LOG(error) << \"NvEnc: NvEncMapInputResource() failed: \" << last_nvenc_error_string;\n      return {};\n    }\n    auto unmap_guard = util::fail_guard([&] {\n      if (nvenc_failed(nvenc->nvEncUnmapInputResource(encoder, mapped_input_buffer.mappedResource))) {\n        BOOST_LOG(error) << \"NvEnc: NvEncUnmapInputResource() failed: \" << last_nvenc_error_string;\n      }\n    });\n\n    NV_ENC_PIC_PARAMS pic_params = { NV_ENC_PIC_PARAMS_VER };\n    pic_params.inputWidth = encoder_params.width;\n    pic_params.inputHeight = encoder_params.height;\n    pic_params.encodePicFlags = force_idr ? NV_ENC_PIC_FLAG_FORCEIDR : 0;\n    pic_params.inputTimeStamp = frame_index;\n    pic_params.pictureStruct = NV_ENC_PIC_STRUCT_FRAME;\n    pic_params.inputBuffer = mapped_input_buffer.mappedResource;\n    pic_params.bufferFmt = mapped_input_buffer.mappedBufferFmt;\n    pic_params.outputBitstream = output_bitstream;\n    pic_params.completionEvent = async_event_handle;\n\n#if NVENC_INT_VERSION >= 1202\n    // Prepare HDR metadata structures for per-frame passing (NVENC SDK 12.2+)\n    MASTERING_DISPLAY_INFO mastering_display = {};\n    CONTENT_LIGHT_LEVEL content_light_level = {};\n\n    if (hdr_metadata) {\n      // Convert nvenc_hdr_metadata to NVENC structures\n      // Display primaries are normalized to 50,000 in SS_HDR_METADATA\n      // NVENC expects values normalized to 50,000 as well\n      mastering_display.r.x = hdr_metadata->displayPrimaries[0].x;\n      mastering_display.r.y = hdr_metadata->displayPrimaries[0].y;\n      mastering_display.g.x = hdr_metadata->displayPrimaries[1].x;\n      mastering_display.g.y = hdr_metadata->displayPrimaries[1].y;\n      mastering_display.b.x = hdr_metadata->displayPrimaries[2].x;\n      mastering_display.b.y = hdr_metadata->displayPrimaries[2].y;\n      mastering_display.whitePoint.x = hdr_metadata->whitePoint.x;\n      mastering_display.whitePoint.y = hdr_metadata->whitePoint.y;\n      // maxLuma is in nits, NVENC expects candelas per square meter (same as nits)\n      mastering_display.maxLuma = hdr_metadata->maxDisplayLuminance;\n      // minLuma is in 1/10000th of a nit in SS_HDR_METADATA, NVENC expects same unit\n      mastering_display.minLuma = hdr_metadata->minDisplayLuminance;\n\n      content_light_level.maxContentLightLevel = hdr_metadata->maxContentLightLevel;\n      content_light_level.maxPicAverageLightLevel = hdr_metadata->maxFrameAverageLightLevel;\n\n      // Set HDR metadata for HEVC\n      if (video_format == 1) {\n        pic_params.codecPicParams.hevcPicParams.pMasteringDisplay = &mastering_display;\n        pic_params.codecPicParams.hevcPicParams.pMaxCll = &content_light_level;\n      }\n#if NVENCAPI_MAJOR_VERSION >= 12\n      // Set HDR metadata for AV1 (AV1 encoding support added in NVENC SDK 12.0)\n      else if (video_format == 2) {\n        pic_params.codecPicParams.av1PicParams.pMasteringDisplay = &mastering_display;\n        pic_params.codecPicParams.av1PicParams.pMaxCll = &content_light_level;\n      }\n#endif\n    }\n#endif\n\n    // Inject HDR10+ and/or Vivid dynamic metadata as custom SEI/OBU payloads\n    std::vector<uint8_t> hdr10plus_payload;\n    std::vector<uint8_t> vivid_payload;\n    NV_ENC_SEI_PAYLOAD sei_payloads[2] = {};\n    uint32_t sei_count = 0;\n\n    if (luminance_stats.valid && hdr_metadata && (video_format == 1 || video_format == 2)) {\n      uint16_t max_lum = hdr_metadata->maxDisplayLuminance;\n\n      // HDR10+ (PQ only — HDR10+ requires absolute luminance)\n      if (serialize_hdr10plus_sei(luminance_stats, max_lum, hdr10plus_payload) > 0) {\n        sei_payloads[sei_count].payloadSize = static_cast<uint32_t>(hdr10plus_payload.size());\n        sei_payloads[sei_count].payloadType = 4;  // user_data_registered_itu_t_t35\n        sei_payloads[sei_count].payload = hdr10plus_payload.data();\n        sei_count++;\n      }\n\n      // HDR Vivid (both PQ and HLG)\n      if (serialize_vivid_sei(luminance_stats, max_lum, vivid_payload) > 0) {\n        sei_payloads[sei_count].payloadSize = static_cast<uint32_t>(vivid_payload.size());\n        sei_payloads[sei_count].payloadType = 4;  // user_data_registered_itu_t_t35\n        sei_payloads[sei_count].payload = vivid_payload.data();\n        sei_count++;\n      }\n\n      if (sei_count > 0) {\n        if (video_format == 1) {\n          pic_params.codecPicParams.hevcPicParams.seiPayloadArrayCnt = sei_count;\n          pic_params.codecPicParams.hevcPicParams.seiPayloadArray = sei_payloads;\n        }\n#if NVENCAPI_MAJOR_VERSION >= 12\n        else if (video_format == 2) {\n          pic_params.codecPicParams.av1PicParams.obuPayloadArrayCnt = sei_count;\n          pic_params.codecPicParams.av1PicParams.obuPayloadArray = sei_payloads;\n        }\n#endif\n      }\n    }\n\n    NVENCSTATUS encode_status = nvenc->nvEncEncodePicture(encoder, &pic_params);\n    if (encode_status == NV_ENC_ERR_NEED_MORE_INPUT) {\n      // This is not a fatal error - encoder needs more input frames before it can produce output.\n      // This can happen with B-frame reordering or lookahead. Return empty frame to signal\n      // the caller should continue without treating this as an error.\n      BOOST_LOG(debug) << \"NvEnc: frame \" << frame_index << \" buffered (need more input)\";\n      return { {}, frame_index, false, false };\n    }\n    if (nvenc_failed(encode_status)) {\n      BOOST_LOG(error) << \"NvEnc: NvEncEncodePicture() failed: \" << last_nvenc_error_string;\n      return {};\n    }\n\n    NV_ENC_LOCK_BITSTREAM lock_bitstream = { NV_ENC_LOCK_BITSTREAM_VER };\n    lock_bitstream.outputBitstream = output_bitstream;\n    lock_bitstream.doNotWait = async_event_handle ? 1 : 0;\n\n    if (async_event_handle && !wait_for_async_event(100)) {\n      BOOST_LOG(error) << \"NvEnc: frame \" << frame_index << \" encode wait timeout\";\n      return {};\n    }\n\n    if (nvenc_failed(nvenc->nvEncLockBitstream(encoder, &lock_bitstream))) {\n      BOOST_LOG(error) << \"NvEnc: NvEncLockBitstream() failed: \" << last_nvenc_error_string;\n      return {};\n    }\n\n    auto data_pointer = (uint8_t *) lock_bitstream.bitstreamBufferPtr;\n    nvenc_encoded_frame encoded_frame {\n      { data_pointer, data_pointer + lock_bitstream.bitstreamSizeInBytes },\n      lock_bitstream.outputTimeStamp,\n      lock_bitstream.pictureType == NV_ENC_PIC_TYPE_IDR,\n      encoder_state.rfi_needs_confirmation,\n    };\n\n    if (encoder_state.rfi_needs_confirmation) {\n      // Invalidation request has been fulfilled, and video network packet will be marked as such\n      encoder_state.rfi_needs_confirmation = false;\n    }\n\n    encoder_state.last_encoded_frame_index = frame_index;\n\n    if (encoded_frame.idr) {\n      BOOST_LOG(debug) << \"NvEnc: idr frame \" << encoded_frame.frame_index;\n    }\n\n    if (nvenc_failed(nvenc->nvEncUnlockBitstream(encoder, lock_bitstream.outputBitstream))) {\n      BOOST_LOG(error) << \"NvEnc: NvEncUnlockBitstream() failed: \" << last_nvenc_error_string;\n    }\n\n    encoder_state.frame_size_logger.collect_and_log(encoded_frame.data.size() / 1000.);\n\n    return encoded_frame;\n  }\n\n  bool\n  nvenc_base::invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) {\n    if (!encoder || !encoder_params.rfi) return false;\n\n    if (first_frame >= encoder_state.last_rfi_range.first &&\n        last_frame <= encoder_state.last_rfi_range.second) {\n      BOOST_LOG(debug) << \"NvEnc: rfi request \" << first_frame << \"-\" << last_frame << \" already done\";\n      return true;\n    }\n\n    encoder_state.rfi_needs_confirmation = true;\n\n    if (last_frame < first_frame) {\n      BOOST_LOG(error) << \"NvEnc: invaid rfi request \" << first_frame << \"-\" << last_frame << \", generating IDR\";\n      return false;\n    }\n\n    BOOST_LOG(debug) << \"NvEnc: rfi request \" << first_frame << \"-\" << last_frame\n                     << \" expanding to last encoded frame \" << encoder_state.last_encoded_frame_index;\n    last_frame = encoder_state.last_encoded_frame_index;\n\n    encoder_state.last_rfi_range = { first_frame, last_frame };\n\n    if (last_frame - first_frame + 1 >= encoder_params.ref_frames_in_dpb) {\n      BOOST_LOG(debug) << \"NvEnc: rfi request too large, generating IDR\";\n      return false;\n    }\n\n    for (auto i = first_frame; i <= last_frame; i++) {\n      if (nvenc_failed(nvenc->nvEncInvalidateRefFrames(encoder, i))) {\n        BOOST_LOG(error) << \"NvEnc: NvEncInvalidateRefFrames() \" << i << \" failed: \" << last_nvenc_error_string;\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  void\n  nvenc_base::set_bitrate(int bitrate_kbps) {\n    if (!encoder) {\n      BOOST_LOG(warning) << \"NvEnc: 编码器未初始化，无法设置码率\";\n      return;\n    }\n    if (!nvenc) {\n      BOOST_LOG(warning) << \"NvEnc: NVENC接口未初始化，无法设置码率\";\n      return;\n    }\n    if (NVENC_INT_VERSION < 1100) {\n      BOOST_LOG(error) << \"NvEnc: NVENC API版本过低(\" << NVENC_INT_VERSION << \")，不支持动态码率调整\";\n      return;\n    }\n    if (bitrate_kbps <= 0 || bitrate_kbps > 800000) {\n      BOOST_LOG(error) << \"NvEnc: 码率无效: \" << bitrate_kbps << \" Kbps (有效范围: 1~800000)\";\n      return;\n    }\n\n    bool is_hevc = (saved_init_params.encodeGUID == NV_ENC_CODEC_HEVC_GUID);\n    bool is_av1 = false;\n#if NVENC_INT_VERSION >= 1200\n    is_av1 = (saved_init_params.encodeGUID == NV_ENC_CODEC_AV1_GUID);\n#endif\n\n    // 复制当前配置，准备修改\n    NV_ENC_CONFIG enc_config = current_enc_config;\n    enc_config.rcParams.averageBitRate = bitrate_kbps * 1000;\n    enc_config.rcParams.maxBitRate = bitrate_kbps * 1000;\n\n    // HEVC需调整VBV缓冲区应对更大码率\n    if (is_hevc) {\n      uint32_t prev_bitrate = current_enc_config.rcParams.averageBitRate;\n      uint32_t old_vbv_size = current_enc_config.rcParams.vbvBufferSize;\n      uint32_t new_vbv_size = old_vbv_size;\n\n      new_vbv_size = static_cast<uint32_t>((static_cast<uint64_t>(bitrate_kbps) * 1000 * old_vbv_size) / prev_bitrate);\n\n      // 防止VBV缓冲区过小\n      if (new_vbv_size < 1000 * 100) new_vbv_size = 1000 * 100;  // 至少100K\n      enc_config.rcParams.vbvBufferSize = new_vbv_size;\n      BOOST_LOG(debug) << \"NvEnc: VBV缓冲区调整为 \" << new_vbv_size / 1000 << \" Kbps\";\n    }\n\n    // 构造重配置参数\n    NV_ENC_RECONFIGURE_PARAMS reconfigure_params = { NV_ENC_RECONFIGURE_PARAMS_VER };\n    reconfigure_params.reInitEncodeParams = saved_init_params;\n    reconfigure_params.reInitEncodeParams.encodeConfig = &enc_config;\n\n    // HEVC码率提升时重置编码器状态\n    if (is_hevc && bitrate_kbps * 1000 > current_enc_config.rcParams.averageBitRate) {\n      BOOST_LOG(debug) << \"NvEnc: HEVC码率提升，重置编码器状态\";\n      reconfigure_params.resetEncoder = 1;\n      reconfigure_params.forceIDR = 1;\n    }\n\n    if (nvenc_failed(nvenc->nvEncReconfigureEncoder(encoder, &reconfigure_params))) {\n      BOOST_LOG(error) << \"NvEnc: 设置码率失败(\" << bitrate_kbps << \" Kbps): \" << last_nvenc_error_string;\n      return;\n    }\n\n    // 更新当前配置\n    current_enc_config.rcParams.averageBitRate = bitrate_kbps * 1000;\n    current_enc_config.rcParams.maxBitRate = bitrate_kbps * 1000;\n    if (is_hevc) {\n      current_enc_config.rcParams.vbvBufferSize = enc_config.rcParams.vbvBufferSize;\n    }\n\n    const char *codec_name = is_hevc ? \"HEVC\" : (is_av1 ? \"AV1\" : \"AVC\");\n    BOOST_LOG(info) << \"NvEnc: \" << codec_name << \" 码率已成功调整为 \" << bitrate_kbps << \" Kbps\";\n  }\n\n  void\n  nvenc_base::set_hdr_metadata(const std::optional<nvenc_hdr_metadata> &metadata) {\n    hdr_metadata = metadata;\n    if (metadata) {\n      BOOST_LOG(debug) << \"NvEnc: HDR metadata set - maxDisplayLuminance: \" << metadata->maxDisplayLuminance\n                       << \" nits, minDisplayLuminance: \" << (metadata->minDisplayLuminance / 10000.0)\n                       << \" nits, maxCLL: \" << metadata->maxContentLightLevel\n                       << \" nits, maxFALL: \" << metadata->maxFrameAverageLightLevel << \" nits\";\n    }\n    else {\n      BOOST_LOG(debug) << \"NvEnc: HDR metadata cleared\";\n    }\n  }\n\n  void\n  nvenc_base::set_luminance_stats(const platf::hdr_frame_luminance_stats_t &stats) {\n    luminance_stats = stats;\n  }\n\n  size_t\n  nvenc_base::serialize_hdr10plus_sei(const platf::hdr_frame_luminance_stats_t &stats,\n    uint16_t max_display_luminance,\n    std::vector<uint8_t> &payload) {\n    // HDR10+ (ST 2094-40) ITU-T T.35 registered SEI payload structure:\n    //   country_code:          0xB5 (USA)\n    //   terminal_provider_code: 0x003C (Samsung)\n    //   terminal_provider_oriented_code: 0x0001 (HDR10+)\n    //   application_identifier: 4\n    //   application_version:    1\n    //   num_windows:            1\n    //   Then per-window: maxscl[3], average_maxrgb, distribution percentiles\n    //   targeted_system_display_maximum_luminance\n    //\n    // Simplified profile: no tone mapping curve, no bezier anchors, no percentile distribution\n\n    float peak_nits = max_display_luminance > 0 ? static_cast<float>(max_display_luminance) : 1000.0f;\n\n    // Use P95 as effective peak (same logic as update_hdr_dynamic_metadata in video.cpp)\n    float effective_max = stats.percentile_95;\n\n    // Normalize to [0, 1] relative to peak_nits, expressed as 27-bit values (maxscl precision)\n    // HDR10+ maxscl is in 0.00001 cd/m² units\n    auto to_maxscl = [&](float nits) -> uint32_t {\n      return static_cast<uint32_t>(std::clamp(nits, 0.0f, 100000.0f) * 10.0f);  // 0.00001 cd/m² unit → stored as integer\n    };\n\n    payload.clear();\n    payload.reserve(64);\n\n    // ITU-T T.35 header\n    payload.push_back(0xB5);        // country_code (USA)\n    payload.push_back(0x00);        // terminal_provider_code (Samsung) high byte\n    payload.push_back(0x3C);        // terminal_provider_code low byte\n    payload.push_back(0x00);        // terminal_provider_oriented_code high byte\n    payload.push_back(0x01);        // terminal_provider_oriented_code low byte\n\n    // application_identifier (4) + application_version (1) — packed as 8+8 bits\n    payload.push_back(4);           // application_identifier\n    payload.push_back(1);           // application_version\n\n    // Bitstream-packed fields follow. We pack into a bit buffer.\n    // For simplicity, we'll use byte-aligned approximation where possible.\n\n    // num_windows (2 bits) = 1 (only 1-1=0 written for extra windows, but the spec says\n    // num_windows is 2 bits and actual count; with 1 window, no extra window data needed)\n    // Then for each window i (1..num_windows-1): window geometry (skipped for window 0)\n\n    // The bitstream layout is complex. Let's use a simple bitstream writer.\n    struct bitwriter {\n      std::vector<uint8_t> &buf;\n      uint32_t accumulator = 0;\n      int bits_pending = 0;\n\n      void write(uint32_t value, int num_bits) {\n        for (int i = num_bits - 1; i >= 0; --i) {\n          accumulator = (accumulator << 1) | ((value >> i) & 1);\n          bits_pending++;\n          if (bits_pending == 8) {\n            buf.push_back(static_cast<uint8_t>(accumulator));\n            accumulator = 0;\n            bits_pending = 0;\n          }\n        }\n      }\n\n      void flush() {\n        if (bits_pending > 0) {\n          accumulator <<= (8 - bits_pending);\n          buf.push_back(static_cast<uint8_t>(accumulator));\n          accumulator = 0;\n          bits_pending = 0;\n        }\n      }\n    };\n\n    bitwriter bw { payload };\n\n    // num_windows: 2 bits (value = 1)\n    bw.write(1, 2);\n\n    // For window 0 (always present, no geometry needed):\n    // maxscl[0..2]: 17 bits each (in 0.00001 cd/m² unit)\n    uint32_t maxscl_val = to_maxscl(effective_max);\n    bw.write(maxscl_val, 17);  // maxscl[0] (R)\n    bw.write(maxscl_val, 17);  // maxscl[1] (G)\n    bw.write(maxscl_val, 17);  // maxscl[2] (B)\n\n    // average_maxrgb: 17 bits\n    uint32_t avg_val = to_maxscl(stats.avg_maxrgb);\n    bw.write(avg_val, 17);\n\n    // num_distribution_maxrgb_percentiles: 4 bits (= 0, no percentile data)\n    bw.write(0, 4);\n\n    // fraction_bright_pixels: 10 bits (= 0)\n    bw.write(0, 10);\n\n    // mastering_display_actual_peak_luminance_flag: 1 bit (= 0)\n    bw.write(0, 1);\n\n    // For each window (window 0):\n    // tone_mapping_flag: 1 bit (= 0, no tone mapping)\n    bw.write(0, 1);\n\n    // color_saturation_mapping_flag: 1 bit (= 0)\n    bw.write(0, 1);\n\n    // targeted_system_display_actual_peak_luminance_flag: 1 bit (= 0)\n    bw.write(0, 1);\n\n    // targeted_system_display_maximum_luminance: 27 bits\n    // In 0.0001 cd/m² units\n    uint32_t target_lum = static_cast<uint32_t>(peak_nits * 10000);\n    bw.write(target_lum, 27);\n\n    bw.flush();\n\n    return payload.size();\n  }\n\n  size_t\n  nvenc_base::serialize_vivid_sei(const platf::hdr_frame_luminance_stats_t &stats,\n    uint16_t max_display_luminance,\n    std::vector<uint8_t> &payload) {\n    // HDR Vivid (CUVA / T/UWA 005.3) ITU-T T.35 registered SEI payload:\n    //   country_code:          0x26 (China)\n    //   terminal_provider_code: 0x0004 (CUVA HDR)\n    //   terminal_provider_oriented_code: 0x0005\n    //   system_start_code:     0x01\n    //   Then per-window: minimum_maxrgb, average_maxrgb, variance_maxrgb, maximum_maxrgb\n    //   tone_mapping_mode, color_saturation_mapping\n\n    float peak_nits = max_display_luminance > 0 ? static_cast<float>(max_display_luminance) : 1000.0f;\n\n    payload.clear();\n    payload.reserve(64);\n\n    // ITU-T T.35 header for CUVA HDR Vivid\n    payload.push_back(0x26);        // country_code (China)\n    payload.push_back(0x00);        // terminal_provider_code high byte\n    payload.push_back(0x04);        // terminal_provider_code low byte (CUVA)\n    payload.push_back(0x00);        // terminal_provider_oriented_code high byte\n    payload.push_back(0x05);        // terminal_provider_oriented_code low byte\n\n    // system_start_code: 8 bits\n    payload.push_back(0x01);\n\n    // Bitstream-packed fields\n    struct bitwriter {\n      std::vector<uint8_t> &buf;\n      uint32_t accumulator = 0;\n      int bits_pending = 0;\n\n      void write(uint32_t value, int num_bits) {\n        for (int i = num_bits - 1; i >= 0; --i) {\n          accumulator = (accumulator << 1) | ((value >> i) & 1);\n          bits_pending++;\n          if (bits_pending == 8) {\n            buf.push_back(static_cast<uint8_t>(accumulator));\n            accumulator = 0;\n            bits_pending = 0;\n          }\n        }\n      }\n\n      void flush() {\n        if (bits_pending > 0) {\n          accumulator <<= (8 - bits_pending);\n          buf.push_back(static_cast<uint8_t>(accumulator));\n          accumulator = 0;\n          bits_pending = 0;\n        }\n      }\n    };\n\n    bitwriter bw { payload };\n\n    // num_windows: 3 bits (value = 1)\n    bw.write(1, 3);\n\n    // For window 0:\n    // CUVA uses Q4.12 format (denominator 4095) for normalized values\n\n    // minimum_maxrgb: 12 bits\n    float min_norm = std::clamp(stats.min_maxrgb / peak_nits, 0.0f, 1.0f);\n    bw.write(static_cast<uint32_t>(min_norm * 4095), 12);\n\n    // average_maxrgb: 12 bits\n    float avg_norm = std::clamp(stats.avg_maxrgb / peak_nits, 0.0f, 1.0f);\n    bw.write(static_cast<uint32_t>(avg_norm * 4095), 12);\n\n    // variance_maxrgb: 12 bits\n    float variance_norm = std::clamp((stats.percentile_99 - stats.min_maxrgb) / peak_nits, 0.0f, 1.0f);\n    bw.write(static_cast<uint32_t>(variance_norm * 4095), 12);\n\n    // maximum_maxrgb: 12 bits\n    float max_norm = std::clamp(stats.percentile_95 / peak_nits, 0.0f, 1.0f);\n    bw.write(static_cast<uint32_t>(max_norm * 4095), 12);\n\n    // tone_mapping_mode_flag: 1 bit (= 0, no tone mapping)\n    bw.write(0, 1);\n\n    // tone_mapping_param_num: 1 bit (= 0)\n    bw.write(0, 1);\n\n    // color_saturation_mapping_flag: 1 bit (= 0)\n    bw.write(0, 1);\n\n    bw.flush();\n\n    return payload.size();\n  }\n\n  bool\n  nvenc_base::nvenc_failed(NVENCSTATUS status) {\n    auto status_string = [](NVENCSTATUS status) -> std::string {\n      switch (status) {\n#define nvenc_status_case(x) \\\n  case x:                    \\\n    return #x;\n        nvenc_status_case(NV_ENC_SUCCESS);\n        nvenc_status_case(NV_ENC_ERR_NO_ENCODE_DEVICE);\n        nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_DEVICE);\n        nvenc_status_case(NV_ENC_ERR_INVALID_ENCODERDEVICE);\n        nvenc_status_case(NV_ENC_ERR_INVALID_DEVICE);\n        nvenc_status_case(NV_ENC_ERR_DEVICE_NOT_EXIST);\n        nvenc_status_case(NV_ENC_ERR_INVALID_PTR);\n        nvenc_status_case(NV_ENC_ERR_INVALID_EVENT);\n        nvenc_status_case(NV_ENC_ERR_INVALID_PARAM);\n        nvenc_status_case(NV_ENC_ERR_INVALID_CALL);\n        nvenc_status_case(NV_ENC_ERR_OUT_OF_MEMORY);\n        nvenc_status_case(NV_ENC_ERR_ENCODER_NOT_INITIALIZED);\n        nvenc_status_case(NV_ENC_ERR_UNSUPPORTED_PARAM);\n        nvenc_status_case(NV_ENC_ERR_LOCK_BUSY);\n        nvenc_status_case(NV_ENC_ERR_NOT_ENOUGH_BUFFER);\n        nvenc_status_case(NV_ENC_ERR_INVALID_VERSION);\n        nvenc_status_case(NV_ENC_ERR_MAP_FAILED);\n        nvenc_status_case(NV_ENC_ERR_NEED_MORE_INPUT);\n        nvenc_status_case(NV_ENC_ERR_ENCODER_BUSY);\n        nvenc_status_case(NV_ENC_ERR_EVENT_NOT_REGISTERD);\n        nvenc_status_case(NV_ENC_ERR_GENERIC);\n        nvenc_status_case(NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY);\n        nvenc_status_case(NV_ENC_ERR_UNIMPLEMENTED);\n        nvenc_status_case(NV_ENC_ERR_RESOURCE_REGISTER_FAILED);\n        nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_REGISTERED);\n        nvenc_status_case(NV_ENC_ERR_RESOURCE_NOT_MAPPED);\n        // Newer versions of sdk may add more constants, look for them at the end of NVENCSTATUS enum\n#undef nvenc_status_case\n        default:\n          return std::to_string(status);\n      }\n    };\n\n    last_nvenc_error_string.clear();\n    if (status != NV_ENC_SUCCESS) {\n      /* This API function gives broken strings more often than not\n      if (nvenc && encoder) {\n        last_nvenc_error_string = nvenc->nvEncGetLastErrorString(encoder);\n        if (!last_nvenc_error_string.empty()) last_nvenc_error_string += \" \";\n      }\n      */\n      last_nvenc_error_string += status_string(status);\n      return true;\n    }\n\n    return false;\n  }\n}\n"
  },
  {
    "path": "src/nvenc/common_impl/nvenc_base.h",
    "content": "/**\n * @file src/nvenc/common_impl/nvenc_base.h\n * @brief Declarations for abstract platform-agnostic base of standalone NVENC encoder.\n */\n#pragma once\n\n#include \"../nvenc_config.h\"\n#include \"../nvenc_encoded_frame.h\"\n#include \"../nvenc_encoder.h\"\n\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/video.h\"\n\n#include <vector>\n\n#ifdef NVENC_NAMESPACE\nnamespace NVENC_NAMESPACE {\n#else\n  #include <ffnvcodec/nvEncodeAPI.h>\nnamespace nvenc {\n#endif\n\n  /**\n   * @brief Abstract platform-agnostic base of standalone NVENC encoder.\n   *        Derived classes perform platform-specific operations.\n   */\n  class nvenc_base: virtual public nvenc_encoder {\n  public:\n    /**\n     * @param device_type Underlying device type used by derived class.\n     */\n    explicit nvenc_base(NV_ENC_DEVICE_TYPE device_type);\n    ~nvenc_base();\n\n    bool\n    create_encoder(const nvenc_config &config,\n      const video::config_t &client_config,\n      const video::sunshine_colorspace_t &sunshine_colorspace,\n      platf::pix_fmt_e sunshine_buffer_format) override;\n\n    void\n    destroy_encoder() override;\n\n    nvenc_encoded_frame\n    encode_frame(uint64_t frame_index, bool force_idr) override;\n\n    bool\n    invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) override;\n\n    void\n    set_bitrate(int bitrate_kbps) override;\n\n    void\n    set_hdr_metadata(const std::optional<nvenc_hdr_metadata> &metadata) override;\n\n    void\n    set_luminance_stats(const platf::hdr_frame_luminance_stats_t &stats) override;\n\n  protected:\n    /**\n     * @brief Required. Used for loading NvEnc library and setting `nvenc` variable with `NvEncodeAPICreateInstance()`.\n     *        Called during `create_encoder()` if `nvenc` variable is not initialized.\n     * @return `true` on success, `false` on error\n     */\n    virtual bool\n    init_library() = 0;\n\n    /**\n     * @brief Required. Used for creating outside-facing input surface,\n     *        registering this surface with `nvenc->nvEncRegisterResource()` and setting `registered_input_buffer` variable.\n     *        Called during `create_encoder()`.\n     * @return `true` on success, `false` on error\n     */\n    virtual bool\n    create_and_register_input_buffer() = 0;\n\n    /**\n     * @brief Optional. Override if you must perform additional operations on the registered input surface in the beginning of `encode_frame()`.\n     *        Typically used for interop copy.\n     * @return `true` on success, `false` on error\n     */\n    virtual bool\n    synchronize_input_buffer() { return true; }\n\n    /**\n     * @brief Optional. Override if you want to create encoder in async mode.\n     *        In this case must also set `async_event_handle` variable.\n     * @param timeout_ms Wait timeout in milliseconds\n     * @return `true` on success, `false` on timeout or error\n     */\n    virtual bool\n    wait_for_async_event(uint32_t timeout_ms) { return false; }\n\n    bool\n    nvenc_failed(NVENCSTATUS status);\n\n    const NV_ENC_DEVICE_TYPE device_type;\n\n    void *encoder = nullptr;\n\n    struct {\n      uint32_t width = 0;\n      uint32_t height = 0;\n      NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED;\n      uint32_t ref_frames_in_dpb = 0;\n      bool rfi = false;\n    } encoder_params;\n\n    std::string last_nvenc_error_string;\n\n    // Derived classes set these variables\n    void *device = nullptr;  ///< Platform-specific handle of encoding device.\n                             ///< Should be set in constructor or `init_library()`.\n    std::shared_ptr<NV_ENCODE_API_FUNCTION_LIST> nvenc;  ///< Function pointers list produced by `NvEncodeAPICreateInstance()`.\n                                                         ///< Should be set in `init_library()`.\n    NV_ENC_REGISTERED_PTR registered_input_buffer = nullptr;  ///< Platform-specific input surface registered with `NvEncRegisterResource()`.\n                                                              ///< Should be set in `create_and_register_input_buffer()`.\n    void *async_event_handle = nullptr;  ///< (optional) Platform-specific handle of event object event.\n                                         ///< Can be set in constructor or `init_library()`, must override `wait_for_async_event()`.\n\n  private:\n    NV_ENC_OUTPUT_PTR output_bitstream = nullptr;\n\n    struct {\n      uint64_t last_encoded_frame_index = 0;\n      bool rfi_needs_confirmation = false;\n      std::pair<uint64_t, uint64_t> last_rfi_range;\n      logging::min_max_avg_periodic_logger<double> frame_size_logger = { debug, \"NvEnc: encoded frame sizes in kB\", \"\" };\n    } encoder_state;\n\n    NV_ENC_INITIALIZE_PARAMS saved_init_params;  // 保存初始化参数\n    NV_ENC_CONFIG current_enc_config;  // 保存当前的编码器配置\n\n    // HDR metadata support\n    std::optional<nvenc_hdr_metadata> hdr_metadata;\n    int video_format = 0;  // 0 = H.264, 1 = HEVC, 2 = AV1\n\n    // Per-frame HDR luminance stats for dynamic metadata\n    platf::hdr_frame_luminance_stats_t luminance_stats;\n\n    /**\n     * @brief Serialize HDR10+ dynamic metadata into ITU-T T.35 SEI payload.\n     *        Format follows ST 2094-40 (Samsung HDR10+).\n     * @param stats EMA-smoothed luminance statistics.\n     * @param max_display_luminance Display peak luminance in nits.\n     * @param[out] payload Output buffer for serialized payload.\n     * @return Size of serialized payload in bytes, or 0 on failure.\n     */\n    size_t\n    serialize_hdr10plus_sei(const platf::hdr_frame_luminance_stats_t &stats,\n      uint16_t max_display_luminance,\n      std::vector<uint8_t> &payload);\n\n    /**\n     * @brief Serialize HDR Vivid (CUVA) dynamic metadata into ITU-T T.35 SEI payload.\n     *        Format follows T/UWA 005.3 (China Ultrahigh-definition Video Association).\n     * @param stats EMA-smoothed luminance statistics.\n     * @param max_display_luminance Display peak luminance in nits.\n     * @param[out] payload Output buffer for serialized payload.\n     * @return Size of serialized payload in bytes, or 0 on failure.\n     */\n    size_t\n    serialize_vivid_sei(const platf::hdr_frame_luminance_stats_t &stats,\n      uint16_t max_display_luminance,\n      std::vector<uint8_t> &payload);\n  };\n}\n"
  },
  {
    "path": "src/nvenc/common_impl/nvenc_utils.cpp",
    "content": "/**\r\n * @file src/nvenc/common_impl/nvenc_utils.cpp\r\n * @brief Definitions for NVENC utilities.\r\n */\r\n#include \"nvenc_utils.h\"\r\n\r\n#include <cassert>\r\n\r\n#ifdef NVENC_NAMESPACE\r\nnamespace NVENC_NAMESPACE {\r\n#else\r\nnamespace nvenc {\r\n#endif\r\n\r\n#ifdef _WIN32\r\n  DXGI_FORMAT\r\n  dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format) {\r\n    switch (format) {\r\n      case NV_ENC_BUFFER_FORMAT_YUV420_10BIT:\r\n        return DXGI_FORMAT_P010;\r\n\r\n      case NV_ENC_BUFFER_FORMAT_NV12:\r\n        return DXGI_FORMAT_NV12;\r\n\r\n      case NV_ENC_BUFFER_FORMAT_AYUV:\r\n        return DXGI_FORMAT_AYUV;\r\n\r\n      case NV_ENC_BUFFER_FORMAT_YUV444_10BIT:\r\n        return DXGI_FORMAT_R16_UINT;\r\n\r\n      default:\r\n        return DXGI_FORMAT_UNKNOWN;\r\n    }\r\n  }\r\n#endif\r\n\r\n  NV_ENC_BUFFER_FORMAT\r\n  nvenc_format_from_sunshine_format(platf::pix_fmt_e format) {\r\n    switch (format) {\r\n      case platf::pix_fmt_e::nv12:\r\n        return NV_ENC_BUFFER_FORMAT_NV12;\r\n\r\n      case platf::pix_fmt_e::p010:\r\n        return NV_ENC_BUFFER_FORMAT_YUV420_10BIT;\r\n\r\n      case platf::pix_fmt_e::ayuv:\r\n        return NV_ENC_BUFFER_FORMAT_AYUV;\r\n\r\n      case platf::pix_fmt_e::yuv444p16:\r\n        return NV_ENC_BUFFER_FORMAT_YUV444_10BIT;\r\n\r\n      default:\r\n        return NV_ENC_BUFFER_FORMAT_UNDEFINED;\r\n    }\r\n  }\r\n\r\n  nvenc_colorspace_t\r\n  nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace) {\r\n    nvenc_colorspace_t colorspace;\r\n\r\n    switch (sunshine_colorspace.colorspace) {\r\n      case video::colorspace_e::rec601:\r\n        // Rec. 601\r\n        colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_SMPTE170M;\r\n        colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE170M;\r\n        colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M;\r\n        break;\r\n\r\n      case video::colorspace_e::rec709:\r\n        // Rec. 709\r\n        colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT709;\r\n        colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709;\r\n        colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT709;\r\n        break;\r\n\r\n      case video::colorspace_e::bt2020sdr:\r\n        // Rec. 2020\r\n        colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020;\r\n        assert(sunshine_colorspace.bit_depth == 10);\r\n        colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT2020_10;\r\n        colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL;\r\n        break;\r\n\r\n      case video::colorspace_e::bt2020:\r\n        // Rec. 2020 with ST 2084 perceptual quantizer (PQ)\r\n        colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020;\r\n        assert(sunshine_colorspace.bit_depth == 10);\r\n        colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084;\r\n        colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL;\r\n        break;\r\n\r\n      case video::colorspace_e::bt2020hlg:\r\n        // Rec. 2020 with Hybrid Log-Gamma (HLG)\r\n        colorspace.primaries = NV_ENC_VUI_COLOR_PRIMARIES_BT2020;\r\n        assert(sunshine_colorspace.bit_depth == 10);\r\n        colorspace.tranfer_function = NV_ENC_VUI_TRANSFER_CHARACTERISTIC_ARIB_STD_B67;\r\n        colorspace.matrix = NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL;\r\n        break;\r\n    }\r\n\r\n    colorspace.full_range = sunshine_colorspace.full_range;\r\n\r\n    return colorspace;\r\n  }\r\n}\r\n"
  },
  {
    "path": "src/nvenc/common_impl/nvenc_utils.h",
    "content": "/**\r\n * @file src/nvenc/common_impl/nvenc_utils.h\r\n * @brief Declarations for NVENC utilities.\r\n */\r\n#pragma once\r\n\r\n#ifdef _WIN32\r\n  #include <dxgiformat.h>\r\n#endif\r\n\r\n#include \"src/platform/common.h\"\r\n#include \"src/video_colorspace.h\"\r\n\r\n#ifdef NVENC_NAMESPACE\r\nnamespace NVENC_NAMESPACE {\r\n#else\r\n  #include <ffnvcodec/nvEncodeAPI.h>\r\nnamespace nvenc {\r\n#endif\r\n\r\n  /**\r\n   * @brief YUV colorspace and color range.\r\n   */\r\n  struct nvenc_colorspace_t {\r\n    NV_ENC_VUI_COLOR_PRIMARIES primaries;\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC tranfer_function;\r\n    NV_ENC_VUI_MATRIX_COEFFS matrix;\r\n    bool full_range;\r\n  };\r\n\r\n#ifdef _WIN32\r\n  DXGI_FORMAT\r\n  dxgi_format_from_nvenc_format(NV_ENC_BUFFER_FORMAT format);\r\n#endif\r\n\r\n  NV_ENC_BUFFER_FORMAT\r\n  nvenc_format_from_sunshine_format(platf::pix_fmt_e format);\r\n\r\n  nvenc_colorspace_t\r\n  nvenc_colorspace_from_sunshine_colorspace(const video::sunshine_colorspace_t &sunshine_colorspace);\r\n}\r\n"
  },
  {
    "path": "src/nvenc/nvenc_config.h",
    "content": "/**\n * @file src/nvenc/nvenc_config.h\n * @brief Declarations for NVENC encoder configuration.\n */\n#pragma once\n\n#include <cstdint>\n#include <optional>\n\nnamespace nvenc {\n\n  /**\n   * @brief HDR metadata for NVENC encoder.\n   *        Based on SS_HDR_METADATA from moonlight-common-c.\n   */\n  struct nvenc_hdr_metadata {\n    // RGB order - display primaries\n    struct {\n      uint16_t x;  // Normalized to 50,000\n      uint16_t y;  // Normalized to 50,000\n    } displayPrimaries[3];\n\n    struct {\n      uint16_t x;  // Normalized to 50,000\n      uint16_t y;  // Normalized to 50,000\n    } whitePoint;\n\n    uint16_t maxDisplayLuminance;       // Nits\n    uint16_t minDisplayLuminance;       // 1/10000th of a nit\n\n    // Content-specific values\n    uint16_t maxContentLightLevel;      // Nits\n    uint16_t maxFrameAverageLightLevel; // Nits\n  };\n\n  enum class nvenc_two_pass {\n    disabled,  ///< Single pass, the fastest and no extra vram\n    quarter_resolution,  ///< Larger motion vectors being caught, faster and uses less extra vram\n    full_resolution,  ///< Better overall statistics, slower and uses more extra vram\n  };\n\n  enum class nvenc_split_frame_encoding {\n    disabled,  ///< Disable\n    driver_decides,  ///< Let driver decide\n    force_enabled,  ///< Force-enable\n    two_strips,  ///< Force 2-strip split (requires 2+ NVENC engines)\n    three_strips,  ///< Force 3-strip split (requires 3+ NVENC engines)\n    four_strips,  ///< Force 4-strip split (requires 4+ NVENC engines)\n  };\n\n  enum class nvenc_lookahead_level {\n    disabled = 0,  ///< Disable lookahead\n    level_1 = 1,  ///< Lookahead level 1\n    level_2 = 2,  ///< Lookahead level 2\n    level_3 = 3,  ///< Lookahead level 3\n    autoselect = 15,  ///< Let driver auto-select level\n  };\n\n  enum class nvenc_temporal_filter_level {\n    disabled = 0,  ///< Disable temporal filter\n    level_4 = 4,  ///< Temporal filter level 4\n  };\n\n  enum class nvenc_rate_control_mode {\n    cbr,  ///< Constant Bitrate - fixed bitrate, best for low latency streaming\n    vbr,  ///< Variable Bitrate - variable bitrate, better quality for complex scenes\n  };\n\n  /**\n   * @brief NVENC encoder configuration.\n   */\n  struct nvenc_config {\n    // Quality preset from 1 to 7, higher is slower\n    int quality_preset = 1;\n\n    // Use optional preliminary pass for better motion vectors, bitrate distribution and stricter VBV(HRD), uses CUDA cores\n    nvenc_two_pass two_pass = nvenc_two_pass::quarter_resolution;\n\n    // Percentage increase of VBV/HRD from the default single frame, allows low-latency variable bitrate\n    int vbv_percentage_increase = 0;\n\n    // Improves fades compression, uses CUDA cores\n    bool weighted_prediction = false;\n\n    // Allocate more bitrate to flat regions since they're visually more perceptible, uses CUDA cores\n    bool adaptive_quantization = false;\n\n    // Enable temporal adaptive quantization (requires lookahead)\n    bool enable_temporal_aq = false;\n\n    // Don't use QP below certain value, limits peak image quality to save bitrate\n    bool enable_min_qp = false;\n\n    // Min QP value for H.264 when enable_min_qp is selected\n    unsigned min_qp_h264 = 19;\n\n    // Min QP value for HEVC when enable_min_qp is selected\n    unsigned min_qp_hevc = 23;\n\n    // Min QP value for AV1 when enable_min_qp is selected\n    unsigned min_qp_av1 = 23;\n\n    // Use CAVLC entropy coding in H.264 instead of CABAC, not relevant and here for historical reasons\n    bool h264_cavlc = false;\n\n    // Add filler data to encoded frames to stay at target bitrate, mainly for testing\n    bool insert_filler_data = false;\n\n    // Enable split-frame encoding if the gpu has multiple NVENC hardware clusters\n    nvenc_split_frame_encoding split_frame_encoding = nvenc_split_frame_encoding::driver_decides;\n\n    // Lookahead level (0-3, higher = better quality but more latency). Requires enable_lookahead=true\n    nvenc_lookahead_level lookahead_level = nvenc_lookahead_level::disabled;\n\n    // Lookahead depth (number of frames to look ahead, 0-32). 0 = disabled\n    int lookahead_depth = 0;\n\n    // Temporal filter level (reduces noise, improves compression). Requires frameIntervalP >= 5\n    nvenc_temporal_filter_level temporal_filter_level = nvenc_temporal_filter_level::disabled;\n\n    // Rate control mode (CBR for low latency, VBR for better quality)\n    nvenc_rate_control_mode rate_control_mode = nvenc_rate_control_mode::cbr;\n\n    // Target quality for VBR mode (0-51 for H.264/HEVC, 0-63 for AV1, 0=auto). Lower value = higher quality\n    // Only used when rate_control_mode is VBR\n    int target_quality = 0;  // 0 = automatic\n  };\n\n}  // namespace nvenc\n"
  },
  {
    "path": "src/nvenc/nvenc_encoded_frame.h",
    "content": "/**\r\n * @file src/nvenc/nvenc_encoded_frame.h\r\n * @brief Declarations for NVENC encoded frame.\r\n */\r\n#pragma once\r\n\r\n#include <cstdint>\r\n#include <vector>\r\n\r\nnamespace nvenc {\r\n\r\n  /**\r\n   * @brief Encoded frame.\r\n   */\r\n  struct nvenc_encoded_frame {\r\n    std::vector<uint8_t> data;\r\n    uint64_t frame_index = 0;\r\n    bool idr = false;\r\n    bool after_ref_frame_invalidation = false;\r\n  };\r\n\r\n}  // namespace nvenc\r\n"
  },
  {
    "path": "src/nvenc/nvenc_encoder.h",
    "content": "/**\n * @file src/nvenc/nvenc_encoder.h\n * @brief Declarations for NVENC encoder interface.\n */\n#pragma once\n\n#include \"nvenc_config.h\"\n#include \"nvenc_encoded_frame.h\"\n\n#include \"src/platform/common.h\"\n#include \"src/video.h\"\n#include \"src/video_colorspace.h\"\n\n/**\n * @brief Standalone NVENC encoder\n */\nnamespace nvenc {\n\n  /**\n   * @brief Standalone NVENC encoder interface.\n   */\n  class nvenc_encoder {\n  public:\n    virtual ~nvenc_encoder() = default;\n\n    /**\n     * @brief Create the encoder.\n     * @param config NVENC encoder configuration.\n     * @param client_config Stream configuration requested by the client.\n     * @param colorspace YUV colorspace.\n     * @param buffer_format Platform-agnostic input surface format.\n     * @return `true` on success, `false` on error\n     */\n    virtual bool\n    create_encoder(const nvenc_config &config,\n      const video::config_t &client_config,\n      const video::sunshine_colorspace_t &colorspace,\n      platf::pix_fmt_e buffer_format) = 0;\n\n    /**\n     * @brief Destroy the encoder.\n     *        Also called in the destructor.\n     */\n    virtual void\n    destroy_encoder() = 0;\n\n    /**\n     * @brief Encode the next frame using platform-specific input surface.\n     * @param frame_index Frame index that uniquely identifies the frame.\n     *        Afterwards serves as parameter for `invalidate_ref_frames()`.\n     *        No restrictions on the first frame index, but later frame indexes must be subsequent.\n     * @param force_idr Whether to encode frame as forced IDR.\n     * @return Encoded frame.\n     */\n    virtual nvenc_encoded_frame\n    encode_frame(uint64_t frame_index, bool force_idr) = 0;\n\n    /**\n     * @brief Perform reference frame invalidation (RFI) procedure.\n     * @param first_frame First frame index of the invalidation range.\n     * @param last_frame Last frame index of the invalidation range.\n     * @return `true` on success, `false` on error.\n     *         After error next frame must be encoded with `force_idr = true`.\n     */\n    virtual bool\n    invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) = 0;\n\n    /**\n     * @brief Set the bitrate for the encoder.\n     * @param bitrate_kbps Bitrate in kilobits per second.\n     */\n    virtual void\n    set_bitrate(int bitrate_kbps) = 0;\n\n    /**\n     * @brief Set HDR metadata for the encoder.\n     *        When set, the encoder will include mastering display and content light level\n     *        metadata in the encoded bitstream (HEVC and AV1 only).\n     * @param metadata HDR metadata to set, or nullopt to disable HDR metadata.\n     */\n    virtual void\n    set_hdr_metadata(const std::optional<nvenc_hdr_metadata> &metadata) = 0;\n\n    /**\n     * @brief Set per-frame HDR luminance statistics for dynamic metadata injection.\n     *        When valid stats are provided, the encoder will generate HDR10+ SEI/OBU\n     *        payloads and inject them into each encoded frame.\n     * @param stats Per-frame luminance statistics from GPU analysis.\n     */\n    virtual void\n    set_luminance_stats(const platf::hdr_frame_luminance_stats_t &stats) = 0;\n  };\n\n}  // namespace nvenc\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_d3d11_base.cpp",
    "content": "/**\r\n * @file src/nvenc/win/impl/nvenc_d3d11_base.cpp\r\n * @brief Definitions for abstract Direct3D11 NVENC encoder.\r\n */\r\n#include \"nvenc_d3d11_base.h\"\r\n\r\n#ifdef NVENC_NAMESPACE\r\nnamespace NVENC_NAMESPACE {\r\n#else\r\nnamespace nvenc {\r\n#endif\r\n  nvenc_d3d11_base::nvenc_d3d11_base(NV_ENC_DEVICE_TYPE device_type, shared_dll dll):\r\n      nvenc_base(device_type),\r\n      dll(dll) {\r\n    async_event_handle = CreateEvent(NULL, FALSE, FALSE, NULL);\r\n  }\r\n\r\n  nvenc_d3d11_base::~nvenc_d3d11_base() {\r\n    if (async_event_handle) {\r\n      CloseHandle(async_event_handle);\r\n    }\r\n  }\r\n\r\n  bool nvenc_d3d11_base::init_library() {\r\n    if (nvenc) return true;\r\n    if (!dll) return false;\r\n\r\n    if (auto create_instance = (decltype(NvEncodeAPICreateInstance) *) GetProcAddress(dll.get(), \"NvEncodeAPICreateInstance\")) {\r\n      auto new_nvenc = std::make_unique<NV_ENCODE_API_FUNCTION_LIST>();\r\n      new_nvenc->version = NV_ENCODE_API_FUNCTION_LIST_VER;\r\n      if (nvenc_failed(create_instance(new_nvenc.get()))) {\r\n        BOOST_LOG(error) << \"NvEnc: NvEncodeAPICreateInstance() failed: \" << last_nvenc_error_string;\r\n      }\r\n      else {\r\n        nvenc = std::move(new_nvenc);\r\n        return true;\r\n      }\r\n    }\r\n    else {\r\n      BOOST_LOG(error) << \"NvEnc: No NvEncodeAPICreateInstance() in dynamic library\";\r\n    }\r\n\r\n    return false;\r\n  }\r\n  \r\n  bool nvenc_d3d11_base::wait_for_async_event(uint32_t timeout_ms) {\r\n    return WaitForSingleObject(async_event_handle, timeout_ms) == WAIT_OBJECT_0;\r\n  }\r\n}\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_d3d11_base.h",
    "content": "/**\r\n * @file src/nvenc/win/impl/nvenc_d3d11_base.h\r\n * @brief Declarations for abstract Direct3D11 NVENC encoder.\r\n */\r\n#pragma once\r\n\r\n#include \"../../common_impl/nvenc_base.h\"\r\n#include \"../nvenc_d3d11.h\"\r\n#include \"nvenc_shared_dll.h\"\r\n\r\n#include <comdef.h>\r\n#include <d3d11.h>\r\n\r\n#ifdef NVENC_NAMESPACE\r\nnamespace NVENC_NAMESPACE {\r\n#else\r\nnamespace nvenc {\r\n#endif\r\n\r\n  _COM_SMARTPTR_TYPEDEF(ID3D11Device, IID_ID3D11Device);\r\n  _COM_SMARTPTR_TYPEDEF(ID3D11Texture2D, IID_ID3D11Texture2D);\r\n  _COM_SMARTPTR_TYPEDEF(IDXGIDevice, IID_IDXGIDevice);\r\n  _COM_SMARTPTR_TYPEDEF(IDXGIAdapter, IID_IDXGIAdapter);\r\n\r\n  /**\r\n   * @brief Abstract Direct3D11 NVENC encoder.\r\n   *        Encapsulates common code used by native and interop implementations.\r\n   */\r\n  class nvenc_d3d11_base: public nvenc_base, virtual public nvenc_d3d11 {\r\n  public:\r\n    explicit nvenc_d3d11_base(NV_ENC_DEVICE_TYPE device_type, shared_dll dll);\r\n    ~nvenc_d3d11_base();\r\n    /**\r\n     * @brief Get input surface texture.\r\n     * @return Input surface texture.\r\n     */\r\n    virtual ID3D11Texture2D *get_input_texture() = 0;\r\n  protected:\r\n    bool init_library() override;\r\n    bool wait_for_async_event(uint32_t timeout_ms) override;\r\n\r\n  private:\r\n    shared_dll dll;\r\n  };\r\n}\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_d3d11_native.cpp",
    "content": "/**\r\n * @file src/nvenc/win/impl/nvenc_d3d11_native.cpp\r\n * @brief Definitions for native Direct3D11 NVENC encoder.\r\n */\r\n#include \"nvenc_d3d11_native.h\"\r\n\r\n#include \"../../common_impl/nvenc_utils.h\"\r\n\r\n#ifdef NVENC_NAMESPACE\r\nnamespace NVENC_NAMESPACE {\r\n#else\r\nnamespace nvenc {\r\n#endif\r\n\r\n  nvenc_d3d11_native::nvenc_d3d11_native(ID3D11Device *d3d_device, shared_dll dll):\r\n      nvenc_d3d11_base(NV_ENC_DEVICE_TYPE_DIRECTX, dll),\r\n      d3d_device(d3d_device) {\r\n    device = d3d_device;\r\n  }\r\n\r\n  nvenc_d3d11_native::~nvenc_d3d11_native() {\r\n    if (encoder) destroy_encoder();\r\n  }\r\n\r\n  ID3D11Texture2D *\r\n  nvenc_d3d11_native::get_input_texture() {\r\n    return d3d_input_texture.GetInterfacePtr();\r\n  }\r\n\r\n  bool\r\n  nvenc_d3d11_native::create_and_register_input_buffer() {\r\n    if (encoder_params.buffer_format == NV_ENC_BUFFER_FORMAT_YUV444_10BIT) {\r\n      BOOST_LOG(error) << \"NvEnc: 10-bit 4:4:4 encoding is incompatible with D3D11 surface formats, use CUDA interop\";\r\n      return false;\r\n    }\r\n\r\n    if (!d3d_input_texture) {\r\n      D3D11_TEXTURE2D_DESC desc = {};\r\n      desc.Width = encoder_params.width;\r\n      desc.Height = encoder_params.height;\r\n      desc.MipLevels = 1;\r\n      desc.ArraySize = 1;\r\n      desc.Format = dxgi_format_from_nvenc_format(encoder_params.buffer_format);\r\n      desc.SampleDesc.Count = 1;\r\n      desc.Usage = D3D11_USAGE_DEFAULT;\r\n      desc.BindFlags = D3D11_BIND_RENDER_TARGET;\r\n      if (d3d_device->CreateTexture2D(&desc, nullptr, &d3d_input_texture) != S_OK) {\r\n        BOOST_LOG(error) << \"NvEnc: couldn't create input texture\";\r\n        return false;\r\n      }\r\n    }\r\n\r\n    if (!registered_input_buffer) {\r\n      NV_ENC_REGISTER_RESOURCE register_resource = { NV_ENC_REGISTER_RESOURCE_VER };\r\n      register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_DIRECTX;\r\n      register_resource.width = encoder_params.width;\r\n      register_resource.height = encoder_params.height;\r\n      register_resource.resourceToRegister = d3d_input_texture.GetInterfacePtr();\r\n      register_resource.bufferFormat = encoder_params.buffer_format;\r\n      register_resource.bufferUsage = NV_ENC_INPUT_IMAGE;\r\n\r\n      if (nvenc_failed(nvenc->nvEncRegisterResource(encoder, &register_resource))) {\r\n        BOOST_LOG(error) << \"NvEnc: NvEncRegisterResource() failed: \" << last_nvenc_error_string;\r\n        return false;\r\n      }\r\n\r\n      registered_input_buffer = register_resource.registeredResource;\r\n    }\r\n\r\n    return true;\r\n  }\r\n}\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_d3d11_native.h",
    "content": "/**\r\n * @file src/nvenc/win/impl/nvenc_d3d11_native.h\r\n * @brief Declarations for native Direct3D11 NVENC encoder.\r\n */\r\n#pragma once\r\n\r\n#include \"nvenc_d3d11_base.h\"\r\n\r\n#ifdef NVENC_NAMESPACE\r\nnamespace NVENC_NAMESPACE {\r\n#else\r\nnamespace nvenc {\r\n#endif\r\n\r\n  /**\r\n   * @brief Native Direct3D11 NVENC encoder.\r\n   */\r\n  class nvenc_d3d11_native final: public nvenc_d3d11_base {\r\n  public:\r\n    /**\r\n     * @param d3d_device Direct3D11 device used for encoding.\r\n     */\r\n    nvenc_d3d11_native(ID3D11Device *d3d_device, shared_dll dll);\r\n    ~nvenc_d3d11_native();\r\n\r\n    ID3D11Texture2D *\r\n    get_input_texture() override;\r\n\r\n  private:\r\n    bool\r\n    create_and_register_input_buffer() override;\r\n\r\n    const ID3D11DevicePtr d3d_device;\r\n    ID3D11Texture2DPtr d3d_input_texture;\r\n  };\r\n}\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_d3d11_on_cuda.cpp",
    "content": "/**\r\n * @file src/nvenc/win/impl/nvenc_d3d11_on_cuda.cpp\r\n * @brief Definitions for CUDA NVENC encoder with Direct3D11 input surfaces.\r\n */\r\n#include \"nvenc_d3d11_on_cuda.h\"\r\n\r\n#include \"../../common_impl/nvenc_utils.h\"\r\n\r\n#ifdef NVENC_NAMESPACE\r\nnamespace NVENC_NAMESPACE {\r\n#else\r\nnamespace nvenc {\r\n#endif\r\n\r\n  nvenc_d3d11_on_cuda::nvenc_d3d11_on_cuda(ID3D11Device *d3d_device, shared_dll dll):\r\n      nvenc_d3d11_base(NV_ENC_DEVICE_TYPE_CUDA, dll),\r\n      d3d_device(d3d_device) {\r\n  }\r\n\r\n  nvenc_d3d11_on_cuda::~nvenc_d3d11_on_cuda() {\r\n    if (encoder) destroy_encoder();\r\n\r\n    if (cuda_context) {\r\n      {\r\n        auto autopop_context = push_context();\r\n\r\n        if (cuda_d3d_input_texture) {\r\n          if (cuda_failed(cuda_functions.cuGraphicsUnregisterResource(cuda_d3d_input_texture))) {\r\n            BOOST_LOG(error) << \"NvEnc: cuGraphicsUnregisterResource() failed: error \" << last_cuda_error;\r\n          }\r\n          cuda_d3d_input_texture = nullptr;\r\n        }\r\n\r\n        if (cuda_surface) {\r\n          if (cuda_failed(cuda_functions.cuMemFree(cuda_surface))) {\r\n            BOOST_LOG(error) << \"NvEnc: cuMemFree() failed: error \" << last_cuda_error;\r\n          }\r\n          cuda_surface = 0;\r\n        }\r\n      }\r\n\r\n      if (cuda_failed(cuda_functions.cuCtxDestroy(cuda_context))) {\r\n        BOOST_LOG(error) << \"NvEnc: cuCtxDestroy() failed: error \" << last_cuda_error;\r\n      }\r\n      cuda_context = nullptr;\r\n    }\r\n  }\r\n\r\n  ID3D11Texture2D *\r\n  nvenc_d3d11_on_cuda::get_input_texture() {\r\n    return d3d_input_texture.GetInterfacePtr();\r\n  }\r\n\r\n  bool\r\n  nvenc_d3d11_on_cuda::init_library() {\r\n    if (!nvenc_d3d11_base::init_library()) return false;\r\n\r\n    constexpr auto dll_name = \"nvcuda.dll\";\r\n\r\n    if ((cuda_functions.dll = make_shared_dll(LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32)))) {\r\n      auto load_function = [&]<typename T>(T &location, auto symbol) -> bool {\r\n        location = (T) GetProcAddress(cuda_functions.dll.get(), symbol);\r\n        return location != nullptr;\r\n      };\r\n      if (!load_function(cuda_functions.cuInit, \"cuInit\") ||\r\n          !load_function(cuda_functions.cuD3D11GetDevice, \"cuD3D11GetDevice\") ||\r\n          !load_function(cuda_functions.cuCtxCreate, \"cuCtxCreate_v2\") ||\r\n          !load_function(cuda_functions.cuCtxDestroy, \"cuCtxDestroy_v2\") ||\r\n          !load_function(cuda_functions.cuCtxPushCurrent, \"cuCtxPushCurrent_v2\") ||\r\n          !load_function(cuda_functions.cuCtxPopCurrent, \"cuCtxPopCurrent_v2\") ||\r\n          !load_function(cuda_functions.cuMemAllocPitch, \"cuMemAllocPitch_v2\") ||\r\n          !load_function(cuda_functions.cuMemFree, \"cuMemFree_v2\") ||\r\n          !load_function(cuda_functions.cuGraphicsD3D11RegisterResource, \"cuGraphicsD3D11RegisterResource\") ||\r\n          !load_function(cuda_functions.cuGraphicsUnregisterResource, \"cuGraphicsUnregisterResource\") ||\r\n          !load_function(cuda_functions.cuGraphicsMapResources, \"cuGraphicsMapResources\") ||\r\n          !load_function(cuda_functions.cuGraphicsUnmapResources, \"cuGraphicsUnmapResources\") ||\r\n          !load_function(cuda_functions.cuGraphicsSubResourceGetMappedArray, \"cuGraphicsSubResourceGetMappedArray\") ||\r\n          !load_function(cuda_functions.cuMemcpy2D, \"cuMemcpy2D_v2\")) {\r\n        BOOST_LOG(error) << \"NvEnc: missing CUDA functions in \" << dll_name;\r\n        cuda_functions = {};\r\n      }\r\n    }\r\n    else {\r\n      BOOST_LOG(debug) << \"NvEnc: couldn't load CUDA dynamic library \" << dll_name;\r\n    }\r\n\r\n    if (cuda_functions.dll) {\r\n      IDXGIDevicePtr dxgi_device;\r\n      IDXGIAdapterPtr dxgi_adapter;\r\n      if (d3d_device &&\r\n          SUCCEEDED(d3d_device->QueryInterface(IID_PPV_ARGS(&dxgi_device))) &&\r\n          SUCCEEDED(dxgi_device->GetAdapter(&dxgi_adapter))) {\r\n        CUdevice cuda_device;\r\n        if (cuda_succeeded(cuda_functions.cuInit(0)) &&\r\n            cuda_succeeded(cuda_functions.cuD3D11GetDevice(&cuda_device, dxgi_adapter)) &&\r\n            cuda_succeeded(cuda_functions.cuCtxCreate(&cuda_context, CU_CTX_SCHED_BLOCKING_SYNC, cuda_device)) &&\r\n            cuda_succeeded(cuda_functions.cuCtxPopCurrent(&cuda_context))) {\r\n          device = cuda_context;\r\n        }\r\n        else {\r\n          BOOST_LOG(error) << \"NvEnc: couldn't create CUDA interop context: error \" << last_cuda_error;\r\n        }\r\n      }\r\n      else {\r\n        BOOST_LOG(error) << \"NvEnc: couldn't get DXGI adapter for CUDA interop\";\r\n      }\r\n    }\r\n\r\n    return device != nullptr;\r\n  }\r\n\r\n  bool\r\n  nvenc_d3d11_on_cuda::create_and_register_input_buffer() {\r\n    if (encoder_params.buffer_format != NV_ENC_BUFFER_FORMAT_YUV444_10BIT) {\r\n      BOOST_LOG(error) << \"NvEnc: CUDA interop is expected to be used only for 10-bit 4:4:4 encoding\";\r\n      return false;\r\n    }\r\n\r\n    if (!d3d_input_texture) {\r\n      D3D11_TEXTURE2D_DESC desc = {};\r\n      desc.Width = encoder_params.width;\r\n      desc.Height = encoder_params.height * 3;  // Planar YUV\r\n      desc.MipLevels = 1;\r\n      desc.ArraySize = 1;\r\n      desc.Format = dxgi_format_from_nvenc_format(encoder_params.buffer_format);\r\n      desc.SampleDesc.Count = 1;\r\n      desc.Usage = D3D11_USAGE_DEFAULT;\r\n      desc.BindFlags = D3D11_BIND_RENDER_TARGET;\r\n\r\n      if (d3d_device->CreateTexture2D(&desc, nullptr, &d3d_input_texture) != S_OK) {\r\n        BOOST_LOG(error) << \"NvEnc: couldn't create input texture\";\r\n        return false;\r\n      }\r\n    }\r\n\r\n    {\r\n      auto autopop_context = push_context();\r\n      if (!autopop_context) return false;\r\n\r\n      if (!cuda_d3d_input_texture) {\r\n        if (cuda_failed(cuda_functions.cuGraphicsD3D11RegisterResource(\r\n              &cuda_d3d_input_texture,\r\n              d3d_input_texture,\r\n              CU_GRAPHICS_REGISTER_FLAGS_NONE))) {\r\n          BOOST_LOG(error) << \"NvEnc: cuGraphicsD3D11RegisterResource() failed: error \" << last_cuda_error;\r\n          return false;\r\n        }\r\n      }\r\n\r\n      if (!cuda_surface) {\r\n        if (cuda_failed(cuda_functions.cuMemAllocPitch(\r\n              &cuda_surface,\r\n              &cuda_surface_pitch,\r\n              // Planar 16-bit YUV\r\n              encoder_params.width * 2,\r\n              encoder_params.height * 3, 16))) {\r\n          BOOST_LOG(error) << \"NvEnc: cuMemAllocPitch() failed: error \" << last_cuda_error;\r\n          return false;\r\n        }\r\n      }\r\n    }\r\n\r\n    if (!registered_input_buffer) {\r\n      NV_ENC_REGISTER_RESOURCE register_resource = { NV_ENC_REGISTER_RESOURCE_VER };\r\n      register_resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR;\r\n      register_resource.width = encoder_params.width;\r\n      register_resource.height = encoder_params.height;\r\n      register_resource.pitch = cuda_surface_pitch;\r\n      register_resource.resourceToRegister = (void *) cuda_surface;\r\n      register_resource.bufferFormat = encoder_params.buffer_format;\r\n      register_resource.bufferUsage = NV_ENC_INPUT_IMAGE;\r\n\r\n      if (nvenc_failed(nvenc->nvEncRegisterResource(encoder, &register_resource))) {\r\n        BOOST_LOG(error) << \"NvEnc: NvEncRegisterResource() failed: \" << last_nvenc_error_string;\r\n        return false;\r\n      }\r\n\r\n      registered_input_buffer = register_resource.registeredResource;\r\n    }\r\n\r\n    return true;\r\n  }\r\n\r\n  bool\r\n  nvenc_d3d11_on_cuda::synchronize_input_buffer() {\r\n    auto autopop_context = push_context();\r\n    if (!autopop_context) return false;\r\n\r\n    if (cuda_failed(cuda_functions.cuGraphicsMapResources(1, &cuda_d3d_input_texture, 0))) {\r\n      BOOST_LOG(error) << \"NvEnc: cuGraphicsMapResources() failed: error \" << last_cuda_error;\r\n      return false;\r\n    }\r\n\r\n    auto unmap = [&]() -> bool {\r\n      if (cuda_failed(cuda_functions.cuGraphicsUnmapResources(1, &cuda_d3d_input_texture, 0))) {\r\n        BOOST_LOG(error) << \"NvEnc: cuGraphicsUnmapResources() failed: error \" << last_cuda_error;\r\n        return false;\r\n      }\r\n      return true;\r\n    };\r\n    auto unmap_guard = util::fail_guard(unmap);\r\n\r\n    CUarray input_texture_array;\r\n    if (cuda_failed(cuda_functions.cuGraphicsSubResourceGetMappedArray(&input_texture_array, cuda_d3d_input_texture, 0, 0))) {\r\n      BOOST_LOG(error) << \"NvEnc: cuGraphicsSubResourceGetMappedArray() failed: error \" << last_cuda_error;\r\n      return false;\r\n    }\r\n\r\n    {\r\n      CUDA_MEMCPY2D copy_params = {};\r\n      copy_params.srcMemoryType = CU_MEMORYTYPE_ARRAY;\r\n      copy_params.srcArray = input_texture_array;\r\n      copy_params.dstMemoryType = CU_MEMORYTYPE_DEVICE;\r\n      copy_params.dstDevice = cuda_surface;\r\n      copy_params.dstPitch = cuda_surface_pitch;\r\n      // Planar 16-bit YUV\r\n      copy_params.WidthInBytes = encoder_params.width * 2;\r\n      copy_params.Height = encoder_params.height * 3;\r\n\r\n      if (cuda_failed(cuda_functions.cuMemcpy2D(&copy_params))) {\r\n        BOOST_LOG(error) << \"NvEnc: cuMemcpy2D() failed: error \" << last_cuda_error;\r\n        return false;\r\n      }\r\n    }\r\n\r\n    unmap_guard.disable();\r\n    return unmap();\r\n  }\r\n\r\n  bool\r\n  nvenc_d3d11_on_cuda::cuda_succeeded(CUresult result) {\r\n    last_cuda_error = result;\r\n    return result == CUDA_SUCCESS;\r\n  }\r\n\r\n  bool\r\n  nvenc_d3d11_on_cuda::cuda_failed(CUresult result) {\r\n    last_cuda_error = result;\r\n    return result != CUDA_SUCCESS;\r\n  }\r\n\r\n  nvenc_d3d11_on_cuda::autopop_context::~autopop_context() {\r\n    if (pushed_context) {\r\n      CUcontext popped_context;\r\n      if (parent.cuda_failed(parent.cuda_functions.cuCtxPopCurrent(&popped_context))) {\r\n        BOOST_LOG(error) << \"NvEnc: cuCtxPopCurrent() failed: error \" << parent.last_cuda_error;\r\n      }\r\n    }\r\n  }\r\n\r\n  nvenc_d3d11_on_cuda::autopop_context\r\n  nvenc_d3d11_on_cuda::push_context() {\r\n    if (cuda_context &&\r\n        cuda_succeeded(cuda_functions.cuCtxPushCurrent(cuda_context))) {\r\n      return { *this, cuda_context };\r\n    }\r\n    else {\r\n      BOOST_LOG(error) << \"NvEnc: cuCtxPushCurrent() failed: error \" << last_cuda_error;\r\n      return { *this, nullptr };\r\n    }\r\n  }\r\n}\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_d3d11_on_cuda.h",
    "content": "/**\r\n * @file src/nvenc/win/impl/nvenc_d3d11_on_cuda.h\r\n * @brief Declarations for CUDA NVENC encoder with Direct3D11 input surfaces.\r\n */\r\n#pragma once\r\n\r\n#include \"nvenc_d3d11_base.h\"\r\n\r\n#ifdef NVENC_NAMESPACE\r\nnamespace NVENC_NAMESPACE {\r\n#else\r\n  #include <ffnvcodec/dynlink_cuda.h>\r\nnamespace nvenc {\r\n#endif\r\n\r\n  /**\r\n   * @brief Interop Direct3D11 on CUDA NVENC encoder.\r\n   *        Input surface is Direct3D11, encoding is performed by CUDA.\r\n   */\r\n  class nvenc_d3d11_on_cuda final: public nvenc_d3d11_base {\r\n  public:\r\n    /**\r\n     * @param d3d_device Direct3D11 device that will create input surface texture.\r\n     *                   CUDA encoding device will be derived from it.\r\n     */\r\n    nvenc_d3d11_on_cuda(ID3D11Device *d3d_device, shared_dll dll);\r\n    ~nvenc_d3d11_on_cuda();\r\n\r\n    ID3D11Texture2D *\r\n    get_input_texture() override;\r\n\r\n  private:\r\n    bool\r\n    init_library() override;\r\n\r\n    bool\r\n    create_and_register_input_buffer() override;\r\n\r\n    bool\r\n    synchronize_input_buffer() override;\r\n\r\n    bool\r\n    cuda_succeeded(CUresult result);\r\n\r\n    bool\r\n    cuda_failed(CUresult result);\r\n\r\n    struct autopop_context {\r\n      autopop_context(nvenc_d3d11_on_cuda &parent, CUcontext pushed_context):\r\n          parent(parent),\r\n          pushed_context(pushed_context) {\r\n      }\r\n\r\n      ~autopop_context();\r\n\r\n      explicit\r\n      operator bool() const {\r\n        return pushed_context != nullptr;\r\n      }\r\n\r\n      nvenc_d3d11_on_cuda &parent;\r\n      CUcontext pushed_context = nullptr;\r\n    };\r\n\r\n    autopop_context\r\n    push_context();\r\n\r\n    HMODULE dll = NULL;\r\n    const ID3D11DevicePtr d3d_device;\r\n    ID3D11Texture2DPtr d3d_input_texture;\r\n\r\n    struct {\r\n      tcuInit *cuInit;\r\n      tcuD3D11GetDevice *cuD3D11GetDevice;\r\n      tcuCtxCreate_v2 *cuCtxCreate;\r\n      tcuCtxDestroy_v2 *cuCtxDestroy;\r\n      tcuCtxPushCurrent_v2 *cuCtxPushCurrent;\r\n      tcuCtxPopCurrent_v2 *cuCtxPopCurrent;\r\n      tcuMemAllocPitch_v2 *cuMemAllocPitch;\r\n      tcuMemFree_v2 *cuMemFree;\r\n      tcuGraphicsD3D11RegisterResource *cuGraphicsD3D11RegisterResource;\r\n      tcuGraphicsUnregisterResource *cuGraphicsUnregisterResource;\r\n      tcuGraphicsMapResources *cuGraphicsMapResources;\r\n      tcuGraphicsUnmapResources *cuGraphicsUnmapResources;\r\n      tcuGraphicsSubResourceGetMappedArray *cuGraphicsSubResourceGetMappedArray;\r\n      tcuMemcpy2D_v2 *cuMemcpy2D;\r\n      shared_dll dll;\r\n    } cuda_functions = {};\r\n\r\n    CUresult last_cuda_error = CUDA_SUCCESS;\r\n    CUcontext cuda_context = nullptr;\r\n    CUgraphicsResource cuda_d3d_input_texture = nullptr;\r\n    CUdeviceptr cuda_surface = 0;\r\n    size_t cuda_surface_pitch = 0;\r\n  };\r\n}\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_dynamic_factory_1100.cpp",
    "content": "\r\nnamespace nvenc_1100 {\r\n  enum NV_ENC_VUI_VIDEO_FORMAT {\r\n    NV_ENC_VUI_VIDEO_FORMAT_COMPONENT = 0,\r\n    NV_ENC_VUI_VIDEO_FORMAT_PAL = 1,\r\n    NV_ENC_VUI_VIDEO_FORMAT_NTSC = 2,\r\n    NV_ENC_VUI_VIDEO_FORMAT_SECAM = 3,\r\n    NV_ENC_VUI_VIDEO_FORMAT_MAC = 4,\r\n    NV_ENC_VUI_VIDEO_FORMAT_UNSPECIFIED = 5,\r\n  };\r\n  enum NV_ENC_VUI_COLOR_PRIMARIES {\r\n    NV_ENC_VUI_COLOR_PRIMARIES_UNDEFINED = 0,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_BT709 = 1,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_UNSPECIFIED = 2,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_RESERVED = 3,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_BT470M = 4,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_BT470BG = 5,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_SMPTE170M = 6,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_SMPTE240M = 7,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_FILM = 8,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_BT2020 = 9,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_SMPTE428 = 10,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_SMPTE431 = 11,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_SMPTE432 = 12,\r\n    NV_ENC_VUI_COLOR_PRIMARIES_JEDEC_P22 = 22,\r\n  };\r\n  enum NV_ENC_VUI_TRANSFER_CHARACTERISTIC {\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_UNDEFINED = 0,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709 = 1,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_UNSPECIFIED = 2,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_RESERVED = 3,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT470M = 4,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT470BG = 5,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE170M = 6,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE240M = 7,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_LINEAR = 8,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_LOG = 9,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_LOG_SQRT = 10,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_IEC61966_2_4 = 11,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT1361_ECG = 12,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SRGB = 13,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT2020_10 = 14,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT2020_12 = 15,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084 = 16,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE428 = 17,\r\n    NV_ENC_VUI_TRANSFER_CHARACTERISTIC_ARIB_STD_B67 = 18,\r\n  };\r\n  enum NV_ENC_VUI_MATRIX_COEFFS {\r\n    NV_ENC_VUI_MATRIX_COEFFS_RGB = 0,\r\n    NV_ENC_VUI_MATRIX_COEFFS_BT709 = 1,\r\n    NV_ENC_VUI_MATRIX_COEFFS_UNSPECIFIED = 2,\r\n    NV_ENC_VUI_MATRIX_COEFFS_RESERVED = 3,\r\n    NV_ENC_VUI_MATRIX_COEFFS_FCC = 4,\r\n    NV_ENC_VUI_MATRIX_COEFFS_BT470BG = 5,\r\n    NV_ENC_VUI_MATRIX_COEFFS_SMPTE170M = 6,\r\n    NV_ENC_VUI_MATRIX_COEFFS_SMPTE240M = 7,\r\n    NV_ENC_VUI_MATRIX_COEFFS_YCGCO = 8,\r\n    NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL = 9,\r\n    NV_ENC_VUI_MATRIX_COEFFS_BT2020_CL = 10,\r\n    NV_ENC_VUI_MATRIX_COEFFS_SMPTE2085 = 11,\r\n  };\r\n}  // namespace nvenc_1100\r\n\r\n#define NVENC_FACTORY_DEFINITION\r\n#include \"nvenc_dynamic_factory_1100.h\"\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_dynamic_factory_1100.h",
    "content": "#pragma once\r\n\r\n#undef NVENC_FACTORY_VERSION\r\n#define NVENC_FACTORY_VERSION 1100\r\n\r\n#include \"nvenc_dynamic_factory_blueprint.h\"\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_dynamic_factory_1200.cpp",
    "content": "#define NVENC_FACTORY_DEFINITION\r\n#include \"nvenc_dynamic_factory_1200.h\"\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_dynamic_factory_1200.h",
    "content": "#pragma once\r\n\r\n#undef NVENC_FACTORY_VERSION\r\n#define NVENC_FACTORY_VERSION 1200\r\n\r\n#include \"nvenc_dynamic_factory_blueprint.h\"\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_dynamic_factory_1202.cpp",
    "content": "#define NVENC_FACTORY_DEFINITION\r\n#include \"nvenc_dynamic_factory_1202.h\"\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_dynamic_factory_1202.h",
    "content": "#pragma once\r\n\r\n#undef NVENC_FACTORY_VERSION\r\n#define NVENC_FACTORY_VERSION 1202\r\n\r\n#include \"nvenc_dynamic_factory_blueprint.h\"\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_dynamic_factory_blueprint.h",
    "content": "/**\r\n * @file src/nvenc/win/impl/nvenc_dynamic_factory_blueprint.h\r\n * @brief Special blueprint used for declaring and defining factories for specific NVENC SDK versions.\r\n */\r\n#include <boost/preprocessor/cat.hpp>\r\n\r\n#ifndef NVENC_FACTORY_VERSION\r\n  #error Missing NVENC_FACTORY_VERSION preprocessor definition\r\n#endif\r\n\r\n#define NVENC_FACTORY_CLASS BOOST_PP_CAT(nvenc_dynamic_factory_, NVENC_FACTORY_VERSION)\r\n\r\n#include \"nvenc_shared_dll.h\"\r\n\r\n#include \"../nvenc_dynamic_factory.h\"\r\n\r\nnamespace nvenc {\r\n\r\n  class NVENC_FACTORY_CLASS: public nvenc_dynamic_factory {\r\n  public:\r\n    NVENC_FACTORY_CLASS(shared_dll dll):\r\n        dll(dll) {}\r\n\r\n    static std::shared_ptr<nvenc_dynamic_factory>\r\n    get(shared_dll dll) {\r\n      return std::make_shared<NVENC_FACTORY_CLASS>(dll);\r\n    }\r\n\r\n    std::unique_ptr<nvenc_d3d11>\r\n    create_nvenc_d3d11_native(ID3D11Device *d3d_device) override;\r\n\r\n    std::unique_ptr<nvenc_d3d11>\r\n    create_nvenc_d3d11_on_cuda(ID3D11Device *d3d_device) override;\r\n\r\n  private:\r\n    shared_dll dll;\r\n  };\r\n\r\n}  // namespace nvenc\r\n\r\n#ifdef NVENC_FACTORY_DEFINITION\r\n\r\n  #define NVENC_NAMESPACE BOOST_PP_CAT(nvenc_, NVENC_FACTORY_VERSION)\r\n  #define NVENC_FACTORY_INCLUDE(x) <NVENC_FACTORY_VERSION/include/ffnvcodec/x>\r\n\r\nnamespace NVENC_NAMESPACE {\r\n  #include NVENC_FACTORY_INCLUDE(dynlink_cuda.h)\r\n  #include NVENC_FACTORY_INCLUDE(nvEncodeAPI.h)\r\n}  // namespace NVENC_NAMESPACE\r\n\r\nusing namespace nvenc;\r\n\r\n  #include \"../../common_impl/nvenc_base.cpp\"\r\n  #include \"../../common_impl/nvenc_utils.cpp\"\r\n  #include \"nvenc_d3d11_base.cpp\"\r\n  #include \"nvenc_d3d11_native.cpp\"\r\n  #include \"nvenc_d3d11_on_cuda.cpp\"\r\n\r\nnamespace nvenc {\r\n\r\n  std::unique_ptr<nvenc_d3d11>\r\n  NVENC_FACTORY_CLASS::create_nvenc_d3d11_native(ID3D11Device *d3d_device) {\r\n    return std::make_unique<NVENC_NAMESPACE::nvenc_d3d11_native>(d3d_device, dll);\r\n  }\r\n\r\n  std::unique_ptr<nvenc_d3d11>\r\n  NVENC_FACTORY_CLASS::create_nvenc_d3d11_on_cuda(ID3D11Device *d3d_device) {\r\n    return std::make_unique<NVENC_NAMESPACE::nvenc_d3d11_on_cuda>(d3d_device, dll);\r\n  }\r\n\r\n}  // namespace nvenc\r\n\r\n#endif\r\n"
  },
  {
    "path": "src/nvenc/win/impl/nvenc_shared_dll.h",
    "content": "/**\r\n * @file src/nvenc/win/impl/nvenc_shared_dll.h\r\n * @brief Declarations for Windows HMODULE RAII helpers.\r\n */\r\n#pragma once\r\n\r\n#include <memory>\r\n#include <type_traits>\r\n\r\n#include <windows.h>\r\n\r\nnamespace nvenc {\r\n\r\n  using shared_dll = std::shared_ptr<std::remove_pointer_t<HMODULE>>;\r\n\r\n  struct shared_dll_deleter {\r\n    void\r\n    operator()(HMODULE dll) {\r\n      if (dll) FreeLibrary(dll);\r\n    }\r\n  };\r\n\r\n  inline shared_dll\r\n  make_shared_dll(HMODULE dll) {\r\n    return shared_dll(dll, shared_dll_deleter());\r\n  }\r\n\r\n}  // namespace nvenc\r\n"
  },
  {
    "path": "src/nvenc/win/nvenc_d3d11.h",
    "content": "/**\r\n * @file src/nvenc/win/nvenc_d3d11.h\r\n * @brief Declarations for Direct3D11 NVENC encoder interface.\r\n */\r\n#pragma once\r\n\r\n#include \"../nvenc_encoder.h\"\r\n\r\n#include <d3d11.h>\r\n\r\nnamespace nvenc {\r\n\r\n  /**\r\n   * @brief Direct3D11 NVENC encoder interface.\r\n   */\r\n  class nvenc_d3d11: virtual public nvenc_encoder {\r\n  public:\r\n    virtual ~nvenc_d3d11() = default;\r\n\r\n    /**\r\n     * @brief Get input surface texture.\r\n     * @return Input surface texture.\r\n     */\r\n    virtual ID3D11Texture2D *\r\n    get_input_texture() = 0;\r\n  };\r\n\r\n}  // namespace nvenc\r\n"
  },
  {
    "path": "src/nvenc/win/nvenc_dynamic_factory.cpp",
    "content": "/**\r\n * @file src/nvenc/win/nvenc_dynamic_factory.cpp\r\n * @brief Definitions for Windows NVENC encoder factory.\r\n */\r\n#include \"nvenc_dynamic_factory.h\"\r\n\r\n#include \"impl/nvenc_dynamic_factory_1100.h\"\r\n#include \"impl/nvenc_dynamic_factory_1200.h\"\r\n#include \"impl/nvenc_dynamic_factory_1202.h\"\r\n\r\n#include \"impl/nvenc_shared_dll.h\"\r\n\r\n#include \"src/logging.h\"\r\n\r\n#include <windows.h>\r\n\r\n#include <array>\r\n#include <tuple>\r\n\r\nuint32_t\r\nNvEncodeAPIGetMaxSupportedVersion(uint32_t *version);\r\n\r\nnamespace {\r\n  using namespace nvenc;\r\n\r\n  constexpr std::array factory_priorities = {\r\n    std::tuple(&nvenc_dynamic_factory_1202::get, 1202),\r\n    std::tuple(&nvenc_dynamic_factory_1200::get, 1200),\r\n    std::tuple(&nvenc_dynamic_factory_1100::get, 1100),\r\n  };\r\n  constexpr auto min_driver_version = \"456.71\";\r\n\r\n#ifdef _WIN64\r\n  constexpr auto dll_name = \"nvEncodeAPI64.dll\";\r\n#else\r\n  constexpr auto dll_name = \"nvEncodeAPI.dll\";\r\n#endif\r\n\r\n  std::tuple<shared_dll, uint32_t>\r\n  load_dll() {\r\n    auto dll = make_shared_dll(LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32));\r\n    if (!dll) {\r\n      BOOST_LOG(debug) << \"NvEnc: Couldn't load NvEnc library \" << dll_name;\r\n      return {};\r\n    }\r\n\r\n    auto get_max_version = (decltype(NvEncodeAPIGetMaxSupportedVersion) *) GetProcAddress(dll.get(), \"NvEncodeAPIGetMaxSupportedVersion\");\r\n    if (!get_max_version) {\r\n      BOOST_LOG(error) << \"NvEnc: No NvEncodeAPIGetMaxSupportedVersion() in \" << dll_name;\r\n      return {};\r\n    }\r\n\r\n    uint32_t max_version = 0;\r\n    if (get_max_version(&max_version) != 0) {\r\n      BOOST_LOG(error) << \"NvEnc: NvEncodeAPIGetMaxSupportedVersion() failed\";\r\n      return {};\r\n    }\r\n    max_version = (max_version >> 4) * 100 + (max_version & 0xf);\r\n\r\n    return { dll, max_version };\r\n  }\r\n\r\n}  // namespace\r\n\r\nnamespace nvenc {\r\n\r\n  std::shared_ptr<nvenc_dynamic_factory>\r\n  nvenc_dynamic_factory::get() {\r\n    auto [dll, max_version] = load_dll();\r\n    if (!dll) return {};\r\n\r\n    for (const auto &[factory_init, version] : factory_priorities) {\r\n      if (max_version >= version) {\r\n        return factory_init(dll);\r\n      }\r\n    }\r\n\r\n    BOOST_LOG(error) << \"NvEnc: minimum required driver version is \" << min_driver_version;\r\n    return {};\r\n  }\r\n\r\n}  // namespace nvenc\r\n\r\n#ifdef SUNSHINE_TESTS\r\n  #include \"tests/tests_common.h\"\r\n\r\n  #include <comdef.h>\r\n  #include <d3d11.h>\r\n\r\nnamespace {\r\n  _COM_SMARTPTR_TYPEDEF(IDXGIFactory1, IID_IDXGIFactory1);\r\n  _COM_SMARTPTR_TYPEDEF(IDXGIAdapter, IID_IDXGIAdapter);\r\n  _COM_SMARTPTR_TYPEDEF(ID3D11Device, IID_ID3D11Device);\r\n}  // namespace\r\n\r\nstruct NvencVersionTests: testing::TestWithParam<decltype(factory_priorities)::value_type> {\r\n  static void\r\n  SetUpTestSuite() {\r\n    std::tie(suite.dll, suite.max_version) = load_dll();\r\n    if (!suite.dll) {\r\n      GTEST_SKIP() << \"Can't load \" << dll_name;\r\n    }\r\n\r\n    IDXGIFactory1Ptr dxgi_factory;\r\n    ASSERT_HRESULT_SUCCEEDED(CreateDXGIFactory1(IID_PPV_ARGS(&dxgi_factory)));\r\n\r\n    IDXGIAdapterPtr dxgi_adapter;\r\n    for (UINT i = 0; dxgi_factory->EnumAdapters(i, &dxgi_adapter) != DXGI_ERROR_NOT_FOUND; i++) {\r\n      DXGI_ADAPTER_DESC desc;\r\n      ASSERT_HRESULT_SUCCEEDED(dxgi_adapter->GetDesc(&desc));\r\n      if (desc.VendorId == 0x10de) break;\r\n    }\r\n    if (!dxgi_adapter) GTEST_SKIP();\r\n\r\n    ASSERT_HRESULT_SUCCEEDED(D3D11CreateDevice(dxgi_adapter, D3D_DRIVER_TYPE_UNKNOWN, NULL, 0,\r\n      nullptr, 0, D3D11_SDK_VERSION, &suite.device, nullptr, nullptr));\r\n  }\r\n\r\n  static void\r\n  TearDownTestSuite() {\r\n    suite = {};\r\n  }\r\n\r\n  inline static struct {\r\n    nvenc::shared_dll dll;\r\n    uint32_t max_version;\r\n    ID3D11DevicePtr device;\r\n  } suite = {};\r\n};\r\n\r\nTEST_P(NvencVersionTests, CreateAndEncode) {\r\n  auto [factory_init, version] = GetParam();\r\n  if (version > suite.max_version) {\r\n    GTEST_SKIP() << \"Need dll version \" << version << \", have \" << suite.max_version;\r\n  }\r\n\r\n  auto factory = factory_init(suite.dll);\r\n  ASSERT_TRUE(factory);\r\n\r\n  auto nvenc = factory->create_nvenc_d3d11_native(suite.device);\r\n  ASSERT_TRUE(nvenc);\r\n\r\n  video::config_t config = {\r\n    .width = 1920,\r\n    .height = 1080,\r\n    .framerate = 60,\r\n    .bitrate = 10 * 1000,\r\n  };\r\n  video::sunshine_colorspace_t colorspace = {\r\n    .colorspace = video::colorspace_e::rec601,\r\n    .bit_depth = 8,\r\n  };\r\n  ASSERT_TRUE(nvenc->create_encoder({}, config, colorspace, platf::pix_fmt_e::nv12));\r\n  ASSERT_FALSE(nvenc->encode_frame(0, false).data.empty());\r\n}\r\n\r\nINSTANTIATE_TEST_SUITE_P(NvencFactoryTestsPrivate, NvencVersionTests, testing::ValuesIn(factory_priorities),\r\n  [](const auto &info) { return std::to_string(std::get<1>(info.param)); });\r\n\r\n#endif\r\n"
  },
  {
    "path": "src/nvenc/win/nvenc_dynamic_factory.h",
    "content": "/**\r\n * @file src/nvenc/win/nvenc_dynamic_factory.h\r\n * @brief Declarations for Windows NVENC encoder factory.\r\n */\r\n#pragma once\r\n\r\n#include \"nvenc_d3d11.h\"\r\n\r\n#include <memory>\r\n\r\nnamespace nvenc {\r\n\r\n  /**\r\n   * @brief Windows NVENC encoder factory.\r\n   */\r\n  class nvenc_dynamic_factory {\r\n  public:\r\n    virtual ~nvenc_dynamic_factory() = default;\r\n\r\n    /**\r\n     * @brief Initialize NVENC factory, depends on NVIDIA drivers present in the system.\r\n     * @return `shared_ptr` containing factory on success, empty `shared_ptr` on error.\r\n     */\r\n    static std::shared_ptr<nvenc_dynamic_factory>\r\n    get();\r\n\r\n    /**\r\n     * @brief Create native Direct3D11 NVENC encoder.\r\n     * @param d3d_device Direct3D11 device.\r\n     * @return `unique_ptr` containing encoder on success, empty `unique_ptr` on error.\r\n     */\r\n    virtual std::unique_ptr<nvenc_d3d11>\r\n    create_nvenc_d3d11_native(ID3D11Device *d3d_device) = 0;\r\n\r\n    /**\r\n     * @brief Create CUDA NVENC encoder with Direct3D11 input surfaces.\r\n     * @param d3d_device Direct3D11 device.\r\n     * @return `unique_ptr` containing encoder on success, empty `unique_ptr` on error.\r\n     */\r\n    virtual std::unique_ptr<nvenc_d3d11>\r\n    create_nvenc_d3d11_on_cuda(ID3D11Device *d3d_device) = 0;\r\n  };\r\n\r\n}  // namespace nvenc\r\n"
  },
  {
    "path": "src/nvhttp.cpp",
    "content": "/**\n * @file src/nvhttp.cpp\n * @brief Definitions for the nvhttp (GameStream) server.\n */\n// macros\n#define BOOST_BIND_GLOBAL_PLACEHOLDERS\n\n// standard includes\n#include <chrono>\n#include <filesystem>\n#include <map>\n#include <memory>\n#include <mutex>\n#include <shared_mutex>\n#include <sstream>\n#include <string>\n#include <unordered_map>\n#include <unordered_set>\n#include <utility>\n\n// lib includes\n#include <Simple-Web-Server/server_http.hpp>\n#include <boost/asio/ssl/context.hpp>\n#include <boost/asio/ssl/context_base.hpp>\n#include <boost/property_tree/json_parser.hpp>\n#include <boost/property_tree/ptree.hpp>\n#include <boost/property_tree/xml_parser.hpp>\n#include <nlohmann/json.hpp>\n#include <openssl/ssl.h>\n\n// local includes\n#include \"config.h\"\n#include \"confighttp.h\"\n#include \"display_device/display_device.h\"\n#include \"display_device/session.h\"\n#include \"file_handler.h\"\n#include \"globals.h\"\n#include \"httpcommon.h\"\n#include \"logging.h\"\n#include \"network.h\"\n#include \"nvhttp.h\"\n#include \"platform/common.h\"\n#include \"platform/run_command.h\"\n#include \"process.h\"\n#include \"rtsp.h\"\n#include \"stream.h\"\n#include \"system_tray.h\"\n#include \"utility.h\"\n#include \"uuid.h\"\n#include \"abr.h\"\n#include \"video.h\"\n#include \"webhook.h\"\n\n#ifdef _WIN32\n#include \"platform/windows/display_device/windows_utils.h\"\n#endif\n\nusing json = nlohmann::json;\n\nusing namespace std::literals;\nnamespace nvhttp {\n\n  static constexpr std::string_view EMPTY_PROPERTY_TREE_ERROR_MSG = \"Property tree is empty. Probably, control flow got interrupted by an unexpected C++ exception. This is a bug in Sunshine. Moonlight-qt will report Malformed XML (missing root element).\"sv;\n\n  namespace fs = std::filesystem;\n  namespace pt = boost::property_tree;\n\n  static const std::unordered_set<std::string> blocked_paths = {\n    \"/\", \"/index.html\", \"/index.htm\", \"/index\",\n    \"/favicon.ico\", \"/favicon.png\", \"/favicon.svg\"\n  };\n\n  crypto::cert_chain_t cert_chain;\n  std::shared_mutex cert_chain_mutex;\n\n  struct named_cert_t {\n    std::string name;\n    std::string uuid;\n    std::string cert;\n  };\n\n  struct client_t {\n    std::vector<named_cert_t> named_devices;\n  };\n\n  // uniqueID, session\n  std::unordered_map<std::string, pair_session_t> map_id_sess;\n  client_t client_root;\n  std::atomic<uint32_t> session_id_counter;\n\n  static std::string last_pair_name;\n\n  // Preset PIN for QR code pairing\n  static struct {\n    std::string pin;\n    std::string name;\n    std::chrono::steady_clock::time_point expires_at;\n    bool paired = false;\n    std::mutex mutex;\n  } preset_pin_state;\n\n  // Rate limiting for pairing attempts per IP\n  struct pair_rate_limiter_t {\n    std::unordered_map<std::string, std::pair<int, std::chrono::steady_clock::time_point>> attempts;\n    std::mutex mutex;\n\n    bool\n    check_and_record(const std::string &ip) {\n      constexpr int max_attempts = 5;\n      constexpr int window_seconds = 60;\n      std::lock_guard lock { mutex };\n      auto now = std::chrono::steady_clock::now();\n      auto &entry = attempts[ip];\n\n      // Reset window if expired\n      if (now - entry.second > std::chrono::seconds(window_seconds)) {\n        entry = { 0, now };\n      }\n\n      if (entry.first >= max_attempts) {\n        return false;  // rate limited\n      }\n\n      entry.first++;\n      return true;\n    }\n\n    void\n    cleanup() {\n      constexpr int window_seconds = 60;\n      std::lock_guard lock { mutex };\n      auto now = std::chrono::steady_clock::now();\n      for (auto it = attempts.begin(); it != attempts.end();) {\n        if (now - it->second.second > std::chrono::seconds(window_seconds * 2)) {\n          it = attempts.erase(it);\n        }\n        else {\n          ++it;\n        }\n      }\n    }\n  };\n  static pair_rate_limiter_t pair_rate_limit;\n\n  // Map to store certificate UUIDs keyed by request pointer\n  // Using weak_ptr to track request lifetime and prevent memory leaks\n  static std::map<const void *, std::pair<std::weak_ptr<void>, std::string>> request_cert_uuid_map;\n  static std::mutex request_cert_uuid_map_mutex;\n\n  class SunshineHTTPSServer: public SimpleWeb::ServerBase<SunshineHTTPS> {\n  public:\n    SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file):\n        ServerBase<SunshineHTTPS>::ServerBase(443),\n        context(boost::asio::ssl::context::tls_server) {\n      // Disabling TLS 1.0 and 1.1 (see RFC 8996)\n      context.set_options(boost::asio::ssl::context::no_tlsv1);\n      context.set_options(boost::asio::ssl::context::no_tlsv1_1);\n      context.use_certificate_chain_file(certification_file);\n      context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem);\n    }\n\n    std::function<int(SSL *)> verify;\n    std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)> on_verify_failed;\n\n  protected:\n    boost::asio::ssl::context context;\n\n    void\n    after_bind() override {\n      if (verify) {\n        context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once);\n        context.set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) {\n          // To respond with an error message, a connection must be established\n          return 1;\n        });\n      }\n    }\n\n    // This is Server<HTTPS>::accept() with SSL validation support added\n    void\n    accept() override {\n      auto connection = create_connection(*io_service, context);\n\n      acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const SimpleWeb::error_code &ec) {\n        auto lock = connection->handler_runner->continue_lock();\n        if (!lock)\n          return;\n\n        if (ec != SimpleWeb::error::operation_aborted)\n          this->accept();\n\n        auto session = std::make_shared<Session>(config.max_request_streambuf_size, connection);\n\n        if (!ec) {\n          boost::asio::ip::tcp::no_delay option(true);\n          SimpleWeb::error_code ec;\n          session->connection->socket->lowest_layer().set_option(option, ec);\n\n          session->connection->set_timeout(config.timeout_request);\n          session->connection->socket->async_handshake(boost::asio::ssl::stream_base::server, [this, session](const SimpleWeb::error_code &ec) {\n            session->connection->cancel_timeout();\n            auto lock = session->connection->handler_runner->continue_lock();\n            if (!lock)\n              return;\n            if (!ec) {\n              // Extract and store certificate UUID during handshake\n              try {\n                SSL *ssl = session->connection->socket->native_handle();\n                if (ssl) {\n                  crypto::x509_t x509 {\n#if OPENSSL_VERSION_MAJOR >= 3\n                    SSL_get1_peer_certificate(ssl)\n#else\n                    SSL_get_peer_certificate(ssl)\n#endif\n                  };\n                  if (x509) {\n                    std::string client_cert_pem = crypto::pem(x509);\n                    // Find matching certificate UUID\n                    for (const auto &named_cert : client_root.named_devices) {\n                      if (named_cert.cert == client_cert_pem) {\n                        // Store UUID in map using request pointer as key\n                        std::lock_guard<std::mutex> lock(request_cert_uuid_map_mutex);\n                        request_cert_uuid_map[session->request.get()] =\n                          std::make_pair(std::weak_ptr<void>(std::static_pointer_cast<void>(session->request)), named_cert.uuid);\n                        break;\n                      }\n                    }\n                  }\n                }\n              }\n              catch (const std::exception &e) {\n                BOOST_LOG(debug) << \"Failed to extract certificate UUID during handshake: \" << e.what();\n              }\n\n              if (verify && !verify(session->connection->socket->native_handle()))\n                this->write(session, on_verify_failed);\n              else\n                this->read(session);\n            }\n            else if (this->on_error)\n              this->on_error(session->request, ec);\n          });\n        }\n        else if (this->on_error)\n          this->on_error(session->request, ec);\n      });\n    }\n  };\n\n  using https_server_t = SunshineHTTPSServer;\n  using http_server_t = SimpleWeb::Server<SimpleWeb::HTTP>;\n\n  struct conf_intern_t {\n    std::string servercert;\n    std::string pkey;\n  } conf_intern;\n\n  using args_t = SimpleWeb::CaseInsensitiveMultimap;\n  using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>;\n  using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Request>;\n  using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;\n  using req_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Request>;\n\n  // Get client certificate UUID from request\n  std::string\n  get_client_cert_uuid_from_request(req_https_t request) {\n    try {\n      // Retrieve UUID from map using request pointer as key\n      std::lock_guard<std::mutex> lock(request_cert_uuid_map_mutex);\n      auto it = request_cert_uuid_map.find(request.get());\n      if (it != request_cert_uuid_map.end()) {\n        // Check if request is still valid (not expired)\n        if (!it->second.first.expired()) {\n          std::string uuid = it->second.second;\n          // Clean up after retrieval to prevent memory leaks\n          // (assuming UUID is only needed once per request)\n          request_cert_uuid_map.erase(it);\n          return uuid;\n        }\n        else {\n          // Request expired, remove from map\n          request_cert_uuid_map.erase(it);\n        }\n      }\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(debug) << \"获取客户端证书UUID失败: \" << e.what();\n    }\n    return \"\";\n  }\n\n  enum class op_e {\n    ADD,  ///< Add certificate\n    REMOVE  ///< Remove certificate\n  };\n\n  std::string\n  get_arg(const args_t &args, const char *name, const char *default_value = nullptr) {\n    auto it = args.find(name);\n    if (it == std::end(args)) {\n      if (default_value != NULL) {\n        return std::string(default_value);\n      }\n\n      throw std::out_of_range(name);\n    }\n    return it->second;\n  }\n\n  void\n  save_state() {\n    pt::ptree root;\n\n    if (fs::exists(config::nvhttp.file_state)) {\n      try {\n        pt::read_json(config::nvhttp.file_state, root);\n      }\n      catch (std::exception &e) {\n        BOOST_LOG(error) << \"Couldn't read \"sv << config::nvhttp.file_state << \": \"sv << e.what();\n        return;\n      }\n    }\n\n    root.erase(\"root\"s);\n\n    root.put(\"root.uniqueid\", http::unique_id);\n    client_t &client = client_root;\n    pt::ptree node;\n\n    pt::ptree named_cert_nodes;\n    for (auto &named_cert : client.named_devices) {\n      pt::ptree named_cert_node;\n      named_cert_node.put(\"name\"s, named_cert.name);\n      named_cert_node.put(\"cert\"s, named_cert.cert);\n      named_cert_node.put(\"uuid\"s, named_cert.uuid);\n      named_cert_nodes.push_back(std::make_pair(\"\"s, named_cert_node));\n    }\n    root.add_child(\"root.named_devices\"s, named_cert_nodes);\n\n    try {\n      pt::write_json(config::nvhttp.file_state, root);\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(error) << \"Couldn't write \"sv << config::nvhttp.file_state << \": \"sv << e.what();\n      return;\n    }\n  }\n\n  void\n  load_state() {\n    if (!fs::exists(config::nvhttp.file_state)) {\n      BOOST_LOG(debug) << \"File \"sv << config::nvhttp.file_state << \" doesn't exist\"sv;\n      http::unique_id = uuid_util::uuid_t::generate().string();\n      return;\n    }\n\n    pt::ptree tree;\n    try {\n      pt::read_json(config::nvhttp.file_state, tree);\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(error) << \"Couldn't read \"sv << config::nvhttp.file_state << \": \"sv << e.what();\n\n      return;\n    }\n\n    auto unique_id_p = tree.get_optional<std::string>(\"root.uniqueid\");\n    if (!unique_id_p) {\n      // This file doesn't contain moonlight credentials\n      http::unique_id = uuid_util::uuid_t::generate().string();\n      return;\n    }\n    http::unique_id = std::move(*unique_id_p);\n\n    auto root = tree.get_child(\"root\");\n    client_t client;\n\n    // Import from old format\n    if (root.get_child_optional(\"devices\")) {\n      auto device_nodes = root.get_child(\"devices\");\n      for (auto &[_, device_node] : device_nodes) {\n        auto uniqID = device_node.get<std::string>(\"uniqueid\");\n\n        if (device_node.count(\"certs\")) {\n          for (auto &[_, el] : device_node.get_child(\"certs\")) {\n            named_cert_t named_cert;\n            named_cert.name = \"\"s;\n            named_cert.cert = el.get_value<std::string>();\n            named_cert.uuid = uuid_util::uuid_t::generate().string();\n            client.named_devices.emplace_back(named_cert);\n          }\n        }\n      }\n    }\n\n    if (root.count(\"named_devices\")) {\n      for (auto &[_, el] : root.get_child(\"named_devices\")) {\n        named_cert_t named_cert;\n        named_cert.name = el.get_child(\"name\").get_value<std::string>();\n        named_cert.cert = el.get_child(\"cert\").get_value<std::string>();\n        named_cert.uuid = el.get_child(\"uuid\").get_value<std::string>();\n        client.named_devices.emplace_back(named_cert);\n      }\n    }\n\n    // Empty certificate chain and import certs from file\n    {\n      std::unique_lock<std::shared_mutex> ul(cert_chain_mutex);\n      cert_chain.clear();\n      for (auto &named_cert : client.named_devices) {\n        cert_chain.add(crypto::x509(named_cert.cert));\n      }\n    }\n\n    client_root = client;\n  }\n\n  void\n  add_authorized_client(const std::string &name, std::string &&cert) {\n    client_t &client = client_root;\n    named_cert_t named_cert;\n    named_cert.name = name;\n    named_cert.cert = std::move(cert);\n    named_cert.uuid = uuid_util::uuid_t::generate().string();\n    client.named_devices.emplace_back(named_cert);\n\n    if (!config::sunshine.flags[config::flag::FRESH_STATE]) {\n      save_state();\n    }\n  }\n\n  std::shared_ptr<rtsp_stream::launch_session_t>\n  make_launch_session(bool host_audio, const args_t &args) {\n    auto launch_session = std::make_shared<rtsp_stream::launch_session_t>();\n\n    launch_session->id = ++session_id_counter;\n\n    auto rikey = util::from_hex_vec(get_arg(args, \"rikey\"), true);\n    std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key));\n\n    launch_session->host_audio = host_audio;\n    std::stringstream mode = std::stringstream(get_arg(args, \"mode\", \"0x0x0\"));\n    // Split mode by the char \"x\", to populate width/height/fps\n    int x = 0;\n    std::string segment;\n    while (std::getline(mode, segment, 'x')) {\n      if (x == 0) launch_session->width = atoi(segment.c_str());\n      if (x == 1) launch_session->height = atoi(segment.c_str());\n      if (x == 2) launch_session->fps = atoi(segment.c_str());\n      x++;\n    }\n    launch_session->unique_id = (get_arg(args, \"uniqueid\", \"unknown\"));\n    launch_session->client_name = (get_arg(args, \"clientname\", \"unknown\"));\n    launch_session->appid = util::from_view(get_arg(args, \"appid\", \"unknown\"));\n    launch_session->enable_sops = util::from_view(get_arg(args, \"sops\", \"0\"));\n    launch_session->surround_info = util::from_view(get_arg(args, \"surroundAudioInfo\", \"196610\"));\n    launch_session->surround_params = (get_arg(args, \"surroundParams\", \"\"));\n    launch_session->gcmap = util::from_view(get_arg(args, \"gcmap\", \"0\"));\n    launch_session->enable_hdr = util::from_view(get_arg(args, \"hdrMode\", \"0\"));\n    launch_session->use_vdd = util::from_view(get_arg(args, \"useVdd\", \"0\"));\n    launch_session->custom_screen_mode = util::from_view(get_arg(args, \"customScreenMode\", \"-1\"));\n    launch_session->max_nits = std::stof(get_arg(args, \"maxBrightness\", \"1000\"));\n    launch_session->min_nits = std::stof(get_arg(args, \"minBrightness\", \"0.001\"));\n    launch_session->max_full_nits = std::stof(get_arg(args, \"maxAverageBrightness\", \"1000\"));\n\n    // Get display_name from query parameter if provided\n    std::string display_name = get_arg(args, \"display_name\", \"\");\n    if (!display_name.empty()) {\n      launch_session->env[\"SUNSHINE_CLIENT_DISPLAY_NAME\"] = display_name;\n      BOOST_LOG(info) << \"Launch session will use specified display: \" << display_name;\n    }\n\n    // Encrypted RTSP is enabled with client reported corever >= 1\n    auto corever = util::from_view(get_arg(args, \"corever\", \"0\"));\n    if (corever >= 1) {\n      launch_session->rtsp_cipher = crypto::cipher::gcm_t {\n        launch_session->gcm_key, false\n      };\n      launch_session->rtsp_iv_counter = 0;\n    }\n    launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? \"rtspenc://\"s : \"rtsp://\"s;\n\n    // Generate the unique identifiers for this connection that we will send later during RTSP handshake\n    unsigned char raw_payload[8];\n    RAND_bytes(raw_payload, sizeof(raw_payload));\n    launch_session->av_ping_payload = util::hex_vec(raw_payload);\n    RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data));\n\n    launch_session->iv.resize(16);\n    uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(get_arg(args, \"rikeyid\")));\n    auto prepend_iv_p = (uint8_t *) &prepend_iv;\n    std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv));\n\n    // set auto enable sops\n    launch_session->enable_sops = \"1\";\n\n    launch_session->env[\"SUNSHINE_CLIENT_ID\"] = std::to_string(launch_session->id);\n    launch_session->env[\"SUNSHINE_CLIENT_UNIQUE_ID\"] = launch_session->unique_id;\n    launch_session->env[\"SUNSHINE_CLIENT_NAME\"] = launch_session->client_name;\n    launch_session->env[\"SUNSHINE_CLIENT_WIDTH\"] = std::to_string(launch_session->width);\n    launch_session->env[\"SUNSHINE_CLIENT_HEIGHT\"] = std::to_string(launch_session->height);\n    launch_session->env[\"SUNSHINE_CLIENT_FPS\"] = std::to_string(launch_session->fps);\n    launch_session->env[\"SUNSHINE_CLIENT_HDR\"] = launch_session->enable_hdr ? \"true\" : \"false\";\n    launch_session->env[\"SUNSHINE_CLIENT_GCMAP\"] = std::to_string(launch_session->gcmap);\n    launch_session->env[\"SUNSHINE_CLIENT_HOST_AUDIO\"] = launch_session->host_audio ? \"true\" : \"false\";\n    launch_session->env[\"SUNSHINE_CLIENT_ENABLE_SOPS\"] = launch_session->enable_sops ? \"true\" : \"false\";\n    launch_session->env[\"SUNSHINE_CLIENT_ENABLE_MIC\"] = launch_session->enable_mic ? \"true\" : \"false\";\n    launch_session->env[\"SUNSHINE_CLIENT_USE_VDD\"] = launch_session->use_vdd ? \"true\" : \"false\";\n    launch_session->env[\"SUNSHINE_CLIENT_CUSTOM_SCREEN_MODE\"] = std::to_string(launch_session->custom_screen_mode);\n    int channelCount = launch_session->surround_info & (65535);\n    switch (channelCount) {\n      case 2:\n        launch_session->env[\"SUNSHINE_CLIENT_AUDIO_CONFIGURATION\"] = \"2.0\";\n        break;\n      case 6:\n        launch_session->env[\"SUNSHINE_CLIENT_AUDIO_CONFIGURATION\"] = \"5.1\";\n        break;\n      case 8:\n        launch_session->env[\"SUNSHINE_CLIENT_AUDIO_CONFIGURATION\"] = \"7.1\";\n        break;\n      case 12:\n        launch_session->env[\"SUNSHINE_CLIENT_AUDIO_CONFIGURATION\"] = \"7.1.4\";\n        break;\n    }\n\n    return launch_session;\n  }\n\n  void\n  remove_session(const pair_session_t &sess) {\n    map_id_sess.erase(sess.client.uniqueID);\n  }\n\n  void\n  fail_pair(pair_session_t &sess, pt::ptree &tree, const std::string status_msg) {\n    tree.put(\"root.paired\", 0);\n    tree.put(\"root.<xmlattr>.status_code\", 400);\n    tree.put(\"root.<xmlattr>.status_message\", status_msg);\n    remove_session(sess);  // Security measure, delete the session when something went wrong and force a re-pair\n  }\n\n  void\n  getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin, const std::string &client_name) {\n    if (sess.last_phase != PAIR_PHASE::NONE) {\n      fail_pair(sess, tree, \"Out of order call to getservercert\");\n      return;\n    }\n    sess.last_phase = PAIR_PHASE::GETSERVERCERT;\n\n    if (sess.async_insert_pin.salt.size() < 32) {\n      fail_pair(sess, tree, \"Salt too short\");\n      return;\n    }\n\n    std::string_view salt_view { sess.async_insert_pin.salt.data(), 32 };\n\n    auto salt = util::from_hex<std::array<uint8_t, 16>>(salt_view, true);\n\n    auto key = crypto::gen_aes_key(salt, pin);\n    sess.cipher_key = std::make_unique<crypto::aes_t>(key);\n\n    tree.put(\"root.paired\", 1);\n    // 增加自定义客户端名字告诉客户端\n    tree.put(\"root.pairname\", client_name);\n    tree.put(\"root.plaincert\", util::hex_vec(conf_intern.servercert, true));\n    tree.put(\"root.<xmlattr>.status_code\", 200);\n  }\n\n  void\n  clientchallenge(pair_session_t &sess, pt::ptree &tree, const std::string &challenge) {\n    if (sess.last_phase != PAIR_PHASE::GETSERVERCERT) {\n      fail_pair(sess, tree, \"Out of order call to clientchallenge\");\n      return;\n    }\n    sess.last_phase = PAIR_PHASE::CLIENTCHALLENGE;\n\n    if (!sess.cipher_key) {\n      fail_pair(sess, tree, \"Cipher key not set\");\n      return;\n    }\n\n    crypto::cipher::ecb_t cipher(*sess.cipher_key, false);\n\n    std::vector<uint8_t> decrypted;\n    cipher.decrypt(challenge, decrypted);\n\n    auto x509 = crypto::x509(conf_intern.servercert);\n    auto sign = crypto::signature(x509);\n    auto serversecret = crypto::rand(16);\n\n    decrypted.insert(std::end(decrypted), std::begin(sign), std::end(sign));\n    decrypted.insert(std::end(decrypted), std::begin(serversecret), std::end(serversecret));\n\n    auto hash = crypto::hash({ (char *) decrypted.data(), decrypted.size() });\n    auto serverchallenge = crypto::rand(16);\n\n    std::string plaintext;\n    plaintext.reserve(hash.size() + serverchallenge.size());\n\n    plaintext.insert(std::end(plaintext), std::begin(hash), std::end(hash));\n    plaintext.insert(std::end(plaintext), std::begin(serverchallenge), std::end(serverchallenge));\n\n    std::vector<uint8_t> encrypted;\n    cipher.encrypt(plaintext, encrypted);\n\n    sess.serversecret = std::move(serversecret);\n    sess.serverchallenge = std::move(serverchallenge);\n\n    tree.put(\"root.paired\", 1);\n    tree.put(\"root.challengeresponse\", util::hex_vec(encrypted, true));\n    tree.put(\"root.<xmlattr>.status_code\", 200);\n  }\n\n  void\n  serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const std::string &encrypted_response) {\n    if (sess.last_phase != PAIR_PHASE::CLIENTCHALLENGE) {\n      fail_pair(sess, tree, \"Out of order call to serverchallengeresp\");\n      return;\n    }\n    sess.last_phase = PAIR_PHASE::SERVERCHALLENGERESP;\n\n    if (!sess.cipher_key || sess.serversecret.empty()) {\n      fail_pair(sess, tree, \"Cipher key or serversecret not set\");\n      return;\n    }\n\n    std::vector<uint8_t> decrypted;\n    crypto::cipher::ecb_t cipher(*sess.cipher_key, false);\n\n    cipher.decrypt(encrypted_response, decrypted);\n\n    sess.clienthash = std::move(decrypted);\n\n    auto serversecret = sess.serversecret;\n    auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret);\n\n    serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign));\n\n    tree.put(\"root.pairingsecret\", util::hex_vec(serversecret, true));\n    tree.put(\"root.paired\", 1);\n    tree.put(\"root.<xmlattr>.status_code\", 200);\n  }\n\n  void\n  clientpairingsecret(pair_session_t &sess, pt::ptree &tree, const std::string &client_pairing_secret) {\n    if (sess.last_phase != PAIR_PHASE::SERVERCHALLENGERESP) {\n      fail_pair(sess, tree, \"Out of order call to clientpairingsecret\");\n      return;\n    }\n    sess.last_phase = PAIR_PHASE::CLIENTPAIRINGSECRET;\n\n    auto &client = sess.client;\n\n    if (client_pairing_secret.size() <= 16) {\n      fail_pair(sess, tree, \"Client pairing secret too short\");\n      return;\n    }\n\n    std::string_view secret { client_pairing_secret.data(), 16 };\n    std::string_view sign { client_pairing_secret.data() + secret.size(), client_pairing_secret.size() - secret.size() };\n\n    auto x509 = crypto::x509(client.cert);\n    if (!x509) {\n      fail_pair(sess, tree, \"Invalid client certificate\");\n      return;\n    }\n    auto x509_sign = crypto::signature(x509);\n\n    std::string data;\n    data.reserve(sess.serverchallenge.size() + x509_sign.size() + secret.size());\n\n    data.insert(std::end(data), std::begin(sess.serverchallenge), std::end(sess.serverchallenge));\n    data.insert(std::end(data), std::begin(x509_sign), std::end(x509_sign));\n    data.insert(std::end(data), std::begin(secret), std::end(secret));\n\n    auto hash = crypto::hash(data);\n\n    // if hash not correct, probably MITM\n    bool same_hash = hash.size() == sess.clienthash.size() && std::equal(hash.begin(), hash.end(), sess.clienthash.begin());\n    auto verify = crypto::verify256(crypto::x509(client.cert), secret, sign);\n    if (same_hash && verify) {\n      tree.put(\"root.paired\", 1);\n\n      // Add cert to chain directly under exclusive lock\n      {\n        std::unique_lock<std::shared_mutex> ul(cert_chain_mutex);\n        cert_chain.add(crypto::x509(client.cert));\n      }\n\n      // The client is now successfully paired and will be authorized to connect\n      add_authorized_client(client.name, std::move(client.cert));\n    }\n    else {\n      tree.put(\"root.paired\", 0);\n    }\n    remove_session(sess);\n    tree.put(\"root.<xmlattr>.status_code\", 200);\n  }\n\n  template <class T>\n  struct tunnel;\n\n  template <>\n  struct tunnel<SunshineHTTPS> {\n    static auto constexpr to_string = \"HTTPS\"sv;\n  };\n\n  template <>\n  struct tunnel<SimpleWeb::HTTP> {\n    static auto constexpr to_string = \"NONE\"sv;\n  };\n\n  template <class T>\n  void\n  print_req(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {\n    auto debug_flag = debug.open_record();\n    auto verbose_flag = verbose.open_record();\n    if (!debug_flag && !verbose_flag) {\n      return;\n    }\n    std::ostringstream log_stream;\n    log_stream << \"Request - Protocol: \" << tunnel<T>::to_string\n               << \", IP: \" << request->remote_endpoint().address().to_string()\n               << \", PORT: \" << request->remote_endpoint().port()\n               << \", METHOD: \" << request->method\n               << \", PATH: \" << request->path;\n\n    if (verbose_flag) {\n      // Headers\n      if (!request->header.empty()) {\n        log_stream << \", HEADERS: \";\n        bool first = true;\n        for (auto &[name, val] : request->header) {\n          if (!first) log_stream << \", \";\n          log_stream << name << \"=\" << val;\n          first = false;\n        }\n      }\n\n      // Query parameters\n      auto query_params = request->parse_query_string();\n      if (!query_params.empty()) {\n        log_stream << \", PARAMS: \";\n        bool first = true;\n        for (auto &[name, val] : query_params) {\n          if (!first) log_stream << \"&\";\n          log_stream << name << \"=\" << val;\n          first = false;\n        }\n      }\n    }\n    BOOST_LOG(debug) << log_stream.str();\n  }\n\n  template <class T>\n  void\n  print_request_ip(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request, const std::string &message) {\n    BOOST_LOG(info) << message << \" from IP: \" << request->remote_endpoint().address().to_string() << \", Port: \" << request->remote_endpoint().port();\n  }\n\n  template <class T>\n  void\n  print_request_warning_ip(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request, const std::string &message) {\n    BOOST_LOG(warning) << message << \" [\" << request->query_string << \"] from IP: \" << request->remote_endpoint().address().to_string() << \", Port: \" << request->remote_endpoint().port();\n  }\n\n  template <class T>\n  void\n  not_found(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {\n    print_req<T>(request);\n\n    // Security hardening: Return 444 for root paths to prevent probing\n    if (blocked_paths.count(request->path)) {\n      *response << \"HTTP/1.1 444 No Response\\r\\n\";\n      response->close_connection_after_response = true;\n      return;\n    }\n\n    pt::ptree tree;\n    tree.put(\"root.<xmlattr>.status_code\", 404);\n\n    std::ostringstream data;\n\n    pt::write_xml(data, tree);\n    response->write(data.str());\n\n    *response\n      << \"HTTP/1.1 404 NOT FOUND\\r\\n\"\n      << data.str();\n\n    response->close_connection_after_response = true;\n  }\n\n  template <class T>\n  void\n  pair(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {\n    print_req<T>(request);\n\n    // Rate limit pairing attempts per IP\n    auto client_ip = request->remote_endpoint().address().to_string();\n    if (!pair_rate_limit.check_and_record(client_ip)) {\n      BOOST_LOG(warning) << \"Pairing rate limited for IP: \" << client_ip;\n      pt::ptree rate_tree;\n      rate_tree.put(\"root.<xmlattr>.status_code\", 429);\n      rate_tree.put(\"root.<xmlattr>.status_message\", \"Too many pairing attempts. Try again later.\");\n      rate_tree.put(\"root.paired\", 0);\n      std::ostringstream data;\n      pt::write_xml(data, rate_tree);\n      response->write(data.str());\n      response->close_connection_after_response = true;\n      return;\n    }\n\n    // Periodically clean up stale rate limit entries\n    pair_rate_limit.cleanup();\n\n    pt::ptree tree;\n\n    auto fg = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      pt::write_xml(data, tree);\n      response->write(data.str());\n      response->close_connection_after_response = true;\n    });\n\n    auto args = request->parse_query_string();\n    if (args.find(\"uniqueid\"s) == std::end(args)) {\n      tree.put(\"root.<xmlattr>.status_code\", 400);\n      tree.put(\"root.<xmlattr>.status_message\", \"Missing uniqueid parameter\");\n\n      return;\n    }\n\n    auto uniqID { get_arg(args, \"uniqueid\") };\n\n    args_t::const_iterator it;\n    if (it = args.find(\"phrase\"); it != std::end(args)) {\n      if (it->second == \"getservercert\"sv) {\n        pair_session_t sess;\n\n        sess.client.uniqueID = std::move(uniqID);\n        sess.client.cert = util::from_hex_vec(get_arg(args, \"clientcert\"), true);\n        last_pair_name = get_arg(args, \"clientname\", \"Named Zako\");\n\n        BOOST_LOG(verbose) << \"Client cert: \" << sess.client.cert.substr(0, 100) << \"...\";\n        auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first;\n\n        ptr->second.async_insert_pin.salt = std::move(get_arg(args, \"salt\"));\n        if (config::sunshine.flags[config::flag::PIN_STDIN]) {\n          std::string pin;\n\n          std::cout << \"Please insert pin: \"sv;\n          std::getline(std::cin, pin);\n\n          getservercert(ptr->second, tree, pin, last_pair_name);\n        }\n        else {\n          // Check for preset PIN (from QR code pairing)\n          // Only allow preset PIN for LAN/localhost clients\n          auto remote_addr = request->remote_endpoint().address();\n          auto nettype = net::from_address(remote_addr.to_string());\n          auto preset = (nettype == net::net_e::PC || nettype == net::net_e::LAN) ? consume_preset_pin() : std::string {};\n          if (!preset.empty()) {\n            BOOST_LOG(info) << \"Using preset PIN for QR code pairing with \" << last_pair_name\n                            << \" from \" << remote_addr.to_string();\n            ptr->second.client.name = last_pair_name;\n            getservercert(ptr->second, tree, preset, last_pair_name);\n          }\n          else {\n#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1\n            system_tray::update_tray_require_pin(last_pair_name);\n#endif\n            ptr->second.async_insert_pin.response = std::move(response);\n\n            fg.disable();\n            return;\n          }\n        }\n      }\n      else if (it->second == \"pairchallenge\"sv) {\n        tree.put(\"root.paired\", 1);\n        tree.put(\"root.<xmlattr>.status_code\", 200);\n        return;\n      }\n    }\n\n    auto sess_it = map_id_sess.find(uniqID);\n    if (sess_it == std::end(map_id_sess)) {\n      tree.put(\"root.<xmlattr>.status_code\", 400);\n      tree.put(\"root.<xmlattr>.status_message\", \"Invalid uniqueid\");\n\n      return;\n    }\n\n    if (it = args.find(\"clientchallenge\"); it != std::end(args)) {\n      auto challenge = util::from_hex_vec(it->second, true);\n      clientchallenge(sess_it->second, tree, challenge);\n    }\n    else if (it = args.find(\"serverchallengeresp\"); it != std::end(args)) {\n      auto encrypted_response = util::from_hex_vec(it->second, true);\n      serverchallengeresp(sess_it->second, tree, encrypted_response);\n    }\n    else if (it = args.find(\"clientpairingsecret\"); it != std::end(args)) {\n      auto pairingsecret = util::from_hex_vec(it->second, true);\n      clientpairingsecret(sess_it->second, tree, pairingsecret);\n    }\n    else {\n      tree.put(\"root.<xmlattr>.status_code\", 404);\n      tree.put(\"root.<xmlattr>.status_message\", \"Invalid pairing request\");\n    }\n  }\n\n  bool\n  pin(std::string pin, std::string name) {\n    pt::ptree tree;\n    if (map_id_sess.empty()) {\n      return false;\n    }\n\n    // ensure pin is 4 digits\n    if (pin.size() != 4) {\n      tree.put(\"root.paired\", 0);\n      tree.put(\"root.<xmlattr>.status_code\", 400);\n      tree.put(\n        \"root.<xmlattr>.status_message\", \"Pin must be 4 digits, \" + std::to_string(pin.size()) + \" provided\");\n      return false;\n    }\n\n    // ensure all pin characters are numeric\n    if (!std::all_of(pin.begin(), pin.end(), ::isdigit)) {\n      tree.put(\"root.paired\", 0);\n      tree.put(\"root.<xmlattr>.status_code\", 400);\n      tree.put(\"root.<xmlattr>.status_message\", \"Pin must be numeric\");\n      return false;\n    }\n\n    auto &sess = std::begin(map_id_sess)->second;\n    getservercert(sess, tree, pin, name);\n    sess.client.name = name;\n\n    // response to the request for pin\n    std::ostringstream data;\n    pt::write_xml(data, tree);\n\n    auto &async_response = sess.async_insert_pin.response;\n    if (async_response.has_left() && async_response.left()) {\n      async_response.left()->write(data.str());\n    }\n    else if (async_response.has_right() && async_response.right()) {\n      async_response.right()->write(data.str());\n    }\n    else {\n      return false;\n    }\n\n    // reset async_response\n    async_response = std::decay_t<decltype(async_response.left())>();\n    // response to the current request\n    return true;\n  }\n\n  template <class T>\n  void\n  serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response, std::shared_ptr<typename SimpleWeb::ServerBase<T>::Request> request) {\n    print_req<T>(request);\n\n    int pair_status = 0;\n    if constexpr (std::is_same_v<SunshineHTTPS, T>) {\n      auto args = request->parse_query_string();\n      auto clientID = args.find(\"uniqueid\"s);\n\n      if (clientID != std::end(args)) {\n        pair_status = 1;\n      }\n    }\n\n    auto local_endpoint = request->local_endpoint();\n\n    pt::ptree tree;\n\n    tree.put(\"root.<xmlattr>.status_code\", 200);\n    tree.put(\"root.hostname\", config::nvhttp.sunshine_name);\n\n    tree.put(\"root.appversion\", VERSION);\n    tree.put(\"root.GfeVersion\", GFE_VERSION);\n    tree.put(\"root.SunshineVersion\", SUNSHINE_VERSION);\n    tree.put(\"root.uniqueid\", http::unique_id);\n    tree.put(\"root.HttpsPort\", net::map_port(PORT_HTTPS));\n    tree.put(\"root.ExternalPort\", net::map_port(PORT_HTTP));\n    tree.put(\"root.MaxLumaPixelsHEVC\", video::active_hevc_mode > 1 ? \"1869449984\" : \"0\");\n\n    // Only include the MAC address for requests sent from paired clients over HTTPS.\n    // For HTTP requests, use a placeholder MAC address that Moonlight knows to ignore.\n    if constexpr (std::is_same_v<SunshineHTTPS, T>) {\n      tree.put(\"root.mac\", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address())));\n    }\n    else {\n      tree.put(\"root.mac\", \"00:00:00:00:00:00\");\n    }\n\n    // Moonlight clients track LAN IPv6 addresses separately from LocalIP which is expected to\n    // always be an IPv4 address. If we return that same IPv6 address here, it will clobber the\n    // stored LAN IPv4 address. To avoid this, we need to return an IPv4 address in this field\n    // when we get a request over IPv6.\n    //\n    // HACK: We should return the IPv4 address of local interface here, but we don't currently\n    // have that implemented. For now, we will emulate the behavior of GFE+GS-IPv6-Forwarder,\n    // which returns 127.0.0.1 as LocalIP for IPv6 connections. Moonlight clients with IPv6\n    // support know to ignore this bogus address.\n    if (local_endpoint.address().is_v6() && !local_endpoint.address().to_v6().is_v4_mapped()) {\n      tree.put(\"root.LocalIP\", \"127.0.0.1\");\n    }\n    else {\n      tree.put(\"root.LocalIP\", net::addr_to_normalized_string(local_endpoint.address()));\n    }\n\n    uint32_t codec_mode_flags = SCM_H264;\n    if (video::last_encoder_probe_supported_yuv444_for_codec[0]) {\n      codec_mode_flags |= SCM_H264_HIGH8_444;\n    }\n    if (video::active_hevc_mode >= 2) {\n      codec_mode_flags |= SCM_HEVC;\n      if (video::last_encoder_probe_supported_yuv444_for_codec[1]) {\n        codec_mode_flags |= SCM_HEVC_REXT8_444;\n      }\n    }\n    if (video::active_hevc_mode >= 3) {\n      codec_mode_flags |= SCM_HEVC_MAIN10;\n      if (video::last_encoder_probe_supported_yuv444_for_codec[1]) {\n        codec_mode_flags |= SCM_HEVC_REXT10_444;\n      }\n    }\n    if (video::active_av1_mode >= 2) {\n      codec_mode_flags |= SCM_AV1_MAIN8;\n      if (video::last_encoder_probe_supported_yuv444_for_codec[2]) {\n        codec_mode_flags |= SCM_AV1_HIGH8_444;\n      }\n    }\n    if (video::active_av1_mode >= 3) {\n      codec_mode_flags |= SCM_AV1_MAIN10;\n      if (video::last_encoder_probe_supported_yuv444_for_codec[2]) {\n        codec_mode_flags |= SCM_AV1_HIGH10_444;\n      }\n    }\n    tree.put(\"root.ServerCodecModeSupport\", codec_mode_flags);\n\n    auto current_appid = proc::proc.running();\n    tree.put(\"root.PairStatus\", pair_status);\n    tree.put(\"root.currentgame\", current_appid);\n    tree.put(\"root.state\", current_appid > 0 ? \"SUNSHINE_SERVER_BUSY\" : \"SUNSHINE_SERVER_FREE\");\n    tree.put(\"root.appListEtag\", proc::proc.get_apps_etag());\n\n    // AI capability: inform client if AI proxy is available\n    tree.put(\"root.AiCapability\", confighttp::isAiEnabled() ? 1 : 0);\n\n    std::ostringstream data;\n\n    pt::write_xml(data, tree);\n    response->write(data.str());\n  }\n\n  nlohmann::json\n  get_all_clients() {\n    nlohmann::json named_cert_nodes = nlohmann::json::array();\n    client_t &client = client_root;\n    for (auto &named_cert : client.named_devices) {\n      nlohmann::json named_cert_node;\n      named_cert_node[\"name\"] = named_cert.name;\n      named_cert_node[\"uuid\"] = named_cert.uuid;\n      named_cert_nodes.push_back(named_cert_node);\n    }\n\n    return named_cert_nodes;\n  }\n\n  std::string\n  get_pair_name() {\n    return last_pair_name;\n  }\n\n  bool\n  set_preset_pin(const std::string &pin, const std::string &name, int timeout_seconds) {\n    if (pin.size() != 4 || !std::all_of(pin.begin(), pin.end(), ::isdigit)) {\n      BOOST_LOG(warning) << \"Invalid preset PIN: must be 4 digits\";\n      return false;\n    }\n\n    std::lock_guard lock { preset_pin_state.mutex };\n    preset_pin_state.pin = pin;\n    preset_pin_state.name = name;\n    preset_pin_state.expires_at = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_seconds);\n    preset_pin_state.paired = false;\n    BOOST_LOG(info) << \"Preset PIN set for QR pairing, expires in \" << timeout_seconds << \"s\";\n    return true;\n  }\n\n  std::string\n  consume_preset_pin() {\n    std::lock_guard lock { preset_pin_state.mutex };\n    if (preset_pin_state.pin.empty()) {\n      return std::string {};\n    }\n    if (std::chrono::steady_clock::now() > preset_pin_state.expires_at) {\n      preset_pin_state.pin.clear();\n      preset_pin_state.name.clear();\n      return std::string {};\n    }\n    std::string pin = std::move(preset_pin_state.pin);\n    preset_pin_state.pin.clear();\n    preset_pin_state.name.clear();\n    return pin;\n  }\n\n  void\n  clear_preset_pin() {\n    std::lock_guard lock { preset_pin_state.mutex };\n    preset_pin_state.pin.clear();\n    preset_pin_state.name.clear();\n    preset_pin_state.paired = true;\n  }\n\n  std::string\n  get_qr_pair_status() {\n    std::lock_guard lock { preset_pin_state.mutex };\n    if (preset_pin_state.paired) return \"paired\";\n    if (preset_pin_state.pin.empty()) return \"inactive\";\n    if (std::chrono::steady_clock::now() > preset_pin_state.expires_at) {\n      preset_pin_state.pin.clear();\n      preset_pin_state.name.clear();\n      return \"expired\";\n    }\n    return \"active\";\n  }\n\n  // Use keep-alive connection\n  void\n  applist(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    pt::ptree tree;\n\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      pt::write_xml(data, tree);\n      response->write(data.str());\n    });\n\n    auto &apps = tree.add_child(\"root\", pt::ptree {});\n\n    apps.put(\"<xmlattr>.status_code\", 200);\n\n    for (auto &proc : proc::proc.get_apps()) {\n      pt::ptree app;\n\n      app.put(\"IsHdrSupported\"s, video::active_hevc_mode == 3 ? 1 : 0);\n      app.put(\"AppTitle\"s, proc.name);\n      app.put(\"ID\"s, proc.id);\n\n      json json_cmds;\n\n      for (auto &cmd : proc.menu_cmds) {\n        json json_cmd;\n        json_cmd[\"id\"] = cmd.id;\n        json_cmd[\"name\"] = cmd.name;\n        // do_cmd and elevated intentionally omitted for security\n\n        json_cmds.push_back(json_cmd);\n      }\n\n      app.put(\"SuperCmds\"s, json_cmds.dump(4));\n\n      apps.push_back(std::make_pair(\"App\", std::move(app)));\n    }\n  }\n\n  void\n  changeBitrate(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    pt::ptree tree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_xml(data, tree);\n      response->write(data.str());\n      response->close_connection_after_response = true;\n    });\n\n    try {\n      auto args = request->parse_query_string();\n      auto bitrate_param = args.find(\"bitrate\");\n      auto clientname_param = args.find(\"clientname\");\n\n      if (bitrate_param == args.end()) {\n        tree.put(\"root.bitrate\", 0);\n        tree.put(\"root.<xmlattr>.status_code\", 400);\n        tree.put(\"root.<xmlattr>.status_message\", \"Missing bitrate parameter\");\n        return;\n      }\n\n      if (clientname_param == args.end()) {\n        tree.put(\"root.bitrate\", 0);\n        tree.put(\"root.<xmlattr>.status_code\", 400);\n        tree.put(\"root.<xmlattr>.status_message\", \"Missing clientname parameter\");\n        return;\n      }\n\n      int bitrate = std::stoi(bitrate_param->second);\n      std::string client_name = clientname_param->second;\n\n      if (bitrate <= 0 || bitrate > 800000) {\n        tree.put(\"root.bitrate\", 0);\n        tree.put(\"root.<xmlattr>.status_code\", 400);\n        tree.put(\"root.<xmlattr>.status_message\", \"Invalid bitrate value. Must be between 1 and 800000 Kbps\");\n        return;\n      }\n\n      video::dynamic_param_t param;\n      param.type = video::dynamic_param_type_e::BITRATE;\n      param.value.int_value = bitrate;\n      param.valid = true;\n\n      bool success = stream::session::change_dynamic_param_for_client(client_name, param);\n\n      if (success) {\n        tree.put(\"root.bitrate\", 1);\n        tree.put(\"root.<xmlattr>.status_code\", 200);\n        tree.put(\"root.<xmlattr>.bitrate\", bitrate);\n        tree.put(\"root.<xmlattr>.clientname\", client_name);\n        tree.put(\"root.<xmlattr>.status_message\", \"Bitrate change request sent to client session\");\n        BOOST_LOG(info) << \"NVHTTP API: Dynamic bitrate change requested for client '\"\n                        << client_name << \"': \" << bitrate << \" Kbps\";\n      }\n      else {\n        tree.put(\"root.bitrate\", 0);\n        tree.put(\"root.<xmlattr>.status_code\", 404);\n        tree.put(\"root.<xmlattr>.status_message\", \"No active streaming session found for client: \" + client_name);\n      }\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"ChangeBitrate: \"sv << e.what();\n      tree.put(\"root.bitrate\", 0);\n      tree.put(\"root.<xmlattr>.status_code\", 500);\n      tree.put(\"root.<xmlattr>.status_message\", e.what());\n    }\n  }\n\n  void\n  changeDynamicParam(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    pt::ptree tree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n      pt::write_xml(data, tree);\n      response->write(data.str());\n      response->close_connection_after_response = true;\n    });\n\n    auto set_error = [&tree](int code, const std::string &message) {\n      tree.put(\"root.success\", 0);\n      tree.put(\"root.<xmlattr>.status_code\", code);\n      tree.put(\"root.<xmlattr>.status_message\", message);\n    };\n\n    try {\n      auto args = request->parse_query_string();\n      auto param_type_param = args.find(\"type\");\n      auto param_value_param = args.find(\"value\");\n      auto clientname_param = args.find(\"clientname\");\n\n      if (param_type_param == args.end()) {\n        print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: miss type\");\n        set_error(400, \"Missing param_type parameter\");\n        return;\n      }\n\n      if (param_value_param == args.end()) {\n        print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: miss value\");\n        set_error(400, \"Missing param_value parameter\");\n        return;\n      }\n\n      if (clientname_param == args.end()) {\n        print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: miss clientname\");\n        set_error(400, \"Missing clientname parameter\");\n        return;\n      }\n\n      int param_type = std::stoi(param_type_param->second);\n      std::string param_value = param_value_param->second;\n      std::string client_name = clientname_param->second;\n\n      if (param_type < 0 || param_type >= static_cast<int>(video::dynamic_param_type_e::MAX_PARAM_TYPE)) {\n        print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: invalid type\");\n        set_error(400, \"Invalid param_type value\");\n        return;\n      }\n\n      video::dynamic_param_t param;\n      param.type = static_cast<video::dynamic_param_type_e>(param_type);\n      param.valid = true;\n\n      switch (param.type) {\n        case video::dynamic_param_type_e::RESOLUTION: {\n          print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: resolution change should be sent via control stream protocol, not HTTP API\");\n          set_error(400, \"Resolution change should be sent via control stream protocol, not HTTP API\");\n          return;\n        }\n        case video::dynamic_param_type_e::FPS: {\n          float fps = std::stof(param_value);\n          if (fps <= 0.0f || fps > 1000.0f) {\n            print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: invalid FPS value\");\n            set_error(400, \"Invalid FPS value. Must be between 0 and 1000\");\n            return;\n          }\n          param.value.float_value = fps;\n          break;\n        }\n        case video::dynamic_param_type_e::BITRATE: {\n          int bitrate = std::stoi(param_value);\n          if (bitrate <= 0 || bitrate > 800000) {\n            print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: invalid bitrate value\");\n            set_error(400, \"Invalid bitrate value. Must be between 1 and 800000 Kbps\");\n            return;\n          }\n          param.value.int_value = bitrate;\n          break;\n        }\n        case video::dynamic_param_type_e::QP: {\n          int qp = std::stoi(param_value);\n          if (qp < 0 || qp > 51) {\n            print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: invalid QP value\");\n            set_error(400, \"Invalid QP value. Must be between 0 and 51\");\n            return;\n          }\n          param.value.int_value = qp;\n          break;\n        }\n        case video::dynamic_param_type_e::FEC_PERCENTAGE: {\n          int fec = std::stoi(param_value);\n          if (fec < 0 || fec > 100) {\n            print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: invalid FEC percentage value\");\n            set_error(400, \"Invalid FEC percentage. Must be between 0 and 100\");\n            return;\n          }\n          param.value.int_value = fec;\n          break;\n        }\n        case video::dynamic_param_type_e::ADAPTIVE_QUANTIZATION: {\n          param.value.bool_value = (param_value == \"true\" || param_value == \"1\");\n          break;\n        }\n        case video::dynamic_param_type_e::MULTI_PASS: {\n          int multi_pass = std::stoi(param_value);\n          if (multi_pass < 0 || multi_pass > 2) {\n            print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: invalid multi-pass value\");\n            set_error(400, \"Invalid multi-pass value. Must be between 0 and 2\");\n            return;\n          }\n          param.value.int_value = multi_pass;\n          break;\n        }\n        case video::dynamic_param_type_e::VBV_BUFFER_SIZE: {\n          int vbv = std::stoi(param_value);\n          if (vbv <= 0) {\n            print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: invalid VBV buffer size value\");\n            set_error(400, \"Invalid VBV buffer size. Must be greater than 0\");\n            return;\n          }\n          param.value.int_value = vbv;\n          break;\n        }\n        default:\n          set_error(400, \"Unsupported parameter type\");\n          return;\n      }\n\n      bool success = stream::session::change_dynamic_param_for_client(client_name, param);\n\n      if (success) {\n        tree.put(\"root.success\", 1);\n        tree.put(\"root.<xmlattr>.status_code\", 200);\n        tree.put(\"root.<xmlattr>.param_type\", param_type);\n        tree.put(\"root.<xmlattr>.param_value\", param_value);\n        tree.put(\"root.<xmlattr>.clientname\", client_name);\n        tree.put(\"root.<xmlattr>.status_message\", \"Dynamic parameter change request sent to client session\");\n        BOOST_LOG(info) << \"NVHTTP API: Dynamic parameter change requested for client '\"\n                        << client_name << \"': type=\" << param_type << \", value=\" << param_value;\n      }\n      else {\n        print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: no active streaming session found for client\");\n        set_error(404, \"No active streaming session found for client: \" + client_name);\n      }\n    }\n    catch (std::exception &e) {\n      print_request_warning_ip<SunshineHTTPS>(request, \"Change dynamic param error: \"s + e.what());\n      set_error(500, e.what());\n    }\n  }\n\n  void\n  getSessionsInfo(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    // 限制只允许 localhost 访问\n    auto client_address = request->remote_endpoint().address();\n    auto address = net::addr_to_normalized_string(client_address);\n    auto ip_type = net::from_address(address);\n    if (ip_type != net::PC) {\n      json response_json;\n      response_json[\"success\"] = false;\n      response_json[\"status_code\"] = 403;\n      std::ostringstream msg_stream;\n      msg_stream << \"Access denied. Only localhost requests are allowed. Client IP: \" << client_address.to_string();\n      response_json[\"status_message\"] = msg_stream.str();\n\n      response->write(response_json.dump());\n      response->close_connection_after_response = true;\n      return;\n    }\n\n    try {\n      auto sessions_info = stream::session::get_all_sessions_info();\n\n      json response_json;\n      response_json[\"success\"] = true;\n      response_json[\"status_code\"] = 200;\n      response_json[\"status_message\"] = \"Success\";\n      response_json[\"total_sessions\"] = sessions_info.size();\n\n      json sessions_array = json::array();\n\n      for (const auto &session_info : sessions_info) {\n        json session_obj;\n        session_obj[\"client_name\"] = session_info.client_name;\n        session_obj[\"client_address\"] = session_info.client_address;\n        session_obj[\"state\"] = session_info.state;\n        session_obj[\"session_id\"] = session_info.session_id;\n        session_obj[\"width\"] = session_info.width;\n        session_obj[\"height\"] = session_info.height;\n        session_obj[\"fps\"] = session_info.fps;\n        session_obj[\"host_audio\"] = session_info.host_audio;\n        session_obj[\"enable_hdr\"] = session_info.enable_hdr;\n        session_obj[\"enable_mic\"] = session_info.enable_mic;\n        session_obj[\"app_name\"] = session_info.app_name;\n        session_obj[\"app_id\"] = session_info.app_id;\n\n        sessions_array.push_back(session_obj);\n      }\n\n      response_json[\"sessions\"] = sessions_array;\n\n      BOOST_LOG(info) << \"NVHTTP API: Session info requested from localhost, returned \" << sessions_info.size() << \" sessions\";\n\n      response->write(response_json.dump());\n      response->close_connection_after_response = true;\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(warning) << \"GetSessionsInfo: \"sv << e.what();\n\n      json error_json;\n      error_json[\"success\"] = false;\n      error_json[\"status_code\"] = 500;\n      error_json[\"status_message\"] = e.what();\n\n      response->write(error_json.dump());\n      response->close_connection_after_response = true;\n    }\n  }\n\n  // ============================================================================\n  // ABR (Adaptive Bitrate) API handlers\n  // ============================================================================\n\n  /**\n   * @brief Resolve the client name from the HTTPS request's source IP.\n   * Matches the connecting client's address against active streaming sessions.\n   * @return {client_name, bitrate, app_name} or empty client_name on failure.\n   */\n  struct resolved_client_t {\n    std::string name;\n    int bitrate = 0;\n    std::string app_name;\n  };\n\n  static resolved_client_t\n  resolve_client(req_https_t request) {\n    auto client_addr = net::addr_to_normalized_string(request->remote_endpoint().address());\n    try {\n      auto sessions_info = stream::session::get_all_sessions_info();\n      for (const auto &si : sessions_info) {\n        if (si.client_address == client_addr && si.state == \"RUNNING\") {\n          return { si.client_name, si.bitrate, si.app_name };\n        }\n      }\n    }\n    catch (...) {}\n    return {};\n  }\n\n  /**\n   * @brief GET /api/abr/capabilities — Query server ABR support.\n   */\n  void\n  getAbrCapabilities(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    auto caps = abr::get_capabilities();\n\n    json resp_json;\n    resp_json[\"supported\"] = caps.supported;\n    resp_json[\"version\"] = caps.version;\n    resp_json[\"features\"] = json::array({ \"llm_ai\", \"game_aware\", \"fallback_threshold\" });\n    resp_json[\"llmEnabled\"] = confighttp::isAiEnabled();\n\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"application/json\");\n    response->write(SimpleWeb::StatusCode::success_ok, resp_json.dump(), headers);\n  }\n\n  /**\n   * @brief POST /api/abr — Enable/disable ABR, set mode and bitrate range.\n   *\n   * Request body (JSON):\n   * {\n   *   \"enabled\": true,\n   *   \"minBitrate\": 2000,\n   *   \"maxBitrate\": 150000,\n   *   \"mode\": \"balanced\"\n   * }\n   *\n   * Client identity is resolved from the TLS connection's source IP.\n   */\n  void\n  configureAbr(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"application/json\");\n\n    try {\n      auto client = resolve_client(request);\n      if (client.name.empty()) {\n        json err;\n        err[\"success\"] = false;\n        err[\"error\"] = \"No active streaming session for this client\";\n        response->write(SimpleWeb::StatusCode::client_error_bad_request, err.dump(), headers);\n        return;\n      }\n\n      std::stringstream ss;\n      ss << request->content.rdbuf();\n      auto body = json::parse(ss.str());\n\n      bool enabled = body.value(\"enabled\", false);\n\n      if (!enabled) {\n        abr::disable(client.name);\n        json resp_json;\n        resp_json[\"success\"] = true;\n        resp_json[\"enabled\"] = false;\n        response->write(SimpleWeb::StatusCode::success_ok, resp_json.dump(), headers);\n        return;\n      }\n\n      // Parse and validate mode\n      std::string mode_str = body.value(\"mode\", \"balanced\");\n      abr::mode_e mode;\n      if (mode_str == \"balanced\") {\n        mode = abr::mode_e::BALANCED;\n      }\n      else if (mode_str == \"quality\") {\n        mode = abr::mode_e::QUALITY;\n      }\n      else if (mode_str == \"lowLatency\") {\n        mode = abr::mode_e::LOW_LATENCY;\n      }\n      else {\n        json err;\n        err[\"success\"] = false;\n        err[\"error\"] = \"Invalid mode: must be 'balanced', 'quality', or 'lowLatency'\";\n        response->write(SimpleWeb::StatusCode::client_error_bad_request, err.dump(), headers);\n        return;\n      }\n\n      abr::config_t cfg;\n      cfg.enabled = true;\n      cfg.min_bitrate_kbps = body.value(\"minBitrate\", 0);\n      cfg.max_bitrate_kbps = body.value(\"maxBitrate\", 0);\n      cfg.mode = mode;\n\n      // Validate bitrate range\n      if (cfg.min_bitrate_kbps < 0 || cfg.max_bitrate_kbps < 0) {\n        json err;\n        err[\"success\"] = false;\n        err[\"error\"] = \"minBitrate and maxBitrate must be non-negative\";\n        response->write(SimpleWeb::StatusCode::client_error_bad_request, err.dump(), headers);\n        return;\n      }\n      if (cfg.min_bitrate_kbps > 0 && cfg.max_bitrate_kbps > 0 && cfg.min_bitrate_kbps > cfg.max_bitrate_kbps) {\n        json err;\n        err[\"success\"] = false;\n        err[\"error\"] = \"minBitrate must not exceed maxBitrate\";\n        response->write(SimpleWeb::StatusCode::client_error_bad_request, err.dump(), headers);\n        return;\n      }\n\n      int initial_bitrate = client.bitrate > 0 ? client.bitrate\n                            : cfg.max_bitrate_kbps > 0 ? cfg.max_bitrate_kbps\n                            : 20000;\n\n      abr::enable(client.name, cfg, initial_bitrate, client.app_name);\n\n      json resp_json;\n      resp_json[\"success\"] = true;\n      resp_json[\"enabled\"] = true;\n      resp_json[\"mode\"] = mode_str;\n      resp_json[\"minBitrate\"] = cfg.min_bitrate_kbps;\n      resp_json[\"maxBitrate\"] = cfg.max_bitrate_kbps;\n      resp_json[\"initialBitrate\"] = initial_bitrate;\n      response->write(SimpleWeb::StatusCode::success_ok, resp_json.dump(), headers);\n    }\n    catch (const json::exception &e) {\n      BOOST_LOG(warning) << \"ABR configure: JSON parse error: \" << e.what();\n      json err;\n      err[\"success\"] = false;\n      err[\"error\"] = \"Invalid JSON body\";\n      response->write(SimpleWeb::StatusCode::client_error_bad_request, err.dump(), headers);\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(error) << \"ABR configure: \" << e.what();\n      json err;\n      err[\"success\"] = false;\n      err[\"error\"] = e.what();\n      response->write(SimpleWeb::StatusCode::server_error_internal_server_error, err.dump(), headers);\n    }\n  }\n\n  /**\n   * @brief POST /api/abr/feedback — Client sends network metrics, server returns bitrate decision.\n   *\n   * Request body (JSON):\n   * {\n   *   \"packetLoss\": 1.5,\n   *   \"rttMs\": 25.0,\n   *   \"decodeFps\": 59.8,\n   *   \"droppedFrames\": 2,\n   *   \"currentBitrate\": 15000\n   * }\n   *\n   * Response (JSON):\n   * {\n   *   \"newBitrate\": 14000,\n   *   \"reason\": \"moderate_drop: packet_loss=1.5%\"\n   * }\n   *\n   * Client identity is resolved from the TLS connection's source IP.\n   */\n  void\n  abrFeedback(resp_https_t response, req_https_t request) {\n    // No verbose logging for per-second feedback to avoid spam\n\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"application/json\");\n\n    try {\n      auto client_name = resolve_client(request).name;\n      if (client_name.empty()) {\n        json err;\n        err[\"error\"] = \"No active streaming session for this client\";\n        response->write(SimpleWeb::StatusCode::client_error_bad_request, err.dump(), headers);\n        return;\n      }\n\n      if (!abr::is_enabled(client_name)) {\n        json err;\n        err[\"error\"] = \"ABR not enabled for this client\";\n        response->write(SimpleWeb::StatusCode::client_error_bad_request, err.dump(), headers);\n        return;\n      }\n\n      std::stringstream ss;\n      ss << request->content.rdbuf();\n      auto body = json::parse(ss.str());\n\n      abr::network_feedback_t feedback;\n      feedback.packet_loss = body.value(\"packetLoss\", 0.0);\n      feedback.rtt_ms = body.value(\"rttMs\", 0.0);\n      feedback.decode_fps = body.value(\"decodeFps\", 0.0);\n      feedback.dropped_frames = body.value(\"droppedFrames\", 0);\n      feedback.current_bitrate_kbps = body.value(\"currentBitrate\", 0);\n\n      auto action = abr::process_feedback(client_name, feedback);\n\n      // If server decided on a new bitrate, apply it to the encoder\n      if (action.new_bitrate_kbps > 0) {\n        video::dynamic_param_t param;\n        param.type = video::dynamic_param_type_e::BITRATE;\n        param.value.int_value = action.new_bitrate_kbps;\n        param.valid = true;\n\n        stream::session::change_dynamic_param_for_client(client_name, param);\n      }\n\n      json resp_json;\n      if (action.new_bitrate_kbps > 0) {\n        resp_json[\"newBitrate\"] = action.new_bitrate_kbps;\n      }\n      resp_json[\"reason\"] = action.reason;\n      response->write(SimpleWeb::StatusCode::success_ok, resp_json.dump(), headers);\n    }\n    catch (const json::exception &e) {\n      json err;\n      err[\"error\"] = \"Invalid JSON body\";\n      response->write(SimpleWeb::StatusCode::client_error_bad_request, err.dump(), headers);\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(error) << \"ABR feedback: \" << e.what();\n      json err;\n      err[\"error\"] = e.what();\n      response->write(SimpleWeb::StatusCode::server_error_internal_server_error, err.dump(), headers);\n    }\n  }\n\n  void\n  launch(bool &host_audio, resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    print_request_ip<SunshineHTTPS>(request, \"Launch request\");\n\n    pt::ptree tree;\n    bool need_to_restore_display_state { false };\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      if (tree.empty()) {\n        BOOST_LOG(error) << EMPTY_PROPERTY_TREE_ERROR_MSG;\n      }\n\n      pt::write_xml(data, tree);\n      response->write(data.str());\n      response->close_connection_after_response = true;\n\n      if (need_to_restore_display_state) {\n        display_device::session_t::get().restore_state();\n      }\n    });\n\n    auto args = request->parse_query_string();\n    if (\n      args.find(\"rikey\"s) == std::end(args) ||\n      args.find(\"rikeyid\"s) == std::end(args) ||\n      args.find(\"localAudioPlayMode\"s) == std::end(args) ||\n      args.find(\"appid\"s) == std::end(args)) {\n      tree.put(\"root.resume\", 0);\n      tree.put(\"root.<xmlattr>.status_code\", 400);\n      tree.put(\"root.<xmlattr>.status_message\", \"Missing a required launch parameter\");\n\n      return;\n    }\n\n    auto appid = util::from_view(get_arg(args, \"appid\"));\n\n    auto current_appid = proc::proc.running();\n    if (current_appid > 0) {\n      tree.put(\"root.resume\", 0);\n      tree.put(\"root.<xmlattr>.status_code\", 400);\n      tree.put(\"root.<xmlattr>.status_message\", \"An app is already running on this host\");\n\n      return;\n    }\n\n    // Early validation of AppID to prevent starting VDD or other expensive operations\n    // if the requested app does not exist.\n    if (proc::proc.get_app_name(appid).empty()) {\n      tree.put(\"root.resume\", 0);\n      tree.put(\"root.<xmlattr>.status_code\", 404);\n      tree.put(\"root.<xmlattr>.status_message\", \"App not found\");\n      BOOST_LOG(error) << \"Launch couldn't find app with ID [\"sv << appid << ']';\n      return;\n    }\n\n    host_audio = util::from_view(get_arg(args, \"localAudioPlayMode\"));\n    const auto launch_session = make_launch_session(host_audio, args);\n\n    // 获取客户端证书UUID（稳定的客户端标识符）\n    std::string client_cert_uuid = get_client_cert_uuid_from_request(request);\n    if (!client_cert_uuid.empty()) {\n      launch_session->env[\"SUNSHINE_CLIENT_CERT_UUID\"] = client_cert_uuid;\n    }\n\n    if (rtsp_stream::session_count() == 0) {\n      // We want to prepare display only if there are no active sessions at\n      // the moment. This should to be done before probing encoders as it could\n      // change display device's state.\n      display_device::session_t::get().configure_display(config::video, *launch_session, true);\n\n      // The display should be restored by the fail guard in case something happens.\n      need_to_restore_display_state = true;\n\n      // Probe encoders again before streaming to ensure our chosen\n      // encoder matches the active GPU (which could have changed\n      // due to hotplugging, driver crash, primary monitor change,\n      // or any number of other factors).\n      if (video::probe_encoders()) {\n        tree.put(\"root.<xmlattr>.status_code\", 503);\n        tree.put(\"root.<xmlattr>.status_message\", \"Failed to initialize video capture/encoding. Is a display connected and turned on?\");\n        tree.put(\"root.gamesession\", 0);\n\n        return;\n      }\n    }\n\n    auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());\n    if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {\n      BOOST_LOG(error) << \"Rejecting client that cannot comply with mandatory encryption requirement\"sv;\n\n      tree.put(\"root.<xmlattr>.status_code\", 403);\n      tree.put(\"root.<xmlattr>.status_message\", \"Encryption is mandatory for this host but unsupported by the client\");\n      tree.put(\"root.gamesession\", 0);\n\n      return;\n    }\n\n    if (appid > 0) {\n      auto err = proc::proc.execute(appid, launch_session);\n      if (err) {\n        tree.put(\"root.<xmlattr>.status_code\", err);\n        tree.put(\"root.<xmlattr>.status_message\", \"Failed to start the specified application\");\n        tree.put(\"root.gamesession\", 0);\n\n        return;\n      }\n    }\n\n    tree.put(\"root.<xmlattr>.status_code\", 200);\n    tree.put(\"root.sessionUrl0\", launch_session->rtsp_url_scheme +\n                                   net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' +\n                                   std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT)));\n    tree.put(\"root.gamesession\", 1);\n\n    rtsp_stream::launch_session_raise(launch_session);\n\n    // Send webhook notification for successful launch\n    webhook::send_event_async(webhook::event_t {\n      .type = webhook::event_type_t::NV_APP_LAUNCH,\n      .alert_type = \"nv_app_launch\",\n      .timestamp = webhook::get_current_timestamp(),\n      .client_name = launch_session->client_name,\n      .client_ip = net::addr_to_normalized_string(request->remote_endpoint().address()),\n      .server_ip = net::addr_to_normalized_string(request->local_endpoint().address()),\n      .app_name = proc::proc.get_app_name(appid),\n      .app_id = appid,\n      .session_id = std::to_string(launch_session->id),\n      .extra_data = {\n        { \"resolution\", std::to_string(launch_session->width) + \"x\" + std::to_string(launch_session->height) },\n        { \"fps\", std::to_string(launch_session->fps) },\n        { \"host_audio\", launch_session->host_audio ? \"true\" : \"false\" } } });\n\n    // Stream was started successfully, we will restore the state when the app or session terminates\n    need_to_restore_display_state = false;\n  }\n\n  void\n  resume(bool &host_audio, resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    print_request_ip<SunshineHTTPS>(request, \"Resume request\");\n\n    // If the system is in Away Mode, exit it now since we're resuming a session\n    if (platf::is_away_mode_active()) {\n      BOOST_LOG(info) << \"Exiting Away Mode due to incoming resume request\"sv;\n      platf::exit_away_mode();\n    }\n\n    pt::ptree tree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      if (tree.empty()) {\n        BOOST_LOG(error) << EMPTY_PROPERTY_TREE_ERROR_MSG;\n      }\n\n      pt::write_xml(data, tree);\n      response->write(data.str());\n      response->close_connection_after_response = true;\n    });\n\n    auto current_appid = proc::proc.running();\n    if (current_appid == 0) {\n      tree.put(\"root.resume\", 0);\n      tree.put(\"root.<xmlattr>.status_code\", 503);\n      tree.put(\"root.<xmlattr>.status_message\", \"No running app to resume\");\n\n      return;\n    }\n\n    auto args = request->parse_query_string();\n    if (\n      args.find(\"rikey\"s) == std::end(args) ||\n      args.find(\"rikeyid\"s) == std::end(args)) {\n      tree.put(\"root.resume\", 0);\n      tree.put(\"root.<xmlattr>.status_code\", 400);\n      tree.put(\"root.<xmlattr>.status_message\", \"Missing a required resume parameter\");\n\n      return;\n    }\n\n    // Newer Moonlight clients send localAudioPlayMode on /resume too,\n    // so we should use it if it's present in the args and there are\n    // no active sessions we could be interfering with.\n    const bool no_active_sessions { rtsp_stream::session_count() == 0 };\n    if (no_active_sessions && args.find(\"localAudioPlayMode\"s) != std::end(args)) {\n      host_audio = util::from_view(get_arg(args, \"localAudioPlayMode\"));\n    }\n    const auto launch_session = make_launch_session(host_audio, args);\n\n    // Get client certificate UUID (stable client identifier) and store it in env\n    std::string client_cert_uuid = get_client_cert_uuid_from_request(request);\n    if (!client_cert_uuid.empty()) {\n      launch_session->env[\"SUNSHINE_CLIENT_CERT_UUID\"] = client_cert_uuid;\n    }\n\n    if (no_active_sessions) {\n      // We want to prepare display only if there are no active sessions at\n      // the moment. This should be done before probing encoders as it could\n      // change the active displays.\n      display_device::session_t::get().configure_display(config::video, *launch_session, false);\n\n      // Probe encoders again before streaming to ensure our chosen\n      // encoder matches the active GPU (which could have changed\n      // due to hotplugging, driver crash, primary monitor change,\n      // or any number of other factors).\n      if (video::probe_encoders()) {\n        tree.put(\"root.resume\", 0);\n        tree.put(\"root.<xmlattr>.status_code\", 503);\n        tree.put(\"root.<xmlattr>.status_message\", \"Failed to initialize video capture/encoding. Is a display connected and turned on?\");\n\n        return;\n      }\n    }\n    auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());\n    if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {\n      BOOST_LOG(error) << \"Rejecting client that cannot comply with mandatory encryption requirement\"sv;\n\n      tree.put(\"root.<xmlattr>.status_code\", 403);\n      tree.put(\"root.<xmlattr>.status_message\", \"Encryption is mandatory for this host but unsupported by the client\");\n      tree.put(\"root.gamesession\", 0);\n\n      return;\n    }\n\n    tree.put(\"root.<xmlattr>.status_code\", 200);\n    tree.put(\"root.sessionUrl0\", launch_session->rtsp_url_scheme +\n                                   net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' +\n                                   std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT)));\n    tree.put(\"root.resume\", 1);\n\n    rtsp_stream::launch_session_raise(launch_session);\n\n    // Send webhook notification for successful resume\n    webhook::send_event_async(webhook::event_t {\n      .type = webhook::event_type_t::NV_APP_RESUME,\n      .alert_type = \"nv_app_resume\",\n      .timestamp = webhook::get_current_timestamp(),\n      .client_name = launch_session->client_name,\n      .client_ip = net::addr_to_normalized_string(request->remote_endpoint().address()),\n      .server_ip = net::addr_to_normalized_string(request->local_endpoint().address()),\n      .app_name = proc::proc.get_app_name(proc::proc.running()),\n      .app_id = proc::proc.running(),\n      .session_id = std::to_string(launch_session->id),\n      .extra_data = {\n        { \"resolution\", std::to_string(launch_session->width) + \"x\" + std::to_string(launch_session->height) },\n        { \"fps\", std::to_string(launch_session->fps) },\n        { \"host_audio\", launch_session->host_audio ? \"true\" : \"false\" } } });\n  }\n\n  void\n  cancel(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    print_request_ip<SunshineHTTPS>(request, \"Cancel request\");\n\n    pt::ptree tree;\n    auto g = util::fail_guard([&]() {\n      std::ostringstream data;\n\n      pt::write_xml(data, tree);\n      response->write(data.str());\n      response->close_connection_after_response = true;\n    });\n\n    tree.put(\"root.cancel\", 1);\n    tree.put(\"root.<xmlattr>.status_code\", 200);\n\n    rtsp_stream::terminate_sessions();\n\n    if (proc::proc.running() > 0) {\n      proc::proc.terminate();\n    }\n\n    // The state needs to be restored regardless of whether \"proc::proc.terminate()\" was called or not.\n    display_device::session_t::get().restore_state();\n  }\n\n  void\n  sleep(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    bool success = true;\n    switch (config::nvhttp.sleep_mode) {\n      case config::SLEEP_MODE_HIBERNATE:\n        BOOST_LOG(info) << \"Sleep command: hibernate (S4)\"sv;\n        success = platf::system_hibernate();\n        break;\n      case config::SLEEP_MODE_AWAY:\n        BOOST_LOG(info) << \"Sleep command: away mode (display off)\"sv;\n        platf::enter_away_mode();\n        break;\n      case config::SLEEP_MODE_SUSPEND:\n      default:\n        BOOST_LOG(info) << \"Sleep command: suspend (S3)\"sv;\n        success = platf::system_sleep();\n        break;\n    }\n\n    if (!success) {\n      BOOST_LOG(warning) << \"Sleep command failed\"sv;\n    }\n\n    pt::ptree tree;\n    tree.put(\"root.pcsleep\", success ? 1 : 0);\n    tree.put(\"root.<xmlattr>.status_code\", success ? 200 : 500);\n\n    std::ostringstream data;\n\n    pt::write_xml(data, tree);\n    response->write(data.str());\n    response->close_connection_after_response = true;\n  }\n\n  void\n  execSuperCmd(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    auto args = request->parse_query_string();\n    auto cmdId = get_arg(args, \"cmdId\", \"\");\n    proc::proc.run_menu_cmd(cmdId);\n\n    pt::ptree tree;\n    tree.put(\"root.supercmd\", 1);\n    tree.put(\"root.<xmlattr>.status_code\", 200);\n\n    std::ostringstream data;\n\n    pt::write_xml(data, tree);\n    response->write(data.str());\n    response->close_connection_after_response = true;\n  }\n\n  // Use keep-alive connection\n  void\n  appasset(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    try {\n      auto args = request->parse_query_string();\n      auto app_image = proc::proc.get_app_image(util::from_view(get_arg(args, \"appid\")));\n\n      std::ifstream in(app_image, std::ios::binary);\n      SimpleWeb::CaseInsensitiveMultimap headers;\n      headers.emplace(\"Content-Type\", \"image/png\");\n      response->write(SimpleWeb::StatusCode::success_ok, in, headers);\n    } catch (const std::exception &e) {\n      print_request_warning_ip<SunshineHTTPS>(request, \"AppAsset error: \"s + e.what());\n      response->write(SimpleWeb::StatusCode::client_error_bad_request, \"Missing or invalid parameters\");\n    }\n  }\n\n  // Use keep-alive connection\n  void\n  get_displays(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    json response_json;\n    response_json[\"status_code\"] = 200;\n    response_json[\"status_message\"] = \"OK\";\n\n    try {\n      std::vector<std::string> display_names;\n\n#ifdef _WIN32\n      display_names = platf::display_names(platf::mem_type_e::dxgi);\n#elif defined(__linux__)\n      for (auto mem_type : { platf::mem_type_e::vaapi, platf::mem_type_e::cuda, platf::mem_type_e::system }) {\n        display_names = platf::display_names(mem_type);\n        if (!display_names.empty()) break;\n      }\n#elif defined(__APPLE__)\n      display_names = platf::display_names(platf::mem_type_e::videotoolbox);\n#else\n      display_names = platf::display_names(platf::mem_type_e::system);\n#endif\n\n      json displays_array = json::array();\n\n#ifdef _WIN32\n      // Build GDI name -> (device_id, friendly_name) mapping\n      std::unordered_map<std::string, std::pair<std::string, std::string>> display_info_map;\n      try {\n        for (const auto &[device_id, device_info] : display_device::enum_available_devices()) {\n          if (std::string gdi_name = display_device::get_display_name(device_id); !gdi_name.empty()) {\n            display_info_map[gdi_name] = { device_id, display_device::get_display_friendly_name(device_id) };\n          }\n        }\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(warning) << \"Failed to get display friendly names: \" << e.what();\n      }\n\n      for (size_t i = 0; i < display_names.size(); ++i) {\n        const auto &name = display_names[i];\n        auto it = display_info_map.find(name);\n        bool found = (it != display_info_map.end());\n        displays_array.push_back({ { \"index\", static_cast<int>(i) },\n          { \"display_name\", name },\n          { \"device_id\", found ? it->second.first : name },\n          { \"friendly_name\", (found && !it->second.second.empty()) ? it->second.second : name } });\n      }\n#else\n      for (size_t i = 0; i < display_names.size(); ++i) {\n        const auto &name = display_names[i];\n        displays_array.push_back({ { \"index\", static_cast<int>(i) },\n          { \"display_name\", name },\n          { \"device_id\", name },\n          { \"friendly_name\", name } });\n      }\n#endif\n\n      response_json[\"displays\"] = std::move(displays_array);\n      response_json[\"count\"] = static_cast<int>(display_names.size());\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(error) << \"Error getting display list: \" << e.what();\n      response_json[\"status_code\"] = 500;\n      response_json[\"status_message\"] = \"Internal server error\";\n      response_json[\"displays\"] = json::array();\n      response_json[\"count\"] = 0;\n    }\n\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    headers.emplace(\"Content-Type\", \"application/json\");\n    response->write(SimpleWeb::StatusCode::success_ok, response_json.dump(), headers);\n  }\n\n  void\n  rotate_display(resp_https_t response, req_https_t request) {\n    print_req<SunshineHTTPS>(request);\n\n    json response_json;\n    response_json[\"status_code\"] = 200;\n    response_json[\"status_message\"] = \"OK\";\n\n    auto send_response = [&](SimpleWeb::StatusCode status_code = SimpleWeb::StatusCode::success_ok) {\n      SimpleWeb::CaseInsensitiveMultimap headers;\n      headers.emplace(\"Content-Type\", \"application/json\");\n      response->write(status_code, response_json.dump(), headers);\n      response->close_connection_after_response = true;\n    };\n\n    try {\n      auto args = request->parse_query_string();\n      auto angle_param = args.find(\"angle\");\n\n      if (angle_param == args.end()) {\n        response_json[\"status_code\"] = 400;\n        response_json[\"status_message\"] = \"Missing angle parameter\";\n        response_json[\"success\"] = false;\n        BOOST_LOG(warning) << \"rotate_display: Missing angle parameter\";\n        send_response(SimpleWeb::StatusCode::client_error_bad_request);\n        return;\n      }\n\n      int angle = std::stoi(angle_param->second);\n\n      if (angle != 0 && angle != 90 && angle != 180 && angle != 270) {\n        response_json[\"status_code\"] = 400;\n        response_json[\"status_message\"] = \"Invalid angle value. Must be 0, 90, 180, or 270\";\n        response_json[\"success\"] = false;\n        BOOST_LOG(warning) << \"rotate_display: Invalid angle value: \" << angle;\n        send_response(SimpleWeb::StatusCode::client_error_bad_request);\n        return;\n      }\n\n      auto display_name_param = args.find(\"display_name\");\n      std::string display_name = display_name_param != args.end() ? display_name_param->second : \"\";\n\n      // URL-decode the display_name parameter\n      if (!display_name.empty()) {\n        std::string decoded_name;\n        decoded_name.reserve(display_name.size());\n        for (size_t i = 0; i < display_name.size(); ++i) {\n          if (display_name[i] == '%' && i + 2 < display_name.size()) {\n            int hex_val;\n            std::istringstream hex_stream(display_name.substr(i + 1, 2));\n            if (hex_stream >> std::hex >> hex_val) {\n              decoded_name += static_cast<char>(hex_val);\n              i += 2;\n              continue;\n            }\n          }\n          decoded_name += display_name[i];\n        }\n        display_name = std::move(decoded_name);\n      }\n\n      // 如果没有指定显示器名称，使用当前捕获的显示器\n      if (display_name.empty() && !config::video.output_name.empty()) {\n        display_name = display_device::get_display_name(config::video.output_name);\n        if (display_name.empty()) {\n          // 如果转换失败，尝试直接使用配置值（可能已经是显示器名称）\n          display_name = config::video.output_name;\n        }\n        BOOST_LOG(debug) << \"rotate_display: Using current capture display: \" << display_name << \" (from config: \" << config::video.output_name << \")\";\n      }\n\n      BOOST_LOG(info) << \"rotate_display: Requested angle=\" << angle << \", display_name=\" << (display_name.empty() ? \"(primary)\" : display_name);\n\n#ifdef _WIN32\n      bool success = display_device::w_utils::rotate_display(angle, display_name);\n      if (success) {\n        response_json[\"success\"] = true;\n        response_json[\"angle\"] = angle;\n        response_json[\"message\"] = \"Display rotation changed successfully\";\n        BOOST_LOG(info) << \"rotate_display: Display rotation changed to \" << angle << \" degrees\";\n      }\n      else {\n        response_json[\"status_code\"] = 500;\n        response_json[\"status_message\"] = \"Failed to change display rotation\";\n        response_json[\"success\"] = false;\n        BOOST_LOG(error) << \"rotate_display: Failed to change display rotation to \" << angle << \" degrees\";\n      }\n#else\n      response_json[\"status_code\"] = 501;\n      response_json[\"status_message\"] = \"Display rotation is not supported on this platform\";\n      response_json[\"success\"] = false;\n      BOOST_LOG(warning) << \"rotate_display: Display rotation is not supported on this platform\";\n#endif\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(error) << \"Error rotating display: \" << e.what();\n      response_json[\"status_code\"] = 500;\n      response_json[\"status_message\"] = \"Internal server error: \" + std::string(e.what());\n      response_json[\"success\"] = false;\n    }\n\n    send_response();\n  }\n\n  void\n  setup(const std::string &pkey, const std::string &cert) {\n    conf_intern.pkey = pkey;\n    conf_intern.servercert = cert;\n  }\n\n  void\n  start() {\n    auto shutdown_event = mail::man->event<bool>(mail::shutdown);\n\n    auto port_http = net::map_port(PORT_HTTP);\n    auto port_https = net::map_port(PORT_HTTPS);\n    auto address_family = net::af_from_enum_string(config::sunshine.address_family);\n\n    bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE];\n    bool close_verify_safe = config::sunshine.flags[config::flag::CLOSE_VERIFY_SAFE];\n    if (close_verify_safe) {\n      BOOST_LOG(warning) << \"SSL close safe verify: \" << close_verify_safe;\n    }\n\n    if (!clean_slate) {\n      load_state();\n    }\n\n    auto pkey = file_handler::read_file(config::nvhttp.pkey.c_str());\n    auto cert = file_handler::read_file(config::nvhttp.cert.c_str());\n    setup(pkey, cert);\n\n    // resume doesn't always get the parameter \"localAudioPlayMode\"\n    // launch will store it in host_audio\n    bool host_audio {};\n\n    https_server_t https_server { config::nvhttp.cert, config::nvhttp.pkey };\n    http_server_t http_server;\n\n    // Verify certificates after establishing connection\n    https_server.verify = [close_verify_safe](SSL *ssl) {\n      crypto::x509_t x509 {\n#if OPENSSL_VERSION_MAJOR >= 3\n        SSL_get1_peer_certificate(ssl)\n#else\n        SSL_get_peer_certificate(ssl)\n#endif\n      };\n      if (!x509) {\n        BOOST_LOG(info) << \"SSL client unknown -- denied\"sv;\n        return 0;\n      }\n\n      int verified = 0;\n\n      auto fg = util::fail_guard([&]() {\n        char subject_name[256];\n\n        X509_NAME_oneline(X509_get_subject_name(x509.get()), subject_name, sizeof(subject_name));\n\n        BOOST_LOG(debug) << subject_name << \" -- \"sv << (verified ? \"verified\"sv : \"denied\"sv);\n      });\n\n      // Verify the client certificate under shared lock.\n      // New certs are added directly in clientpairingsecret() under exclusive lock,\n      // so the verify callback is read-only and can run concurrently.\n      const char *err_str;\n      {\n        std::shared_lock<std::shared_mutex> sl(cert_chain_mutex);\n        if (!close_verify_safe) {\n          err_str = cert_chain.verify_safe(x509.get());  // default\n        }\n        else {\n          err_str = cert_chain.verify(x509.get());\n        }\n      }\n      if (err_str) {\n        BOOST_LOG(warning) << \"SSL Verification error :: \"sv << err_str;\n\n        return verified;\n      }\n\n      verified = 1;\n\n      return verified;\n    };\n\n    https_server.on_verify_failed = [](resp_https_t resp, req_https_t req) {\n      pt::ptree tree;\n      auto g = util::fail_guard([&]() {\n        std::ostringstream data;\n\n        pt::write_xml(data, tree);\n        resp->write(data.str());\n        resp->close_connection_after_response = true;\n      });\n\n      tree.put(\"root.<xmlattr>.status_code\"s, 401);\n      tree.put(\"root.<xmlattr>.query\"s, req->path);\n      tree.put(\"root.<xmlattr>.status_message\"s, \"The client is not authorized. Certificate verification failed.\"s);\n    };\n\n    https_server.default_resource[\"GET\"] = not_found<SunshineHTTPS>;\n    https_server.resource[\"^/serverinfo$\"][\"GET\"] = serverinfo<SunshineHTTPS>;\n    https_server.resource[\"^/pair$\"][\"GET\"] = [](auto resp, auto req) { pair<SunshineHTTPS>(resp, req); };\n    https_server.resource[\"^/applist$\"][\"GET\"] = applist;\n    https_server.resource[\"^/appasset$\"][\"GET\"] = appasset;\n    https_server.resource[\"^/displays$\"][\"GET\"] = get_displays;\n    https_server.resource[\"^/rotate-display$\"][\"GET\"] = rotate_display;\n    https_server.resource[\"^/launch$\"][\"GET\"] = [&host_audio](auto resp, auto req) { launch(host_audio, resp, req); };\n    https_server.resource[\"^/resume$\"][\"GET\"] = [&host_audio](auto resp, auto req) { resume(host_audio, resp, req); };\n    https_server.resource[\"^/cancel$\"][\"GET\"] = cancel;\n    https_server.resource[\"^/pcsleep$\"][\"GET\"] = sleep;\n    https_server.resource[\"^/supercmd$\"][\"GET\"] = execSuperCmd;\n    https_server.resource[\"^/bitrate$\"][\"GET\"] = changeBitrate;\n    https_server.resource[\"^/stream/settings$\"][\"GET\"] = changeDynamicParam;\n    https_server.resource[\"^/sessions$\"][\"GET\"] = getSessionsInfo;\n\n    // ABR (Adaptive Bitrate) API routes - client-facing with cert auth\n    https_server.resource[\"^/api/abr/capabilities$\"][\"GET\"] = getAbrCapabilities;\n    https_server.resource[\"^/api/abr$\"][\"POST\"] = configureAbr;\n    https_server.resource[\"^/api/abr/feedback$\"][\"POST\"] = abrFeedback;\n\n    // AI LLM proxy route — uses client cert auth from pairing\n    https_server.resource[\"^/ai/completions$\"][\"POST\"] = [](resp_https_t response, req_https_t request) {\n      std::stringstream ss;\n      ss << request->content.rdbuf();\n      std::string requestBody = ss.str();\n\n      bool isStream = false;\n      try {\n        auto reqJson = nlohmann::json::parse(requestBody);\n        isStream = reqJson.value(\"stream\", false);\n      } catch (...) {}\n\n      if (isStream) {\n        bool headerSent = false;\n        auto result = confighttp::processAiChatStream(requestBody, [&](const char *data, size_t len) {\n          if (!headerSent) {\n            *response << \"HTTP/1.1 200 OK\\r\\n\";\n            *response << \"Content-Type: text/event-stream\\r\\n\";\n            *response << \"Cache-Control: no-cache\\r\\n\";\n            *response << \"Connection: keep-alive\\r\\n\";\n            *response << \"\\r\\n\";\n            response->send();\n            headerSent = true;\n          }\n          std::string chunk(data, len);\n          *response << chunk;\n          response->send();\n        });\n\n        if (result.httpCode != 200 && !headerSent) {\n          std::string errorResp = result.body;\n          *response << \"HTTP/1.1 502 Bad Gateway\\r\\n\";\n          *response << \"Content-Type: application/json\\r\\n\";\n          *response << \"Content-Length: \" << errorResp.size() << \"\\r\\n\";\n          *response << \"\\r\\n\";\n          *response << errorResp;\n          response->send();\n        }\n      } else {\n        auto result = confighttp::processAiChat(requestBody);\n\n        SimpleWeb::CaseInsensitiveMultimap headers;\n        headers.emplace(\"Content-Type\", result.contentType);\n\n        auto statusCode = SimpleWeb::StatusCode::success_ok;\n        if (result.httpCode == 403) statusCode = SimpleWeb::StatusCode::client_error_forbidden;\n        else if (result.httpCode == 400) statusCode = SimpleWeb::StatusCode::client_error_bad_request;\n        else if (result.httpCode >= 500) statusCode = SimpleWeb::StatusCode::server_error_bad_gateway;\n\n        response->write(statusCode, result.body, headers);\n      }\n    };\n\n    https_server.config.reuse_address = true;\n    https_server.config.address = net::get_bind_address(address_family);\n    https_server.config.port = port_https;\n\n    http_server.default_resource[\"GET\"] = not_found<SimpleWeb::HTTP>;\n    http_server.resource[\"^/serverinfo$\"][\"GET\"] = serverinfo<SimpleWeb::HTTP>;\n    http_server.resource[\"^/pair$\"][\"GET\"] = [](auto resp, auto req) { pair<SimpleWeb::HTTP>(resp, req); };\n\n    http_server.config.reuse_address = true;\n    http_server.config.address = net::get_bind_address(address_family);\n    http_server.config.port = port_http;\n\n    auto accept_and_run_https = [&](nvhttp::https_server_t *server) {\n      try {\n        BOOST_LOG(info) << \"Starting nvhttps server on port [\"sv << server->config.port << \"]\";\n        server->start();\n      }\n      catch (boost::system::system_error &err) {\n        // It's possible the exception gets thrown after calling server->stop() from a different thread\n        if (shutdown_event->peek()) {\n          return;\n        }\n        BOOST_LOG(fatal) << \"Couldn't start nvhttps server on ports [\"sv << server->config.port << \"]: \"sv << err.what();\n        shutdown_event->raise(true);\n        return;\n      }\n    };\n\n    auto accept_and_run_http = [&](nvhttp::http_server_t *server) {\n      try {\n        BOOST_LOG(info) << \"Starting nvhttp server on port [\"sv << server->config.port << \"]\";\n        server->start();\n      }\n      catch (boost::system::system_error &err) {\n        // It's possible the exception gets thrown after calling server->stop() from a different thread\n        if (shutdown_event->peek()) {\n          return;\n        }\n\n        BOOST_LOG(fatal) << \"Couldn't start nvhttp server on ports [\"sv << server->config.port << \"]: \"sv << err.what();\n        shutdown_event->raise(true);\n        return;\n      }\n    };\n    std::thread ssl { accept_and_run_https, &https_server };\n    std::thread tcp { accept_and_run_http, &http_server };\n\n    // Wait for any event\n    shutdown_event->view();\n\n    https_server.stop();\n    http_server.stop();\n\n    ssl.join();\n    tcp.join();\n  }\n\n  void\n  erase_all_clients() {\n    client_t client;\n    client_root = client;\n    {\n      std::unique_lock<std::shared_mutex> ul(cert_chain_mutex);\n      cert_chain.clear();\n    }\n    save_state();\n  }\n\n  int\n  unpair_client(std::string uuid) {\n    bool removed = false;\n    client_t &client = client_root;\n    for (auto it = client.named_devices.begin(); it != client.named_devices.end();) {\n      if ((*it).uuid == uuid) {\n        it = client.named_devices.erase(it);\n        removed = true;\n      }\n      else {\n        ++it;\n      }\n    }\n\n    save_state();\n    load_state();\n    return removed;\n  }\n\n  bool\n  rename_client(const std::string &uuid, const std::string &new_name) {\n    client_t &client = client_root;\n    for (auto &named_cert : client.named_devices) {\n      if (named_cert.uuid == uuid) {\n        named_cert.name = new_name;\n        save_state();\n        return true;\n      }\n    }\n    return false;\n  }\n}  // namespace nvhttp\n"
  },
  {
    "path": "src/nvhttp.h",
    "content": "/**\n * @file src/nvhttp.h\n * @brief Declarations for the nvhttp (GameStream) server.\n */\n// macros\n#pragma once\n\n// standard includes\n#include <string>\n\n// lib includes\n#include <boost/property_tree/ptree.hpp>\n#include <nlohmann/json.hpp>\n#include <Simple-Web-Server/server_https.hpp>\n// local includes\n#include \"crypto.h\"\n#include \"thread_safe.h\"\n#include \"version.h\"\n\n/**\n * @brief Contains all the functions and variables related to the nvhttp (GameStream) server.\n */\nnamespace nvhttp {\n\n  /**\n   * @brief The protocol version.\n   * @details The version of the GameStream protocol we are mocking.\n   * @note The negative 4th number indicates to Moonlight that this is Sunshine.\n   */\n  constexpr auto VERSION = \"7.1.431.-1\";\n\n  /**\n   * @brief The GFE version we are replicating.\n   */\n  constexpr auto GFE_VERSION = \"3.23.0.74\";\n\n  /**\n   * @brief The Sunshine version we are replicating.\n   */\n  constexpr auto SUNSHINE_VERSION = PROJECT_NAME \" \" PROJECT_VER;\n\n  /**\n   * @brief The HTTP port, as a difference from the config port.\n   */\n  constexpr auto PORT_HTTP = 0;\n\n  /**\n   * @brief The HTTPS port, as a difference from the config port.\n   */\n  constexpr auto PORT_HTTPS = -5;\n\n  /**\n   * @brief Start the nvhttp server.\n   * @examples\n   * nvhttp::start();\n   * @examples_end\n   */\n  void\n  start();\n  /**\n   * @brief Setup the nvhttp server.\n   * @param pkey\n   * @param cert\n   */\n  void setup(const std::string &pkey, const std::string &cert);\n\n  class SunshineHTTPS: public SimpleWeb::HTTPS {\n  public:\n    SunshineHTTPS(boost::asio::io_context &io_context, boost::asio::ssl::context &ctx):\n        SimpleWeb::HTTPS(io_context, ctx) {\n    }\n\n    virtual ~SunshineHTTPS() {\n      // Gracefully shutdown the TLS connection\n      SimpleWeb::error_code ec;\n      shutdown(ec);\n    }\n  };\n\n  enum class PAIR_PHASE {\n    NONE,  ///< Sunshine is not in a pairing phase\n    GETSERVERCERT,  ///< Sunshine is in the get server certificate phase\n    CLIENTCHALLENGE,  ///< Sunshine is in the client challenge phase\n    SERVERCHALLENGERESP,  ///< Sunshine is in the server challenge response phase\n    CLIENTPAIRINGSECRET  ///< Sunshine is in the client pairing secret phase\n  };\n\n  struct pair_session_t {\n    struct {\n      std::string uniqueID = {};\n      std::string cert = {};\n      std::string name = {};\n    } client;\n\n    std::unique_ptr<crypto::aes_t> cipher_key = {};\n    std::vector<uint8_t> clienthash = {};\n\n    std::string serversecret = {};\n    std::string serverchallenge = {};\n\n    struct {\n      util::Either<\n        std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>,\n        std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>>\n        response;\n      std::string salt = {};\n    } async_insert_pin;\n\n    /**\n     * @brief used as a security measure to prevent out of order calls\n     */\n    PAIR_PHASE last_phase = PAIR_PHASE::NONE;\n  };\n\n  /**\n   * @brief removes the temporary pairing session\n   * @param sess\n   */\n  void remove_session(const pair_session_t &sess);\n\n  /**\n   * @brief Pair, phase 1\n   *\n   * Moonlight will send a salt and client certificate, we'll also need the user provided pin.\n   *\n   * PIN and SALT will be used to derive a shared AES key that needs to be stored\n   * in order to be used to decrypt_symmetric in the next phases.\n   *\n   * At this stage we only have to send back our public certificate.\n   */\n  void getservercert(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &pin, const std::string &client_name);\n\n  /**\n   * @brief Pair, phase 2\n   *\n   * Using the AES key that we generated in phase 1 we have to decrypt the client challenge,\n   *\n   * We generate a SHA256 hash with the following:\n   *  - Decrypted challenge\n   *  - Server certificate signature\n   *  - Server secret: a randomly generated secret\n   *\n   * The hash + server_challenge will then be AES encrypted and sent as the `challengeresponse` in the returned XML\n   */\n  void clientchallenge(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &challenge);\n\n  /**\n   * @brief Pair, phase 3\n   *\n   * Moonlight will send back a `serverchallengeresp`: an AES encrypted client hash,\n   * we have to send back the `pairingsecret`:\n   * using our private key we have to sign the certificate_signature + server_secret (generated in phase 2)\n   */\n  void serverchallengeresp(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &encrypted_response);\n\n  /**\n   * @brief Pair, phase 4 (final)\n   *\n   * We now have to use everything we exchanged before in order to verify and finally pair the clients\n   *\n   * We'll check the client_hash obtained at phase 3, it should contain the following:\n   *   - The original server_challenge\n   *   - The signature of the X509 client_cert\n   *   - The unencrypted client_pairing_secret\n   * We'll check that SHA256(server_challenge + client_public_cert_signature + client_secret) == client_hash\n   *\n   * Then using the client certificate public key we should be able to verify that\n   * the client secret has been signed by Moonlight\n   */\n  void clientpairingsecret(pair_session_t &sess, std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, boost::property_tree::ptree &tree, const std::string &client_pairing_secret);\n\n  /**\n   * @brief Compare the user supplied pin to the Moonlight pin.\n   * @param pin The user supplied pin.\n   * @param name The user supplied name.\n   * @return `true` if the pin is correct, `false` otherwise.\n   * @examples\n   * bool pin_status = nvhttp::pin(\"1234\", \"laptop\");\n   * @examples_end\n   */\n  bool\n  pin(std::string pin, std::string name);\n\n  /**\n   * @brief Remove single client.\n   * @examples\n   * nvhttp::unpair_client(\"4D7BB2DD-5704-A405-B41C-891A022932E1\");\n   * @examples_end\n   */\n  int\n  unpair_client(std::string uniqueid);\n\n  /**\n   * @brief Rename a paired client.\n   * @param uuid The unique ID of the client to rename.\n   * @param new_name The new display name for the client.\n   * @return True if the client was found and renamed.\n   */\n  bool\n  rename_client(const std::string &uuid, const std::string &new_name);\n\n  /**\n   * @brief Get all paired clients.\n   * @return The list of all paired clients.\n   * @examples\n   * nlohmann::json clients = nvhttp::get_all_clients();\n   * @examples_end\n   */\n  nlohmann::json get_all_clients();;\n\n  std::string\n  get_pair_name();\n\n  /**\n   * @brief Set a preset PIN for QR code pairing.\n   * @param pin The PIN to preset (4 digits).\n   * @param name The client name to use for pairing.\n   * @param timeout_seconds How long the preset PIN is valid (default 120s).\n   * @return True if the preset PIN was set successfully.\n   */\n  bool\n  set_preset_pin(const std::string &pin, const std::string &name, int timeout_seconds = 120);\n\n  /**\n   * @brief Atomically get and clear the preset PIN if it's still valid.\n   * @return The preset PIN string, or empty string if expired/not set.\n   */\n  std::string\n  consume_preset_pin();\n\n  /**\n   * @brief Clear any preset PIN (for cancel action).\n   */\n  void\n  clear_preset_pin();\n\n  /**\n   * @brief Get the current QR pair status.\n   * @return \"active\", \"paired\", \"expired\", or \"inactive\".\n   */\n  std::string\n  get_qr_pair_status();\n\n  /**\n   * @brief Remove all paired clients.\n   * @examples\n   * nvhttp::erase_all_clients();\n   * @examples_end\n   */\n  void\n  erase_all_clients();\n}  // namespace nvhttp\n"
  },
  {
    "path": "src/platform/common.h",
    "content": "/**\n * @file src/platform/common.h\n * @brief Declarations for common platform specific utilities.\n */\n#pragma once\n\n// standard includes\n#include <bitset>\n#include <filesystem>\n#include <functional>\n#include <mutex>\n#include <string>\n\n// lib includes\n#include <boost/core/noncopyable.hpp>\n#ifndef _WIN32\n  #include <boost/asio.hpp>\n  #include <boost/process/v1.hpp>\n#endif\n\n// local includes\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/thread_safe.h\"\n#include \"src/utility.h\"\n#include \"src/video_colorspace.h\"\n\nextern \"C\" {\n#include <moonlight-common-c/src/Limelight.h>\n}\n\nusing namespace std::literals;\n\nstruct sockaddr;\nstruct AVFrame;\nstruct AVBufferRef;\nstruct AVHWFramesContext;\nstruct AVCodecContext;\nstruct AVDictionary;\n\n#ifdef _WIN32\n// Forward declarations of boost classes to avoid having to include boost headers\n// here, which results in issues with Windows.h and WinSock2.h include order.\nnamespace boost {\n  namespace asio {\n    namespace ip {\n      class address;\n    }  // namespace ip\n  }  // namespace asio\n}  // namespace boost\n#endif\nnamespace video {\n  struct config_t;\n}  // namespace video\n\nnamespace nvenc {\n  class nvenc_encoder;\n}\n\nnamespace amf {\n  class amf_encoder;\n}\n\nnamespace platf {\n  // Limited by bits in activeGamepadMask\n  constexpr auto MAX_GAMEPADS = 16;\n\n  constexpr std::uint32_t DPAD_UP = 0x0001;\n  constexpr std::uint32_t DPAD_DOWN = 0x0002;\n  constexpr std::uint32_t DPAD_LEFT = 0x0004;\n  constexpr std::uint32_t DPAD_RIGHT = 0x0008;\n  constexpr std::uint32_t START = 0x0010;\n  constexpr std::uint32_t BACK = 0x0020;\n  constexpr std::uint32_t LEFT_STICK = 0x0040;\n  constexpr std::uint32_t RIGHT_STICK = 0x0080;\n  constexpr std::uint32_t LEFT_BUTTON = 0x0100;\n  constexpr std::uint32_t RIGHT_BUTTON = 0x0200;\n  constexpr std::uint32_t HOME = 0x0400;\n  constexpr std::uint32_t A = 0x1000;\n  constexpr std::uint32_t B = 0x2000;\n  constexpr std::uint32_t X = 0x4000;\n  constexpr std::uint32_t Y = 0x8000;\n  constexpr std::uint32_t PADDLE1 = 0x010000;\n  constexpr std::uint32_t PADDLE2 = 0x020000;\n  constexpr std::uint32_t PADDLE3 = 0x040000;\n  constexpr std::uint32_t PADDLE4 = 0x080000;\n  constexpr std::uint32_t TOUCHPAD_BUTTON = 0x100000;\n  constexpr std::uint32_t MISC_BUTTON = 0x200000;\n\n  struct supported_gamepad_t {\n    std::string name;\n    bool is_enabled;\n    std::string reason_disabled;\n  };\n\n  enum class gamepad_feedback_e {\n    rumble,  ///< Rumble\n    rumble_triggers,  ///< Rumble triggers\n    set_motion_event_state,  ///< Set motion event state\n    set_rgb_led,  ///< Set RGB LED\n    set_adaptive_triggers,  ///< Set adaptive triggers\n  };\n\n  struct gamepad_feedback_msg_t {\n    static gamepad_feedback_msg_t\n    make_rumble(std::uint16_t id, std::uint16_t lowfreq, std::uint16_t highfreq) {\n      gamepad_feedback_msg_t msg;\n      msg.type = gamepad_feedback_e::rumble;\n      msg.id = id;\n      msg.data.rumble = { lowfreq, highfreq };\n      return msg;\n    }\n\n    static gamepad_feedback_msg_t\n    make_rumble_triggers(std::uint16_t id, std::uint16_t left, std::uint16_t right) {\n      gamepad_feedback_msg_t msg;\n      msg.type = gamepad_feedback_e::rumble_triggers;\n      msg.id = id;\n      msg.data.rumble_triggers = { left, right };\n      return msg;\n    }\n\n    static gamepad_feedback_msg_t\n    make_motion_event_state(std::uint16_t id, std::uint8_t motion_type, std::uint16_t report_rate) {\n      gamepad_feedback_msg_t msg;\n      msg.type = gamepad_feedback_e::set_motion_event_state;\n      msg.id = id;\n      msg.data.motion_event_state.motion_type = motion_type;\n      msg.data.motion_event_state.report_rate = report_rate;\n      return msg;\n    }\n\n    static gamepad_feedback_msg_t\n    make_rgb_led(std::uint16_t id, std::uint8_t r, std::uint8_t g, std::uint8_t b) {\n      gamepad_feedback_msg_t msg;\n      msg.type = gamepad_feedback_e::set_rgb_led;\n      msg.id = id;\n      msg.data.rgb_led = { r, g, b };\n      return msg;\n    }\n\n    static gamepad_feedback_msg_t\n    make_adaptive_triggers(std::uint16_t id, uint8_t event_flags, uint8_t type_left, uint8_t type_right, const std::array<uint8_t, 10> &left, const std::array<uint8_t, 10> &right) {\n      gamepad_feedback_msg_t msg;\n      msg.type = gamepad_feedback_e::set_adaptive_triggers;\n      msg.id = id;\n      msg.data.adaptive_triggers = { .event_flags = event_flags, .type_left = type_left, .type_right = type_right, .left = left, .right = right };\n      return msg;\n    }\n\n    gamepad_feedback_e type;\n    std::uint16_t id;\n\n    union {\n      struct {\n        std::uint16_t lowfreq;\n        std::uint16_t highfreq;\n      } rumble;\n\n      struct {\n        std::uint16_t left_trigger;\n        std::uint16_t right_trigger;\n      } rumble_triggers;\n\n      struct {\n        std::uint16_t report_rate;\n        std::uint8_t motion_type;\n      } motion_event_state;\n\n      struct {\n        std::uint8_t r;\n        std::uint8_t g;\n        std::uint8_t b;\n      } rgb_led;\n\n      struct {\n        uint16_t controllerNumber;\n        uint8_t event_flags;\n        uint8_t type_left;\n        uint8_t type_right;\n        std::array<uint8_t, 10> left;\n        std::array<uint8_t, 10> right;\n      } adaptive_triggers;\n    } data;\n  };\n\n  using feedback_queue_t = safe::mail_raw_t::queue_t<gamepad_feedback_msg_t>;\n\n  namespace speaker {\n    enum speaker_e {\n      FRONT_LEFT,  ///< Front left\n      FRONT_RIGHT,  ///< Front right\n      FRONT_CENTER,  ///< Front center\n      LOW_FREQUENCY,  ///< Low frequency\n      BACK_LEFT,  ///< Back left\n      BACK_RIGHT,  ///< Back right\n      SIDE_LEFT,  ///< Side left\n      SIDE_RIGHT,  ///< Side right\n      TOP_FRONT_LEFT,  ///< Top front left\n      TOP_FRONT_RIGHT,  ///< Top front right\n      TOP_BACK_LEFT,  ///< Top back left\n      TOP_BACK_RIGHT,  ///< Top back right\n      MAX_SPEAKERS,  ///< Maximum number of speakers\n    };\n\n    constexpr std::uint8_t map_stereo[] {\n      FRONT_LEFT,\n      FRONT_RIGHT\n    };\n    constexpr std::uint8_t map_surround51[] {\n      FRONT_LEFT,\n      FRONT_RIGHT,\n      FRONT_CENTER,\n      LOW_FREQUENCY,\n      BACK_LEFT,\n      BACK_RIGHT,\n    };\n    constexpr std::uint8_t map_surround71[] {\n      FRONT_LEFT,\n      FRONT_RIGHT,\n      FRONT_CENTER,\n      LOW_FREQUENCY,\n      BACK_LEFT,\n      BACK_RIGHT,\n      SIDE_LEFT,\n      SIDE_RIGHT,\n    };\n\n    constexpr std::uint8_t map_surround714[] {\n      FRONT_LEFT,\n      FRONT_RIGHT,\n      FRONT_CENTER,\n      LOW_FREQUENCY,\n      BACK_LEFT,\n      BACK_RIGHT,\n      SIDE_LEFT,\n      SIDE_RIGHT,\n      TOP_FRONT_LEFT,\n      TOP_FRONT_RIGHT,\n      TOP_BACK_LEFT,\n      TOP_BACK_RIGHT,\n    };\n  }  // namespace speaker\n\n  enum class mem_type_e {\n    system,  ///< System memory\n    vaapi,  ///< VAAPI\n    dxgi,  ///< DXGI\n    cuda,  ///< CUDA\n    videotoolbox,  ///< VideoToolbox\n    vulkan,  ///< Vulkan\n    unknown  ///< Unknown\n  };\n\n  enum class pix_fmt_e {\n    yuv420p,  ///< YUV 4:2:0\n    yuv420p10,  ///< YUV 4:2:0 10-bit\n    nv12,  ///< NV12\n    p010,  ///< P010\n    ayuv,  ///< AYUV\n    yuv444p16,  ///< Planar 10-bit (shifted to 16-bit) YUV 4:4:4\n    y410,  ///< Y410\n    unknown  ///< Unknown\n  };\n\n  inline std::string_view\n  from_pix_fmt(pix_fmt_e pix_fmt) {\n    using namespace std::literals;\n#define _CONVERT(x)  \\\n  case pix_fmt_e::x: \\\n    return #x##sv\n    switch (pix_fmt) {\n      _CONVERT(yuv420p);\n      _CONVERT(yuv420p10);\n      _CONVERT(nv12);\n      _CONVERT(p010);\n      _CONVERT(ayuv);\n      _CONVERT(yuv444p16);\n      _CONVERT(y410);\n      _CONVERT(unknown);\n    }\n#undef _CONVERT\n\n    return \"unknown\"sv;\n  }\n\n  // Dimensions for touchscreen input\n  struct touch_port_t {\n    int offset_x, offset_y;\n    int width, height;\n  };\n\n  // These values must match Limelight-internal.h's SS_FF_* constants!\n  namespace platform_caps {\n    typedef uint32_t caps_t;\n\n    constexpr caps_t pen_touch = 0x01;  // Pen and touch events\n    constexpr caps_t controller_touch = 0x02;  // Controller touch events\n  };  // namespace platform_caps\n\n  struct gamepad_state_t {\n    std::uint32_t buttonFlags;\n    std::uint8_t lt;\n    std::uint8_t rt;\n    std::int16_t lsX;\n    std::int16_t lsY;\n    std::int16_t rsX;\n    std::int16_t rsY;\n  };\n\n  struct gamepad_id_t {\n    // The global index is used when looking up gamepads in the platform's\n    // gamepad array. It identifies gamepads uniquely among all clients.\n    int globalIndex;\n\n    // The client-relative index is the controller number as reported by the\n    // client. It must be used when communicating back to the client via\n    // the input feedback queue.\n    std::uint8_t clientRelativeIndex;\n  };\n\n  struct gamepad_arrival_t {\n    std::uint8_t type;\n    std::uint16_t capabilities;\n    std::uint32_t supportedButtons;\n  };\n\n  struct gamepad_touch_t {\n    gamepad_id_t id;\n    std::uint8_t eventType;\n    std::uint32_t pointerId;\n    float x;\n    float y;\n    float pressure;\n  };\n\n  struct gamepad_motion_t {\n    gamepad_id_t id;\n    std::uint8_t motionType;\n\n    // Accel: m/s^2\n    // Gyro: deg/s\n    float x;\n    float y;\n    float z;\n  };\n\n  struct gamepad_battery_t {\n    gamepad_id_t id;\n    std::uint8_t state;\n    std::uint8_t percentage;\n  };\n\n  struct touch_input_t {\n    std::uint8_t eventType;\n    std::uint16_t rotation;  // Degrees (0..360) or LI_ROT_UNKNOWN\n    std::uint32_t pointerId;\n    float x;\n    float y;\n    float pressureOrDistance;  // Distance for hover and pressure for contact\n    float contactAreaMajor;\n    float contactAreaMinor;\n  };\n\n  struct pen_input_t {\n    std::uint8_t eventType;\n    std::uint8_t toolType;\n    std::uint8_t penButtons;\n    std::uint8_t tilt;  // Degrees (0..90) or LI_TILT_UNKNOWN\n    std::uint16_t rotation;  // Degrees (0..360) or LI_ROT_UNKNOWN\n    float x;\n    float y;\n    float pressureOrDistance;  // Distance for hover and pressure for contact\n    float contactAreaMajor;\n    float contactAreaMinor;\n  };\n\n  class deinit_t {\n  public:\n    virtual ~deinit_t() = default;\n  };\n\n  struct img_t: std::enable_shared_from_this<img_t> {\n  public:\n    img_t() = default;\n\n    img_t(img_t &&) = delete;\n    img_t(const img_t &) = delete;\n    img_t &\n    operator=(img_t &&) = delete;\n    img_t &\n    operator=(const img_t &) = delete;\n\n    std::uint8_t *data {};\n    std::int32_t width {};\n    std::int32_t height {};\n    std::int32_t pixel_pitch {};\n    std::int32_t row_pitch {};\n\n    std::optional<std::chrono::steady_clock::time_point> frame_timestamp;\n\n    virtual ~img_t() = default;\n  };\n\n  struct sink_t {\n    // Play on host PC\n    std::string host;\n\n    // On macOS and Windows, it is not possible to create a virtual sink\n    // Therefore, it is optional\n    struct null_t {\n      std::string stereo;\n      std::string surround51;\n      std::string surround71;\n      std::string surround714;\n    };\n\n    std::optional<null_t> null;\n  };\n\n  /**\n   * @brief Per-frame HDR luminance statistics computed by GPU analysis.\n   *\n   * These statistics are extracted from the captured scRGB FP16 frame\n   * by a D3D11 compute shader and used to generate accurate per-frame\n   * HDR dynamic metadata (CUVA HDR Vivid / HDR10+).\n   *\n   * Values are in nits (cd/m²). scRGB 1.0 = 80 nits.\n   */\n  /// Number of luminance histogram bins (each bin = 78.125 nits, covering 0-10000 nits)\n  static constexpr uint32_t HDR_HISTOGRAM_BINS = 128;\n  /// Maximum nits covered by the histogram\n  static constexpr float HDR_HISTOGRAM_MAX_NITS = 10000.0f;\n  /// Nits per histogram bin\n  static constexpr float HDR_NITS_PER_BIN = HDR_HISTOGRAM_MAX_NITS / HDR_HISTOGRAM_BINS;\n\n  struct hdr_frame_luminance_stats_t {\n    float min_maxrgb = 0.0f;    ///< Minimum of max(R,G,B) across all pixels (nits)\n    float max_maxrgb = 0.0f;    ///< Maximum of max(R,G,B) across all pixels (nits)\n    float avg_maxrgb = 0.0f;    ///< Average of max(R,G,B) across all pixels (nits)\n    float percentile_95 = 0.0f; ///< 95th percentile of maxRGB (nits) — stable peak estimate\n    float percentile_99 = 0.0f; ///< 99th percentile of maxRGB (nits) — near-peak estimate\n    uint32_t histogram[HDR_HISTOGRAM_BINS] = {};  ///< Luminance histogram (128 bins × 78.125 nits)\n    bool valid = false;         ///< Whether stats are available (false on first frame)\n  };\n\n  struct encode_device_t {\n    virtual ~encode_device_t() = default;\n\n    virtual int\n    convert(platf::img_t &img) = 0;\n\n    video::sunshine_colorspace_t colorspace;\n\n    /**\n     * @brief Per-frame HDR luminance statistics from GPU analysis.\n     * Updated during convert() with 1-frame delay (async GPU readback).\n     * Used by video.cpp to generate per-frame HDR dynamic metadata.\n     */\n    hdr_frame_luminance_stats_t hdr_luminance_stats;\n  };\n\n  struct avcodec_encode_device_t: encode_device_t {\n    void *data {};\n    AVFrame *frame {};\n\n    int\n    convert(platf::img_t &img) override {\n      return -1;\n    }\n\n    virtual void\n    apply_colorspace() {\n    }\n\n    /**\n     * @brief Set the frame to be encoded.\n     * @note Implementations must take ownership of 'frame'.\n     */\n    virtual int\n    set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) {\n      BOOST_LOG(error) << \"Illegal call to hwdevice_t::set_frame(). Did you forget to override it?\";\n      return -1;\n    };\n\n    /**\n     * @brief Initialize the hwframes context.\n     * @note Implementations may set parameters during initialization of the hwframes context.\n     */\n    virtual void\n    init_hwframes(AVHWFramesContext *frames) {};\n\n    /**\n     * @brief Provides a hook for allow platform-specific code to adjust codec options.\n     * @note Implementations may set or modify codec options prior to codec initialization.\n     */\n    virtual void\n    init_codec_options(AVCodecContext *ctx, AVDictionary **options) {};\n\n    /**\n     * @brief Prepare to derive a context.\n     * @note Implementations may make modifications required before context derivation\n     */\n    virtual int\n    prepare_to_derive_context(int hw_device_type) {\n      return 0;\n    };\n  };\n\n  struct nvenc_encode_device_t: encode_device_t {\n    virtual bool\n    init_encoder(const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace, bool is_probe = false) = 0;\n\n    nvenc::nvenc_encoder *nvenc = nullptr;\n  };\n\n  struct amf_encode_device_t: encode_device_t {\n    virtual bool\n    init_encoder(const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace, bool is_probe = false) = 0;\n\n    amf::amf_encoder *amf = nullptr;\n  };\n\n  enum class capture_e : int {\n    ok,  ///< Success\n    reinit,  ///< Need to reinitialize\n    timeout,  ///< Timeout\n    interrupted,  ///< Capture was interrupted\n    error  ///< Error\n  };\n\n  class display_t {\n  public:\n    /**\n     * @brief Callback for when a new image is ready.\n     * When display has a new image ready or a timeout occurs, this callback will be called with the image.\n     * If a frame was captured, frame_captured will be true. If a timeout occurred, it will be false.\n     * @retval true On success\n     * @retval false On break request\n     */\n    using push_captured_image_cb_t = std::function<bool(std::shared_ptr<img_t> &&img, bool frame_captured)>;\n\n    /**\n     * @brief Get free image from pool.\n     * Calls must be synchronized.\n     * Blocks until there is free image in the pool or capture is interrupted.\n     * @retval true On success, img_out contains free image\n     * @retval false When capture has been interrupted, img_out contains nullptr\n     */\n    using pull_free_image_cb_t = std::function<bool(std::shared_ptr<img_t> &img_out)>;\n\n    display_t() noexcept:\n        offset_x { 0 },\n        offset_y { 0 } {\n    }\n\n    /**\n     * @brief Capture a frame.\n     * @param push_captured_image_cb The callback that is called with captured image,\n     * must be called from the same thread as capture()\n     * @param pull_free_image_cb Capture backends call this callback to get empty image from the pool.\n     * If backend uses multiple threads, calls to this callback must be synchronized.\n     * Calls to this callback and push_captured_image_cb must be synchronized as well.\n     * @param cursor A pointer to the flag that indicates whether the cursor should be captured as well.\n     * @retval capture_e::ok When stopping\n     * @retval capture_e::error On error\n     * @retval capture_e::reinit When need of reinitialization\n     */\n    virtual capture_e\n    capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) = 0;\n\n    virtual std::shared_ptr<img_t>\n    alloc_img() = 0;\n\n    virtual int\n    dummy_img(img_t *img) = 0;\n\n    virtual std::unique_ptr<avcodec_encode_device_t>\n    make_avcodec_encode_device(pix_fmt_e pix_fmt) {\n      return nullptr;\n    }\n\n    virtual std::unique_ptr<nvenc_encode_device_t>\n    make_nvenc_encode_device(pix_fmt_e pix_fmt) {\n      return nullptr;\n    }\n\n    virtual std::unique_ptr<amf_encode_device_t>\n    make_amf_encode_device(pix_fmt_e pix_fmt) {\n      return nullptr;\n    }\n\n    virtual bool\n    is_hdr() {\n      return false;\n    }\n\n    virtual bool\n    get_hdr_metadata(SS_HDR_METADATA &metadata) {\n      std::memset(&metadata, 0, sizeof(metadata));\n      return false;\n    }\n\n    /**\n     * @brief Check that a given codec is supported by the display device.\n     * @param name The FFmpeg codec name (or similar for non-FFmpeg codecs).\n     * @param config The codec configuration.\n     * @return `true` if supported, `false` otherwise.\n     */\n    virtual bool\n    is_codec_supported(std::string_view name, const ::video::config_t &config) {\n      return true;\n    }\n\n    virtual ~display_t() = default;\n\n    // Offsets for when streaming a specific monitor. By default, they are 0.\n    int offset_x, offset_y;\n    int env_width, env_height;\n\n    int width, height;\n\n  protected:\n    // collect capture timing data (at loglevel debug)\n    logging::time_delta_periodic_logger sleep_overshoot_logger = { debug, \"Frame capture sleep overshoot\" };\n  };\n\n  class mic_t {\n  public:\n    virtual capture_e\n    sample(std::vector<float> &frame_buffer) = 0;\n\n    virtual ~mic_t() = default;\n  };\n\n  class audio_control_t {\n  public:\n    virtual int\n    set_sink(const std::string &sink) = 0;\n\n    virtual std::unique_ptr<mic_t>\n    microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) = 0;\n\n    /**\n     * @brief Check if the audio sink is available in the system.\n     * @param sink Sink to be checked.\n     * @returns True if available, false otherwise.\n     */\n    virtual bool\n    is_sink_available(const std::string &sink) = 0;\n\n    virtual std::optional<sink_t>\n    sink_info() = 0;\n\n    /**\n     * @brief Write microphone data to the virtual audio device.\n     * @param data Pointer to the audio data.\n     * @param size Size of the audio data in bytes.\n     * @param seq Sequence number for FEC recovery (0 = unknown)\n     * @returns Number of bytes written, or -1 on error.\n     */\n    virtual int\n    write_mic_data(const char *data, size_t size, uint16_t seq = 0) = 0;\n\n    /**\n     * @brief Initialize the microphone redirect device.\n     * @returns 0 on success, -1 on error.\n     */\n    virtual int\n    init_mic_redirect_device() = 0;\n\n    /**\n     * @brief Release the microphone redirect device.\n     */\n    virtual void\n    release_mic_redirect_device() = 0;\n\n    virtual ~audio_control_t() = default;\n  };\n\n  void\n  freeInput(void *);\n\n  using input_t = util::safe_ptr<void, freeInput>;\n\n  std::filesystem::path\n  appdata();\n\n  std::string\n  get_mac_address(const std::string_view &address);\n\n  std::string\n  from_sockaddr(const sockaddr *const);\n  std::pair<std::uint16_t, std::string>\n  from_sockaddr_ex(const sockaddr *const);\n\n  std::unique_ptr<audio_control_t>\n  audio_control();\n\n  /**\n   * @brief Get the display_t instance for the given hwdevice_type.\n   * If display_name is empty, use the first monitor that's compatible you can find\n   * If you require to use this parameter in a separate thread, make a copy of it.\n   * @param display_name The name of the monitor that SHOULD be displayed\n   * @param config Stream configuration\n   * @return The display_t instance based on hwdevice_type.\n   */\n  std::shared_ptr<display_t>\n  display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config);\n\n  // A list of names of displays accepted as display_name with the mem_type_e\n  std::vector<std::string>\n  display_names(mem_type_e hwdevice_type);\n\n  std::vector<std::string>\n  adapter_names();\n\n  /**\n   * @brief Check if GPUs/drivers have changed since the last call to this function.\n   * @return `true` if a change has occurred or if it is unknown whether a change occurred.\n   */\n  bool\n  needs_encoder_reenumeration();\n\n  enum class thread_priority_e : int {\n    low,  ///< Low priority\n    normal,  ///< Normal priority\n    high,  ///< High priority\n    critical  ///< Critical priority\n  };\n  void\n  adjust_thread_priority(thread_priority_e priority);\n\n  // Allow OS-specific actions to be taken to prepare for streaming\n  void\n  streaming_will_start();\n  void\n  streaming_will_stop();\n\n  /**\n   * @brief Enter Away Mode - display turns off, system stays running for instant wake.\n   * On Windows, this uses ES_AWAYMODE_REQUIRED + ES_SYSTEM_REQUIRED and turns off the monitor.\n   * On other platforms, this falls back to a no-op (or could be extended).\n   */\n  void\n  enter_away_mode();\n\n  /**\n   * @brief Exit Away Mode - restore display and clear power flags.\n   */\n  void\n  exit_away_mode();\n\n  /**\n   * @brief Check if the system is currently in Away Mode.\n   */\n  bool\n  is_away_mode_active();\n\n  /**\n   * @brief Put the system to sleep (S3 suspend) using the native API.\n   * @return true on success.\n   */\n  bool\n  system_sleep();\n\n  /**\n   * @brief Put the system into hibernation (S4) using the native API.\n   * @return true on success.\n   */\n  bool\n  system_hibernate();\n\n  void\n  restart();\n\n  /**\n   * @brief Set an environment variable.\n   * @param name The name of the environment variable.\n   * @param value The value to set the environment variable to.\n   * @return 0 on success, non-zero on failure.\n   */\n  int\n  set_env(const std::string &name, const std::string &value);\n\n  /**\n   * @brief Unset an environment variable.\n   * @param name The name of the environment variable.\n   * @return 0 on success, non-zero on failure.\n   */\n  int\n  unset_env(const std::string &name);\n\n  struct buffer_descriptor_t {\n    const char *buffer;\n    size_t size;\n\n    // Constructors required for emplace_back() prior to C++20\n    buffer_descriptor_t(const char *buffer, size_t size):\n        buffer(buffer),\n        size(size) {\n    }\n\n    buffer_descriptor_t():\n        buffer(nullptr),\n        size(0) {\n    }\n  };\n\n  struct batched_send_info_t {\n    // Optional headers to be prepended to each packet\n    const char *headers;\n    size_t header_size;\n\n    // One or more data buffers to use for the payloads\n    //\n    // NB: Data buffers must be aligned to payload size!\n    std::vector<buffer_descriptor_t> &payload_buffers;\n    size_t payload_size;\n\n    // The offset (in header+payload message blocks) in the header and payload\n    // buffers to begin sending messages from\n    size_t block_offset;\n\n    // The number of header+payload message blocks to send\n    size_t block_count;\n\n    std::uintptr_t native_socket;\n    boost::asio::ip::address &target_address;\n    uint16_t target_port;\n    boost::asio::ip::address &source_address;\n\n    /**\n     * @brief Returns a payload buffer descriptor for the given payload offset.\n     * @param offset The offset in the total payload data (bytes).\n     * @return Buffer descriptor describing the region at the given offset.\n     */\n    buffer_descriptor_t\n    buffer_for_payload_offset(ptrdiff_t offset) {\n      for (const auto &desc : payload_buffers) {\n        if (offset < desc.size) {\n          return {\n            desc.buffer + offset,\n            desc.size - offset,\n          };\n        }\n        else {\n          offset -= desc.size;\n        }\n      }\n      return {};\n    }\n  };\n\n  bool\n  send_batch(batched_send_info_t &send_info);\n\n  struct send_info_t {\n    const char *header;\n    size_t header_size;\n    const char *payload;\n    size_t payload_size;\n\n    std::uintptr_t native_socket;\n    boost::asio::ip::address &target_address;\n    uint16_t target_port;\n    boost::asio::ip::address &source_address;\n  };\n\n  bool\n  send(send_info_t &send_info);\n\n  enum class qos_data_type_e : int {\n    audio,  ///< Audio\n    video  ///< Video\n  };\n\n  /**\n   * @brief Enable QoS on the given socket for traffic to the specified destination.\n   * @param native_socket The native socket handle.\n   * @param address The destination address for traffic sent on this socket.\n   * @param port The destination port for traffic sent on this socket.\n   * @param data_type The type of traffic sent on this socket.\n   * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic.\n   */\n  std::unique_ptr<deinit_t>\n  enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging);\n\n  /**\n   * @brief Open a url in the default web browser.\n   * @param url The url to open.\n   */\n  void\n  open_url(const std::string &url);\n\n  /**\n   * @brief Open a url directly in the system default browser.\n   * @details This function opens the URL directly using the system's default browser,\n   *          bypassing any intermediate applications. On Windows, it reads the registry\n   *          to find the default browser and launches it directly to avoid browser\n   *          selection dialogs.\n   * @param url The url to open.\n   */\n  void\n  open_url_in_browser(const std::string &url);\n\n  /**\n   * @brief Attempt to gracefully terminate a process group.\n   * @param native_handle The native handle of the process group.\n   * @return `true` if termination was successfully requested.\n   */\n  bool\n  request_process_group_exit(std::uintptr_t native_handle);\n\n  /**\n   * @brief Check if a process group still has running children.\n   * @param native_handle The native handle of the process group.\n   * @return `true` if processes are still running.\n   */\n  bool\n  process_group_running(std::uintptr_t native_handle);\n\n  input_t\n  input();\n  /**\n   * @brief Get the current mouse position on screen\n   * @param input The input_t instance to use.\n   * @return Screen coordinates of the mouse.\n   * @examples\n   * auto [x, y] = get_mouse_loc(input);\n   * @examples_end\n   */\n  util::point_t\n  get_mouse_loc(input_t &input);\n  void\n  move_mouse(input_t &input, int deltaX, int deltaY);\n  void\n  set_mouse_mode(int mode);\n  void\n  abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y);\n  void\n  button_mouse(input_t &input, int button, bool release);\n  void\n  scroll(input_t &input, int distance);\n  void\n  hscroll(input_t &input, int distance);\n  void\n  keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags);\n  void\n  gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state);\n  void\n  unicode(input_t &input, char *utf8, int size);\n\n  typedef deinit_t client_input_t;\n\n  /**\n   * @brief Allocate a context to store per-client input data.\n   * @param input The global input context.\n   * @return A unique pointer to a per-client input data context.\n   */\n  std::unique_ptr<client_input_t>\n  allocate_client_input_context(input_t &input);\n\n  /**\n   * @brief Send a touch event to the OS.\n   * @param input The client-specific input context.\n   * @param touch_port The current viewport for translating to screen coordinates.\n   * @param touch The touch event.\n   */\n  void\n  touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch);\n\n  /**\n   * @brief Send a pen event to the OS.\n   * @param input The client-specific input context.\n   * @param touch_port The current viewport for translating to screen coordinates.\n   * @param pen The pen event.\n   */\n  void\n  pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen);\n\n  /**\n   * @brief Send a gamepad touch event to the OS.\n   * @param input The global input context.\n   * @param touch The touch event.\n   */\n  void\n  gamepad_touch(input_t &input, const gamepad_touch_t &touch);\n\n  /**\n   * @brief Send a gamepad motion event to the OS.\n   * @param input The global input context.\n   * @param motion The motion event.\n   */\n  void\n  gamepad_motion(input_t &input, const gamepad_motion_t &motion);\n\n  /**\n   * @brief Send a gamepad battery event to the OS.\n   * @param input The global input context.\n   * @param battery The battery event.\n   */\n  void\n  gamepad_battery(input_t &input, const gamepad_battery_t &battery);\n\n  /**\n   * @brief Create a new virtual gamepad.\n   * @param input The global input context.\n   * @param id The gamepad ID.\n   * @param metadata Controller metadata from client (empty if none provided).\n   * @param feedback_queue The queue for posting messages back to the client.\n   * @return 0 on success.\n   */\n  int\n  alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue);\n  void\n  free_gamepad(input_t &input, int nr);\n\n  /**\n   * @brief Get the supported platform capabilities to advertise to the client.\n   * @return Capability flags.\n   */\n  platform_caps::caps_t\n  get_capabilities();\n\n#define SERVICE_NAME \"Sunshine\"\n#define SERVICE_TYPE \"_nvstream._tcp\"\n\n  namespace publish {\n    [[nodiscard]] std::unique_ptr<deinit_t>\n    start();\n  }\n\n  [[nodiscard]] std::unique_ptr<deinit_t>\n  init();\n\n  /**\n   * @brief Returns the current computer name in UTF-8.\n   * @return Computer name or a placeholder upon failure.\n   */\n  std::string\n  get_host_name();\n\n  /**\n   * @brief Gets the supported gamepads for this platform backend.\n   * @details This may be called prior to `platf::input()`!\n   * @param input Pointer to the platform's `input_t` or `nullptr`.\n   * @return Vector of gamepad options and status.\n   */\n  std::vector<supported_gamepad_t> &\n  supported_gamepads(input_t *input);\n\n  struct high_precision_timer: private boost::noncopyable {\n    virtual ~high_precision_timer() = default;\n\n    /**\n     * @brief Sleep for the duration\n     * @param duration Sleep duration\n     */\n    virtual void\n    sleep_for(const std::chrono::nanoseconds &duration) = 0;\n\n    /**\n     * @brief Check if platform-specific timer backend has been initialized successfully\n     * @return `true` on success, `false` on error\n     */\n    virtual\n    operator bool() = 0;\n  };\n\n  /**\n   * @brief Create platform-specific timer capable of high-precision sleep\n   * @return A unique pointer to timer\n   */\n  std::unique_ptr<high_precision_timer>\n  create_high_precision_timer();\n\n}  // namespace platf"
  },
  {
    "path": "src/platform/linux/audio.cpp",
    "content": "/**\n * @file src/platform/linux/audio.cpp\n * @brief Definitions for audio control on Linux.\n */\n// standard includes\n#include <bitset>\n#include <sstream>\n#include <thread>\n\n// lib includes\n#include <boost/regex.hpp>\n#include <pulse/error.h>\n#include <pulse/pulseaudio.h>\n#include <pulse/simple.h>\n\n// local includes\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/thread_safe.h\"\n\nnamespace platf {\n  using namespace std::literals;\n\n  constexpr pa_channel_position_t position_mapping[] {\n    PA_CHANNEL_POSITION_FRONT_LEFT,\n    PA_CHANNEL_POSITION_FRONT_RIGHT,\n    PA_CHANNEL_POSITION_FRONT_CENTER,\n    PA_CHANNEL_POSITION_LFE,\n    PA_CHANNEL_POSITION_REAR_LEFT,\n    PA_CHANNEL_POSITION_REAR_RIGHT,\n    PA_CHANNEL_POSITION_SIDE_LEFT,\n    PA_CHANNEL_POSITION_SIDE_RIGHT,\n    PA_CHANNEL_POSITION_TOP_FRONT_LEFT,\n    PA_CHANNEL_POSITION_TOP_FRONT_RIGHT,\n    PA_CHANNEL_POSITION_TOP_REAR_LEFT,\n    PA_CHANNEL_POSITION_TOP_REAR_RIGHT,\n  };\n\n  std::string\n  to_string(const char *name, const std::uint8_t *mapping, int channels) {\n    std::stringstream ss;\n\n    ss << \"rate=48000 sink_name=\"sv << name << \" format=float channels=\"sv << channels << \" channel_map=\"sv;\n    std::for_each_n(mapping, channels - 1, [&ss](std::uint8_t pos) {\n      ss << pa_channel_position_to_string(position_mapping[pos]) << ',';\n    });\n\n    ss << pa_channel_position_to_string(position_mapping[mapping[channels - 1]]);\n\n    ss << \" sink_properties=device.description=\"sv << name;\n    auto result = ss.str();\n\n    BOOST_LOG(debug) << \"null-sink args: \"sv << result;\n    return result;\n  }\n\n  struct mic_attr_t: public mic_t {\n    util::safe_ptr<pa_simple, pa_simple_free> mic;\n\n    capture_e\n    sample(std::vector<float> &sample_buf) override {\n      auto sample_size = sample_buf.size();\n\n      auto buf = sample_buf.data();\n      int status;\n      if (pa_simple_read(mic.get(), buf, sample_size * sizeof(float), &status)) {\n        BOOST_LOG(error) << \"pa_simple_read() failed: \"sv << pa_strerror(status);\n\n        return capture_e::error;\n      }\n\n      return capture_e::ok;\n    }\n  };\n\n  std::unique_ptr<mic_t>\n  microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, std::string source_name) {\n    auto mic = std::make_unique<mic_attr_t>();\n\n    pa_sample_spec ss { PA_SAMPLE_FLOAT32, sample_rate, (std::uint8_t) channels };\n    pa_channel_map pa_map;\n\n    pa_map.channels = channels;\n    std::for_each_n(pa_map.map, pa_map.channels, [mapping](auto &channel) mutable {\n      channel = position_mapping[*mapping++];\n    });\n\n    pa_buffer_attr pa_attr = {\n      .maxlength = uint32_t(-1),\n      .tlength = uint32_t(-1),\n      .prebuf = uint32_t(-1),\n      .minreq = uint32_t(-1),\n      .fragsize = uint32_t(frame_size * channels * sizeof(float))\n    };\n\n    int status;\n\n    mic->mic.reset(\n      pa_simple_new(nullptr, \"sunshine\", pa_stream_direction_t::PA_STREAM_RECORD, source_name.c_str(), \"sunshine-record\", &ss, &pa_map, &pa_attr, &status));\n\n    if (!mic->mic) {\n      auto err_str = pa_strerror(status);\n      BOOST_LOG(error) << \"pa_simple_new() failed: \"sv << err_str;\n      return nullptr;\n    }\n\n    return mic;\n  }\n\n  namespace pa {\n    template <bool B, class T>\n    struct add_const_helper;\n\n    template <class T>\n    struct add_const_helper<true, T> {\n      using type = const std::remove_pointer_t<T> *;\n    };\n\n    template <class T>\n    struct add_const_helper<false, T> {\n      using type = const T *;\n    };\n\n    template <class T>\n    using add_const_t = typename add_const_helper<std::is_pointer_v<T>, T>::type;\n\n    template <class T>\n    void\n    pa_free(T *p) {\n      pa_xfree(p);\n    }\n\n    using ctx_t = util::safe_ptr<pa_context, pa_context_unref>;\n    using loop_t = util::safe_ptr<pa_mainloop, pa_mainloop_free>;\n    using op_t = util::safe_ptr<pa_operation, pa_operation_unref>;\n    using string_t = util::safe_ptr<char, pa_free<char>>;\n\n    template <class T>\n    using cb_simple_t = std::function<void(ctx_t::pointer, add_const_t<T> i)>;\n\n    template <class T>\n    void\n    cb(ctx_t::pointer ctx, add_const_t<T> i, void *userdata) {\n      auto &f = *(cb_simple_t<T> *) userdata;\n\n      // Cannot similarly filter on eol here. Unless reported otherwise assume\n      // we have no need for special filtering like cb?\n      f(ctx, i);\n    }\n\n    template <class T>\n    using cb_t = std::function<void(ctx_t::pointer, add_const_t<T> i, int eol)>;\n\n    template <class T>\n    void\n    cb(ctx_t::pointer ctx, add_const_t<T> i, int eol, void *userdata) {\n      auto &f = *(cb_t<T> *) userdata;\n\n      // For some reason, pulseaudio calls this callback after disconnecting\n      if (i && eol) {\n        return;\n      }\n\n      f(ctx, i, eol);\n    }\n\n    void\n    cb_i(ctx_t::pointer ctx, std::uint32_t i, void *userdata) {\n      auto alarm = (safe::alarm_raw_t<int> *) userdata;\n\n      alarm->ring(i);\n    }\n\n    void\n    ctx_state_cb(ctx_t::pointer ctx, void *userdata) {\n      auto &f = *(std::function<void(ctx_t::pointer)> *) userdata;\n\n      f(ctx);\n    }\n\n    void\n    success_cb(ctx_t::pointer ctx, int status, void *userdata) {\n      assert(userdata != nullptr);\n\n      auto alarm = (safe::alarm_raw_t<int> *) userdata;\n      alarm->ring(status ? 0 : 1);\n    }\n\n    class server_t: public audio_control_t {\n      enum ctx_event_e : int {\n        ready,\n        terminated,\n        failed\n      };\n\n    public:\n      loop_t loop;\n      ctx_t ctx;\n      std::string requested_sink;\n\n      struct {\n        std::uint32_t stereo = PA_INVALID_INDEX;\n        std::uint32_t surround51 = PA_INVALID_INDEX;\n        std::uint32_t surround71 = PA_INVALID_INDEX;\n        std::uint32_t surround714 = PA_INVALID_INDEX;\n      } index;\n\n      std::unique_ptr<safe::event_t<ctx_event_e>> events;\n      std::unique_ptr<std::function<void(ctx_t::pointer)>> events_cb;\n\n      std::thread worker;\n\n      int\n      init() {\n        events = std::make_unique<safe::event_t<ctx_event_e>>();\n        loop.reset(pa_mainloop_new());\n        ctx.reset(pa_context_new(pa_mainloop_get_api(loop.get()), \"sunshine\"));\n\n        events_cb = std::make_unique<std::function<void(ctx_t::pointer)>>([this](ctx_t::pointer ctx) {\n          switch (pa_context_get_state(ctx)) {\n            case PA_CONTEXT_READY:\n              events->raise(ready);\n              break;\n            case PA_CONTEXT_TERMINATED:\n              BOOST_LOG(debug) << \"Pulseadio context terminated\"sv;\n              events->raise(terminated);\n              break;\n            case PA_CONTEXT_FAILED:\n              BOOST_LOG(debug) << \"Pulseadio context failed\"sv;\n              events->raise(failed);\n              break;\n            case PA_CONTEXT_CONNECTING:\n              BOOST_LOG(debug) << \"Connecting to pulseaudio\"sv;\n            case PA_CONTEXT_UNCONNECTED:\n            case PA_CONTEXT_AUTHORIZING:\n            case PA_CONTEXT_SETTING_NAME:\n              break;\n          }\n        });\n\n        pa_context_set_state_callback(ctx.get(), ctx_state_cb, events_cb.get());\n\n        auto status = pa_context_connect(ctx.get(), nullptr, PA_CONTEXT_NOFLAGS, nullptr);\n        if (status) {\n          BOOST_LOG(error) << \"Couldn't connect to pulseaudio: \"sv << pa_strerror(status);\n          return -1;\n        }\n\n        worker = std::thread {\n          [](loop_t::pointer loop) {\n            int retval;\n            auto status = pa_mainloop_run(loop, &retval);\n\n            if (status < 0) {\n              BOOST_LOG(error) << \"Couldn't run pulseaudio main loop\"sv;\n              return;\n            }\n          },\n          loop.get()\n        };\n\n        auto event = events->pop();\n        if (event == failed) {\n          return -1;\n        }\n\n        return 0;\n      }\n\n      int\n      load_null(const char *name, const std::uint8_t *channel_mapping, int channels) {\n        auto alarm = safe::make_alarm<int>();\n\n        op_t op {\n          pa_context_load_module(\n            ctx.get(),\n            \"module-null-sink\",\n            to_string(name, channel_mapping, channels).c_str(),\n            cb_i,\n            alarm.get()),\n        };\n\n        alarm->wait();\n        return *alarm->status();\n      }\n\n      int\n      unload_null(std::uint32_t i) {\n        if (i == PA_INVALID_INDEX) {\n          return 0;\n        }\n\n        auto alarm = safe::make_alarm<int>();\n\n        op_t op {\n          pa_context_unload_module(ctx.get(), i, success_cb, alarm.get())\n        };\n\n        alarm->wait();\n\n        if (*alarm->status()) {\n          BOOST_LOG(error) << \"Couldn't unload null-sink with index [\"sv << i << \"]: \"sv << pa_strerror(pa_context_errno(ctx.get()));\n          return -1;\n        }\n\n        return 0;\n      }\n\n      std::optional<sink_t>\n      sink_info() override {\n        constexpr auto stereo = \"sink-sunshine-stereo\";\n        constexpr auto surround51 = \"sink-sunshine-surround51\";\n        constexpr auto surround71 = \"sink-sunshine-surround71\";\n        constexpr auto surround714 = \"sink-sunshine-surround714\";\n\n        auto alarm = safe::make_alarm<int>();\n\n        sink_t sink;\n\n        // Count of all virtual sinks that are created by us\n        int nullcount = 0;\n\n        cb_t<pa_sink_info *> f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) {\n          if (!sink_info) {\n            if (!eol) {\n              BOOST_LOG(error) << \"Couldn't get pulseaudio sink info: \"sv << pa_strerror(pa_context_errno(ctx));\n\n              alarm->ring(-1);\n            }\n\n            alarm->ring(0);\n            return;\n          }\n\n          // Ensure Sunshine won't create a sink that already exists.\n          if (!std::strcmp(sink_info->name, stereo)) {\n            index.stereo = sink_info->owner_module;\n\n            ++nullcount;\n          }\n          else if (!std::strcmp(sink_info->name, surround51)) {\n            index.surround51 = sink_info->owner_module;\n\n            ++nullcount;\n          }\n          else if (!std::strcmp(sink_info->name, surround71)) {\n            index.surround71 = sink_info->owner_module;\n\n            ++nullcount;\n          }\n          else if (!std::strcmp(sink_info->name, surround714)) {\n            index.surround714 = sink_info->owner_module;\n\n            ++nullcount;\n          }\n        };\n\n        op_t op { pa_context_get_sink_info_list(ctx.get(), cb<pa_sink_info *>, &f) };\n\n        if (!op) {\n          BOOST_LOG(error) << \"Couldn't create card info operation: \"sv << pa_strerror(pa_context_errno(ctx.get()));\n\n          return std::nullopt;\n        }\n\n        alarm->wait();\n\n        if (*alarm->status()) {\n          return std::nullopt;\n        }\n\n        auto sink_name = get_default_sink_name();\n        sink.host = sink_name;\n\n        if (index.stereo == PA_INVALID_INDEX) {\n          index.stereo = load_null(stereo, speaker::map_stereo, sizeof(speaker::map_stereo));\n          if (index.stereo == PA_INVALID_INDEX) {\n            BOOST_LOG(warning) << \"Couldn't create virtual sink for stereo: \"sv << pa_strerror(pa_context_errno(ctx.get()));\n          }\n          else {\n            ++nullcount;\n          }\n        }\n\n        if (index.surround51 == PA_INVALID_INDEX) {\n          index.surround51 = load_null(surround51, speaker::map_surround51, sizeof(speaker::map_surround51));\n          if (index.surround51 == PA_INVALID_INDEX) {\n            BOOST_LOG(warning) << \"Couldn't create virtual sink for surround-51: \"sv << pa_strerror(pa_context_errno(ctx.get()));\n          }\n          else {\n            ++nullcount;\n          }\n        }\n\n        if (index.surround71 == PA_INVALID_INDEX) {\n          index.surround71 = load_null(surround71, speaker::map_surround71, sizeof(speaker::map_surround71));\n          if (index.surround71 == PA_INVALID_INDEX) {\n            BOOST_LOG(warning) << \"Couldn't create virtual sink for surround-71: \"sv << pa_strerror(pa_context_errno(ctx.get()));\n          }\n          else {\n            ++nullcount;\n          }\n        }\n\n        if (index.surround714 == PA_INVALID_INDEX) {\n          index.surround714 = load_null(surround714, speaker::map_surround714, sizeof(speaker::map_surround714));\n          if (index.surround714 == PA_INVALID_INDEX) {\n            BOOST_LOG(warning) << \"Couldn't create virtual sink for surround-714: \"sv << pa_strerror(pa_context_errno(ctx.get()));\n          }\n          else {\n            ++nullcount;\n          }\n        }\n\n        if (sink_name.empty()) {\n          BOOST_LOG(warning) << \"Couldn't find an active default sink. Continuing with virtual audio only.\"sv;\n        }\n\n        if (index.stereo != PA_INVALID_INDEX && index.surround51 != PA_INVALID_INDEX && index.surround71 != PA_INVALID_INDEX) {\n          sink.null = std::make_optional(sink_t::null_t {\n            stereo,\n            surround51,\n            surround71,\n            index.surround714 != PA_INVALID_INDEX ? surround714 : \"\"\n          });\n        }\n\n        return std::make_optional(std::move(sink));\n      }\n\n      std::string\n      get_default_sink_name() {\n        std::string sink_name;\n        auto alarm = safe::make_alarm<int>();\n\n        cb_simple_t<pa_server_info *> server_f = [&](ctx_t::pointer ctx, const pa_server_info *server_info) {\n          if (!server_info) {\n            BOOST_LOG(error) << \"Couldn't get pulseaudio server info: \"sv << pa_strerror(pa_context_errno(ctx));\n            alarm->ring(-1);\n          }\n\n          if (server_info->default_sink_name) {\n            sink_name = server_info->default_sink_name;\n          }\n          alarm->ring(0);\n        };\n\n        op_t server_op { pa_context_get_server_info(ctx.get(), cb<pa_server_info *>, &server_f) };\n        alarm->wait();\n        // No need to check status. If it failed just return default name.\n        return sink_name;\n      }\n\n      std::string\n      get_monitor_name(const std::string &sink_name) {\n        std::string monitor_name;\n        auto alarm = safe::make_alarm<int>();\n\n        if (sink_name.empty()) {\n          return monitor_name;\n        }\n\n        cb_t<pa_sink_info *> sink_f = [&](ctx_t::pointer ctx, const pa_sink_info *sink_info, int eol) {\n          if (!sink_info) {\n            if (!eol) {\n              BOOST_LOG(error) << \"Couldn't get pulseaudio sink info for [\"sv << sink_name\n                               << \"]: \"sv << pa_strerror(pa_context_errno(ctx));\n              alarm->ring(-1);\n            }\n\n            alarm->ring(0);\n            return;\n          }\n\n          monitor_name = sink_info->monitor_source_name;\n        };\n\n        op_t sink_op { pa_context_get_sink_info_by_name(ctx.get(), sink_name.c_str(), cb<pa_sink_info *>, &sink_f) };\n\n        alarm->wait();\n        // No need to check status. If it failed just return default name.\n        BOOST_LOG(info) << \"Found default monitor by name: \"sv << monitor_name;\n        return monitor_name;\n      }\n\n      std::unique_ptr<mic_t>\n      microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {\n        // Sink choice priority:\n        // 1. Config sink\n        // 2. Last sink swapped to (Usually virtual in this case)\n        // 3. Default Sink\n        // An attempt was made to always use default to match the switching mechanic,\n        // but this happens right after the swap so the default returned by PA was not\n        // the new one just set!\n        auto sink_name = config::audio.sink;\n        if (sink_name.empty()) {\n          sink_name = requested_sink;\n        }\n        if (sink_name.empty()) {\n          sink_name = get_default_sink_name();\n        }\n\n        return ::platf::microphone(mapping, channels, sample_rate, frame_size, get_monitor_name(sink_name));\n      }\n\n      bool\n      is_sink_available(const std::string &sink) override {\n        BOOST_LOG(warning) << \"audio_control_t::is_sink_available() unimplemented: \"sv << sink;\n        return true;\n      }\n\n      int\n      set_sink(const std::string &sink) override {\n        auto alarm = safe::make_alarm<int>();\n\n        BOOST_LOG(info) << \"Setting default sink to: [\"sv << sink << \"]\"sv;\n        op_t op {\n          pa_context_set_default_sink(\n            ctx.get(),\n            sink.c_str(),\n            success_cb,\n            alarm.get()),\n        };\n\n        if (!op) {\n          BOOST_LOG(error) << \"Couldn't create set default-sink operation: \"sv << pa_strerror(pa_context_errno(ctx.get()));\n          return -1;\n        }\n\n        alarm->wait();\n        if (*alarm->status()) {\n          BOOST_LOG(error) << \"Couldn't set default-sink [\"sv << sink << \"]: \"sv << pa_strerror(pa_context_errno(ctx.get()));\n\n          return -1;\n        }\n\n        requested_sink = sink;\n\n        return 0;\n      }\n\n      ~server_t() override {\n        unload_null(index.stereo);\n        unload_null(index.surround51);\n        unload_null(index.surround71);\n        unload_null(index.surround714);\n\n        if (worker.joinable()) {\n          pa_context_disconnect(ctx.get());\n\n          KITTY_WHILE_LOOP(auto event = events->pop(), event != terminated && event != failed, {\n            event = events->pop();\n          })\n\n          pa_mainloop_quit(loop.get(), 0);\n          worker.join();\n        }\n      }\n    };\n  }  // namespace pa\n\n  std::unique_ptr<audio_control_t>\n  audio_control() {\n    auto audio = std::make_unique<pa::server_t>();\n\n    if (audio->init()) {\n      return nullptr;\n    }\n\n    return audio;\n  }\n}  // namespace platf"
  },
  {
    "path": "src/platform/linux/cuda.cpp",
    "content": "/**\n * @file src/platform/linux/cuda.cpp\n * @brief Definitions for CUDA encoding.\n */\n#include <bitset>\n#include <fcntl.h>\n#include <filesystem>\n#include <thread>\n\n#include <NvFBC.h>\n#include <ffnvcodec/dynlink_loader.h>\n\nextern \"C\" {\n#include <libavcodec/avcodec.h>\n#include <libavutil/hwcontext_cuda.h>\n#include <libavutil/imgutils.h>\n}\n\n#include \"cuda.h\"\n#include \"graphics.h\"\n#include \"src/logging.h\"\n#include \"src/utility.h\"\n#include \"src/video.h\"\n#include \"wayland.h\"\n\n#define SUNSHINE_STRINGVIEW_HELPER(x) x##sv\n#define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x)\n\n#define CU_CHECK(x, y) \\\n  if (check((x), SUNSHINE_STRINGVIEW(y \": \"))) return -1\n\n#define CU_CHECK_IGNORE(x, y) \\\n  check((x), SUNSHINE_STRINGVIEW(y \": \"))\n\nnamespace fs = std::filesystem;\n\nusing namespace std::literals;\nnamespace cuda {\n  constexpr auto cudaDevAttrMaxThreadsPerBlock = (CUdevice_attribute) 1;\n  constexpr auto cudaDevAttrMaxThreadsPerMultiProcessor = (CUdevice_attribute) 39;\n\n  void\n  pass_error(const std::string_view &sv, const char *name, const char *description) {\n    BOOST_LOG(error) << sv << name << ':' << description;\n  }\n\n  void\n  cff(CudaFunctions *cf) {\n    cuda_free_functions(&cf);\n  }\n\n  using cdf_t = util::safe_ptr<CudaFunctions, cff>;\n\n  static cdf_t cdf;\n\n  inline static int\n  check(CUresult result, const std::string_view &sv) {\n    if (result != CUDA_SUCCESS) {\n      const char *name;\n      const char *description;\n\n      cdf->cuGetErrorName(result, &name);\n      cdf->cuGetErrorString(result, &description);\n\n      BOOST_LOG(error) << sv << name << ':' << description;\n      return -1;\n    }\n\n    return 0;\n  }\n\n  void\n  freeStream(CUstream stream) {\n    CU_CHECK_IGNORE(cdf->cuStreamDestroy(stream), \"Couldn't destroy cuda stream\");\n  }\n\n  void\n  unregisterResource(CUgraphicsResource resource) {\n    CU_CHECK_IGNORE(cdf->cuGraphicsUnregisterResource(resource), \"Couldn't unregister resource\");\n  }\n\n  using registered_resource_t = util::safe_ptr<CUgraphicsResource_st, unregisterResource>;\n\n  class img_t: public platf::img_t {\n  public:\n    tex_t tex;\n  };\n\n  int\n  init() {\n    auto status = cuda_load_functions(&cdf, nullptr);\n    if (status) {\n      BOOST_LOG(error) << \"Couldn't load cuda: \"sv << status;\n\n      return -1;\n    }\n\n    CU_CHECK(cdf->cuInit(0), \"Couldn't initialize cuda\");\n\n    return 0;\n  }\n\n  class cuda_t: public platf::avcodec_encode_device_t {\n  public:\n    int\n    init(int in_width, int in_height) {\n      if (!cdf) {\n        BOOST_LOG(warning) << \"cuda not initialized\"sv;\n        return -1;\n      }\n\n      data = (void *) 0x1;\n\n      width = in_width;\n      height = in_height;\n\n      return 0;\n    }\n\n    int\n    set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {\n      this->hwframe.reset(frame);\n      this->frame = frame;\n\n      auto hwframe_ctx = (AVHWFramesContext *) hw_frames_ctx->data;\n      if (hwframe_ctx->sw_format != AV_PIX_FMT_NV12) {\n        BOOST_LOG(error) << \"cuda::cuda_t doesn't support any format other than AV_PIX_FMT_NV12\"sv;\n        return -1;\n      }\n\n      if (!frame->buf[0]) {\n        if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) {\n          BOOST_LOG(error) << \"Couldn't get hwframe for NVENC\"sv;\n          return -1;\n        }\n      }\n\n      auto cuda_ctx = (AVCUDADeviceContext *) hwframe_ctx->device_ctx->hwctx;\n\n      stream = make_stream();\n      if (!stream) {\n        return -1;\n      }\n\n      cuda_ctx->stream = stream.get();\n\n      auto sws_opt = sws_t::make(width, height, frame->width, frame->height, width * 4);\n      if (!sws_opt) {\n        return -1;\n      }\n\n      sws = std::move(*sws_opt);\n\n      linear_interpolation = width != frame->width || height != frame->height;\n\n      return 0;\n    }\n\n    void\n    apply_colorspace() override {\n      sws.apply_colorspace(colorspace);\n\n      auto tex = tex_t::make(height, width * 4);\n      if (!tex) {\n        return;\n      }\n\n      // The default green color is ugly.\n      // Update the background color\n      platf::img_t img;\n      img.width = width;\n      img.height = height;\n      img.pixel_pitch = 4;\n      img.row_pitch = img.width * img.pixel_pitch;\n\n      std::vector<std::uint8_t> image_data;\n      image_data.resize(img.row_pitch * img.height);\n\n      img.data = image_data.data();\n\n      if (sws.load_ram(img, tex->array)) {\n        return;\n      }\n\n      sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex->texture.linear, stream.get(), { frame->width, frame->height, 0, 0 });\n    }\n\n    cudaTextureObject_t\n    tex_obj(const tex_t &tex) const {\n      return linear_interpolation ? tex.texture.linear : tex.texture.point;\n    }\n\n    stream_t stream;\n    frame_t hwframe;\n\n    int width, height;\n\n    // When height and width don't change, it's not necessary to use linear interpolation\n    bool linear_interpolation;\n\n    sws_t sws;\n  };\n\n  class cuda_ram_t: public cuda_t {\n  public:\n    int\n    convert(platf::img_t &img) override {\n      return sws.load_ram(img, tex.array) || sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(tex), stream.get());\n    }\n\n    int\n    set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) {\n      if (cuda_t::set_frame(frame, hw_frames_ctx)) {\n        return -1;\n      }\n\n      auto tex_opt = tex_t::make(height, width * 4);\n      if (!tex_opt) {\n        return -1;\n      }\n\n      tex = std::move(*tex_opt);\n\n      return 0;\n    }\n\n    tex_t tex;\n  };\n\n  class cuda_vram_t: public cuda_t {\n  public:\n    int\n    convert(platf::img_t &img) override {\n      return sws.convert(frame->data[0], frame->data[1], frame->linesize[0], frame->linesize[1], tex_obj(((img_t *) &img)->tex), stream.get());\n    }\n  };\n\n  /**\n   * @brief Opens the DRM device associated with the CUDA device index.\n   * @param index CUDA device index to open.\n   * @return File descriptor or -1 on failure.\n   */\n  file_t\n  open_drm_fd_for_cuda_device(int index) {\n    CUdevice device;\n    CU_CHECK(cdf->cuDeviceGet(&device, index), \"Couldn't get CUDA device\");\n\n    // There's no way to directly go from CUDA to a DRM device, so we'll\n    // use sysfs to look up the DRM device name from the PCI ID.\n    std::array<char, 13> pci_bus_id;\n    CU_CHECK(cdf->cuDeviceGetPCIBusId(pci_bus_id.data(), pci_bus_id.size(), device), \"Couldn't get CUDA device PCI bus ID\");\n    BOOST_LOG(debug) << \"Found CUDA device with PCI bus ID: \"sv << pci_bus_id.data();\n\n    // Linux uses lowercase hexadecimal while CUDA uses uppercase\n    std::transform(pci_bus_id.begin(), pci_bus_id.end(), pci_bus_id.begin(),\n      [](char c) { return std::tolower(c); });\n\n    // Look for the name of the primary node in sysfs\n    try {\n      char sysfs_path[PATH_MAX];\n      std::snprintf(sysfs_path, sizeof(sysfs_path), \"/sys/bus/pci/devices/%s/drm\", pci_bus_id.data());\n      fs::path sysfs_dir { sysfs_path };\n      for (auto &entry : fs::directory_iterator { sysfs_dir }) {\n        auto file = entry.path().filename();\n        auto filestring = file.generic_string();\n        if (std::string_view { filestring }.substr(0, 4) != \"card\"sv) {\n          continue;\n        }\n\n        BOOST_LOG(debug) << \"Found DRM primary node: \"sv << filestring;\n\n        fs::path dri_path { \"/dev/dri\"sv };\n        auto device_path = dri_path / file;\n        return open(device_path.c_str(), O_RDWR);\n      }\n    }\n    catch (const std::filesystem::filesystem_error &err) {\n      BOOST_LOG(error) << \"Failed to read sysfs: \"sv << err.what();\n    }\n\n    BOOST_LOG(error) << \"Unable to find DRM device with PCI bus ID: \"sv << pci_bus_id.data();\n    return -1;\n  }\n\n  class gl_cuda_vram_t: public platf::avcodec_encode_device_t {\n  public:\n    /**\n     * @brief Initialize the GL->CUDA encoding device.\n     * @param in_width Width of captured frames.\n     * @param in_height Height of captured frames.\n     * @param offset_x Offset of content in captured frame.\n     * @param offset_y Offset of content in captured frame.\n     * @return 0 on success or -1 on failure.\n     */\n    int\n    init(int in_width, int in_height, int offset_x, int offset_y) {\n      // This must be non-zero to tell the video core that it's a hardware encoding device.\n      data = (void *) 0x1;\n\n      // TODO: Support more than one CUDA device\n      file = std::move(open_drm_fd_for_cuda_device(0));\n      if (file.el < 0) {\n        char string[1024];\n        BOOST_LOG(error) << \"Couldn't open DRM FD for CUDA device: \"sv << strerror_r(errno, string, sizeof(string));\n        return -1;\n      }\n\n      gbm.reset(gbm::create_device(file.el));\n      if (!gbm) {\n        BOOST_LOG(error) << \"Couldn't create GBM device: [\"sv << util::hex(eglGetError()).to_string_view() << ']';\n        return -1;\n      }\n\n      display = egl::make_display(gbm.get());\n      if (!display) {\n        return -1;\n      }\n\n      auto ctx_opt = egl::make_ctx(display.get());\n      if (!ctx_opt) {\n        return -1;\n      }\n\n      ctx = std::move(*ctx_opt);\n\n      width = in_width;\n      height = in_height;\n\n      sequence = 0;\n\n      this->offset_x = offset_x;\n      this->offset_y = offset_y;\n\n      return 0;\n    }\n\n    /**\n     * @brief Initialize color conversion into target CUDA frame.\n     * @param frame Destination CUDA frame to write into.\n     * @param hw_frames_ctx_buf FFmpeg hardware frame context.\n     * @return 0 on success or -1 on failure.\n     */\n    int\n    set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override {\n      this->hwframe.reset(frame);\n      this->frame = frame;\n\n      if (!frame->buf[0]) {\n        if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) {\n          BOOST_LOG(error) << \"Couldn't get hwframe for VAAPI\"sv;\n          return -1;\n        }\n      }\n\n      auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data;\n      sw_format = hw_frames_ctx->sw_format;\n\n      auto nv12_opt = egl::create_target(frame->width, frame->height, sw_format);\n      if (!nv12_opt) {\n        return -1;\n      }\n\n      auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, sw_format);\n      if (!sws_opt) {\n        return -1;\n      }\n\n      this->sws = std::move(*sws_opt);\n      this->nv12 = std::move(*nv12_opt);\n\n      auto cuda_ctx = (AVCUDADeviceContext *) hw_frames_ctx->device_ctx->hwctx;\n\n      stream = make_stream();\n      if (!stream) {\n        return -1;\n      }\n\n      cuda_ctx->stream = stream.get();\n\n      CU_CHECK(cdf->cuGraphicsGLRegisterImage(&y_res, nv12->tex[0], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY),\n        \"Couldn't register Y plane texture\");\n      CU_CHECK(cdf->cuGraphicsGLRegisterImage(&uv_res, nv12->tex[1], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY),\n        \"Couldn't register UV plane texture\");\n\n      return 0;\n    }\n\n    /**\n     * @brief Convert the captured image into the target CUDA frame.\n     * @param img Captured screen image.\n     * @return 0 on success or -1 on failure.\n     */\n    int\n    convert(platf::img_t &img) override {\n      auto &descriptor = (egl::img_descriptor_t &) img;\n\n      if (descriptor.sequence == 0) {\n        // For dummy images, use a blank RGB texture instead of importing a DMA-BUF\n        rgb = egl::create_blank(img);\n      }\n      else if (descriptor.sequence > sequence) {\n        sequence = descriptor.sequence;\n\n        rgb = egl::rgb_t {};\n\n        auto rgb_opt = egl::import_source(display.get(), descriptor.sd);\n\n        if (!rgb_opt) {\n          return -1;\n        }\n\n        rgb = std::move(*rgb_opt);\n      }\n\n      // Perform the color conversion and scaling in GL\n      sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0]);\n      sws.convert(nv12->buf);\n\n      auto fmt_desc = av_pix_fmt_desc_get(sw_format);\n\n      // Map the GL textures to read for CUDA\n      CUgraphicsResource resources[2] = { y_res.get(), uv_res.get() };\n      CU_CHECK(cdf->cuGraphicsMapResources(2, resources, stream.get()), \"Couldn't map GL textures in CUDA\");\n\n      // Copy from the GL textures to the target CUDA frame\n      for (int i = 0; i < 2; i++) {\n        CUDA_MEMCPY2D cpy = {};\n        cpy.srcMemoryType = CU_MEMORYTYPE_ARRAY;\n        CU_CHECK(cdf->cuGraphicsSubResourceGetMappedArray(&cpy.srcArray, resources[i], 0, 0), \"Couldn't get mapped plane array\");\n\n        cpy.dstMemoryType = CU_MEMORYTYPE_DEVICE;\n        cpy.dstDevice = (CUdeviceptr) frame->data[i];\n        cpy.dstPitch = frame->linesize[i];\n        cpy.WidthInBytes = (frame->width * fmt_desc->comp[i].step) >> (i ? fmt_desc->log2_chroma_w : 0);\n        cpy.Height = frame->height >> (i ? fmt_desc->log2_chroma_h : 0);\n\n        CU_CHECK_IGNORE(cdf->cuMemcpy2DAsync(&cpy, stream.get()), \"Couldn't copy texture to CUDA frame\");\n      }\n\n      // Unmap the textures to allow modification from GL again\n      CU_CHECK(cdf->cuGraphicsUnmapResources(2, resources, stream.get()), \"Couldn't unmap GL textures from CUDA\");\n      return 0;\n    }\n\n    /**\n     * @brief Configures shader parameters for the specified colorspace.\n     */\n    void\n    apply_colorspace() override {\n      sws.apply_colorspace(colorspace);\n    }\n\n    file_t file;\n    gbm::gbm_t gbm;\n    egl::display_t display;\n    egl::ctx_t ctx;\n\n    // This must be destroyed before display_t\n    stream_t stream;\n    frame_t hwframe;\n\n    egl::sws_t sws;\n    egl::nv12_t nv12;\n    AVPixelFormat sw_format;\n\n    int width, height;\n\n    std::uint64_t sequence;\n    egl::rgb_t rgb;\n\n    registered_resource_t y_res;\n    registered_resource_t uv_res;\n\n    int offset_x, offset_y;\n  };\n\n  std::unique_ptr<platf::avcodec_encode_device_t>\n  make_avcodec_encode_device(int width, int height, bool vram) {\n    if (init()) {\n      return nullptr;\n    }\n\n    std::unique_ptr<cuda_t> cuda;\n\n    if (vram) {\n      cuda = std::make_unique<cuda_vram_t>();\n    }\n    else {\n      cuda = std::make_unique<cuda_ram_t>();\n    }\n\n    if (cuda->init(width, height)) {\n      return nullptr;\n    }\n\n    return cuda;\n  }\n\n  /**\n   * @brief Create a GL->CUDA encoding device for consuming captured dmabufs.\n   * @param width Width of captured frames.\n   * @param height Height of captured frames.\n   * @param offset_x Offset of content in captured frame.\n   * @param offset_y Offset of content in captured frame.\n   * @return FFmpeg encoding device context.\n   */\n  std::unique_ptr<platf::avcodec_encode_device_t>\n  make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y) {\n    if (init()) {\n      return nullptr;\n    }\n\n    auto cuda = std::make_unique<gl_cuda_vram_t>();\n\n    if (cuda->init(width, height, offset_x, offset_y)) {\n      return nullptr;\n    }\n\n    return cuda;\n  }\n\n  namespace nvfbc {\n    static PNVFBCCREATEINSTANCE createInstance {};\n    static NVFBC_API_FUNCTION_LIST func { NVFBC_VERSION };\n\n    static constexpr inline NVFBC_BOOL\n    nv_bool(bool b) {\n      return b ? NVFBC_TRUE : NVFBC_FALSE;\n    }\n\n    static void *handle { nullptr };\n    int\n    init() {\n      static bool funcs_loaded = false;\n\n      if (funcs_loaded) return 0;\n\n      if (!handle) {\n        handle = dyn::handle({ \"libnvidia-fbc.so.1\", \"libnvidia-fbc.so\" });\n        if (!handle) {\n          return -1;\n        }\n      }\n\n      std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {\n        { (dyn::apiproc *) &createInstance, \"NvFBCCreateInstance\" },\n      };\n\n      if (dyn::load(handle, funcs)) {\n        dlclose(handle);\n        handle = nullptr;\n\n        return -1;\n      }\n\n      auto status = cuda::nvfbc::createInstance(&cuda::nvfbc::func);\n      if (status) {\n        BOOST_LOG(error) << \"Unable to create NvFBC instance\"sv;\n\n        dlclose(handle);\n        handle = nullptr;\n        return -1;\n      }\n\n      funcs_loaded = true;\n      return 0;\n    }\n\n    class ctx_t {\n    public:\n      ctx_t(NVFBC_SESSION_HANDLE handle) {\n        NVFBC_BIND_CONTEXT_PARAMS params { NVFBC_BIND_CONTEXT_PARAMS_VER };\n\n        if (func.nvFBCBindContext(handle, &params)) {\n          BOOST_LOG(error) << \"Couldn't bind NvFBC context to current thread: \" << func.nvFBCGetLastErrorStr(handle);\n        }\n\n        this->handle = handle;\n      }\n\n      ~ctx_t() {\n        NVFBC_RELEASE_CONTEXT_PARAMS params { NVFBC_RELEASE_CONTEXT_PARAMS_VER };\n        if (func.nvFBCReleaseContext(handle, &params)) {\n          BOOST_LOG(error) << \"Couldn't release NvFBC context from current thread: \" << func.nvFBCGetLastErrorStr(handle);\n        }\n      }\n\n      NVFBC_SESSION_HANDLE handle;\n    };\n\n    class handle_t {\n      enum flag_e {\n        SESSION_HANDLE,\n        SESSION_CAPTURE,\n        MAX_FLAGS,\n      };\n\n    public:\n      handle_t() = default;\n      handle_t(handle_t &&other):\n          handle_flags { other.handle_flags }, handle { other.handle } {\n        other.handle_flags.reset();\n      }\n\n      handle_t &\n      operator=(handle_t &&other) {\n        std::swap(handle_flags, other.handle_flags);\n        std::swap(handle, other.handle);\n\n        return *this;\n      }\n\n      static std::optional<handle_t>\n      make() {\n        NVFBC_CREATE_HANDLE_PARAMS params { NVFBC_CREATE_HANDLE_PARAMS_VER };\n\n        // Set privateData to allow NvFBC on consumer NVIDIA GPUs.\n        // Based on https://github.com/keylase/nvidia-patch/blob/3193b4b1cea91527bf09ea9b8db5aade6a3f3c0a/win/nvfbcwrp/nvfbcwrp_main.cpp#L23-L25 .\n        const unsigned int MAGIC_PRIVATE_DATA[4] = { 0xAEF57AC5, 0x401D1A39, 0x1B856BBE, 0x9ED0CEBA };\n        params.privateData = MAGIC_PRIVATE_DATA;\n        params.privateDataSize = sizeof(MAGIC_PRIVATE_DATA);\n\n        handle_t handle;\n        auto status = func.nvFBCCreateHandle(&handle.handle, &params);\n        if (status) {\n          BOOST_LOG(error) << \"Failed to create session: \"sv << handle.last_error();\n\n          return std::nullopt;\n        }\n\n        handle.handle_flags[SESSION_HANDLE] = true;\n\n        return handle;\n      }\n\n      const char *\n      last_error() {\n        return func.nvFBCGetLastErrorStr(handle);\n      }\n\n      std::optional<NVFBC_GET_STATUS_PARAMS>\n      status() {\n        NVFBC_GET_STATUS_PARAMS params { NVFBC_GET_STATUS_PARAMS_VER };\n\n        auto status = func.nvFBCGetStatus(handle, &params);\n        if (status) {\n          BOOST_LOG(error) << \"Failed to get NvFBC status: \"sv << last_error();\n\n          return std::nullopt;\n        }\n\n        return params;\n      }\n\n      int\n      capture(NVFBC_CREATE_CAPTURE_SESSION_PARAMS &capture_params) {\n        if (func.nvFBCCreateCaptureSession(handle, &capture_params)) {\n          BOOST_LOG(error) << \"Failed to start capture session: \"sv << last_error();\n          return -1;\n        }\n\n        handle_flags[SESSION_CAPTURE] = true;\n\n        NVFBC_TOCUDA_SETUP_PARAMS setup_params {\n          NVFBC_TOCUDA_SETUP_PARAMS_VER,\n          NVFBC_BUFFER_FORMAT_BGRA,\n        };\n\n        if (func.nvFBCToCudaSetUp(handle, &setup_params)) {\n          BOOST_LOG(error) << \"Failed to setup cuda interop with nvFBC: \"sv << last_error();\n          return -1;\n        }\n        return 0;\n      }\n\n      int\n      stop() {\n        if (!handle_flags[SESSION_CAPTURE]) {\n          return 0;\n        }\n\n        NVFBC_DESTROY_CAPTURE_SESSION_PARAMS params { NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER };\n\n        if (func.nvFBCDestroyCaptureSession(handle, &params)) {\n          BOOST_LOG(error) << \"Couldn't destroy capture session: \"sv << last_error();\n\n          return -1;\n        }\n\n        handle_flags[SESSION_CAPTURE] = false;\n\n        return 0;\n      }\n\n      int\n      reset() {\n        if (!handle_flags[SESSION_HANDLE]) {\n          return 0;\n        }\n\n        stop();\n\n        NVFBC_DESTROY_HANDLE_PARAMS params { NVFBC_DESTROY_HANDLE_PARAMS_VER };\n\n        ctx_t ctx { handle };\n        if (func.nvFBCDestroyHandle(handle, &params)) {\n          BOOST_LOG(error) << \"Couldn't destroy session handle: \"sv << func.nvFBCGetLastErrorStr(handle);\n        }\n\n        handle_flags[SESSION_HANDLE] = false;\n\n        return 0;\n      }\n\n      ~handle_t() {\n        reset();\n      }\n\n      std::bitset<MAX_FLAGS> handle_flags;\n\n      NVFBC_SESSION_HANDLE handle;\n    };\n\n    class display_t: public platf::display_t {\n    public:\n      int\n      init(const std::string_view &display_name, const ::video::config_t &config) {\n        auto handle = handle_t::make();\n        if (!handle) {\n          return -1;\n        }\n\n        ctx_t ctx { handle->handle };\n\n        auto status_params = handle->status();\n        if (!status_params) {\n          return -1;\n        }\n\n        int streamedMonitor = -1;\n        if (!display_name.empty()) {\n          if (status_params->bXRandRAvailable) {\n            auto monitor_nr = util::from_view(display_name);\n\n            if (monitor_nr < 0 || monitor_nr >= status_params->dwOutputNum) {\n              BOOST_LOG(warning) << \"Can't stream monitor [\"sv << monitor_nr << \"], it needs to be between [0] and [\"sv << status_params->dwOutputNum - 1 << \"], defaulting to virtual desktop\"sv;\n            }\n            else {\n              streamedMonitor = monitor_nr;\n            }\n          }\n          else {\n            BOOST_LOG(warning) << \"XrandR not available, streaming entire virtual desktop\"sv;\n          }\n        }\n\n        delay = std::chrono::nanoseconds { 1s } / config.framerate;\n\n        capture_params = NVFBC_CREATE_CAPTURE_SESSION_PARAMS { NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER };\n\n        capture_params.eCaptureType = NVFBC_CAPTURE_SHARED_CUDA;\n        capture_params.bDisableAutoModesetRecovery = nv_bool(true);\n\n        capture_params.dwSamplingRateMs = 1000 /* ms */ / config.framerate;\n\n        if (streamedMonitor != -1) {\n          auto &output = status_params->outputs[streamedMonitor];\n\n          width = output.trackedBox.w;\n          height = output.trackedBox.h;\n          offset_x = output.trackedBox.x;\n          offset_y = output.trackedBox.y;\n\n          capture_params.eTrackingType = NVFBC_TRACKING_OUTPUT;\n          capture_params.dwOutputId = output.dwId;\n        }\n        else {\n          capture_params.eTrackingType = NVFBC_TRACKING_SCREEN;\n\n          width = status_params->screenSize.w;\n          height = status_params->screenSize.h;\n        }\n\n        env_width = status_params->screenSize.w;\n        env_height = status_params->screenSize.h;\n\n        this->handle = std::move(*handle);\n        return 0;\n      }\n\n      platf::capture_e\n      capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {\n        auto next_frame = std::chrono::steady_clock::now();\n\n        {\n          // We must create at least one texture on this thread before calling NvFBCToCudaSetUp()\n          // Otherwise it fails with \"Unable to register an OpenGL buffer to a CUDA resource (result: 201)\" message\n          std::shared_ptr<platf::img_t> img_dummy;\n          pull_free_image_cb(img_dummy);\n        }\n\n        // Force display_t::capture to initialize handle_t::capture\n        cursor_visible = !*cursor;\n\n        ctx_t ctx { handle.handle };\n        auto fg = util::fail_guard([&]() {\n          handle.reset();\n        });\n\n        sleep_overshoot_logger.reset();\n\n        while (true) {\n          auto now = std::chrono::steady_clock::now();\n          if (next_frame > now) {\n            std::this_thread::sleep_for(next_frame - now);\n            sleep_overshoot_logger.first_point(next_frame);\n            sleep_overshoot_logger.second_point_now_and_log();\n          }\n\n          next_frame += delay;\n          if (next_frame < now) {  // some major slowdown happened; we couldn't keep up\n            next_frame = now + delay;\n          }\n\n          std::shared_ptr<platf::img_t> img_out;\n          auto status = snapshot(pull_free_image_cb, img_out, 150ms, *cursor);\n          switch (status) {\n            case platf::capture_e::reinit:\n            case platf::capture_e::error:\n            case platf::capture_e::interrupted:\n              return status;\n            case platf::capture_e::timeout:\n              if (!push_captured_image_cb(std::move(img_out), false)) {\n                return platf::capture_e::ok;\n              }\n              break;\n            case platf::capture_e::ok:\n              if (!push_captured_image_cb(std::move(img_out), true)) {\n                return platf::capture_e::ok;\n              }\n              break;\n            default:\n              BOOST_LOG(error) << \"Unrecognized capture status [\"sv << (int) status << ']';\n              return status;\n          }\n        }\n\n        return platf::capture_e::ok;\n      }\n\n      // Reinitialize the capture session.\n      platf::capture_e\n      reinit(bool cursor) {\n        if (handle.stop()) {\n          return platf::capture_e::error;\n        }\n\n        cursor_visible = cursor;\n        if (cursor) {\n          capture_params.bPushModel = nv_bool(false);\n          capture_params.bWithCursor = nv_bool(true);\n          capture_params.bAllowDirectCapture = nv_bool(false);\n        }\n        else {\n          capture_params.bPushModel = nv_bool(true);\n          capture_params.bWithCursor = nv_bool(false);\n          capture_params.bAllowDirectCapture = nv_bool(true);\n        }\n\n        if (handle.capture(capture_params)) {\n          return platf::capture_e::error;\n        }\n\n        // If trying to capture directly, test if it actually does.\n        if (capture_params.bAllowDirectCapture) {\n          CUdeviceptr device_ptr;\n          NVFBC_FRAME_GRAB_INFO info;\n\n          NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab {\n            NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER,\n            NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT,\n            &device_ptr,\n            &info,\n            0,\n          };\n\n          // Direct Capture may fail the first few times, even if it's possible\n          for (int x = 0; x < 3; ++x) {\n            if (auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) {\n              if (status == NVFBC_ERR_MUST_RECREATE) {\n                return platf::capture_e::reinit;\n              }\n\n              BOOST_LOG(error) << \"Couldn't capture nvFramebuffer: \"sv << handle.last_error();\n\n              return platf::capture_e::error;\n            }\n\n            if (info.bDirectCapture) {\n              break;\n            }\n\n            BOOST_LOG(debug) << \"Direct capture failed attempt [\"sv << x << ']';\n          }\n\n          if (!info.bDirectCapture) {\n            BOOST_LOG(debug) << \"Direct capture failed, trying the extra copy method\"sv;\n            // Direct capture failed\n            capture_params.bPushModel = nv_bool(false);\n            capture_params.bWithCursor = nv_bool(false);\n            capture_params.bAllowDirectCapture = nv_bool(false);\n\n            if (handle.stop() || handle.capture(capture_params)) {\n              return platf::capture_e::error;\n            }\n          }\n        }\n\n        return platf::capture_e::ok;\n      }\n\n      platf::capture_e\n      snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {\n        if (cursor != cursor_visible) {\n          auto status = reinit(cursor);\n          if (status != platf::capture_e::ok) {\n            return status;\n          }\n        }\n\n        CUdeviceptr device_ptr;\n        NVFBC_FRAME_GRAB_INFO info;\n\n        NVFBC_TOCUDA_GRAB_FRAME_PARAMS grab {\n          NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER,\n          NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT,\n          &device_ptr,\n          &info,\n          (std::uint32_t) timeout.count(),\n        };\n\n        if (auto status = func.nvFBCToCudaGrabFrame(handle.handle, &grab)) {\n          if (status == NVFBC_ERR_MUST_RECREATE) {\n            return platf::capture_e::reinit;\n          }\n\n          BOOST_LOG(error) << \"Couldn't capture nvFramebuffer: \"sv << handle.last_error();\n          return platf::capture_e::error;\n        }\n\n        if (!pull_free_image_cb(img_out)) {\n          return platf::capture_e::interrupted;\n        }\n        auto img = (img_t *) img_out.get();\n\n        if (img->tex.copy((std::uint8_t *) device_ptr, img->height, img->row_pitch)) {\n          return platf::capture_e::error;\n        }\n\n        return platf::capture_e::ok;\n      }\n\n      std::unique_ptr<platf::avcodec_encode_device_t>\n      make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) {\n        return ::cuda::make_avcodec_encode_device(width, height, true);\n      }\n\n      std::shared_ptr<platf::img_t>\n      alloc_img() override {\n        auto img = std::make_shared<cuda::img_t>();\n\n        img->data = nullptr;\n        img->width = width;\n        img->height = height;\n        img->pixel_pitch = 4;\n        img->row_pitch = img->width * img->pixel_pitch;\n\n        auto tex_opt = tex_t::make(height, width * img->pixel_pitch);\n        if (!tex_opt) {\n          return nullptr;\n        }\n\n        img->tex = std::move(*tex_opt);\n\n        return img;\n      };\n\n      int\n      dummy_img(platf::img_t *) override {\n        return 0;\n      }\n\n      std::chrono::nanoseconds delay;\n\n      bool cursor_visible;\n      handle_t handle;\n\n      NVFBC_CREATE_CAPTURE_SESSION_PARAMS capture_params;\n    };\n  }  // namespace nvfbc\n}  // namespace cuda\n\nnamespace platf {\n  std::shared_ptr<display_t>\n  nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {\n    if (hwdevice_type != mem_type_e::cuda) {\n      BOOST_LOG(error) << \"Could not initialize nvfbc display with the given hw device type\"sv;\n      return nullptr;\n    }\n\n    auto display = std::make_shared<cuda::nvfbc::display_t>();\n\n    if (display->init(display_name, config)) {\n      return nullptr;\n    }\n\n    return display;\n  }\n\n  std::vector<std::string>\n  nvfbc_display_names() {\n    if (cuda::init() || cuda::nvfbc::init()) {\n      return {};\n    }\n\n    std::vector<std::string> display_names;\n\n    auto handle = cuda::nvfbc::handle_t::make();\n    if (!handle) {\n      return {};\n    }\n\n    auto status_params = handle->status();\n    if (!status_params) {\n      return {};\n    }\n\n    if (!status_params->bIsCapturePossible) {\n      BOOST_LOG(error) << \"NVidia driver doesn't support NvFBC screencasting\"sv;\n    }\n\n    BOOST_LOG(info) << \"Found [\"sv << status_params->dwOutputNum << \"] outputs\"sv;\n    BOOST_LOG(info) << \"Virtual Desktop: \"sv << status_params->screenSize.w << 'x' << status_params->screenSize.h;\n    BOOST_LOG(info) << \"XrandR: \"sv << (status_params->bXRandRAvailable ? \"available\"sv : \"unavailable\"sv);\n\n    for (auto x = 0; x < status_params->dwOutputNum; ++x) {\n      auto &output = status_params->outputs[x];\n      BOOST_LOG(info) << \"-- Output --\"sv;\n      BOOST_LOG(debug) << \"  ID: \"sv << output.dwId;\n      BOOST_LOG(debug) << \"  Name: \"sv << output.name;\n      BOOST_LOG(info) << \"  Resolution: \"sv << output.trackedBox.w << 'x' << output.trackedBox.h;\n      BOOST_LOG(info) << \"  Offset: \"sv << output.trackedBox.x << 'x' << output.trackedBox.y;\n      display_names.emplace_back(std::to_string(x));\n    }\n\n    return display_names;\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/linux/cuda.cu",
    "content": "/**\n * @file src/platform/linux/cuda.cu\n * @brief CUDA implementation for Linux.\n */\n// #include <algorithm>\n#include <helper_math.h>\n#include <chrono>\n#include <limits>\n#include <memory>\n#include <optional>\n#include <string_view>\n\n#include \"cuda.h\"\n\nusing namespace std::literals;\n\n#define SUNSHINE_STRINGVIEW_HELPER(x) x##sv\n#define SUNSHINE_STRINGVIEW(x) SUNSHINE_STRINGVIEW_HELPER(x)\n\n#define CU_CHECK(x, y) \\\n  if(check((x), SUNSHINE_STRINGVIEW(y \": \"))) return -1\n\n#define CU_CHECK_VOID(x, y) \\\n  if(check((x), SUNSHINE_STRINGVIEW(y \": \"))) return;\n\n#define CU_CHECK_PTR(x, y) \\\n  if(check((x), SUNSHINE_STRINGVIEW(y \": \"))) return nullptr;\n\n#define CU_CHECK_OPT(x, y) \\\n  if(check((x), SUNSHINE_STRINGVIEW(y \": \"))) return std::nullopt;\n\n#define CU_CHECK_IGNORE(x, y) \\\n  check((x), SUNSHINE_STRINGVIEW(y \": \"))\n\nusing namespace std::literals;\n\n// Special declarations\n/**\n * NVCC tends to have problems with standard headers.\n * Don't include common.h, instead use bare minimum\n * of standard headers and duplicate declarations of necessary classes.\n * Not pretty and extremely error-prone, fix at earliest convenience.\n */\nnamespace platf {\nstruct img_t: std::enable_shared_from_this<img_t> {\npublic:\n  std::uint8_t *data {};\n  std::int32_t width {};\n  std::int32_t height {};\n  std::int32_t pixel_pitch {};\n  std::int32_t row_pitch {};\n\n  std::optional<std::chrono::steady_clock::time_point> frame_timestamp;\n\n  virtual ~img_t() = default;\n};\n} // namespace platf\n\n// End special declarations\n\nnamespace cuda {\n\nstruct alignas(16) cuda_color_t {\n  float4 color_vec_y;\n  float4 color_vec_u;\n  float4 color_vec_v;\n  float2 range_y;\n  float2 range_uv;\n};\n\nstatic_assert(sizeof(video::color_t) == sizeof(cuda::cuda_color_t), \"color matrix struct mismatch\");\n\nauto constexpr INVALID_TEXTURE = std::numeric_limits<cudaTextureObject_t>::max();\n\ntemplate<class T>\ninline T div_align(T l, T r) {\n  return (l + r - 1) / r;\n}\n\nvoid pass_error(const std::string_view &sv, const char *name, const char *description);\ninline static int check(cudaError_t result, const std::string_view &sv) {\n  if(result) {\n    auto name        = cudaGetErrorName(result);\n    auto description = cudaGetErrorString(result);\n\n    pass_error(sv, name, description);\n    return -1;\n  }\n\n  return 0;\n}\n\ntemplate<class T>\nptr_t make_ptr() {\n  void *p;\n  CU_CHECK_PTR(cudaMalloc(&p, sizeof(T)), \"Couldn't allocate color matrix\");\n\n  ptr_t ptr { p };\n\n  return ptr;\n}\n\nvoid freeCudaPtr_t::operator()(void *ptr) {\n  CU_CHECK_IGNORE(cudaFree(ptr), \"Couldn't free cuda device pointer\");\n}\n\nvoid freeCudaStream_t::operator()(cudaStream_t ptr) {\n  CU_CHECK_IGNORE(cudaStreamDestroy(ptr), \"Couldn't free cuda stream\");\n}\n\nstream_t make_stream(int flags) {\n  cudaStream_t stream;\n\n  if(!flags) {\n    CU_CHECK_PTR(cudaStreamCreate(&stream), \"Couldn't create cuda stream\");\n  }\n  else {\n    CU_CHECK_PTR(cudaStreamCreateWithFlags(&stream, flags), \"Couldn't create cuda stream with flags\");\n  }\n\n  return stream_t { stream };\n}\n\ninline __device__ float3 bgra_to_rgb(uchar4 vec) {\n  return make_float3((float)vec.z, (float)vec.y, (float)vec.x);\n}\n\ninline __device__ float3 bgra_to_rgb(float4 vec) {\n  return make_float3(vec.z, vec.y, vec.x);\n}\n\ninline __device__ float2 calcUV(float3 pixel, const cuda_color_t *const color_matrix) {\n  float4 vec_u = color_matrix->color_vec_u;\n  float4 vec_v = color_matrix->color_vec_v;\n\n  float u = dot(pixel, make_float3(vec_u)) + vec_u.w;\n  float v = dot(pixel, make_float3(vec_v)) + vec_v.w;\n\n  u = u * color_matrix->range_uv.x + color_matrix->range_uv.y;\n  v = v * color_matrix->range_uv.x + color_matrix->range_uv.y;\n\n  return make_float2(u, v);\n}\n\ninline __device__ float calcY(float3 pixel, const cuda_color_t *const color_matrix) {\n  float4 vec_y = color_matrix->color_vec_y;\n\n  return (dot(pixel, make_float3(vec_y)) + vec_y.w) * color_matrix->range_y.x + color_matrix->range_y.y;\n}\n\n__global__ void RGBA_to_NV12(\n  cudaTextureObject_t srcImage, std::uint8_t *dstY, std::uint8_t *dstUV,\n  std::uint32_t dstPitchY, std::uint32_t dstPitchUV,\n  float scale, const viewport_t viewport, const cuda_color_t *const color_matrix) {\n\n  int idX = (threadIdx.x + blockDim.x * blockIdx.x) * 2;\n  int idY = (threadIdx.y + blockDim.y * blockIdx.y) * 2;\n\n  if(idX >= viewport.width) return;\n  if(idY >= viewport.height) return;\n\n  float x = idX * scale;\n  float y = idY * scale;\n\n  idX += viewport.offsetX;\n  idY += viewport.offsetY;\n\n  uint8_t *dstY0  = dstY + idX + idY * dstPitchY;\n  uint8_t *dstY1  = dstY + idX + (idY + 1) * dstPitchY;\n  dstUV = dstUV + idX + (idY / 2 * dstPitchUV);\n\n  float3 rgb_lt = bgra_to_rgb(tex2D<float4>(srcImage, x, y));\n  float3 rgb_rt = bgra_to_rgb(tex2D<float4>(srcImage, x + scale, y));\n  float3 rgb_lb = bgra_to_rgb(tex2D<float4>(srcImage, x, y + scale));\n  float3 rgb_rb = bgra_to_rgb(tex2D<float4>(srcImage, x + scale, y + scale));\n\n  float2 uv_lt = calcUV(rgb_lt, color_matrix) * 256.0f;\n  float2 uv_rt = calcUV(rgb_rt, color_matrix) * 256.0f;\n  float2 uv_lb = calcUV(rgb_lb, color_matrix) * 256.0f;\n  float2 uv_rb = calcUV(rgb_rb, color_matrix) * 256.0f;\n\n  float2 uv = (uv_lt + uv_lb + uv_rt + uv_rb) * 0.25f;\n\n  dstUV[0] = uv.x;\n  dstUV[1] = uv.y;\n  dstY0[0]  = calcY(rgb_lt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible\n  dstY0[1]  = calcY(rgb_rt, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible\n  dstY1[0]  = calcY(rgb_lb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible\n  dstY1[1]  = calcY(rgb_rb, color_matrix) * 245.0f; // 245.0f is a magic number to ensure slight changes in luminosity are more visible\n}\n\nint tex_t::copy(std::uint8_t *src, int height, int pitch) {\n  CU_CHECK(cudaMemcpy2DToArray(array, 0, 0, src, pitch, pitch, height, cudaMemcpyDeviceToDevice), \"Couldn't copy to cuda array from deviceptr\");\n\n  return 0;\n}\n\nstd::optional<tex_t> tex_t::make(int height, int pitch) {\n  tex_t tex;\n\n  auto format = cudaCreateChannelDesc<uchar4>();\n  CU_CHECK_OPT(cudaMallocArray(&tex.array, &format, pitch, height, cudaArrayDefault), \"Couldn't allocate cuda array\");\n\n  cudaResourceDesc res {};\n  res.resType         = cudaResourceTypeArray;\n  res.res.array.array = tex.array;\n\n  cudaTextureDesc desc {};\n\n  desc.readMode         = cudaReadModeNormalizedFloat;\n  desc.filterMode       = cudaFilterModePoint;\n  desc.normalizedCoords = false;\n\n  std::fill_n(std::begin(desc.addressMode), 2, cudaAddressModeClamp);\n\n  CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.point, &res, &desc, nullptr), \"Couldn't create cuda texture that uses point interpolation\");\n\n  desc.filterMode = cudaFilterModeLinear;\n\n  CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.linear, &res, &desc, nullptr), \"Couldn't create cuda texture that uses linear interpolation\");\n\n  return tex;\n}\n\ntex_t::tex_t() : array {}, texture { INVALID_TEXTURE, INVALID_TEXTURE } {}\ntex_t::tex_t(tex_t &&other) : array { other.array }, texture { other.texture } {\n  other.array          = 0;\n  other.texture.point  = INVALID_TEXTURE;\n  other.texture.linear = INVALID_TEXTURE;\n}\n\ntex_t &tex_t::operator=(tex_t &&other) {\n  std::swap(array, other.array);\n  std::swap(texture, other.texture);\n\n  return *this;\n}\n\ntex_t::~tex_t() {\n  if(texture.point != INVALID_TEXTURE) {\n    CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.point), \"Couldn't deallocate cuda texture that uses point interpolation\");\n\n    texture.point = INVALID_TEXTURE;\n  }\n\n  if(texture.linear != INVALID_TEXTURE) {\n    CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.linear), \"Couldn't deallocate cuda texture that uses linear interpolation\");\n\n    texture.linear = INVALID_TEXTURE;\n  }\n\n  if(array) {\n    CU_CHECK_IGNORE(cudaFreeArray(array), \"Couldn't deallocate cuda array\");\n\n    array = cudaArray_t {};\n  }\n}\n\nsws_t::sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix)\n    : threadsPerBlock { threadsPerBlock }, color_matrix { std::move(color_matrix) } {\n  // Ensure aspect ratio is maintained\n  auto scalar       = std::fminf(out_width / (float)in_width, out_height / (float)in_height);\n  auto out_width_f  = in_width * scalar;\n  auto out_height_f = in_height * scalar;\n\n  // result is always positive\n  auto offsetX_f = (out_width - out_width_f) / 2;\n  auto offsetY_f = (out_height - out_height_f) / 2;\n\n  viewport.width  = out_width_f;\n  viewport.height = out_height_f;\n\n  viewport.offsetX = offsetX_f;\n  viewport.offsetY = offsetY_f;\n\n  scale = 1.0f / scalar;\n}\n\nstd::optional<sws_t> sws_t::make(int in_width, int in_height, int out_width, int out_height, int pitch) {\n  cudaDeviceProp props;\n  int device;\n  CU_CHECK_OPT(cudaGetDevice(&device), \"Couldn't get cuda device\");\n  CU_CHECK_OPT(cudaGetDeviceProperties(&props, device), \"Couldn't get cuda device properties\");\n\n  auto ptr = make_ptr<cuda_color_t>();\n  if(!ptr) {\n    return std::nullopt;\n  }\n\n  return std::make_optional<sws_t>(in_width, in_height, out_width, out_height, pitch, props.maxThreadsPerMultiProcessor / props.maxBlocksPerMultiProcessor, std::move(ptr));\n}\n\nint sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream) {\n  return convert(Y, UV, pitchY, pitchUV, texture, stream, viewport);\n}\n\nint sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport) {\n  int threadsX = viewport.width / 2;\n  int threadsY = viewport.height / 2;\n\n  dim3 block(threadsPerBlock);\n  dim3 grid(div_align(threadsX, threadsPerBlock), threadsY);\n\n  RGBA_to_NV12<<<grid, block, 0, stream>>>(texture, Y, UV, pitchY, pitchUV, scale, viewport, (cuda_color_t *)color_matrix.get());\n\n  return CU_CHECK_IGNORE(cudaGetLastError(), \"RGBA_to_NV12 failed\");\n}\n\nvoid sws_t::apply_colorspace(const video::sunshine_colorspace_t& colorspace) {\n  auto color_p = video::color_vectors_from_colorspace(colorspace);\n  CU_CHECK_IGNORE(cudaMemcpy(color_matrix.get(), color_p, sizeof(video::color_t), cudaMemcpyHostToDevice), \"Couldn't copy color matrix to cuda\");\n}\n\nint sws_t::load_ram(platf::img_t &img, cudaArray_t array) {\n  return CU_CHECK_IGNORE(cudaMemcpy2DToArray(array, 0, 0, img.data, img.row_pitch, img.width * img.pixel_pitch, img.height, cudaMemcpyHostToDevice), \"Couldn't copy to cuda array\");\n}\n\n    return make_float2(u, v);\n  }\n\n  inline __device__ float calcY(float3 pixel, const cuda_color_t *const color_matrix) {\n    float4 vec_y = color_matrix->color_vec_y;\n\n    return (dot(pixel, make_float3(vec_y)) + vec_y.w) * color_matrix->range_y.x + color_matrix->range_y.y;\n  }\n\n  __global__ void RGBA_to_NV12(\n    cudaTextureObject_t srcImage,\n    std::uint8_t *dstY,\n    std::uint8_t *dstUV,\n    std::uint32_t dstPitchY,\n    std::uint32_t dstPitchUV,\n    float scale,\n    const viewport_t viewport,\n    const cuda_color_t *const color_matrix\n  ) {\n    int idX = (threadIdx.x + blockDim.x * blockIdx.x) * 2;\n    int idY = (threadIdx.y + blockDim.y * blockIdx.y) * 2;\n\n    if (idX >= viewport.width) {\n      return;\n    }\n    if (idY >= viewport.height) {\n      return;\n    }\n\n    float x = idX * scale;\n    float y = idY * scale;\n\n    idX += viewport.offsetX;\n    idY += viewport.offsetY;\n\n    uint8_t *dstY0 = dstY + idX + idY * dstPitchY;\n    uint8_t *dstY1 = dstY + idX + (idY + 1) * dstPitchY;\n    dstUV = dstUV + idX + (idY / 2 * dstPitchUV);\n\n    float3 rgb_lt = bgra_to_rgb(tex2D<float4>(srcImage, x, y));\n    float3 rgb_rt = bgra_to_rgb(tex2D<float4>(srcImage, x + scale, y));\n    float3 rgb_lb = bgra_to_rgb(tex2D<float4>(srcImage, x, y + scale));\n    float3 rgb_rb = bgra_to_rgb(tex2D<float4>(srcImage, x + scale, y + scale));\n\n    float2 uv_lt = calcUV(rgb_lt, color_matrix) * 256.0f;\n    float2 uv_rt = calcUV(rgb_rt, color_matrix) * 256.0f;\n    float2 uv_lb = calcUV(rgb_lb, color_matrix) * 256.0f;\n    float2 uv_rb = calcUV(rgb_rb, color_matrix) * 256.0f;\n\n    float2 uv = (uv_lt + uv_lb + uv_rt + uv_rb) * 0.25f;\n\n    dstUV[0] = uv.x;\n    dstUV[1] = uv.y;\n    dstY0[0] = calcY(rgb_lt, color_matrix) * 245.0f;  // 245.0f is a magic number to ensure slight changes in luminosity are more visible\n    dstY0[1] = calcY(rgb_rt, color_matrix) * 245.0f;  // 245.0f is a magic number to ensure slight changes in luminosity are more visible\n    dstY1[0] = calcY(rgb_lb, color_matrix) * 245.0f;  // 245.0f is a magic number to ensure slight changes in luminosity are more visible\n    dstY1[1] = calcY(rgb_rb, color_matrix) * 245.0f;  // 245.0f is a magic number to ensure slight changes in luminosity are more visible\n  }\n\n  int tex_t::copy(std::uint8_t *src, int height, int pitch) {\n    CU_CHECK(cudaMemcpy2DToArray(array, 0, 0, src, pitch, pitch, height, cudaMemcpyDeviceToDevice), \"Couldn't copy to cuda array from deviceptr\");\n\n    return 0;\n  }\n\n  std::optional<tex_t> tex_t::make(int height, int pitch) {\n    tex_t tex;\n\n    auto format = cudaCreateChannelDesc<uchar4>();\n    CU_CHECK_OPT(cudaMallocArray(&tex.array, &format, pitch, height, cudaArrayDefault), \"Couldn't allocate cuda array\");\n\n    cudaResourceDesc res {};\n    res.resType = cudaResourceTypeArray;\n    res.res.array.array = tex.array;\n\n    cudaTextureDesc desc {};\n\n    desc.readMode = cudaReadModeNormalizedFloat;\n    desc.filterMode = cudaFilterModePoint;\n    desc.normalizedCoords = false;\n\n    std::fill_n(std::begin(desc.addressMode), 2, cudaAddressModeClamp);\n\n    CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.point, &res, &desc, nullptr), \"Couldn't create cuda texture that uses point interpolation\");\n\n    desc.filterMode = cudaFilterModeLinear;\n\n    CU_CHECK_OPT(cudaCreateTextureObject(&tex.texture.linear, &res, &desc, nullptr), \"Couldn't create cuda texture that uses linear interpolation\");\n\n    return tex;\n  }\n\n  tex_t::tex_t():\n      array {},\n      texture {INVALID_TEXTURE, INVALID_TEXTURE} {\n  }\n\n  tex_t::tex_t(tex_t &&other):\n      array {other.array},\n      texture {other.texture} {\n    other.array = 0;\n    other.texture.point = INVALID_TEXTURE;\n    other.texture.linear = INVALID_TEXTURE;\n  }\n\n  tex_t &tex_t::operator=(tex_t &&other) {\n    std::swap(array, other.array);\n    std::swap(texture, other.texture);\n\n    return *this;\n  }\n\n  tex_t::~tex_t() {\n    if (texture.point != INVALID_TEXTURE) {\n      CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.point), \"Couldn't deallocate cuda texture that uses point interpolation\");\n\n      texture.point = INVALID_TEXTURE;\n    }\n\n    if (texture.linear != INVALID_TEXTURE) {\n      CU_CHECK_IGNORE(cudaDestroyTextureObject(texture.linear), \"Couldn't deallocate cuda texture that uses linear interpolation\");\n\n      texture.linear = INVALID_TEXTURE;\n    }\n\n    if (array) {\n      CU_CHECK_IGNORE(cudaFreeArray(array), \"Couldn't deallocate cuda array\");\n\n      array = cudaArray_t {};\n    }\n  }\n\n  sws_t::sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix):\n      threadsPerBlock {threadsPerBlock},\n      color_matrix {std::move(color_matrix)} {\n    // Ensure aspect ratio is maintained\n    auto scalar = std::fminf(out_width / (float) in_width, out_height / (float) in_height);\n    auto out_width_f = in_width * scalar;\n    auto out_height_f = in_height * scalar;\n\n    // result is always positive\n    auto offsetX_f = (out_width - out_width_f) / 2;\n    auto offsetY_f = (out_height - out_height_f) / 2;\n\n    viewport.width = out_width_f;\n    viewport.height = out_height_f;\n\n    viewport.offsetX = offsetX_f;\n    viewport.offsetY = offsetY_f;\n\n    scale = 1.0f / scalar;\n  }\n\n  std::optional<sws_t> sws_t::make(int in_width, int in_height, int out_width, int out_height, int pitch) {\n    cudaDeviceProp props;\n    int device;\n    CU_CHECK_OPT(cudaGetDevice(&device), \"Couldn't get cuda device\");\n    CU_CHECK_OPT(cudaGetDeviceProperties(&props, device), \"Couldn't get cuda device properties\");\n\n    auto ptr = make_ptr<cuda_color_t>();\n    if (!ptr) {\n      return std::nullopt;\n    }\n\n    return std::make_optional<sws_t>(in_width, in_height, out_width, out_height, pitch, props.maxThreadsPerMultiProcessor / props.maxBlocksPerMultiProcessor, std::move(ptr));\n  }\n\n  int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream) {\n    return convert(Y, UV, pitchY, pitchUV, texture, stream, viewport);\n  }\n\n  int sws_t::convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport) {\n    int threadsX = viewport.width / 2;\n    int threadsY = viewport.height / 2;\n\n    dim3 block(threadsPerBlock);\n    dim3 grid(div_align(threadsX, threadsPerBlock), threadsY);\n\n    RGBA_to_NV12<<<grid, block, 0, stream>>>(texture, Y, UV, pitchY, pitchUV, scale, viewport, (cuda_color_t *) color_matrix.get());\n\n    return CU_CHECK_IGNORE(cudaGetLastError(), \"RGBA_to_NV12 failed\");\n  }\n\n  void sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) {\n    auto color_p = video::color_vectors_from_colorspace(colorspace, true);\n    CU_CHECK_IGNORE(cudaMemcpy(color_matrix.get(), color_p, sizeof(video::color_t), cudaMemcpyHostToDevice), \"Couldn't copy color matrix to cuda\");\n  }\n\n  int sws_t::load_ram(platf::img_t &img, cudaArray_t array) {\n    return CU_CHECK_IGNORE(cudaMemcpy2DToArray(array, 0, 0, img.data, img.row_pitch, img.width * img.pixel_pitch, img.height, cudaMemcpyHostToDevice), \"Couldn't copy to cuda array\");\n  }\n\n}  // namespace cuda\n"
  },
  {
    "path": "src/platform/linux/cuda.h",
    "content": "/**\n * @file src/platform/linux/cuda.h\n * @brief Definitions for CUDA implementation.\n */\n#pragma once\n\n#if defined(SUNSHINE_BUILD_CUDA)\n\n  #include \"src/video_colorspace.h\"\n\n  #include <cstdint>\n  #include <memory>\n  #include <optional>\n  #include <string>\n  #include <vector>\n\nnamespace platf {\n  class avcodec_encode_device_t;\n  class img_t;\n}  // namespace platf\n\nnamespace cuda {\n\n  namespace nvfbc {\n    std::vector<std::string>\n    display_names();\n  }\n  std::unique_ptr<platf::avcodec_encode_device_t>\n  make_avcodec_encode_device(int width, int height, bool vram);\n\n  /**\n   * @brief Create a GL->CUDA encoding device for consuming captured dmabufs.\n   * @param in_width Width of captured frames.\n   * @param in_height Height of captured frames.\n   * @param offset_x Offset of content in captured frame.\n   * @param offset_y Offset of content in captured frame.\n   * @return FFmpeg encoding device context.\n   */\n  std::unique_ptr<platf::avcodec_encode_device_t>\n  make_avcodec_gl_encode_device(int width, int height, int offset_x, int offset_y);\n\n  int\n  init();\n}  // namespace cuda\n\ntypedef struct cudaArray *cudaArray_t;\n\n  #if !defined(__CUDACC__)\ntypedef struct CUstream_st *cudaStream_t;\ntypedef unsigned long long cudaTextureObject_t;\n  #else /* defined(__CUDACC__) */\ntypedef __location__(device_builtin) struct CUstream_st *cudaStream_t;\ntypedef __location__(device_builtin) unsigned long long cudaTextureObject_t;\n  #endif /* !defined(__CUDACC__) */\n\nnamespace cuda {\n\n  class freeCudaPtr_t {\n  public:\n    void\n    operator()(void *ptr);\n  };\n\n  class freeCudaStream_t {\n  public:\n    void\n    operator()(cudaStream_t ptr);\n  };\n\n  using ptr_t = std::unique_ptr<void, freeCudaPtr_t>;\n  using stream_t = std::unique_ptr<CUstream_st, freeCudaStream_t>;\n\n  stream_t\n  make_stream(int flags = 0);\n\n  struct viewport_t {\n    int width, height;\n    int offsetX, offsetY;\n  };\n\n  class tex_t {\n  public:\n    static std::optional<tex_t>\n    make(int height, int pitch);\n\n    tex_t();\n    tex_t(tex_t &&);\n\n    tex_t &\n    operator=(tex_t &&other);\n\n    ~tex_t();\n\n    int\n    copy(std::uint8_t *src, int height, int pitch);\n\n    cudaArray_t array;\n\n    struct texture {\n      cudaTextureObject_t point;\n      cudaTextureObject_t linear;\n    } texture;\n  };\n\n  class sws_t {\n  public:\n    sws_t() = default;\n    sws_t(int in_width, int in_height, int out_width, int out_height, int pitch, int threadsPerBlock, ptr_t &&color_matrix);\n\n    /**\n     * in_width, in_height -- The width and height of the captured image in pixels\n     * out_width, out_height -- the width and height of the NV12 image in pixels\n     *\n     * pitch -- The size of a single row of pixels in bytes\n     */\n    static std::optional<sws_t>\n    make(int in_width, int in_height, int out_width, int out_height, int pitch);\n\n    // Converts loaded image into a CUDevicePtr\n    int\n    convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream);\n    int\n    convert(std::uint8_t *Y, std::uint8_t *UV, std::uint32_t pitchY, std::uint32_t pitchUV, cudaTextureObject_t texture, stream_t::pointer stream, const viewport_t &viewport);\n\n    void\n    apply_colorspace(const video::sunshine_colorspace_t &colorspace);\n\n    int\n    load_ram(platf::img_t &img, cudaArray_t array);\n\n    ptr_t color_matrix;\n\n    int threadsPerBlock;\n\n    viewport_t viewport;\n\n    float scale;\n  };\n}  // namespace cuda\n\n#endif"
  },
  {
    "path": "src/platform/linux/display_device.cpp",
    "content": "// local includes\n#include \"src/display_device/settings.h\"\n\nnamespace display_device {\n\n  device_info_map_t\n  enum_available_devices() {\n    // Not implemented\n    return {};\n  }\n\n  std::string\n  get_display_name(const std::string &value) {\n    // Not implemented, but just passthrough the value\n    return value;\n  }\n\n  device_display_mode_map_t\n  get_current_display_modes(const std::unordered_set<std::string> &) {\n    // Not implemented\n    return {};\n  }\n\n  bool\n  set_display_modes(const device_display_mode_map_t &) {\n    // Not implemented\n    return false;\n  }\n\n  bool\n  is_primary_device(const std::string &) {\n    // Not implemented\n    return false;\n  }\n\n  bool\n  set_as_primary_device(const std::string &) {\n    // Not implemented\n    return false;\n  }\n\n  hdr_state_map_t\n  get_current_hdr_states(const std::unordered_set<std::string> &) {\n    // Not implemented\n    return {};\n  }\n\n  bool\n  set_hdr_states(const hdr_state_map_t &) {\n    // Not implemented\n    return false;\n  }\n\n  active_topology_t\n  get_current_topology() {\n    // Not implemented\n    return {};\n  }\n\n  bool\n  is_topology_valid(const active_topology_t &topology) {\n    // Not implemented\n    return false;\n  }\n\n  bool\n  is_topology_the_same(const active_topology_t &a, const active_topology_t &b) {\n    // Not implemented\n    return false;\n  }\n\n  bool\n  set_topology(const active_topology_t &) {\n    // Not implemented\n    return false;\n  }\n\n  struct settings_t::audio_data_t {\n    // Not implemented\n  };\n\n  struct settings_t::persistent_data_t {\n    // Not implemented\n  };\n\n  settings_t::settings_t() {\n    // Not implemented\n  }\n\n  settings_t::~settings_t() {\n    // Not implemented\n  }\n\n  bool\n  settings_t::is_changing_settings_going_to_fail() const {\n    // Not implemented\n    return false;\n  }\n\n  settings_t::apply_result_t\n  settings_t::apply_config(const parsed_config_t &) {\n    // Not implemented\n    return { apply_result_t::result_e::success };\n  }\n\n  bool\n  settings_t::revert_settings(revert_reason_e reason, bool skip_vdd_destroy) {\n    // Not implemented\n    (void)reason;  // Unused parameter\n    (void)skip_vdd_destroy;  // Unused parameter\n    return true;\n  }\n\n  void\n  settings_t::reset_persistence() {\n    // Not implemented\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/linux/graphics.cpp",
    "content": "/**\n * @file src/platform/linux/graphics.cpp\n * @brief Definitions for graphics related functions.\n */\n#include \"graphics.h\"\n#include \"src/file_handler.h\"\n#include \"src/logging.h\"\n#include \"src/video.h\"\n\n#include <fcntl.h>\n\nextern \"C\" {\n#include <libavutil/pixdesc.h>\n}\n\n// I want to have as little build dependencies as possible\n// There aren't that many DRM_FORMAT I need to use, so define them here\n//\n// They aren't likely to change any time soon.\n#define fourcc_code(a, b, c, d) ((std::uint32_t)(a) | ((std::uint32_t)(b) << 8) | \\\n                                 ((std::uint32_t)(c) << 16) | ((std::uint32_t)(d) << 24))\n#define fourcc_mod_code(vendor, val) ((((uint64_t) vendor) << 56) | ((val) &0x00ffffffffffffffULL))\n#define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1))\n\n#if !defined(SUNSHINE_SHADERS_DIR)  // for testing this needs to be defined in cmake as we don't do an install\n  #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR \"/shaders/opengl\"\n#endif\n\nusing namespace std::literals;\nnamespace gl {\n  GladGLContext ctx;\n\n  void\n  drain_errors(const std::string_view &prefix) {\n    GLenum err;\n    while ((err = ctx.GetError()) != GL_NO_ERROR) {\n      BOOST_LOG(error) << \"GL: \"sv << prefix << \": [\"sv << util::hex(err).to_string_view() << ']';\n    }\n  }\n\n  tex_t::~tex_t() {\n    if (size() != 0) {\n      ctx.DeleteTextures(size(), begin());\n    }\n  }\n\n  tex_t\n  tex_t::make(std::size_t count) {\n    tex_t textures { count };\n\n    ctx.GenTextures(textures.size(), textures.begin());\n\n    float color[] = { 0.0f, 0.0f, 0.0f, 1.0f };\n\n    for (auto tex : textures) {\n      gl::ctx.BindTexture(GL_TEXTURE_2D, tex);\n      gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);  // x\n      gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);  // y\n      gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n      gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);\n      gl::ctx.TexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, color);\n    }\n\n    return textures;\n  }\n\n  frame_buf_t::~frame_buf_t() {\n    if (begin()) {\n      ctx.DeleteFramebuffers(size(), begin());\n    }\n  }\n\n  frame_buf_t\n  frame_buf_t::make(std::size_t count) {\n    frame_buf_t frame_buf { count };\n\n    ctx.GenFramebuffers(frame_buf.size(), frame_buf.begin());\n\n    return frame_buf;\n  }\n\n  void\n  frame_buf_t::copy(int id, int texture, int offset_x, int offset_y, int width, int height) {\n    gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[id]);\n    gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0 + id);\n    gl::ctx.BindTexture(GL_TEXTURE_2D, texture);\n    gl::ctx.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, offset_x, offset_y, width, height);\n  }\n\n  std::string\n  shader_t::err_str() {\n    int length;\n    ctx.GetShaderiv(handle(), GL_INFO_LOG_LENGTH, &length);\n\n    std::string string;\n    string.resize(length);\n\n    ctx.GetShaderInfoLog(handle(), length, &length, string.data());\n\n    string.resize(length - 1);\n\n    return string;\n  }\n\n  util::Either<shader_t, std::string>\n  shader_t::compile(const std::string_view &source, GLenum type) {\n    shader_t shader;\n\n    auto data = source.data();\n    GLint length = source.length();\n\n    shader._shader.el = ctx.CreateShader(type);\n    ctx.ShaderSource(shader.handle(), 1, &data, &length);\n    ctx.CompileShader(shader.handle());\n\n    int status = 0;\n    ctx.GetShaderiv(shader.handle(), GL_COMPILE_STATUS, &status);\n\n    if (!status) {\n      return shader.err_str();\n    }\n\n    return shader;\n  }\n\n  GLuint\n  shader_t::handle() const {\n    return _shader.el;\n  }\n\n  buffer_t\n  buffer_t::make(util::buffer_t<GLint> &&offsets, const char *block, const std::string_view &data) {\n    buffer_t buffer;\n    buffer._block = block;\n    buffer._size = data.size();\n    buffer._offsets = std::move(offsets);\n\n    ctx.GenBuffers(1, &buffer._buffer.el);\n    ctx.BindBuffer(GL_UNIFORM_BUFFER, buffer.handle());\n    ctx.BufferData(GL_UNIFORM_BUFFER, data.size(), (const std::uint8_t *) data.data(), GL_DYNAMIC_DRAW);\n\n    return buffer;\n  }\n\n  GLuint\n  buffer_t::handle() const {\n    return _buffer.el;\n  }\n\n  const char *\n  buffer_t::block() const {\n    return _block;\n  }\n\n  void\n  buffer_t::update(const std::string_view &view, std::size_t offset) {\n    ctx.BindBuffer(GL_UNIFORM_BUFFER, handle());\n    ctx.BufferSubData(GL_UNIFORM_BUFFER, offset, view.size(), (const void *) view.data());\n  }\n\n  void\n  buffer_t::update(std::string_view *members, std::size_t count, std::size_t offset) {\n    util::buffer_t<std::uint8_t> buffer { _size };\n\n    for (int x = 0; x < count; ++x) {\n      auto val = members[x];\n\n      std::copy_n((const std::uint8_t *) val.data(), val.size(), &buffer[_offsets[x]]);\n    }\n\n    update(util::view(buffer.begin(), buffer.end()), offset);\n  }\n\n  std::string\n  program_t::err_str() {\n    int length;\n    ctx.GetProgramiv(handle(), GL_INFO_LOG_LENGTH, &length);\n\n    std::string string;\n    string.resize(length);\n\n    ctx.GetShaderInfoLog(handle(), length, &length, string.data());\n\n    string.resize(length - 1);\n\n    return string;\n  }\n\n  util::Either<program_t, std::string>\n  program_t::link(const shader_t &vert, const shader_t &frag) {\n    program_t program;\n\n    program._program.el = ctx.CreateProgram();\n\n    ctx.AttachShader(program.handle(), vert.handle());\n    ctx.AttachShader(program.handle(), frag.handle());\n\n    // p_handle stores a copy of the program handle, since program will be moved before\n    // the fail guard function is called.\n    auto fg = util::fail_guard([p_handle = program.handle(), &vert, &frag]() {\n      ctx.DetachShader(p_handle, vert.handle());\n      ctx.DetachShader(p_handle, frag.handle());\n    });\n\n    ctx.LinkProgram(program.handle());\n\n    int status = 0;\n    ctx.GetProgramiv(program.handle(), GL_LINK_STATUS, &status);\n\n    if (!status) {\n      return program.err_str();\n    }\n\n    return program;\n  }\n\n  void\n  program_t::bind(const buffer_t &buffer) {\n    ctx.UseProgram(handle());\n    auto i = ctx.GetUniformBlockIndex(handle(), buffer.block());\n\n    ctx.BindBufferBase(GL_UNIFORM_BUFFER, i, buffer.handle());\n  }\n\n  std::optional<buffer_t>\n  program_t::uniform(const char *block, std::pair<const char *, std::string_view> *members, std::size_t count) {\n    auto i = ctx.GetUniformBlockIndex(handle(), block);\n    if (i == GL_INVALID_INDEX) {\n      BOOST_LOG(error) << \"Couldn't find index of [\"sv << block << ']';\n      return std::nullopt;\n    }\n\n    int size;\n    ctx.GetActiveUniformBlockiv(handle(), i, GL_UNIFORM_BLOCK_DATA_SIZE, &size);\n\n    bool error_flag = false;\n\n    util::buffer_t<GLint> offsets { count };\n    auto indices = (std::uint32_t *) alloca(count * sizeof(std::uint32_t));\n    auto names = (const char **) alloca(count * sizeof(const char *));\n    auto names_p = names;\n\n    std::for_each_n(members, count, [names_p](auto &member) mutable {\n      *names_p++ = std::get<0>(member);\n    });\n\n    std::fill_n(indices, count, GL_INVALID_INDEX);\n    ctx.GetUniformIndices(handle(), count, names, indices);\n\n    for (int x = 0; x < count; ++x) {\n      if (indices[x] == GL_INVALID_INDEX) {\n        error_flag = true;\n\n        BOOST_LOG(error) << \"Couldn't find [\"sv << block << '.' << members[x].first << ']';\n      }\n    }\n\n    if (error_flag) {\n      return std::nullopt;\n    }\n\n    ctx.GetActiveUniformsiv(handle(), count, indices, GL_UNIFORM_OFFSET, offsets.begin());\n    util::buffer_t<std::uint8_t> buffer { (std::size_t) size };\n\n    for (int x = 0; x < count; ++x) {\n      auto val = std::get<1>(members[x]);\n\n      std::copy_n((const std::uint8_t *) val.data(), val.size(), &buffer[offsets[x]]);\n    }\n\n    return buffer_t::make(std::move(offsets), block, std::string_view { (char *) buffer.begin(), buffer.size() });\n  }\n\n  GLuint\n  program_t::handle() const {\n    return _program.el;\n  }\n\n}  // namespace gl\n\nnamespace gbm {\n  device_destroy_fn device_destroy;\n  create_device_fn create_device;\n\n  int\n  init() {\n    static void *handle { nullptr };\n    static bool funcs_loaded = false;\n\n    if (funcs_loaded) return 0;\n\n    if (!handle) {\n      handle = dyn::handle({ \"libgbm.so.1\", \"libgbm.so\" });\n      if (!handle) {\n        return -1;\n      }\n    }\n\n    std::vector<std::tuple<GLADapiproc *, const char *>> funcs {\n      { (GLADapiproc *) &device_destroy, \"gbm_device_destroy\" },\n      { (GLADapiproc *) &create_device, \"gbm_create_device\" },\n    };\n\n    if (dyn::load(handle, funcs)) {\n      return -1;\n    }\n\n    funcs_loaded = true;\n    return 0;\n  }\n}  // namespace gbm\n\nnamespace egl {\n  constexpr auto EGL_LINUX_DMA_BUF_EXT = 0x3270;\n  constexpr auto EGL_LINUX_DRM_FOURCC_EXT = 0x3271;\n  constexpr auto EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272;\n  constexpr auto EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273;\n  constexpr auto EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274;\n  constexpr auto EGL_DMA_BUF_PLANE1_FD_EXT = 0x3275;\n  constexpr auto EGL_DMA_BUF_PLANE1_OFFSET_EXT = 0x3276;\n  constexpr auto EGL_DMA_BUF_PLANE1_PITCH_EXT = 0x3277;\n  constexpr auto EGL_DMA_BUF_PLANE2_FD_EXT = 0x3278;\n  constexpr auto EGL_DMA_BUF_PLANE2_OFFSET_EXT = 0x3279;\n  constexpr auto EGL_DMA_BUF_PLANE2_PITCH_EXT = 0x327A;\n  constexpr auto EGL_DMA_BUF_PLANE3_FD_EXT = 0x3440;\n  constexpr auto EGL_DMA_BUF_PLANE3_OFFSET_EXT = 0x3441;\n  constexpr auto EGL_DMA_BUF_PLANE3_PITCH_EXT = 0x3442;\n  constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT = 0x3443;\n  constexpr auto EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT = 0x3444;\n  constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT = 0x3445;\n  constexpr auto EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT = 0x3446;\n  constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT = 0x3447;\n  constexpr auto EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT = 0x3448;\n  constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT = 0x3449;\n  constexpr auto EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT = 0x344A;\n\n  bool\n  fail() {\n    return eglGetError() != EGL_SUCCESS;\n  }\n\n  /**\n   * @memberof egl::display_t\n   */\n  display_t\n  make_display(std::variant<gbm::gbm_t::pointer, wl_display *, _XDisplay *> native_display) {\n    constexpr auto EGL_PLATFORM_GBM_MESA = 0x31D7;\n    constexpr auto EGL_PLATFORM_WAYLAND_KHR = 0x31D8;\n    constexpr auto EGL_PLATFORM_X11_KHR = 0x31D5;\n\n    int egl_platform;\n    void *native_display_p;\n\n    switch (native_display.index()) {\n      case 0:\n        egl_platform = EGL_PLATFORM_GBM_MESA;\n        native_display_p = std::get<0>(native_display);\n        break;\n      case 1:\n        egl_platform = EGL_PLATFORM_WAYLAND_KHR;\n        native_display_p = std::get<1>(native_display);\n        break;\n      case 2:\n        egl_platform = EGL_PLATFORM_X11_KHR;\n        native_display_p = std::get<2>(native_display);\n        break;\n      default:\n        BOOST_LOG(error) << \"egl::make_display(): Index [\"sv << native_display.index() << \"] not implemented\"sv;\n        return nullptr;\n    }\n\n    // native_display.left() equals native_display.right()\n    display_t display = eglGetPlatformDisplay(egl_platform, native_display_p, nullptr);\n\n    if (fail()) {\n      BOOST_LOG(error) << \"Couldn't open EGL display: [\"sv << util::hex(eglGetError()).to_string_view() << ']';\n      return nullptr;\n    }\n\n    int major, minor;\n    if (!eglInitialize(display.get(), &major, &minor)) {\n      BOOST_LOG(error) << \"Couldn't initialize EGL display: [\"sv << util::hex(eglGetError()).to_string_view() << ']';\n      return nullptr;\n    }\n\n    const char *extension_st = eglQueryString(display.get(), EGL_EXTENSIONS);\n    const char *version = eglQueryString(display.get(), EGL_VERSION);\n    const char *vendor = eglQueryString(display.get(), EGL_VENDOR);\n    const char *apis = eglQueryString(display.get(), EGL_CLIENT_APIS);\n\n    BOOST_LOG(debug) << \"EGL: [\"sv << vendor << \"]: version [\"sv << version << ']';\n    BOOST_LOG(debug) << \"API's supported: [\"sv << apis << ']';\n\n    const char *extensions[] {\n      \"EGL_KHR_create_context\",\n      \"EGL_KHR_surfaceless_context\",\n      \"EGL_EXT_image_dma_buf_import\",\n      \"EGL_EXT_image_dma_buf_import_modifiers\",\n    };\n\n    for (auto ext : extensions) {\n      if (!std::strstr(extension_st, ext)) {\n        BOOST_LOG(error) << \"Missing extension: [\"sv << ext << ']';\n        return nullptr;\n      }\n    }\n\n    return display;\n  }\n\n  std::optional<ctx_t>\n  make_ctx(display_t::pointer display) {\n    constexpr int conf_attr[] {\n      EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE\n    };\n\n    int count;\n    EGLConfig conf;\n    if (!eglChooseConfig(display, conf_attr, &conf, 1, &count)) {\n      BOOST_LOG(error) << \"Couldn't set config attributes: [\"sv << util::hex(eglGetError()).to_string_view() << ']';\n      return std::nullopt;\n    }\n\n    if (!eglBindAPI(EGL_OPENGL_API)) {\n      BOOST_LOG(error) << \"Couldn't bind API: [\"sv << util::hex(eglGetError()).to_string_view() << ']';\n      return std::nullopt;\n    }\n\n    constexpr int attr[] {\n      EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE\n    };\n\n    ctx_t ctx { display, eglCreateContext(display, conf, EGL_NO_CONTEXT, attr) };\n    if (fail()) {\n      BOOST_LOG(error) << \"Couldn't create EGL context: [\"sv << util::hex(eglGetError()).to_string_view() << ']';\n      return std::nullopt;\n    }\n\n    TUPLE_EL_REF(ctx_p, 1, ctx.el);\n    if (!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx_p)) {\n      BOOST_LOG(error) << \"Couldn't make current display\"sv;\n      return std::nullopt;\n    }\n\n    if (!gladLoadGLContext(&gl::ctx, eglGetProcAddress)) {\n      BOOST_LOG(error) << \"Couldn't load OpenGL library\"sv;\n      return std::nullopt;\n    }\n\n    BOOST_LOG(debug) << \"GL: vendor: \"sv << gl::ctx.GetString(GL_VENDOR);\n    BOOST_LOG(debug) << \"GL: renderer: \"sv << gl::ctx.GetString(GL_RENDERER);\n    BOOST_LOG(debug) << \"GL: version: \"sv << gl::ctx.GetString(GL_VERSION);\n    BOOST_LOG(debug) << \"GL: shader: \"sv << gl::ctx.GetString(GL_SHADING_LANGUAGE_VERSION);\n\n    gl::ctx.PixelStorei(GL_UNPACK_ALIGNMENT, 1);\n\n    return ctx;\n  }\n\n  struct plane_attr_t {\n    EGLAttrib fd;\n    EGLAttrib offset;\n    EGLAttrib pitch;\n    EGLAttrib lo;\n    EGLAttrib hi;\n  };\n\n  inline plane_attr_t\n  get_plane(std::uint32_t plane_indice) {\n    switch (plane_indice) {\n      case 0:\n        return {\n          EGL_DMA_BUF_PLANE0_FD_EXT,\n          EGL_DMA_BUF_PLANE0_OFFSET_EXT,\n          EGL_DMA_BUF_PLANE0_PITCH_EXT,\n          EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,\n          EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,\n        };\n      case 1:\n        return {\n          EGL_DMA_BUF_PLANE1_FD_EXT,\n          EGL_DMA_BUF_PLANE1_OFFSET_EXT,\n          EGL_DMA_BUF_PLANE1_PITCH_EXT,\n          EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT,\n          EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT,\n        };\n      case 2:\n        return {\n          EGL_DMA_BUF_PLANE2_FD_EXT,\n          EGL_DMA_BUF_PLANE2_OFFSET_EXT,\n          EGL_DMA_BUF_PLANE2_PITCH_EXT,\n          EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT,\n          EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT,\n        };\n      case 3:\n        return {\n          EGL_DMA_BUF_PLANE3_FD_EXT,\n          EGL_DMA_BUF_PLANE3_OFFSET_EXT,\n          EGL_DMA_BUF_PLANE3_PITCH_EXT,\n          EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT,\n          EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT,\n        };\n    }\n\n    // Avoid warning\n    return {};\n  }\n\n  /**\n   * @brief Get EGL attributes for eglCreateImage() to import the provided surface.\n   * @param surface The surface descriptor.\n   * @return Vector of EGL attributes.\n   */\n  std::vector<EGLAttrib>\n  surface_descriptor_to_egl_attribs(const surface_descriptor_t &surface) {\n    std::vector<EGLAttrib> attribs;\n\n    attribs.emplace_back(EGL_WIDTH);\n    attribs.emplace_back(surface.width);\n    attribs.emplace_back(EGL_HEIGHT);\n    attribs.emplace_back(surface.height);\n    attribs.emplace_back(EGL_LINUX_DRM_FOURCC_EXT);\n    attribs.emplace_back(surface.fourcc);\n\n    for (auto x = 0; x < 4; ++x) {\n      auto fd = surface.fds[x];\n      if (fd < 0) {\n        continue;\n      }\n\n      auto plane_attr = get_plane(x);\n\n      attribs.emplace_back(plane_attr.fd);\n      attribs.emplace_back(fd);\n      attribs.emplace_back(plane_attr.offset);\n      attribs.emplace_back(surface.offsets[x]);\n      attribs.emplace_back(plane_attr.pitch);\n      attribs.emplace_back(surface.pitches[x]);\n\n      if (surface.modifier != DRM_FORMAT_MOD_INVALID) {\n        attribs.emplace_back(plane_attr.lo);\n        attribs.emplace_back(surface.modifier & 0xFFFFFFFF);\n        attribs.emplace_back(plane_attr.hi);\n        attribs.emplace_back(surface.modifier >> 32);\n      }\n    }\n\n    attribs.emplace_back(EGL_NONE);\n    return attribs;\n  }\n\n  std::optional<rgb_t>\n  import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) {\n    auto attribs = surface_descriptor_to_egl_attribs(xrgb);\n\n    rgb_t rgb {\n      egl_display,\n      eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()),\n      gl::tex_t::make(1)\n    };\n\n    if (!rgb->xrgb8) {\n      BOOST_LOG(error) << \"Couldn't import RGB Image: \"sv << util::hex(eglGetError()).to_string_view();\n\n      return std::nullopt;\n    }\n\n    gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);\n    gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, rgb->xrgb8);\n\n    gl::ctx.BindTexture(GL_TEXTURE_2D, 0);\n\n    gl_drain_errors;\n\n    return rgb;\n  }\n\n  /**\n   * @brief Create a black RGB texture of the specified image size.\n   * @param img The image to use for texture sizing.\n   * @return The new RGB texture.\n   */\n  rgb_t\n  create_blank(platf::img_t &img) {\n    rgb_t rgb {\n      EGL_NO_DISPLAY,\n      EGL_NO_IMAGE,\n      gl::tex_t::make(1)\n    };\n\n    gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);\n    gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, img.width, img.height);\n    gl::ctx.BindTexture(GL_TEXTURE_2D, 0);\n\n    auto framebuf = gl::frame_buf_t::make(1);\n    framebuf.bind(&rgb->tex[0], &rgb->tex[0] + 1);\n\n    GLenum attachment = GL_COLOR_ATTACHMENT0;\n    gl::ctx.DrawBuffers(1, &attachment);\n    const GLuint rgb_black[] = { 0, 0, 0, 0 };\n    gl::ctx.ClearBufferuiv(GL_COLOR, 0, rgb_black);\n\n    gl_drain_errors;\n\n    return rgb;\n  }\n\n  std::optional<nv12_t>\n  import_target(display_t::pointer egl_display, std::array<file_t, nv12_img_t::num_fds> &&fds, const surface_descriptor_t &y, const surface_descriptor_t &uv) {\n    auto y_attribs = surface_descriptor_to_egl_attribs(y);\n    auto uv_attribs = surface_descriptor_to_egl_attribs(uv);\n\n    nv12_t nv12 {\n      egl_display,\n      eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, y_attribs.data()),\n      eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, uv_attribs.data()),\n      gl::tex_t::make(2),\n      gl::frame_buf_t::make(2),\n      std::move(fds)\n    };\n\n    if (!nv12->r8 || !nv12->bg88) {\n      BOOST_LOG(error) << \"Couldn't import YUV target: \"sv << util::hex(eglGetError()).to_string_view();\n\n      return std::nullopt;\n    }\n\n    gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]);\n    gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->r8);\n\n    gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]);\n    gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, nv12->bg88);\n\n    nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex));\n\n    GLenum attachments[] {\n      GL_COLOR_ATTACHMENT0,\n      GL_COLOR_ATTACHMENT1\n    };\n\n    for (int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) {\n      gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, nv12->buf[x]);\n      gl::ctx.DrawBuffers(1, &attachments[x]);\n\n      const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f };\n      const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f };\n      gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black);\n    }\n\n    gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);\n\n    gl_drain_errors;\n\n    return nv12;\n  }\n\n  /**\n   * @brief Create biplanar YUV textures to render into.\n   * @param width Width of the target frame.\n   * @param height Height of the target frame.\n   * @param format Format of the target frame.\n   * @return The new RGB texture.\n   */\n  std::optional<nv12_t>\n  create_target(int width, int height, AVPixelFormat format) {\n    nv12_t nv12 {\n      EGL_NO_DISPLAY,\n      EGL_NO_IMAGE,\n      EGL_NO_IMAGE,\n      gl::tex_t::make(2),\n      gl::frame_buf_t::make(2),\n    };\n\n    GLint y_format;\n    GLint uv_format;\n\n    // Determine the size of each plane element\n    auto fmt_desc = av_pix_fmt_desc_get(format);\n    if (fmt_desc->comp[0].depth <= 8) {\n      y_format = GL_R8;\n      uv_format = GL_RG8;\n    }\n    else if (fmt_desc->comp[0].depth <= 16) {\n      y_format = GL_R16;\n      uv_format = GL_RG16;\n    }\n    else {\n      BOOST_LOG(error) << \"Unsupported target pixel format: \"sv << format;\n      return std::nullopt;\n    }\n\n    gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[0]);\n    gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, y_format, width, height);\n\n    gl::ctx.BindTexture(GL_TEXTURE_2D, nv12->tex[1]);\n    gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, uv_format,\n      width >> fmt_desc->log2_chroma_w, height >> fmt_desc->log2_chroma_h);\n\n    nv12->buf.bind(std::begin(nv12->tex), std::end(nv12->tex));\n\n    GLenum attachments[] {\n      GL_COLOR_ATTACHMENT0,\n      GL_COLOR_ATTACHMENT1\n    };\n\n    for (int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) {\n      gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, nv12->buf[x]);\n      gl::ctx.DrawBuffers(1, &attachments[x]);\n\n      const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f };\n      const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f };\n      gl::ctx.ClearBufferfv(GL_COLOR, 0, x == 0 ? y_black : uv_black);\n    }\n\n    gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);\n\n    gl_drain_errors;\n\n    return nv12;\n  }\n\n  void sws_t::apply_colorspace(const video::sunshine_colorspace_t &colorspace) {\n    auto color_p = video::color_vectors_from_colorspace(colorspace, true);\n\n    std::string_view members[] {\n      util::view(color_p->color_vec_y),\n      util::view(color_p->color_vec_u),\n      util::view(color_p->color_vec_v),\n      util::view(color_p->range_y),\n      util::view(color_p->range_uv),\n    };\n\n    color_matrix.update(members, sizeof(members) / sizeof(decltype(members[0])));\n\n    program[0].bind(color_matrix);\n    program[1].bind(color_matrix);\n  }\n\n  std::optional<sws_t>\n  sws_t::make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex) {\n    sws_t sws;\n\n    sws.serial = std::numeric_limits<std::uint64_t>::max();\n\n    // Ensure aspect ratio is maintained\n    auto scalar = std::fminf(out_width / (float) in_width, out_height / (float) in_height);\n    auto out_width_f = in_width * scalar;\n    auto out_height_f = in_height * scalar;\n\n    // result is always positive\n    auto offsetX_f = (out_width - out_width_f) / 2;\n    auto offsetY_f = (out_height - out_height_f) / 2;\n\n    sws.out_width = out_width_f;\n    sws.out_height = out_height_f;\n\n    sws.in_width = in_width;\n    sws.in_height = in_height;\n\n    sws.offsetX = offsetX_f;\n    sws.offsetY = offsetY_f;\n\n    auto width_i = 1.0f / sws.out_width;\n\n    {\n      const char *sources[] {\n        SUNSHINE_SHADERS_DIR \"/ConvertUV.frag\",\n        SUNSHINE_SHADERS_DIR \"/ConvertUV.vert\",\n        SUNSHINE_SHADERS_DIR \"/ConvertY.frag\",\n        SUNSHINE_SHADERS_DIR \"/Scene.vert\",\n        SUNSHINE_SHADERS_DIR \"/Scene.frag\",\n      };\n\n      GLenum shader_type[2] {\n        GL_FRAGMENT_SHADER,\n        GL_VERTEX_SHADER,\n      };\n\n      constexpr auto count = sizeof(sources) / sizeof(const char *);\n\n      util::Either<gl::shader_t, std::string> compiled_sources[count];\n\n      bool error_flag = false;\n      for (int x = 0; x < count; ++x) {\n        auto &compiled_source = compiled_sources[x];\n\n        compiled_source = gl::shader_t::compile(file_handler::read_file(sources[x]), shader_type[x % 2]);\n        gl_drain_errors;\n\n        if (compiled_source.has_right()) {\n          BOOST_LOG(error) << sources[x] << \": \"sv << compiled_source.right();\n          error_flag = true;\n        }\n      }\n\n      if (error_flag) {\n        return std::nullopt;\n      }\n\n      auto program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[4].left());\n      if (program.has_right()) {\n        BOOST_LOG(error) << \"GL linker: \"sv << program.right();\n        return std::nullopt;\n      }\n\n      // Cursor - shader\n      sws.program[2] = std::move(program.left());\n\n      program = gl::program_t::link(compiled_sources[1].left(), compiled_sources[0].left());\n      if (program.has_right()) {\n        BOOST_LOG(error) << \"GL linker: \"sv << program.right();\n        return std::nullopt;\n      }\n\n      // UV - shader\n      sws.program[1] = std::move(program.left());\n\n      program = gl::program_t::link(compiled_sources[3].left(), compiled_sources[2].left());\n      if (program.has_right()) {\n        BOOST_LOG(error) << \"GL linker: \"sv << program.right();\n        return std::nullopt;\n      }\n\n      // Y - shader\n      sws.program[0] = std::move(program.left());\n    }\n\n    auto loc_width_i = gl::ctx.GetUniformLocation(sws.program[1].handle(), \"width_i\");\n    if (loc_width_i < 0) {\n      BOOST_LOG(error) << \"Couldn't find uniform [width_i]\"sv;\n      return std::nullopt;\n    }\n\n    gl::ctx.UseProgram(sws.program[1].handle());\n    gl::ctx.Uniform1fv(loc_width_i, 1, &width_i);\n\n    auto color_p = video::color_vectors_from_colorspace({video::colorspace_e::rec601, false, 8}, true);\n    std::pair<const char *, std::string_view> members[] {\n      std::make_pair(\"color_vec_y\", util::view(color_p->color_vec_y)),\n      std::make_pair(\"color_vec_u\", util::view(color_p->color_vec_u)),\n      std::make_pair(\"color_vec_v\", util::view(color_p->color_vec_v)),\n      std::make_pair(\"range_y\", util::view(color_p->range_y)),\n      std::make_pair(\"range_uv\", util::view(color_p->range_uv)),\n    };\n\n    auto color_matrix = sws.program[0].uniform(\"ColorMatrix\", members, sizeof(members) / sizeof(decltype(members[0])));\n    if (!color_matrix) {\n      return std::nullopt;\n    }\n\n    sws.color_matrix = std::move(*color_matrix);\n\n    sws.tex = std::move(tex);\n\n    sws.cursor_framebuffer = gl::frame_buf_t::make(1);\n    sws.cursor_framebuffer.bind(&sws.tex[0], &sws.tex[1]);\n\n    sws.program[0].bind(sws.color_matrix);\n    sws.program[1].bind(sws.color_matrix);\n\n    gl::ctx.BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\n\n    gl_drain_errors;\n\n    return sws;\n  }\n\n  int\n  sws_t::blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height) {\n    auto f = [&]() {\n      std::swap(offsetX, this->offsetX);\n      std::swap(offsetY, this->offsetY);\n      std::swap(width, this->out_width);\n      std::swap(height, this->out_height);\n    };\n\n    f();\n    auto fg = util::fail_guard(f);\n\n    return convert(fb);\n  }\n\n  std::optional<sws_t>\n  sws_t::make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format) {\n    GLint gl_format;\n\n    // Decide the bit depth format of the backing texture based the target frame format\n    auto fmt_desc = av_pix_fmt_desc_get(format);\n    switch (fmt_desc->comp[0].depth) {\n      case 8:\n        gl_format = GL_RGBA8;\n        break;\n\n      case 10:\n        gl_format = GL_RGB10_A2;\n        break;\n\n      case 12:\n        gl_format = GL_RGBA12;\n        break;\n\n      case 16:\n        gl_format = GL_RGBA16;\n        break;\n\n      default:\n        BOOST_LOG(error) << \"Unsupported pixel format for EGL frame: \"sv << (int) format;\n        return std::nullopt;\n    }\n\n    auto tex = gl::tex_t::make(2);\n    gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]);\n    gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, gl_format, in_width, in_height);\n\n    return make(in_width, in_height, out_width, out_height, std::move(tex));\n  }\n\n  void\n  sws_t::load_ram(platf::img_t &img) {\n    loaded_texture = tex[0];\n\n    gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture);\n    gl::ctx.TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img.width, img.height, GL_BGRA, GL_UNSIGNED_BYTE, img.data);\n  }\n\n  void\n  sws_t::load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture) {\n    // When only a sub-part of the image must be encoded...\n    const bool copy = offset_x || offset_y || img.sd.width != in_width || img.sd.height != in_height;\n    if (copy) {\n      auto framebuf = gl::frame_buf_t::make(1);\n      framebuf.bind(&texture, &texture + 1);\n\n      loaded_texture = tex[0];\n      framebuf.copy(0, loaded_texture, offset_x, offset_y, in_width, in_height);\n    }\n    else {\n      loaded_texture = texture;\n    }\n\n    if (img.data) {\n      GLenum attachment = GL_COLOR_ATTACHMENT0;\n\n      gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, cursor_framebuffer[0]);\n      gl::ctx.UseProgram(program[2].handle());\n\n      // When a copy has already been made...\n      if (!copy) {\n        gl::ctx.BindTexture(GL_TEXTURE_2D, texture);\n        gl::ctx.DrawBuffers(1, &attachment);\n\n        gl::ctx.Viewport(0, 0, in_width, in_height);\n        gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);\n\n        loaded_texture = tex[0];\n      }\n\n      gl::ctx.BindTexture(GL_TEXTURE_2D, tex[1]);\n      if (serial != img.serial) {\n        serial = img.serial;\n\n        gl::ctx.TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, img.src_w, img.src_h, 0, GL_BGRA, GL_UNSIGNED_BYTE, img.data);\n      }\n\n      gl::ctx.Enable(GL_BLEND);\n\n      gl::ctx.DrawBuffers(1, &attachment);\n\n#ifndef NDEBUG\n      auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER);\n      if (status != GL_FRAMEBUFFER_COMPLETE) {\n        BOOST_LOG(error) << \"Pass Cursor: CheckFramebufferStatus() --> [0x\"sv << util::hex(status).to_string_view() << ']';\n        return;\n      }\n#endif\n\n      gl::ctx.Viewport(img.x, img.y, img.width, img.height);\n      gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);\n\n      gl::ctx.Disable(GL_BLEND);\n\n      gl::ctx.BindTexture(GL_TEXTURE_2D, 0);\n      gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);\n    }\n  }\n\n  int\n  sws_t::convert(gl::frame_buf_t &fb) {\n    gl::ctx.BindTexture(GL_TEXTURE_2D, loaded_texture);\n\n    GLenum attachments[] {\n      GL_COLOR_ATTACHMENT0,\n      GL_COLOR_ATTACHMENT1\n    };\n\n    for (int x = 0; x < sizeof(attachments) / sizeof(decltype(attachments[0])); ++x) {\n      gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, fb[x]);\n      gl::ctx.DrawBuffers(1, &attachments[x]);\n\n#ifndef NDEBUG\n      auto status = gl::ctx.CheckFramebufferStatus(GL_FRAMEBUFFER);\n      if (status != GL_FRAMEBUFFER_COMPLETE) {\n        BOOST_LOG(error) << \"Pass \"sv << x << \": CheckFramebufferStatus() --> [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n#endif\n\n      gl::ctx.UseProgram(program[x].handle());\n      gl::ctx.Viewport(offsetX / (x + 1), offsetY / (x + 1), out_width / (x + 1), out_height / (x + 1));\n      gl::ctx.DrawArrays(GL_TRIANGLES, 0, 3);\n    }\n\n    gl::ctx.BindTexture(GL_TEXTURE_2D, 0);\n\n    gl::ctx.Flush();\n\n    return 0;\n  }\n}  // namespace egl\n\nvoid\nfree_frame(AVFrame *frame) {\n  av_frame_free(&frame);\n}\n"
  },
  {
    "path": "src/platform/linux/graphics.h",
    "content": "/**\n * @file src/platform/linux/graphics.h\n * @brief Declarations for graphics related functions.\n */\n#pragma once\n\n#include <optional>\n#include <string_view>\n\n#include <glad/egl.h>\n#include <glad/gl.h>\n\n#include \"misc.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n#include \"src/video_colorspace.h\"\n\n#define SUNSHINE_STRINGIFY_HELPER(x) #x\n#define SUNSHINE_STRINGIFY(x) SUNSHINE_STRINGIFY_HELPER(x)\n#define gl_drain_errors_helper(x) gl::drain_errors(x)\n#define gl_drain_errors gl_drain_errors_helper(__FILE__ \":\" SUNSHINE_STRINGIFY(__LINE__))\n\nextern \"C\" int\nclose(int __fd);\n\n// X11 Display\nextern \"C\" struct _XDisplay;\n\nstruct AVFrame;\nvoid\nfree_frame(AVFrame *frame);\n\nusing frame_t = util::safe_ptr<AVFrame, free_frame>;\n\nnamespace gl {\n  extern GladGLContext ctx;\n  void\n  drain_errors(const std::string_view &prefix);\n\n  class tex_t: public util::buffer_t<GLuint> {\n    using util::buffer_t<GLuint>::buffer_t;\n\n  public:\n    tex_t(tex_t &&) = default;\n    tex_t &\n    operator=(tex_t &&) = default;\n\n    ~tex_t();\n\n    static tex_t\n    make(std::size_t count);\n  };\n\n  class frame_buf_t: public util::buffer_t<GLuint> {\n    using util::buffer_t<GLuint>::buffer_t;\n\n  public:\n    frame_buf_t(frame_buf_t &&) = default;\n    frame_buf_t &\n    operator=(frame_buf_t &&) = default;\n\n    ~frame_buf_t();\n\n    static frame_buf_t\n    make(std::size_t count);\n\n    inline void\n    bind(std::nullptr_t, std::nullptr_t) {\n      int x = 0;\n      for (auto fb : (*this)) {\n        ctx.BindFramebuffer(GL_FRAMEBUFFER, fb);\n        ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, 0, 0);\n\n        ++x;\n      }\n      return;\n    }\n\n    template <class It>\n    void\n    bind(It it_begin, It it_end) {\n      using namespace std::literals;\n      if (std::distance(it_begin, it_end) > size()) {\n        BOOST_LOG(warning) << \"To many elements to bind\"sv;\n        return;\n      }\n\n      int x = 0;\n      std::for_each(it_begin, it_end, [&](auto tex) {\n        ctx.BindFramebuffer(GL_FRAMEBUFFER, (*this)[x]);\n        ctx.BindTexture(GL_TEXTURE_2D, tex);\n\n        ctx.FramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + x, tex, 0);\n\n        ++x;\n      });\n    }\n\n    /**\n     * Copies a part of the framebuffer to texture\n     */\n    void\n    copy(int id, int texture, int offset_x, int offset_y, int width, int height);\n  };\n\n  class shader_t {\n    KITTY_USING_MOVE_T(shader_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {\n      if (el != std::numeric_limits<GLuint>::max()) {\n        ctx.DeleteShader(el);\n      }\n    });\n\n  public:\n    std::string\n    err_str();\n\n    static util::Either<shader_t, std::string>\n    compile(const std::string_view &source, GLenum type);\n\n    GLuint\n    handle() const;\n\n  private:\n    shader_internal_t _shader;\n  };\n\n  class buffer_t {\n    KITTY_USING_MOVE_T(buffer_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {\n      if (el != std::numeric_limits<GLuint>::max()) {\n        ctx.DeleteBuffers(1, &el);\n      }\n    });\n\n  public:\n    static buffer_t\n    make(util::buffer_t<GLint> &&offsets, const char *block, const std::string_view &data);\n\n    GLuint\n    handle() const;\n\n    const char *\n    block() const;\n\n    void\n    update(const std::string_view &view, std::size_t offset = 0);\n    void\n    update(std::string_view *members, std::size_t count, std::size_t offset = 0);\n\n  private:\n    const char *_block;\n\n    std::size_t _size;\n\n    util::buffer_t<GLint> _offsets;\n\n    buffer_internal_t _buffer;\n  };\n\n  class program_t {\n    KITTY_USING_MOVE_T(program_internal_t, GLuint, std::numeric_limits<GLuint>::max(), {\n      if (el != std::numeric_limits<GLuint>::max()) {\n        ctx.DeleteProgram(el);\n      }\n    });\n\n  public:\n    std::string\n    err_str();\n\n    static util::Either<program_t, std::string>\n    link(const shader_t &vert, const shader_t &frag);\n\n    void\n    bind(const buffer_t &buffer);\n\n    std::optional<buffer_t>\n    uniform(const char *block, std::pair<const char *, std::string_view> *members, std::size_t count);\n\n    GLuint\n    handle() const;\n\n  private:\n    program_internal_t _program;\n  };\n}  // namespace gl\n\nnamespace gbm {\n  struct device;\n  typedef void (*device_destroy_fn)(device *gbm);\n  typedef device *(*create_device_fn)(int fd);\n\n  extern device_destroy_fn device_destroy;\n  extern create_device_fn create_device;\n\n  using gbm_t = util::dyn_safe_ptr<device, &device_destroy>;\n\n  int\n  init();\n\n}  // namespace gbm\n\nnamespace egl {\n  using display_t = util::dyn_safe_ptr_v2<void, EGLBoolean, &eglTerminate>;\n\n  struct rgb_img_t {\n    display_t::pointer display;\n    EGLImage xrgb8;\n\n    gl::tex_t tex;\n  };\n\n  struct nv12_img_t {\n    display_t::pointer display;\n    EGLImage r8;\n    EGLImage bg88;\n\n    gl::tex_t tex;\n    gl::frame_buf_t buf;\n\n    // sizeof(va::DRMPRIMESurfaceDescriptor::objects) / sizeof(va::DRMPRIMESurfaceDescriptor::objects[0]);\n    static constexpr std::size_t num_fds = 4;\n\n    std::array<file_t, num_fds> fds;\n  };\n\n  KITTY_USING_MOVE_T(rgb_t, rgb_img_t, , {\n    if (el.xrgb8) {\n      eglDestroyImage(el.display, el.xrgb8);\n    }\n  });\n\n  KITTY_USING_MOVE_T(nv12_t, nv12_img_t, , {\n    if (el.r8) {\n      eglDestroyImage(el.display, el.r8);\n    }\n\n    if (el.bg88) {\n      eglDestroyImage(el.display, el.bg88);\n    }\n  });\n\n  KITTY_USING_MOVE_T(ctx_t, (std::tuple<display_t::pointer, EGLContext>), , {\n    TUPLE_2D_REF(disp, ctx, el);\n    if (ctx) {\n      eglMakeCurrent(disp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);\n      eglDestroyContext(disp, ctx);\n    }\n  });\n\n  struct surface_descriptor_t {\n    int width;\n    int height;\n    int fds[4];\n    std::uint32_t fourcc;\n    std::uint64_t modifier;\n    std::uint32_t pitches[4];\n    std::uint32_t offsets[4];\n  };\n\n  display_t\n  make_display(std::variant<gbm::gbm_t::pointer, wl_display *, _XDisplay *> native_display);\n  std::optional<ctx_t>\n  make_ctx(display_t::pointer display);\n\n  std::optional<rgb_t>\n  import_source(\n    display_t::pointer egl_display,\n    const surface_descriptor_t &xrgb);\n\n  rgb_t\n  create_blank(platf::img_t &img);\n\n  std::optional<nv12_t>\n  import_target(\n    display_t::pointer egl_display,\n    std::array<file_t, nv12_img_t::num_fds> &&fds,\n    const surface_descriptor_t &y, const surface_descriptor_t &uv);\n\n  /**\n   * @brief Creates biplanar YUV textures to render into.\n   * @param width Width of the target frame.\n   * @param height Height of the target frame.\n   * @param format Format of the target frame.\n   * @return The new RGB texture.\n   */\n  std::optional<nv12_t>\n  create_target(int width, int height, AVPixelFormat format);\n\n  class cursor_t: public platf::img_t {\n  public:\n    int x, y;\n    int src_w, src_h;\n\n    unsigned long serial;\n\n    std::vector<std::uint8_t> buffer;\n  };\n\n  // Allow cursor and the underlying image to be kept together\n  class img_descriptor_t: public cursor_t {\n  public:\n    ~img_descriptor_t() {\n      reset();\n    }\n\n    void\n    reset() {\n      for (auto x = 0; x < 4; ++x) {\n        if (sd.fds[x] >= 0) {\n          close(sd.fds[x]);\n\n          sd.fds[x] = -1;\n        }\n      }\n    }\n\n    surface_descriptor_t sd;\n\n    // Increment sequence when new rgb_t needs to be created\n    std::uint64_t sequence;\n  };\n\n  class sws_t {\n  public:\n    static std::optional<sws_t>\n    make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex);\n    static std::optional<sws_t>\n    make(int in_width, int in_height, int out_width, int out_height, AVPixelFormat format);\n\n    // Convert the loaded image into the first two framebuffers\n    int\n    convert(gl::frame_buf_t &fb);\n\n    // Make an area of the image black\n    int\n    blank(gl::frame_buf_t &fb, int offsetX, int offsetY, int width, int height);\n\n    void\n    load_ram(platf::img_t &img);\n    void\n    load_vram(img_descriptor_t &img, int offset_x, int offset_y, int texture);\n\n    void\n    apply_colorspace(const video::sunshine_colorspace_t &colorspace);\n\n    // The first texture is the monitor image.\n    // The second texture is the cursor image\n    gl::tex_t tex;\n\n    // The cursor image will be blended into this framebuffer\n    gl::frame_buf_t cursor_framebuffer;\n    gl::frame_buf_t copy_framebuffer;\n\n    // Y - shader, UV - shader, Cursor - shader\n    gl::program_t program[3];\n    gl::buffer_t color_matrix;\n\n    int out_width, out_height;\n    int in_width, in_height;\n    int offsetX, offsetY;\n\n    // Pointer to the texture to be converted to nv12\n    int loaded_texture;\n\n    // Store latest cursor for load_vram\n    std::uint64_t serial;\n  };\n\n  bool\n  fail();\n}  // namespace egl\n"
  },
  {
    "path": "src/platform/linux/input/inputtino.cpp",
    "content": "/**\n * @file src/platform/linux/input/inputtino.cpp\n * @brief Definitions for the inputtino Linux input handling.\n */\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"src/config.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\n#include \"inputtino_common.h\"\n#include \"inputtino_gamepad.h\"\n#include \"inputtino_keyboard.h\"\n#include \"inputtino_mouse.h\"\n#include \"inputtino_pen.h\"\n#include \"inputtino_touch.h\"\n\nusing namespace std::literals;\n\nnamespace platf {\n\n  input_t\n  input() {\n    return { new input_raw_t() };\n  }\n\n  std::unique_ptr<client_input_t>\n  allocate_client_input_context(input_t &input) {\n    return std::make_unique<client_input_raw_t>(input);\n  }\n\n  void\n  freeInput(void *p) {\n    auto *input = (input_raw_t *) p;\n    delete input;\n  }\n\n  void\n  set_mouse_mode(int mode) {\n    // Virtual mouse driver is Windows-only; no-op on Linux\n  }\n\n  void\n  move_mouse(input_t &input, int deltaX, int deltaY) {\n    auto raw = (input_raw_t *) input.get();\n    platf::mouse::move(raw, deltaX, deltaY);\n  }\n\n  void\n  abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {\n    auto raw = (input_raw_t *) input.get();\n    platf::mouse::move_abs(raw, touch_port, x, y);\n  }\n\n  void\n  button_mouse(input_t &input, int button, bool release) {\n    auto raw = (input_raw_t *) input.get();\n    platf::mouse::button(raw, button, release);\n  }\n\n  void\n  scroll(input_t &input, int high_res_distance) {\n    auto raw = (input_raw_t *) input.get();\n    platf::mouse::scroll(raw, high_res_distance);\n  }\n\n  void\n  hscroll(input_t &input, int high_res_distance) {\n    auto raw = (input_raw_t *) input.get();\n    platf::mouse::hscroll(raw, high_res_distance);\n  }\n\n  void\n  keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) {\n    auto raw = (input_raw_t *) input.get();\n    platf::keyboard::update(raw, modcode, release, flags);\n  }\n\n  void\n  unicode(input_t &input, char *utf8, int size) {\n    auto raw = (input_raw_t *) input.get();\n    platf::keyboard::unicode(raw, utf8, size);\n  }\n\n  void\n  touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) {\n    auto raw = (client_input_raw_t *) input;\n    platf::touch::update(raw, touch_port, touch);\n  }\n\n  void\n  pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) {\n    auto raw = (client_input_raw_t *) input;\n    platf::pen::update(raw, touch_port, pen);\n  }\n\n  int\n  alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {\n    auto raw = (input_raw_t *) input.get();\n    return platf::gamepad::alloc(raw, id, metadata, feedback_queue);\n  }\n\n  void\n  free_gamepad(input_t &input, int nr) {\n    auto raw = (input_raw_t *) input.get();\n    platf::gamepad::free(raw, nr);\n  }\n\n  void\n  gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) {\n    auto raw = (input_raw_t *) input.get();\n    platf::gamepad::update(raw, nr, gamepad_state);\n  }\n\n  void\n  gamepad_touch(input_t &input, const gamepad_touch_t &touch) {\n    auto raw = (input_raw_t *) input.get();\n    platf::gamepad::touch(raw, touch);\n  }\n\n  void\n  gamepad_motion(input_t &input, const gamepad_motion_t &motion) {\n    auto raw = (input_raw_t *) input.get();\n    platf::gamepad::motion(raw, motion);\n  }\n\n  void\n  gamepad_battery(input_t &input, const gamepad_battery_t &battery) {\n    auto raw = (input_raw_t *) input.get();\n    platf::gamepad::battery(raw, battery);\n  }\n\n  platform_caps::caps_t\n  get_capabilities() {\n    platform_caps::caps_t caps = 0;\n    // TODO: if has_uinput\n    caps |= platform_caps::pen_touch;\n\n    // We support controller touchpad input only when emulating the PS5 controller\n    if (config::input.gamepad == \"ds5\"sv || config::input.gamepad == \"auto\"sv) {\n      caps |= platform_caps::controller_touch;\n    }\n\n    return caps;\n  }\n\n  util::point_t\n  get_mouse_loc(input_t &input) {\n    auto raw = (input_raw_t *) input.get();\n    return platf::mouse::get_location(raw);\n  }\n\n  std::vector<supported_gamepad_t> &\n  supported_gamepads(input_t *input) {\n    return platf::gamepad::supported_gamepads(input);\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/linux/input/inputtino_common.h",
    "content": "/**\n * @file src/platform/linux/input/inputtino_common.h\n * @brief Declarations for inputtino common input handling.\n */\n#pragma once\n\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\nusing namespace std::literals;\n\nnamespace platf {\n\n  using joypads_t = std::variant<inputtino::XboxOneJoypad, inputtino::SwitchJoypad, inputtino::PS5Joypad>;\n\n  struct joypad_state {\n    std::unique_ptr<joypads_t> joypad;\n    gamepad_feedback_msg_t last_rumble;\n    gamepad_feedback_msg_t last_rgb_led;\n  };\n\n  struct input_raw_t {\n    input_raw_t():\n        mouse(inputtino::Mouse::create({\n          .name = \"Mouse passthrough\",\n          .vendor_id = 0xBEEF,\n          .product_id = 0xDEAD,\n          .version = 0x111,\n        })),\n        keyboard(inputtino::Keyboard::create({\n          .name = \"Keyboard passthrough\",\n          .vendor_id = 0xBEEF,\n          .product_id = 0xDEAD,\n          .version = 0x111,\n        })),\n        gamepads(MAX_GAMEPADS) {\n      if (!mouse) {\n        BOOST_LOG(warning) << \"Unable to create virtual mouse: \" << mouse.getErrorMessage();\n      }\n      if (!keyboard) {\n        BOOST_LOG(warning) << \"Unable to create virtual keyboard: \" << keyboard.getErrorMessage();\n      }\n    }\n\n    ~input_raw_t() = default;\n\n    // All devices are wrapped in Result because it might be that we aren't able to create them (ex: udev permission denied)\n    inputtino::Result<inputtino::Mouse> mouse;\n    inputtino::Result<inputtino::Keyboard> keyboard;\n\n    /**\n     * A list of gamepads that are currently connected.\n     * The pointer is shared because that state will be shared with background threads that deal with rumble and LED\n     */\n    std::vector<std::shared_ptr<joypad_state>> gamepads;\n  };\n\n  struct client_input_raw_t: public client_input_t {\n    client_input_raw_t(input_t &input):\n        touch(inputtino::TouchScreen::create({\n          .name = \"Touch passthrough\",\n          .vendor_id = 0xBEEF,\n          .product_id = 0xDEAD,\n          .version = 0x111,\n        })),\n        pen(inputtino::PenTablet::create({\n          .name = \"Pen passthrough\",\n          .vendor_id = 0xBEEF,\n          .product_id = 0xDEAD,\n          .version = 0x111,\n        })) {\n      global = (input_raw_t *) input.get();\n      if (!touch) {\n        BOOST_LOG(warning) << \"Unable to create virtual touch screen: \" << touch.getErrorMessage();\n      }\n      if (!pen) {\n        BOOST_LOG(warning) << \"Unable to create virtual pen tablet: \" << pen.getErrorMessage();\n      }\n    }\n\n    input_raw_t *global;\n\n    // Device state and handles for pen and touch input must be stored in the per-client\n    // input context, because each connected client may be sending their own independent\n    // pen/touch events. To maintain separation, we expose separate pen and touch devices\n    // for each client.\n    inputtino::Result<inputtino::TouchScreen> touch;\n    inputtino::Result<inputtino::PenTablet> pen;\n  };\n\n  inline float\n  deg2rad(float degree) {\n    return degree * (M_PI / 180.f);\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/linux/input/inputtino_gamepad.cpp",
    "content": "/**\n * @file src/platform/linux/input/inputtino_gamepad.cpp\n * @brief Definitions for inputtino gamepad input handling.\n */\n// lib includes\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n// local includes\n#include \"inputtino_common.h\"\n#include \"inputtino_gamepad.h\"\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\nusing namespace std::literals;\n\nnamespace platf::gamepad {\n\n  enum GamepadStatus {\n    UHID_NOT_AVAILABLE = 0,  ///< UHID is not available\n    UINPUT_NOT_AVAILABLE,  ///< UINPUT is not available\n    XINPUT_NOT_AVAILABLE,  ///< XINPUT is not available\n    GAMEPAD_STATUS  ///< Helper to indicate the number of status\n  };\n\n  auto create_xbox_one() {\n    return inputtino::XboxOneJoypad::create({.name = \"Sunshine X-Box One (virtual) pad\",\n                                             // https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c#L147\n                                             .vendor_id = 0x045E,\n                                             .product_id = 0x02EA,\n                                             .version = 0x0408});\n  }\n\n  auto create_switch() {\n    return inputtino::SwitchJoypad::create({.name = \"Sunshine Nintendo (virtual) pad\",\n                                            // https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981\n                                            .vendor_id = 0x057e,\n                                            .product_id = 0x2009,\n                                            .version = 0x8111});\n  }\n\n  auto create_ds5() {\n    return inputtino::PS5Joypad::create({.name = \"Sunshine PS5 (virtual) pad\", .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111});\n  }\n\n  int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {\n    ControllerType selectedGamepadType;\n\n    if (config::input.gamepad == \"xone\"sv) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be Xbox One controller (manual selection)\"sv;\n      selectedGamepadType = XboxOneWired;\n    } else if (config::input.gamepad == \"ds5\"sv) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be DualSense 5 controller (manual selection)\"sv;\n      selectedGamepadType = DualSenseWired;\n    } else if (config::input.gamepad == \"switch\"sv) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be Nintendo Pro controller (manual selection)\"sv;\n      selectedGamepadType = SwitchProWired;\n    } else if (metadata.type == LI_CTYPE_XBOX) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be Xbox One controller (auto-selected by client-reported type)\"sv;\n      selectedGamepadType = XboxOneWired;\n    } else if (metadata.type == LI_CTYPE_PS) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be DualShock 5 controller (auto-selected by client-reported type)\"sv;\n      selectedGamepadType = DualSenseWired;\n    } else if (metadata.type == LI_CTYPE_NINTENDO) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be Nintendo Pro controller (auto-selected by client-reported type)\"sv;\n      selectedGamepadType = SwitchProWired;\n    } else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be DualShock 5 controller (auto-selected by motion sensor presence)\"sv;\n      selectedGamepadType = DualSenseWired;\n    } else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be DualShock 5 controller (auto-selected by touchpad presence)\"sv;\n      selectedGamepadType = DualSenseWired;\n    } else {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be Xbox One controller (default)\"sv;\n      selectedGamepadType = XboxOneWired;\n    }\n\n    if (selectedGamepadType == XboxOneWired || selectedGamepadType == SwitchProWired) {\n      if (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) {\n        BOOST_LOG(warning) << \"Gamepad \" << id.globalIndex << \" has motion sensors, but they are not usable when emulating a joypad different from DS5\"sv;\n      }\n      if (metadata.capabilities & LI_CCAP_TOUCHPAD) {\n        BOOST_LOG(warning) << \"Gamepad \" << id.globalIndex << \" has a touchpad, but it is not usable when emulating a joypad different from DS5\"sv;\n      }\n      if (metadata.capabilities & LI_CCAP_RGB_LED) {\n        BOOST_LOG(warning) << \"Gamepad \" << id.globalIndex << \" has an RGB LED, but it is not usable when emulating a joypad different from DS5\"sv;\n      }\n    } else if (selectedGamepadType == DualSenseWired) {\n      if (!(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) {\n        BOOST_LOG(warning) << \"Gamepad \" << id.globalIndex << \" is emulating a DualShock 5 controller, but the client gamepad doesn't have motion sensors active\"sv;\n      }\n      if (!(metadata.capabilities & LI_CCAP_TOUCHPAD)) {\n        BOOST_LOG(warning) << \"Gamepad \" << id.globalIndex << \" is emulating a DualShock 5 controller, but the client gamepad doesn't have a touchpad\"sv;\n      }\n    }\n\n    auto gamepad = std::make_shared<joypad_state>(joypad_state {});\n    auto on_rumble_fn = [feedback_queue, idx = id.clientRelativeIndex, gamepad](int low_freq, int high_freq) {\n      // Don't resend duplicate rumble data\n      if (gamepad->last_rumble.type == platf::gamepad_feedback_e::rumble && gamepad->last_rumble.data.rumble.lowfreq == low_freq && gamepad->last_rumble.data.rumble.highfreq == high_freq) {\n        return;\n      }\n\n      gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rumble(idx, low_freq, high_freq);\n      feedback_queue->raise(msg);\n      gamepad->last_rumble = msg;\n    };\n\n    switch (selectedGamepadType) {\n      case XboxOneWired:\n        {\n          auto xOne = create_xbox_one();\n          if (xOne) {\n            (*xOne).set_on_rumble(on_rumble_fn);\n            gamepad->joypad = std::make_unique<joypads_t>(std::move(*xOne));\n            raw->gamepads[id.globalIndex] = std::move(gamepad);\n            return 0;\n          } else {\n            BOOST_LOG(warning) << \"Unable to create virtual Xbox One controller: \" << xOne.getErrorMessage();\n            return -1;\n          }\n        }\n      case SwitchProWired:\n        {\n          auto switchPro = create_switch();\n          if (switchPro) {\n            (*switchPro).set_on_rumble(on_rumble_fn);\n            gamepad->joypad = std::make_unique<joypads_t>(std::move(*switchPro));\n            raw->gamepads[id.globalIndex] = std::move(gamepad);\n            return 0;\n          } else {\n            BOOST_LOG(warning) << \"Unable to create virtual Switch Pro controller: \" << switchPro.getErrorMessage();\n            return -1;\n          }\n        }\n      case DualSenseWired:\n        {\n          auto ds5 = create_ds5();\n          if (ds5) {\n            (*ds5).set_on_rumble(on_rumble_fn);\n            (*ds5).set_on_led([feedback_queue, idx = id.clientRelativeIndex, gamepad](int r, int g, int b) {\n              // Don't resend duplicate LED data\n              if (gamepad->last_rgb_led.type == platf::gamepad_feedback_e::set_rgb_led && gamepad->last_rgb_led.data.rgb_led.r == r && gamepad->last_rgb_led.data.rgb_led.g == g && gamepad->last_rgb_led.data.rgb_led.b == b) {\n                return;\n              }\n\n              auto msg = gamepad_feedback_msg_t::make_rgb_led(idx, r, g, b);\n              feedback_queue->raise(msg);\n              gamepad->last_rgb_led = msg;\n            });\n\n            (*ds5).set_on_trigger_effect([feedback_queue, idx = id.clientRelativeIndex](const inputtino::PS5Joypad::TriggerEffect &trigger_effect) {\n              feedback_queue->raise(gamepad_feedback_msg_t::make_adaptive_triggers(idx, trigger_effect.event_flags, trigger_effect.type_left, trigger_effect.type_right, trigger_effect.left, trigger_effect.right));\n            });\n\n            // Activate the motion sensors\n            feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100));\n            feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_GYRO, 100));\n\n            gamepad->joypad = std::make_unique<joypads_t>(std::move(*ds5));\n            raw->gamepads[id.globalIndex] = std::move(gamepad);\n            return 0;\n          } else {\n            BOOST_LOG(warning) << \"Unable to create virtual DualShock 5 controller: \" << ds5.getErrorMessage();\n            return -1;\n          }\n        }\n    }\n    return -1;\n  }\n\n  void free(input_raw_t *raw, int nr) {\n    // This will call the destructor which in turn will stop the background threads for rumble and LED (and ultimately remove the joypad device)\n    raw->gamepads[nr]->joypad.reset();\n    raw->gamepads[nr].reset();\n  }\n\n  void update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state) {\n    auto gamepad = raw->gamepads[nr];\n    if (!gamepad) {\n      return;\n    }\n\n    std::visit([gamepad_state](inputtino::Joypad &gc) {\n      gc.set_pressed_buttons(gamepad_state.buttonFlags);\n      gc.set_stick(inputtino::Joypad::LS, gamepad_state.lsX, gamepad_state.lsY);\n      gc.set_stick(inputtino::Joypad::RS, gamepad_state.rsX, gamepad_state.rsY);\n      gc.set_triggers(gamepad_state.lt, gamepad_state.rt);\n    },\n               *gamepad->joypad);\n  }\n\n  void touch(input_raw_t *raw, const gamepad_touch_t &touch) {\n    auto gamepad = raw->gamepads[touch.id.globalIndex];\n    if (!gamepad) {\n      return;\n    }\n    // Only the PS5 controller supports touch input\n    if (std::holds_alternative<inputtino::PS5Joypad>(*gamepad->joypad)) {\n      if (touch.pressure > 0.5) {\n        std::get<inputtino::PS5Joypad>(*gamepad->joypad).place_finger(touch.pointerId, touch.x * inputtino::PS5Joypad::touchpad_width, touch.y * inputtino::PS5Joypad::touchpad_height);\n      } else {\n        std::get<inputtino::PS5Joypad>(*gamepad->joypad).release_finger(touch.pointerId);\n      }\n    }\n  }\n\n  void motion(input_raw_t *raw, const gamepad_motion_t &motion) {\n    auto gamepad = raw->gamepads[motion.id.globalIndex];\n    if (!gamepad) {\n      return;\n    }\n    // Only the PS5 controller supports motion\n    if (std::holds_alternative<inputtino::PS5Joypad>(*gamepad->joypad)) {\n      switch (motion.motionType) {\n        case LI_MOTION_TYPE_ACCEL:\n          std::get<inputtino::PS5Joypad>(*gamepad->joypad).set_motion(inputtino::PS5Joypad::ACCELERATION, motion.x, motion.y, motion.z);\n          break;\n        case LI_MOTION_TYPE_GYRO:\n          std::get<inputtino::PS5Joypad>(*gamepad->joypad).set_motion(inputtino::PS5Joypad::GYROSCOPE, deg2rad(motion.x), deg2rad(motion.y), deg2rad(motion.z));\n          break;\n      }\n    }\n  }\n\n  void battery(input_raw_t *raw, const gamepad_battery_t &battery) {\n    auto gamepad = raw->gamepads[battery.id.globalIndex];\n    if (!gamepad) {\n      return;\n    }\n    // Only the PS5 controller supports battery reports\n    if (std::holds_alternative<inputtino::PS5Joypad>(*gamepad->joypad)) {\n      inputtino::PS5Joypad::BATTERY_STATE state;\n      switch (battery.state) {\n        case LI_BATTERY_STATE_CHARGING:\n          state = inputtino::PS5Joypad::BATTERY_CHARGHING;\n          break;\n        case LI_BATTERY_STATE_DISCHARGING:\n          state = inputtino::PS5Joypad::BATTERY_DISCHARGING;\n          break;\n        case LI_BATTERY_STATE_FULL:\n          state = inputtino::PS5Joypad::BATTERY_FULL;\n          break;\n        case LI_BATTERY_STATE_UNKNOWN:\n        case LI_BATTERY_STATE_NOT_PRESENT:\n        default:\n          return;\n      }\n      if (battery.percentage != LI_BATTERY_PERCENTAGE_UNKNOWN) {\n        std::get<inputtino::PS5Joypad>(*gamepad->joypad).set_battery(state, battery.percentage);\n      }\n    }\n  }\n\n  std::vector<supported_gamepad_t> &supported_gamepads(input_t *input) {\n    if (!input) {\n      static std::vector gps {\n        supported_gamepad_t {\"auto\", true, \"\"},\n        supported_gamepad_t {\"xone\", false, \"\"},\n        supported_gamepad_t {\"ds5\", false, \"\"},\n        supported_gamepad_t {\"switch\", false, \"\"},\n      };\n\n      return gps;\n    }\n\n    auto ds5 = create_ds5();\n    auto switchPro = create_switch();\n    auto xOne = create_xbox_one();\n\n    static std::vector gps {\n      supported_gamepad_t {\"auto\", true, \"\"},\n      supported_gamepad_t {\"xone\", static_cast<bool>(xOne), !xOne ? xOne.getErrorMessage() : \"\"},\n      supported_gamepad_t {\"ds5\", static_cast<bool>(ds5), !ds5 ? ds5.getErrorMessage() : \"\"},\n      supported_gamepad_t {\"switch\", static_cast<bool>(switchPro), !switchPro ? switchPro.getErrorMessage() : \"\"},\n    };\n\n    for (auto &[name, is_enabled, reason_disabled] : gps) {\n      if (!is_enabled) {\n        BOOST_LOG(warning) << \"Gamepad \" << name << \" is disabled due to \" << reason_disabled;\n      }\n    }\n\n    return gps;\n  }\n}  // namespace platf::gamepad"
  },
  {
    "path": "src/platform/linux/input/inputtino_gamepad.h",
    "content": "/**\n * @file src/platform/linux/input/inputtino_gamepad.h\n * @brief Declarations for inputtino gamepad input handling.\n */\n#pragma once\n\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"src/platform/common.h\"\n\n#include \"inputtino_common.h\"\n\nusing namespace std::literals;\n\nnamespace platf::gamepad {\n\n  enum ControllerType {\n    XboxOneWired,  ///< Xbox One Wired Controller\n    DualSenseWired,  ///< DualSense Wired Controller\n    SwitchProWired  ///< Switch Pro Wired Controller\n  };\n\n  int\n  alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue);\n\n  void\n  free(input_raw_t *raw, int nr);\n\n  void\n  update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state);\n\n  void\n  touch(input_raw_t *raw, const gamepad_touch_t &touch);\n\n  void\n  motion(input_raw_t *raw, const gamepad_motion_t &motion);\n\n  void\n  battery(input_raw_t *raw, const gamepad_battery_t &battery);\n\n  std::vector<supported_gamepad_t> &\n  supported_gamepads(input_t *input);\n}  // namespace platf::gamepad\n"
  },
  {
    "path": "src/platform/linux/input/inputtino_keyboard.cpp",
    "content": "/**\n * @file src/platform/linux/input/inputtino_keyboard.cpp\n * @brief Definitions for inputtino keyboard input handling.\n */\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\n#include \"inputtino_common.h\"\n#include \"inputtino_keyboard.h\"\n\nusing namespace std::literals;\n\nnamespace platf::keyboard {\n\n  /**\n   * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase)\n   *\n   * ex: ['👱'] = \"1F471\" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471\n   *\n   * adapted from: https://stackoverflow.com/a/7639754\n   */\n  std::string\n  to_hex(const std::basic_string<char32_t> &str) {\n    std::stringstream ss;\n    ss << std::hex << std::setfill('0');\n    for (const auto &ch : str) {\n      ss << static_cast<uint32_t>(ch);\n    }\n\n    std::string hex_unicode(ss.str());\n    std::ranges::transform(hex_unicode, hex_unicode.begin(), ::toupper);\n    return hex_unicode;\n  }\n\n  /**\n   * A map of linux scan code -> Moonlight keyboard code\n   */\n  static const std::map<short, short> key_mappings = {\n    { KEY_BACKSPACE, 0x08 }, { KEY_TAB, 0x09 }, { KEY_ENTER, 0x0D }, { KEY_LEFTSHIFT, 0x10 },\n    { KEY_LEFTCTRL, 0x11 }, { KEY_CAPSLOCK, 0x14 }, { KEY_ESC, 0x1B }, { KEY_SPACE, 0x20 },\n    { KEY_PAGEUP, 0x21 }, { KEY_PAGEDOWN, 0x22 }, { KEY_END, 0x23 }, { KEY_HOME, 0x24 },\n    { KEY_LEFT, 0x25 }, { KEY_UP, 0x26 }, { KEY_RIGHT, 0x27 }, { KEY_DOWN, 0x28 },\n    { KEY_SYSRQ, 0x2C }, { KEY_INSERT, 0x2D }, { KEY_DELETE, 0x2E }, { KEY_0, 0x30 },\n    { KEY_1, 0x31 }, { KEY_2, 0x32 }, { KEY_3, 0x33 }, { KEY_4, 0x34 },\n    { KEY_5, 0x35 }, { KEY_6, 0x36 }, { KEY_7, 0x37 }, { KEY_8, 0x38 },\n    { KEY_9, 0x39 }, { KEY_A, 0x41 }, { KEY_B, 0x42 }, { KEY_C, 0x43 },\n    { KEY_D, 0x44 }, { KEY_E, 0x45 }, { KEY_F, 0x46 }, { KEY_G, 0x47 },\n    { KEY_H, 0x48 }, { KEY_I, 0x49 }, { KEY_J, 0x4A }, { KEY_K, 0x4B },\n    { KEY_L, 0x4C }, { KEY_M, 0x4D }, { KEY_N, 0x4E }, { KEY_O, 0x4F },\n    { KEY_P, 0x50 }, { KEY_Q, 0x51 }, { KEY_R, 0x52 }, { KEY_S, 0x53 },\n    { KEY_T, 0x54 }, { KEY_U, 0x55 }, { KEY_V, 0x56 }, { KEY_W, 0x57 },\n    { KEY_X, 0x58 }, { KEY_Y, 0x59 }, { KEY_Z, 0x5A }, { KEY_LEFTMETA, 0x5B },\n    { KEY_RIGHTMETA, 0x5C }, { KEY_KP0, 0x60 }, { KEY_KP1, 0x61 }, { KEY_KP2, 0x62 },\n    { KEY_KP3, 0x63 }, { KEY_KP4, 0x64 }, { KEY_KP5, 0x65 }, { KEY_KP6, 0x66 },\n    { KEY_KP7, 0x67 }, { KEY_KP8, 0x68 }, { KEY_KP9, 0x69 }, { KEY_KPASTERISK, 0x6A },\n    { KEY_KPPLUS, 0x6B }, { KEY_KPMINUS, 0x6D }, { KEY_KPDOT, 0x6E }, { KEY_KPSLASH, 0x6F },\n    { KEY_F1, 0x70 }, { KEY_F2, 0x71 }, { KEY_F3, 0x72 }, { KEY_F4, 0x73 },\n    { KEY_F5, 0x74 }, { KEY_F6, 0x75 }, { KEY_F7, 0x76 }, { KEY_F8, 0x77 },\n    { KEY_F9, 0x78 }, { KEY_F10, 0x79 }, { KEY_F11, 0x7A }, { KEY_F12, 0x7B },\n    { KEY_NUMLOCK, 0x90 }, { KEY_SCROLLLOCK, 0x91 }, { KEY_LEFTSHIFT, 0xA0 }, { KEY_RIGHTSHIFT, 0xA1 },\n    { KEY_LEFTCTRL, 0xA2 }, { KEY_RIGHTCTRL, 0xA3 }, { KEY_LEFTALT, 0xA4 }, { KEY_RIGHTALT, 0xA5 },\n    { KEY_SEMICOLON, 0xBA }, { KEY_EQUAL, 0xBB }, { KEY_COMMA, 0xBC }, { KEY_MINUS, 0xBD },\n    { KEY_DOT, 0xBE }, { KEY_SLASH, 0xBF }, { KEY_GRAVE, 0xC0 }, { KEY_LEFTBRACE, 0xDB },\n    { KEY_BACKSLASH, 0xDC }, { KEY_RIGHTBRACE, 0xDD }, { KEY_APOSTROPHE, 0xDE }, { KEY_102ND, 0xE2 }\n  };\n\n  void\n  update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags) {\n    if (raw->keyboard) {\n      if (release) {\n        (*raw->keyboard).release(modcode);\n      }\n      else {\n        (*raw->keyboard).press(modcode);\n      }\n    }\n  }\n\n  void\n  unicode(input_raw_t *raw, char *utf8, int size) {\n    if (raw->keyboard) {\n      /* Reading input text as UTF-8 */\n      auto utf8_str = boost::locale::conv::to_utf<wchar_t>(utf8, utf8 + size, \"UTF-8\");\n      /* Converting to UTF-32 */\n      auto utf32_str = boost::locale::conv::utf_to_utf<char32_t>(utf8_str);\n      /* To HEX string */\n      auto hex_unicode = to_hex(utf32_str);\n      BOOST_LOG(debug) << \"Unicode, typing U+\"sv << hex_unicode;\n\n      /* pressing <CTRL> + <SHIFT> + U */\n      (*raw->keyboard).press(0xA2);  // LEFTCTRL\n      (*raw->keyboard).press(0xA0);  // LEFTSHIFT\n      (*raw->keyboard).press(0x55);  // U\n      (*raw->keyboard).release(0x55);  // U\n\n      /* input each HEX character */\n      for (auto &ch : hex_unicode) {\n        auto key_str = \"KEY_\"s + ch;\n        auto keycode = libevdev_event_code_from_name(EV_KEY, key_str.c_str());\n        auto wincode = key_mappings.find(keycode);\n        if (keycode == -1 || wincode == key_mappings.end()) {\n          BOOST_LOG(warning) << \"Unicode, unable to find keycode for: \"sv << ch;\n        }\n        else {\n          (*raw->keyboard).press(wincode->second);\n          (*raw->keyboard).release(wincode->second);\n        }\n      }\n\n      /* releasing <SHIFT> and <CTRL> */\n      (*raw->keyboard).release(0xA0);  // LEFTSHIFT\n      (*raw->keyboard).release(0xA2);  // LEFTCTRL\n    }\n  }\n}  // namespace platf::keyboard\n"
  },
  {
    "path": "src/platform/linux/input/inputtino_keyboard.h",
    "content": "/**\n * @file src/platform/linux/input/inputtino_keyboard.h\n * @brief Declarations for inputtino keyboard input handling.\n */\n#pragma once\n\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"inputtino_common.h\"\n\nusing namespace std::literals;\n\nnamespace platf::keyboard {\n  void\n  update(input_raw_t *raw, uint16_t modcode, bool release, uint8_t flags);\n\n  void\n  unicode(input_raw_t *raw, char *utf8, int size);\n}  // namespace platf::keyboard\n"
  },
  {
    "path": "src/platform/linux/input/inputtino_mouse.cpp",
    "content": "/**\n * @file src/platform/linux/input/inputtino_mouse.cpp\n * @brief Definitions for inputtino mouse input handling.\n */\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\n#include \"inputtino_common.h\"\n#include \"inputtino_mouse.h\"\n\nusing namespace std::literals;\n\nnamespace platf::mouse {\n\n  void\n  move(input_raw_t *raw, int deltaX, int deltaY) {\n    if (raw->mouse) {\n      (*raw->mouse).move(deltaX, deltaY);\n    }\n  }\n\n  void\n  move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y) {\n    if (raw->mouse) {\n      (*raw->mouse).move_abs(x, y, touch_port.width, touch_port.height);\n    }\n  }\n\n  void\n  button(input_raw_t *raw, int button, bool release) {\n    if (raw->mouse) {\n      inputtino::Mouse::MOUSE_BUTTON btn_type;\n      switch (button) {\n        case BUTTON_LEFT:\n          btn_type = inputtino::Mouse::LEFT;\n          break;\n        case BUTTON_MIDDLE:\n          btn_type = inputtino::Mouse::MIDDLE;\n          break;\n        case BUTTON_RIGHT:\n          btn_type = inputtino::Mouse::RIGHT;\n          break;\n        case BUTTON_X1:\n          btn_type = inputtino::Mouse::SIDE;\n          break;\n        case BUTTON_X2:\n          btn_type = inputtino::Mouse::EXTRA;\n          break;\n        default:\n          BOOST_LOG(warning) << \"Unknown mouse button: \" << button;\n          return;\n      }\n      if (release) {\n        (*raw->mouse).release(btn_type);\n      }\n      else {\n        (*raw->mouse).press(btn_type);\n      }\n    }\n  }\n\n  void\n  scroll(input_raw_t *raw, int high_res_distance) {\n    if (raw->mouse) {\n      (*raw->mouse).vertical_scroll(high_res_distance);\n    }\n  }\n\n  void\n  hscroll(input_raw_t *raw, int high_res_distance) {\n    if (raw->mouse) {\n      (*raw->mouse).horizontal_scroll(high_res_distance);\n    }\n  }\n\n  util::point_t\n  get_location(input_raw_t *raw) {\n    if (raw->mouse) {\n      // TODO: decide what to do after https://github.com/games-on-whales/inputtino/issues/6 is resolved.\n      // TODO: auto x = (*raw->mouse).get_absolute_x();\n      // TODO: auto y = (*raw->mouse).get_absolute_y();\n      return { 0, 0 };\n    }\n    return { 0, 0 };\n  }\n}  // namespace platf::mouse\n"
  },
  {
    "path": "src/platform/linux/input/inputtino_mouse.h",
    "content": "/**\n * @file src/platform/linux/input/inputtino_mouse.h\n * @brief Declarations for inputtino mouse input handling.\n */\n#pragma once\n\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"src/platform/common.h\"\n\n#include \"inputtino_common.h\"\n\nusing namespace std::literals;\n\nnamespace platf::mouse {\n  void\n  move(input_raw_t *raw, int deltaX, int deltaY);\n\n  void\n  move_abs(input_raw_t *raw, const touch_port_t &touch_port, float x, float y);\n\n  void\n  button(input_raw_t *raw, int button, bool release);\n\n  void\n  scroll(input_raw_t *raw, int high_res_distance);\n\n  void\n  hscroll(input_raw_t *raw, int high_res_distance);\n\n  util::point_t\n  get_location(input_raw_t *raw);\n}  // namespace platf::mouse\n"
  },
  {
    "path": "src/platform/linux/input/inputtino_pen.cpp",
    "content": "/**\n * @file src/platform/linux/input/inputtino_pen.cpp\n * @brief Definitions for inputtino pen input handling.\n */\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\n#include \"inputtino_common.h\"\n#include \"inputtino_pen.h\"\n\nusing namespace std::literals;\n\nnamespace platf::pen {\n  void\n  update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen) {\n    if (raw->pen) {\n      // First set the buttons\n      (*raw->pen).set_btn(inputtino::PenTablet::PRIMARY, pen.penButtons & LI_PEN_BUTTON_PRIMARY);\n      (*raw->pen).set_btn(inputtino::PenTablet::SECONDARY, pen.penButtons & LI_PEN_BUTTON_SECONDARY);\n      (*raw->pen).set_btn(inputtino::PenTablet::TERTIARY, pen.penButtons & LI_PEN_BUTTON_TERTIARY);\n\n      // Set the tool\n      inputtino::PenTablet::TOOL_TYPE tool;\n      switch (pen.toolType) {\n        case LI_TOOL_TYPE_PEN:\n          tool = inputtino::PenTablet::PEN;\n          break;\n        case LI_TOOL_TYPE_ERASER:\n          tool = inputtino::PenTablet::ERASER;\n          break;\n        default:\n          tool = inputtino::PenTablet::SAME_AS_BEFORE;\n          break;\n      }\n\n      // Normalize rotation value to 0-359 degree range\n      auto rotation = pen.rotation;\n      if (rotation != LI_ROT_UNKNOWN) {\n        rotation %= 360;\n      }\n\n      // Here we receive:\n      //  - Rotation: degrees from vertical in Y dimension (parallel to screen, 0..360)\n      //  - Tilt: degrees from vertical in Z dimension (perpendicular to screen, 0..90)\n      float tilt_x = 0;\n      float tilt_y = 0;\n      // Convert polar coordinates into Y tilt angles\n      if (pen.tilt != LI_TILT_UNKNOWN && rotation != LI_ROT_UNKNOWN) {\n        auto rotation_rads = deg2rad(rotation);\n        auto tilt_rads = deg2rad(pen.tilt);\n        auto r = std::sin(tilt_rads);\n        auto z = std::cos(tilt_rads);\n\n        tilt_x = std::atan2(std::sin(-rotation_rads) * r, z) * 180.f / M_PI;\n        tilt_y = std::atan2(std::cos(-rotation_rads) * r, z) * 180.f / M_PI;\n      }\n\n      bool is_touching = pen.eventType == LI_TOUCH_EVENT_DOWN || pen.eventType == LI_TOUCH_EVENT_MOVE;\n\n      (*raw->pen).place_tool(tool,\n        pen.x,\n        pen.y,\n        is_touching ? pen.pressureOrDistance : -1,\n        is_touching ? -1 : pen.pressureOrDistance,\n        tilt_x,\n        tilt_y);\n    }\n  }\n}  // namespace platf::pen\n"
  },
  {
    "path": "src/platform/linux/input/inputtino_pen.h",
    "content": "/**\n * @file src/platform/linux/input/inputtino_pen.h\n * @brief Declarations for inputtino pen input handling.\n */\n#pragma once\n\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"src/platform/common.h\"\n\n#include \"inputtino_common.h\"\n\nusing namespace std::literals;\n\nnamespace platf::pen {\n  void\n  update(client_input_raw_t *raw, const touch_port_t &touch_port, const pen_input_t &pen);\n}\n"
  },
  {
    "path": "src/platform/linux/input/inputtino_touch.cpp",
    "content": "/**\n * @file src/platform/linux/input/inputtino_touch.cpp\n * @brief Definitions for inputtino touch input handling.\n */\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\n#include \"inputtino_common.h\"\n#include \"inputtino_touch.h\"\n\nusing namespace std::literals;\n\nnamespace platf::touch {\n  void\n  update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch) {\n    if (raw->touch) {\n      switch (touch.eventType) {\n        case LI_TOUCH_EVENT_HOVER:\n        case LI_TOUCH_EVENT_DOWN:\n        case LI_TOUCH_EVENT_MOVE: {\n          // Convert our 0..360 range to -90..90 relative to Y axis\n          int adjusted_angle = touch.rotation;\n\n          if (adjusted_angle > 90 && adjusted_angle < 270) {\n            // Lower hemisphere\n            adjusted_angle = 180 - adjusted_angle;\n          }\n\n          // Wrap the value if it's out of range\n          if (adjusted_angle > 90) {\n            adjusted_angle -= 360;\n          }\n          else if (adjusted_angle < -90) {\n            adjusted_angle += 360;\n          }\n          (*raw->touch).place_finger(touch.pointerId, touch.x, touch.y, touch.pressureOrDistance, adjusted_angle);\n          break;\n        }\n        case LI_TOUCH_EVENT_CANCEL:\n        case LI_TOUCH_EVENT_UP:\n        case LI_TOUCH_EVENT_HOVER_LEAVE: {\n          (*raw->touch).release_finger(touch.pointerId);\n          break;\n        }\n          // TODO: LI_TOUCH_EVENT_CANCEL_ALL\n      }\n    }\n  }\n}  // namespace platf::touch\n"
  },
  {
    "path": "src/platform/linux/input/inputtino_touch.h",
    "content": "/**\n * @file src/platform/linux/input/inputtino_touch.h\n * @brief Declarations for inputtino touch input handling.\n */\n#pragma once\n\n#include <boost/locale.hpp>\n#include <inputtino/input.hpp>\n#include <libevdev/libevdev.h>\n\n#include \"src/platform/common.h\"\n\n#include \"inputtino_common.h\"\n\nusing namespace std::literals;\n\nnamespace platf::touch {\n  void\n  update(client_input_raw_t *raw, const touch_port_t &touch_port, const touch_input_t &touch);\n}\n"
  },
  {
    "path": "src/platform/linux/input/legacy_input.cpp",
    "content": "/**\n * @file src/platform/linux/input/legacy_input.cpp\n * @brief Implementation of input handling, prior to migration to inputtino\n * @todo Remove this file after the next stable release\n */\n#include <fcntl.h>\n#include <linux/uinput.h>\n#include <poll.h>\n\nextern \"C\" {\n#include <libevdev/libevdev-uinput.h>\n#include <libevdev/libevdev.h>\n}\n\n#ifdef SUNSHINE_BUILD_X11\n  #include <X11/Xutil.h>\n  #include <X11/extensions/XTest.h>\n  #include <X11/keysym.h>\n  #include <X11/keysymdef.h>\n#endif\n\n#include <boost/locale.hpp>\n#include <cmath>\n#include <cstring>\n#include <filesystem>\n#include <thread>\n\n#include \"src/config.h\"\n#include \"src/input.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\n#include \"src/platform/common.h\"\n\n#include \"src/platform/linux/misc.h\"\n\n// Support older versions\n#ifndef REL_HWHEEL_HI_RES\n  #define REL_HWHEEL_HI_RES 0x0c\n#endif\n\n#ifndef REL_WHEEL_HI_RES\n  #define REL_WHEEL_HI_RES 0x0b\n#endif\n\nusing namespace std::literals;\n\nnamespace platf {\n  static bool has_uinput = false;\n\n#ifdef SUNSHINE_BUILD_X11\n  namespace x11 {\n  #define _FN(x, ret, args)    \\\n    typedef ret(*x##_fn) args; \\\n    static x##_fn x\n\n    _FN(OpenDisplay, Display *, (_Xconst char *display_name));\n    _FN(CloseDisplay, int, (Display * display));\n    _FN(InitThreads, Status, (void) );\n    _FN(Flush, int, (Display *) );\n\n    namespace tst {\n      _FN(FakeMotionEvent, int, (Display * dpy, int screen_numer, int x, int y, unsigned long delay));\n      _FN(FakeRelativeMotionEvent, int, (Display * dpy, int deltaX, int deltaY, unsigned long delay));\n      _FN(FakeButtonEvent, int, (Display * dpy, unsigned int button, Bool is_press, unsigned long delay));\n      _FN(FakeKeyEvent, int, (Display * dpy, unsigned int keycode, Bool is_press, unsigned long delay));\n\n      static int\n      init() {\n        static void *handle { nullptr };\n        static bool funcs_loaded = false;\n\n        if (funcs_loaded) return 0;\n\n        if (!handle) {\n          handle = dyn::handle({ \"libXtst.so.6\", \"libXtst.so\" });\n          if (!handle) {\n            return -1;\n          }\n        }\n\n        std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {\n          { (dyn::apiproc *) &FakeMotionEvent, \"XTestFakeMotionEvent\" },\n          { (dyn::apiproc *) &FakeRelativeMotionEvent, \"XTestFakeRelativeMotionEvent\" },\n          { (dyn::apiproc *) &FakeButtonEvent, \"XTestFakeButtonEvent\" },\n          { (dyn::apiproc *) &FakeKeyEvent, \"XTestFakeKeyEvent\" },\n        };\n\n        if (dyn::load(handle, funcs)) {\n          return -1;\n        }\n\n        funcs_loaded = true;\n        return 0;\n      }\n    }  // namespace tst\n\n    static int\n    init() {\n      static void *handle { nullptr };\n      static bool funcs_loaded = false;\n\n      if (funcs_loaded) return 0;\n\n      if (!handle) {\n        handle = dyn::handle({ \"libX11.so.6\", \"libX11.so\" });\n        if (!handle) {\n          return -1;\n        }\n      }\n\n      std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {\n        { (dyn::apiproc *) &OpenDisplay, \"XOpenDisplay\" },\n        { (dyn::apiproc *) &CloseDisplay, \"XCloseDisplay\" },\n        { (dyn::apiproc *) &InitThreads, \"XInitThreads\" },\n        { (dyn::apiproc *) &Flush, \"XFlush\" },\n      };\n\n      if (dyn::load(handle, funcs)) {\n        return -1;\n      }\n\n      funcs_loaded = true;\n      return 0;\n    }\n  }  // namespace x11\n#endif\n\n  constexpr auto mail_evdev = \"platf::evdev\"sv;\n\n  using evdev_t = util::safe_ptr<libevdev, libevdev_free>;\n  using uinput_t = util::safe_ptr<libevdev_uinput, libevdev_uinput_destroy>;\n\n  constexpr pollfd read_pollfd { -1, 0, 0 };\n  KITTY_USING_MOVE_T(pollfd_t, pollfd, read_pollfd, {\n    if (el.fd >= 0) {\n      ioctl(el.fd, EVIOCGRAB, (void *) 0);\n\n      close(el.fd);\n    }\n  });\n\n  using mail_evdev_t = std::tuple<int, uinput_t::pointer, feedback_queue_t, pollfd_t>;\n\n  struct keycode_t {\n    std::uint32_t keycode;\n    std::uint32_t scancode;\n\n#ifdef SUNSHINE_BUILD_X11\n    KeySym keysym;\n#endif\n  };\n\n  constexpr auto UNKNOWN = 0;\n\n  /**\n   * @brief Initializes the keycode constants for translating\n   *        moonlight keycodes to linux/X11 keycodes.\n   */\n  static constexpr std::array<keycode_t, 0xE3>\n  init_keycodes() {\n    std::array<keycode_t, 0xE3> keycodes {};\n\n#ifdef SUNSHINE_BUILD_X11\n  #define __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym) \\\n    keycodes[wincode] = keycode_t { linuxcode, scancode, keysym };\n#else\n  #define __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym) \\\n    keycodes[wincode] = keycode_t { linuxcode, scancode };\n#endif\n\n#define __CONVERT(wincode, linuxcode, scancode, keysym)                               \\\n  static_assert(wincode < keycodes.size(), \"Keycode doesn't fit into keycode array\"); \\\n  static_assert(wincode >= 0, \"Are you mad?, keycode needs to be greater than zero\"); \\\n  __CONVERT_UNSAFE(wincode, linuxcode, scancode, keysym)\n\n    __CONVERT(0x08 /* VKEY_BACK */, KEY_BACKSPACE, 0x7002A, XK_BackSpace);\n    __CONVERT(0x09 /* VKEY_TAB */, KEY_TAB, 0x7002B, XK_Tab);\n    __CONVERT(0x0C /* VKEY_CLEAR */, KEY_CLEAR, UNKNOWN, XK_Clear);\n    __CONVERT(0x0D /* VKEY_RETURN */, KEY_ENTER, 0x70028, XK_Return);\n    __CONVERT(0x10 /* VKEY_SHIFT */, KEY_LEFTSHIFT, 0x700E1, XK_Shift_L);\n    __CONVERT(0x11 /* VKEY_CONTROL */, KEY_LEFTCTRL, 0x700E0, XK_Control_L);\n    __CONVERT(0x12 /* VKEY_MENU */, KEY_LEFTALT, UNKNOWN, XK_Alt_L);\n    __CONVERT(0x13 /* VKEY_PAUSE */, KEY_PAUSE, UNKNOWN, XK_Pause);\n    __CONVERT(0x14 /* VKEY_CAPITAL */, KEY_CAPSLOCK, 0x70039, XK_Caps_Lock);\n    __CONVERT(0x15 /* VKEY_KANA */, KEY_KATAKANAHIRAGANA, UNKNOWN, XK_Kana_Shift);\n    __CONVERT(0x16 /* VKEY_HANGUL */, KEY_HANGEUL, UNKNOWN, XK_Hangul);\n    __CONVERT(0x17 /* VKEY_JUNJA */, KEY_HANJA, UNKNOWN, XK_Hangul_Jeonja);\n    __CONVERT(0x19 /* VKEY_KANJI */, KEY_KATAKANA, UNKNOWN, XK_Kanji);\n    __CONVERT(0x1B /* VKEY_ESCAPE */, KEY_ESC, 0x70029, XK_Escape);\n    __CONVERT(0x20 /* VKEY_SPACE */, KEY_SPACE, 0x7002C, XK_space);\n    __CONVERT(0x21 /* VKEY_PRIOR */, KEY_PAGEUP, 0x7004B, XK_Page_Up);\n    __CONVERT(0x22 /* VKEY_NEXT */, KEY_PAGEDOWN, 0x7004E, XK_Page_Down);\n    __CONVERT(0x23 /* VKEY_END */, KEY_END, 0x7004D, XK_End);\n    __CONVERT(0x24 /* VKEY_HOME */, KEY_HOME, 0x7004A, XK_Home);\n    __CONVERT(0x25 /* VKEY_LEFT */, KEY_LEFT, 0x70050, XK_Left);\n    __CONVERT(0x26 /* VKEY_UP */, KEY_UP, 0x70052, XK_Up);\n    __CONVERT(0x27 /* VKEY_RIGHT */, KEY_RIGHT, 0x7004F, XK_Right);\n    __CONVERT(0x28 /* VKEY_DOWN */, KEY_DOWN, 0x70051, XK_Down);\n    __CONVERT(0x29 /* VKEY_SELECT */, KEY_SELECT, UNKNOWN, XK_Select);\n    __CONVERT(0x2A /* VKEY_PRINT */, KEY_PRINT, UNKNOWN, XK_Print);\n    __CONVERT(0x2C /* VKEY_SNAPSHOT */, KEY_SYSRQ, 0x70046, XK_Sys_Req);\n    __CONVERT(0x2D /* VKEY_INSERT */, KEY_INSERT, 0x70049, XK_Insert);\n    __CONVERT(0x2E /* VKEY_DELETE */, KEY_DELETE, 0x7004C, XK_Delete);\n    __CONVERT(0x2F /* VKEY_HELP */, KEY_HELP, UNKNOWN, XK_Help);\n    __CONVERT(0x30 /* VKEY_0 */, KEY_0, 0x70027, XK_0);\n    __CONVERT(0x31 /* VKEY_1 */, KEY_1, 0x7001E, XK_1);\n    __CONVERT(0x32 /* VKEY_2 */, KEY_2, 0x7001F, XK_2);\n    __CONVERT(0x33 /* VKEY_3 */, KEY_3, 0x70020, XK_3);\n    __CONVERT(0x34 /* VKEY_4 */, KEY_4, 0x70021, XK_4);\n    __CONVERT(0x35 /* VKEY_5 */, KEY_5, 0x70022, XK_5);\n    __CONVERT(0x36 /* VKEY_6 */, KEY_6, 0x70023, XK_6);\n    __CONVERT(0x37 /* VKEY_7 */, KEY_7, 0x70024, XK_7);\n    __CONVERT(0x38 /* VKEY_8 */, KEY_8, 0x70025, XK_8);\n    __CONVERT(0x39 /* VKEY_9 */, KEY_9, 0x70026, XK_9);\n    __CONVERT(0x41 /* VKEY_A */, KEY_A, 0x70004, XK_A);\n    __CONVERT(0x42 /* VKEY_B */, KEY_B, 0x70005, XK_B);\n    __CONVERT(0x43 /* VKEY_C */, KEY_C, 0x70006, XK_C);\n    __CONVERT(0x44 /* VKEY_D */, KEY_D, 0x70007, XK_D);\n    __CONVERT(0x45 /* VKEY_E */, KEY_E, 0x70008, XK_E);\n    __CONVERT(0x46 /* VKEY_F */, KEY_F, 0x70009, XK_F);\n    __CONVERT(0x47 /* VKEY_G */, KEY_G, 0x7000A, XK_G);\n    __CONVERT(0x48 /* VKEY_H */, KEY_H, 0x7000B, XK_H);\n    __CONVERT(0x49 /* VKEY_I */, KEY_I, 0x7000C, XK_I);\n    __CONVERT(0x4A /* VKEY_J */, KEY_J, 0x7000D, XK_J);\n    __CONVERT(0x4B /* VKEY_K */, KEY_K, 0x7000E, XK_K);\n    __CONVERT(0x4C /* VKEY_L */, KEY_L, 0x7000F, XK_L);\n    __CONVERT(0x4D /* VKEY_M */, KEY_M, 0x70010, XK_M);\n    __CONVERT(0x4E /* VKEY_N */, KEY_N, 0x70011, XK_N);\n    __CONVERT(0x4F /* VKEY_O */, KEY_O, 0x70012, XK_O);\n    __CONVERT(0x50 /* VKEY_P */, KEY_P, 0x70013, XK_P);\n    __CONVERT(0x51 /* VKEY_Q */, KEY_Q, 0x70014, XK_Q);\n    __CONVERT(0x52 /* VKEY_R */, KEY_R, 0x70015, XK_R);\n    __CONVERT(0x53 /* VKEY_S */, KEY_S, 0x70016, XK_S);\n    __CONVERT(0x54 /* VKEY_T */, KEY_T, 0x70017, XK_T);\n    __CONVERT(0x55 /* VKEY_U */, KEY_U, 0x70018, XK_U);\n    __CONVERT(0x56 /* VKEY_V */, KEY_V, 0x70019, XK_V);\n    __CONVERT(0x57 /* VKEY_W */, KEY_W, 0x7001A, XK_W);\n    __CONVERT(0x58 /* VKEY_X */, KEY_X, 0x7001B, XK_X);\n    __CONVERT(0x59 /* VKEY_Y */, KEY_Y, 0x7001C, XK_Y);\n    __CONVERT(0x5A /* VKEY_Z */, KEY_Z, 0x7001D, XK_Z);\n    __CONVERT(0x5B /* VKEY_LWIN */, KEY_LEFTMETA, 0x700E3, XK_Meta_L);\n    __CONVERT(0x5C /* VKEY_RWIN */, KEY_RIGHTMETA, 0x700E7, XK_Meta_R);\n    __CONVERT(0x5F /* VKEY_SLEEP */, KEY_SLEEP, UNKNOWN, UNKNOWN);\n    __CONVERT(0x60 /* VKEY_NUMPAD0 */, KEY_KP0, 0x70062, XK_KP_0);\n    __CONVERT(0x61 /* VKEY_NUMPAD1 */, KEY_KP1, 0x70059, XK_KP_1);\n    __CONVERT(0x62 /* VKEY_NUMPAD2 */, KEY_KP2, 0x7005A, XK_KP_2);\n    __CONVERT(0x63 /* VKEY_NUMPAD3 */, KEY_KP3, 0x7005B, XK_KP_3);\n    __CONVERT(0x64 /* VKEY_NUMPAD4 */, KEY_KP4, 0x7005C, XK_KP_4);\n    __CONVERT(0x65 /* VKEY_NUMPAD5 */, KEY_KP5, 0x7005D, XK_KP_5);\n    __CONVERT(0x66 /* VKEY_NUMPAD6 */, KEY_KP6, 0x7005E, XK_KP_6);\n    __CONVERT(0x67 /* VKEY_NUMPAD7 */, KEY_KP7, 0x7005F, XK_KP_7);\n    __CONVERT(0x68 /* VKEY_NUMPAD8 */, KEY_KP8, 0x70060, XK_KP_8);\n    __CONVERT(0x69 /* VKEY_NUMPAD9 */, KEY_KP9, 0x70061, XK_KP_9);\n    __CONVERT(0x6A /* VKEY_MULTIPLY */, KEY_KPASTERISK, 0x70055, XK_KP_Multiply);\n    __CONVERT(0x6B /* VKEY_ADD */, KEY_KPPLUS, 0x70057, XK_KP_Add);\n    __CONVERT(0x6C /* VKEY_SEPARATOR */, KEY_KPCOMMA, UNKNOWN, XK_KP_Separator);\n    __CONVERT(0x6D /* VKEY_SUBTRACT */, KEY_KPMINUS, 0x70056, XK_KP_Subtract);\n    __CONVERT(0x6E /* VKEY_DECIMAL */, KEY_KPDOT, 0x70063, XK_KP_Decimal);\n    __CONVERT(0x6F /* VKEY_DIVIDE */, KEY_KPSLASH, 0x70054, XK_KP_Divide);\n    __CONVERT(0x70 /* VKEY_F1 */, KEY_F1, 0x70046, XK_F1);\n    __CONVERT(0x71 /* VKEY_F2 */, KEY_F2, 0x70047, XK_F2);\n    __CONVERT(0x72 /* VKEY_F3 */, KEY_F3, 0x70048, XK_F3);\n    __CONVERT(0x73 /* VKEY_F4 */, KEY_F4, 0x70049, XK_F4);\n    __CONVERT(0x74 /* VKEY_F5 */, KEY_F5, 0x7004a, XK_F5);\n    __CONVERT(0x75 /* VKEY_F6 */, KEY_F6, 0x7004b, XK_F6);\n    __CONVERT(0x76 /* VKEY_F7 */, KEY_F7, 0x7004c, XK_F7);\n    __CONVERT(0x77 /* VKEY_F8 */, KEY_F8, 0x7004d, XK_F8);\n    __CONVERT(0x78 /* VKEY_F9 */, KEY_F9, 0x7004e, XK_F9);\n    __CONVERT(0x79 /* VKEY_F10 */, KEY_F10, 0x70044, XK_F10);\n    __CONVERT(0x7A /* VKEY_F11 */, KEY_F11, 0x70044, XK_F11);\n    __CONVERT(0x7B /* VKEY_F12 */, KEY_F12, 0x70045, XK_F12);\n    __CONVERT(0x7C /* VKEY_F13 */, KEY_F13, 0x7003a, XK_F13);\n    __CONVERT(0x7D /* VKEY_F14 */, KEY_F14, 0x7003b, XK_F14);\n    __CONVERT(0x7E /* VKEY_F15 */, KEY_F15, 0x7003c, XK_F15);\n    __CONVERT(0x7F /* VKEY_F16 */, KEY_F16, 0x7003d, XK_F16);\n    __CONVERT(0x80 /* VKEY_F17 */, KEY_F17, 0x7003e, XK_F17);\n    __CONVERT(0x81 /* VKEY_F18 */, KEY_F18, 0x7003f, XK_F18);\n    __CONVERT(0x82 /* VKEY_F19 */, KEY_F19, 0x70040, XK_F19);\n    __CONVERT(0x83 /* VKEY_F20 */, KEY_F20, 0x70041, XK_F20);\n    __CONVERT(0x84 /* VKEY_F21 */, KEY_F21, 0x70042, XK_F21);\n    __CONVERT(0x85 /* VKEY_F22 */, KEY_F12, 0x70043, XK_F12);\n    __CONVERT(0x86 /* VKEY_F23 */, KEY_F23, 0x70044, XK_F23);\n    __CONVERT(0x87 /* VKEY_F24 */, KEY_F24, 0x70045, XK_F24);\n    __CONVERT(0x90 /* VKEY_NUMLOCK */, KEY_NUMLOCK, 0x70053, XK_Num_Lock);\n    __CONVERT(0x91 /* VKEY_SCROLL */, KEY_SCROLLLOCK, 0x70047, XK_Scroll_Lock);\n    __CONVERT(0xA0 /* VKEY_LSHIFT */, KEY_LEFTSHIFT, 0x700E1, XK_Shift_L);\n    __CONVERT(0xA1 /* VKEY_RSHIFT */, KEY_RIGHTSHIFT, 0x700E5, XK_Shift_R);\n    __CONVERT(0xA2 /* VKEY_LCONTROL */, KEY_LEFTCTRL, 0x700E0, XK_Control_L);\n    __CONVERT(0xA3 /* VKEY_RCONTROL */, KEY_RIGHTCTRL, 0x700E4, XK_Control_R);\n    __CONVERT(0xA4 /* VKEY_LMENU */, KEY_LEFTALT, 0x7002E, XK_Alt_L);\n    __CONVERT(0xA5 /* VKEY_RMENU */, KEY_RIGHTALT, 0x700E6, XK_Alt_R);\n    __CONVERT(0xBA /* VKEY_OEM_1 */, KEY_SEMICOLON, 0x70033, XK_semicolon);\n    __CONVERT(0xBB /* VKEY_OEM_PLUS */, KEY_EQUAL, 0x7002E, XK_equal);\n    __CONVERT(0xBC /* VKEY_OEM_COMMA */, KEY_COMMA, 0x70036, XK_comma);\n    __CONVERT(0xBD /* VKEY_OEM_MINUS */, KEY_MINUS, 0x7002D, XK_minus);\n    __CONVERT(0xBE /* VKEY_OEM_PERIOD */, KEY_DOT, 0x70037, XK_period);\n    __CONVERT(0xBF /* VKEY_OEM_2 */, KEY_SLASH, 0x70038, XK_slash);\n    __CONVERT(0xC0 /* VKEY_OEM_3 */, KEY_GRAVE, 0x70035, XK_grave);\n    __CONVERT(0xDB /* VKEY_OEM_4 */, KEY_LEFTBRACE, 0x7002F, XK_braceleft);\n    __CONVERT(0xDC /* VKEY_OEM_5 */, KEY_BACKSLASH, 0x70031, XK_backslash);\n    __CONVERT(0xDD /* VKEY_OEM_6 */, KEY_RIGHTBRACE, 0x70030, XK_braceright);\n    __CONVERT(0xDE /* VKEY_OEM_7 */, KEY_APOSTROPHE, 0x70034, XK_apostrophe);\n    __CONVERT(0xE2 /* VKEY_NON_US_BACKSLASH */, KEY_102ND, 0x70064, XK_backslash);\n#undef __CONVERT\n#undef __CONVERT_UNSAFE\n\n    return keycodes;\n  }\n\n  static constexpr auto keycodes = init_keycodes();\n\n  constexpr touch_port_t target_touch_port {\n    0, 0,\n    19200, 12000\n  };\n\n  static std::pair<std::uint32_t, std::uint32_t>\n  operator*(const std::pair<std::uint32_t, std::uint32_t> &l, int r) {\n    return {\n      l.first * r,\n      l.second * r,\n    };\n  }\n\n  static std::pair<std::uint32_t, std::uint32_t>\n  operator/(const std::pair<std::uint32_t, std::uint32_t> &l, int r) {\n    return {\n      l.first / r,\n      l.second / r,\n    };\n  }\n\n  static std::pair<std::uint32_t, std::uint32_t> &\n  operator+=(std::pair<std::uint32_t, std::uint32_t> &l, const std::pair<std::uint32_t, std::uint32_t> &r) {\n    l.first += r.first;\n    l.second += r.second;\n\n    return l;\n  }\n\n  static inline void\n  print(const ff_envelope &envelope) {\n    BOOST_LOG(debug)\n      << \"Envelope:\"sv << std::endl\n      << \"  attack_length: \" << envelope.attack_length << std::endl\n      << \"  attack_level: \" << envelope.attack_level << std::endl\n      << \"  fade_length: \" << envelope.fade_length << std::endl\n      << \"  fade_level: \" << envelope.fade_level;\n  }\n\n  static inline void\n  print(const ff_replay &replay) {\n    BOOST_LOG(debug)\n      << \"Replay:\"sv << std::endl\n      << \"  length: \"sv << replay.length << std::endl\n      << \"  delay: \"sv << replay.delay;\n  }\n\n  static inline void\n  print(const ff_trigger &trigger) {\n    BOOST_LOG(debug)\n      << \"Trigger:\"sv << std::endl\n      << \"  button: \"sv << trigger.button << std::endl\n      << \"  interval: \"sv << trigger.interval;\n  }\n\n  static inline void\n  print(const ff_effect &effect) {\n    BOOST_LOG(debug)\n      << std::endl\n      << std::endl\n      << \"Received rumble effect with id: [\"sv << effect.id << ']';\n\n    switch (effect.type) {\n      case FF_CONSTANT:\n        BOOST_LOG(debug)\n          << \"FF_CONSTANT:\"sv << std::endl\n          << \"  direction: \"sv << effect.direction << std::endl\n          << \"  level: \"sv << effect.u.constant.level;\n\n        print(effect.u.constant.envelope);\n        break;\n\n      case FF_PERIODIC:\n        BOOST_LOG(debug)\n          << \"FF_CONSTANT:\"sv << std::endl\n          << \"  direction: \"sv << effect.direction << std::endl\n          << \"  waveform: \"sv << effect.u.periodic.waveform << std::endl\n          << \"  period: \"sv << effect.u.periodic.period << std::endl\n          << \"  magnitude: \"sv << effect.u.periodic.magnitude << std::endl\n          << \"  offset: \"sv << effect.u.periodic.offset << std::endl\n          << \"  phase: \"sv << effect.u.periodic.phase;\n\n        print(effect.u.periodic.envelope);\n        break;\n\n      case FF_RAMP:\n        BOOST_LOG(debug)\n          << \"FF_RAMP:\"sv << std::endl\n          << \"  direction: \"sv << effect.direction << std::endl\n          << \"  start_level:\" << effect.u.ramp.start_level << std::endl\n          << \"  end_level:\" << effect.u.ramp.end_level;\n\n        print(effect.u.ramp.envelope);\n        break;\n\n      case FF_RUMBLE:\n        BOOST_LOG(debug)\n          << \"FF_RUMBLE:\" << std::endl\n          << \"  direction: \"sv << effect.direction << std::endl\n          << \"  strong_magnitude: \" << effect.u.rumble.strong_magnitude << std::endl\n          << \"  weak_magnitude: \" << effect.u.rumble.weak_magnitude;\n        break;\n\n      case FF_SPRING:\n        BOOST_LOG(debug)\n          << \"FF_SPRING:\" << std::endl\n          << \"  direction: \"sv << effect.direction;\n        break;\n\n      case FF_FRICTION:\n        BOOST_LOG(debug)\n          << \"FF_FRICTION:\" << std::endl\n          << \"  direction: \"sv << effect.direction;\n        break;\n\n      case FF_DAMPER:\n        BOOST_LOG(debug)\n          << \"FF_DAMPER:\" << std::endl\n          << \"  direction: \"sv << effect.direction;\n        break;\n\n      case FF_INERTIA:\n        BOOST_LOG(debug)\n          << \"FF_INERTIA:\" << std::endl\n          << \"  direction: \"sv << effect.direction;\n        break;\n\n      case FF_CUSTOM:\n        BOOST_LOG(debug)\n          << \"FF_CUSTOM:\" << std::endl\n          << \"  direction: \"sv << effect.direction;\n        break;\n\n      default:\n        BOOST_LOG(debug)\n          << \"FF_UNKNOWN:\" << std::endl\n          << \"  direction: \"sv << effect.direction;\n        break;\n    }\n\n    print(effect.replay);\n    print(effect.trigger);\n  }\n\n  // Emulate rumble effects\n  class effect_t {\n  public:\n    KITTY_DEFAULT_CONSTR_MOVE(effect_t)\n\n    effect_t(std::uint8_t gamepadnr, uinput_t::pointer dev, feedback_queue_t &&q):\n        gamepadnr { gamepadnr }, dev { dev }, rumble_queue { std::move(q) }, gain { 0xFFFF }, id_to_data {} {}\n\n    class data_t {\n    public:\n      KITTY_DEFAULT_CONSTR(data_t)\n\n      data_t(const ff_effect &effect):\n          delay { effect.replay.delay },\n          length { effect.replay.length },\n          end_point { std::chrono::steady_clock::time_point::min() },\n          envelope {},\n          start {},\n          end {} {\n        switch (effect.type) {\n          case FF_CONSTANT:\n            start.weak = effect.u.constant.level;\n            start.strong = effect.u.constant.level;\n            end.weak = effect.u.constant.level;\n            end.strong = effect.u.constant.level;\n\n            envelope = effect.u.constant.envelope;\n            break;\n          case FF_PERIODIC:\n            start.weak = effect.u.periodic.magnitude;\n            start.strong = effect.u.periodic.magnitude;\n            end.weak = effect.u.periodic.magnitude;\n            end.strong = effect.u.periodic.magnitude;\n\n            envelope = effect.u.periodic.envelope;\n            break;\n\n          case FF_RAMP:\n            start.weak = effect.u.ramp.start_level;\n            start.strong = effect.u.ramp.start_level;\n            end.weak = effect.u.ramp.end_level;\n            end.strong = effect.u.ramp.end_level;\n\n            envelope = effect.u.ramp.envelope;\n            break;\n\n          case FF_RUMBLE:\n            start.weak = effect.u.rumble.weak_magnitude;\n            start.strong = effect.u.rumble.strong_magnitude;\n            end.weak = effect.u.rumble.weak_magnitude;\n            end.strong = effect.u.rumble.strong_magnitude;\n            break;\n\n          default:\n            BOOST_LOG(warning) << \"Effect type [\"sv << effect.id << \"] not implemented\"sv;\n        }\n      }\n\n      std::uint32_t\n      magnitude(std::chrono::milliseconds time_left, std::uint32_t start, std::uint32_t end) {\n        auto rel = end - start;\n\n        return start + (rel * time_left.count() / length.count());\n      }\n\n      std::pair<std::uint32_t, std::uint32_t>\n      rumble(std::chrono::steady_clock::time_point tp) {\n        if (end_point < tp) {\n          return {};\n        }\n\n        auto time_left =\n          std::chrono::duration_cast<std::chrono::milliseconds>(\n            end_point - tp);\n\n        // If it needs to be delayed'\n        if (time_left > length) {\n          return {};\n        }\n\n        auto t = length - time_left;\n\n        auto weak = magnitude(t, start.weak, end.weak);\n        auto strong = magnitude(t, start.strong, end.strong);\n\n        if (t.count() < envelope.attack_length) {\n          weak = (envelope.attack_level * t.count() + weak * (envelope.attack_length - t.count())) / envelope.attack_length;\n          strong = (envelope.attack_level * t.count() + strong * (envelope.attack_length - t.count())) / envelope.attack_length;\n        }\n        else if (time_left.count() < envelope.fade_length) {\n          auto dt = (t - length).count() + envelope.fade_length;\n\n          weak = (envelope.fade_level * dt + weak * (envelope.fade_length - dt)) / envelope.fade_length;\n          strong = (envelope.fade_level * dt + strong * (envelope.fade_length - dt)) / envelope.fade_length;\n        }\n\n        return {\n          weak, strong\n        };\n      }\n\n      void\n      activate() {\n        end_point = std::chrono::steady_clock::now() + delay + length;\n      }\n\n      void\n      deactivate() {\n        end_point = std::chrono::steady_clock::time_point::min();\n      }\n\n      std::chrono::milliseconds delay;\n      std::chrono::milliseconds length;\n\n      std::chrono::steady_clock::time_point end_point;\n\n      ff_envelope envelope;\n      struct {\n        std::uint32_t weak, strong;\n      } start;\n\n      struct {\n        std::uint32_t weak, strong;\n      } end;\n    };\n\n    std::pair<std::uint32_t, std::uint32_t>\n    rumble(std::chrono::steady_clock::time_point tp) {\n      std::pair<std::uint32_t, std::uint32_t> weak_strong {};\n      for (auto &[_, data] : id_to_data) {\n        weak_strong += data.rumble(tp);\n      }\n\n      weak_strong.first = std::clamp<std::uint32_t>(weak_strong.first, 0, 0xFFFF);\n      weak_strong.second = std::clamp<std::uint32_t>(weak_strong.second, 0, 0xFFFF);\n\n      old_rumble = weak_strong * gain / 0xFFFF;\n      return old_rumble;\n    }\n\n    void\n    upload(const ff_effect &effect) {\n      print(effect);\n\n      auto it = id_to_data.find(effect.id);\n\n      if (it == std::end(id_to_data)) {\n        id_to_data.emplace(effect.id, effect);\n        return;\n      }\n\n      data_t data { effect };\n\n      data.end_point = it->second.end_point;\n      it->second = data;\n    }\n\n    void\n    activate(int id) {\n      auto it = id_to_data.find(id);\n\n      if (it != std::end(id_to_data)) {\n        it->second.activate();\n      }\n    }\n\n    void\n    deactivate(int id) {\n      auto it = id_to_data.find(id);\n\n      if (it != std::end(id_to_data)) {\n        it->second.deactivate();\n      }\n    }\n\n    void\n    erase(int id) {\n      id_to_data.erase(id);\n      BOOST_LOG(debug) << \"Removed rumble effect id [\"sv << id << ']';\n    }\n\n    // Client-relative gamepad index for rumble notifications\n    std::uint8_t gamepadnr;\n\n    // Used as ID for adding/removinf devices from evdev notifications\n    uinput_t::pointer dev;\n\n    feedback_queue_t rumble_queue;\n\n    int gain;\n\n    // No need to send rumble data when old values equals the new values\n    std::pair<std::uint32_t, std::uint32_t> old_rumble;\n\n    std::unordered_map<int, data_t> id_to_data;\n  };\n\n  struct rumble_ctx_t {\n    std::thread rumble_thread;\n\n    safe::queue_t<mail_evdev_t> rumble_queue_queue;\n  };\n\n  void\n  broadcastRumble(safe::queue_t<mail_evdev_t> &ctx);\n  int\n  startRumble(rumble_ctx_t &ctx) {\n    ctx.rumble_thread = std::thread { broadcastRumble, std::ref(ctx.rumble_queue_queue) };\n\n    return 0;\n  }\n\n  void\n  stopRumble(rumble_ctx_t &ctx) {\n    ctx.rumble_queue_queue.stop();\n\n    BOOST_LOG(debug) << \"Waiting for Gamepad notifications to stop...\"sv;\n    ctx.rumble_thread.join();\n    BOOST_LOG(debug) << \"Gamepad notifications stopped\"sv;\n  }\n\n  static auto notifications = safe::make_shared<rumble_ctx_t>(startRumble, stopRumble);\n\n  struct input_raw_t {\n  public:\n    void\n    clear_mouse_rel() {\n      std::filesystem::path mouse_path { appdata() / \"sunshine_mouse_rel\"sv };\n\n      if (std::filesystem::is_symlink(mouse_path)) {\n        std::filesystem::remove(mouse_path);\n      }\n\n      mouse_rel_input.reset();\n    }\n\n    void\n    clear_keyboard() {\n      std::filesystem::path key_path { appdata() / \"sunshine_keyboard\"sv };\n\n      if (std::filesystem::is_symlink(key_path)) {\n        std::filesystem::remove(key_path);\n      }\n\n      keyboard_input.reset();\n    }\n\n    void\n    clear_mouse_abs() {\n      std::filesystem::path mouse_path { appdata() / \"sunshine_mouse_abs\"sv };\n\n      if (std::filesystem::is_symlink(mouse_path)) {\n        std::filesystem::remove(mouse_path);\n      }\n\n      mouse_abs_input.reset();\n    }\n\n    void\n    clear_gamepad(int nr) {\n      auto &[dev, _] = gamepads[nr];\n\n      if (!dev) {\n        return;\n      }\n\n      // Remove this gamepad from notifications\n      rumble_ctx->rumble_queue_queue.raise(nr, dev.get(), nullptr, pollfd_t {});\n\n      std::stringstream ss;\n\n      ss << \"sunshine_gamepad_\"sv << nr;\n\n      auto gamepad_path = platf::appdata() / ss.str();\n      if (std::filesystem::is_symlink(gamepad_path)) {\n        std::filesystem::remove(gamepad_path);\n      }\n\n      gamepads[nr] = std::make_pair(uinput_t {}, gamepad_state_t {});\n    }\n\n    int\n    create_mouse_abs() {\n      int err = libevdev_uinput_create_from_device(mouse_abs_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_abs_input);\n\n      if (err) {\n        BOOST_LOG(error) << \"Could not create Sunshine Mouse (Absolute): \"sv << strerror(-err);\n        return -1;\n      }\n\n      std::filesystem::create_symlink(libevdev_uinput_get_devnode(mouse_abs_input.get()), appdata() / \"sunshine_mouse_abs\"sv);\n\n      return 0;\n    }\n\n    int\n    create_mouse_rel() {\n      int err = libevdev_uinput_create_from_device(mouse_rel_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &mouse_rel_input);\n\n      if (err) {\n        BOOST_LOG(error) << \"Could not create Sunshine Mouse (Relative): \"sv << strerror(-err);\n        return -1;\n      }\n\n      std::filesystem::create_symlink(libevdev_uinput_get_devnode(mouse_rel_input.get()), appdata() / \"sunshine_mouse_rel\"sv);\n\n      return 0;\n    }\n\n    int\n    create_keyboard() {\n      int err = libevdev_uinput_create_from_device(keyboard_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &keyboard_input);\n\n      if (err) {\n        BOOST_LOG(error) << \"Could not create Sunshine Keyboard: \"sv << strerror(-err);\n        return -1;\n      }\n\n      std::filesystem::create_symlink(libevdev_uinput_get_devnode(keyboard_input.get()), appdata() / \"sunshine_keyboard\"sv);\n\n      return 0;\n    }\n\n    int\n    alloc_gamepad(const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t &&feedback_queue) {\n      TUPLE_2D_REF(input, gamepad_state, gamepads[id.globalIndex]);\n\n      int err = libevdev_uinput_create_from_device(gamepad_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &input);\n\n      gamepad_state = gamepad_state_t {};\n\n      if (err) {\n        BOOST_LOG(error) << \"Could not create Sunshine Gamepad: \"sv << strerror(-err);\n        return -1;\n      }\n\n      std::stringstream ss;\n      ss << \"sunshine_gamepad_\"sv << id.globalIndex;\n      auto gamepad_path = platf::appdata() / ss.str();\n\n      if (std::filesystem::is_symlink(gamepad_path)) {\n        std::filesystem::remove(gamepad_path);\n      }\n\n      auto dev_node = libevdev_uinput_get_devnode(input.get());\n\n      rumble_ctx->rumble_queue_queue.raise(\n        id.clientRelativeIndex,\n        input.get(),\n        std::move(feedback_queue),\n        pollfd_t {\n          dup(libevdev_uinput_get_fd(input.get())),\n          (std::int16_t) POLLIN,\n          (std::int16_t) 0,\n        });\n\n      std::filesystem::create_symlink(dev_node, gamepad_path);\n      return 0;\n    }\n\n    void\n    clear() {\n      clear_keyboard();\n      clear_mouse_abs();\n      clear_mouse_rel();\n      for (int x = 0; x < gamepads.size(); ++x) {\n        clear_gamepad(x);\n      }\n\n#ifdef SUNSHINE_BUILD_X11\n      if (display) {\n        x11::CloseDisplay(display);\n        display = nullptr;\n      }\n#endif\n    }\n\n    ~input_raw_t() {\n      clear();\n    }\n\n    safe::shared_t<rumble_ctx_t>::ptr_t rumble_ctx;\n\n    std::vector<std::pair<uinput_t, gamepad_state_t>> gamepads;\n    uinput_t mouse_rel_input;\n    uinput_t mouse_abs_input;\n    uinput_t keyboard_input;\n\n    uint8_t mouse_rel_buttons_down = 0;\n    uint8_t mouse_abs_buttons_down = 0;\n\n    uinput_t::pointer last_mouse_device_used = nullptr;\n    uint8_t *last_mouse_device_buttons_down = nullptr;\n\n    evdev_t gamepad_dev;\n    evdev_t mouse_rel_dev;\n    evdev_t mouse_abs_dev;\n    evdev_t keyboard_dev;\n    evdev_t touchscreen_dev;\n    evdev_t pen_dev;\n\n    int accumulated_vscroll_delta = 0;\n    int accumulated_hscroll_delta = 0;\n\n#ifdef SUNSHINE_BUILD_X11\n    Display *display;\n#endif\n  };\n\n  inline void\n  rumbleIterate(std::vector<effect_t> &effects, std::vector<pollfd_t> &polls, std::chrono::milliseconds to) {\n    std::vector<pollfd> polls_recv;\n    polls_recv.reserve(polls.size());\n    for (auto &poll : polls) {\n      polls_recv.emplace_back(poll.el);\n    }\n\n    auto res = poll(polls_recv.data(), polls_recv.size(), to.count());\n\n    // If timed out\n    if (!res) {\n      return;\n    }\n\n    if (res < 0) {\n      char err_str[1024];\n      BOOST_LOG(error) << \"Couldn't poll Gamepad file descriptors: \"sv << strerror_r(errno, err_str, 1024);\n\n      return;\n    }\n\n    for (int x = 0; x < polls.size(); ++x) {\n      auto poll = std::begin(polls) + x;\n      auto effect_it = std::begin(effects) + x;\n\n      auto fd = (*poll)->fd;\n\n      // TUPLE_2D_REF(dev, q, *dev_q_it);\n\n      // on error\n      if (polls_recv[x].revents & (POLLHUP | POLLRDHUP | POLLERR)) {\n        BOOST_LOG(warning) << \"Gamepad [\"sv << x << \"] file descriptor closed unexpectedly\"sv;\n\n        polls.erase(poll);\n        effects.erase(effect_it);\n\n        --x;\n        continue;\n      }\n\n      if (!(polls_recv[x].revents & POLLIN)) {\n        continue;\n      }\n\n      input_event events[64];\n\n      // Read all available events\n      auto bytes = read(fd, &events, sizeof(events));\n\n      if (bytes < 0) {\n        char err_str[1024];\n\n        BOOST_LOG(error) << \"Couldn't read evdev input [\"sv << errno << \"]: \"sv << strerror_r(errno, err_str, 1024);\n\n        polls.erase(poll);\n        effects.erase(effect_it);\n\n        --x;\n        continue;\n      }\n\n      if (bytes < sizeof(input_event)) {\n        BOOST_LOG(warning) << \"Reading evdev input: Expected at least \"sv << sizeof(input_event) << \" bytes, got \"sv << bytes << \" instead\"sv;\n        continue;\n      }\n\n      auto event_count = bytes / sizeof(input_event);\n\n      for (auto event = events; event != (events + event_count); ++event) {\n        switch (event->type) {\n          case EV_FF:\n            // BOOST_LOG(debug) << \"EV_FF: \"sv << event->value << \" aka \"sv << util::hex(event->value).to_string_view();\n\n            if (event->code == FF_GAIN) {\n              BOOST_LOG(debug) << \"EV_FF: code [FF_GAIN]: value: \"sv << event->value << \" aka \"sv << util::hex(event->value).to_string_view();\n              effect_it->gain = std::clamp(event->value, 0, 0xFFFF);\n\n              break;\n            }\n\n            BOOST_LOG(debug) << \"EV_FF: id [\"sv << event->code << \"]: value: \"sv << event->value << \" aka \"sv << util::hex(event->value).to_string_view();\n\n            if (event->value) {\n              effect_it->activate(event->code);\n            }\n            else {\n              effect_it->deactivate(event->code);\n            }\n            break;\n          case EV_UINPUT:\n            switch (event->code) {\n              case UI_FF_UPLOAD: {\n                uinput_ff_upload upload {};\n\n                // *VERY* important, without this you break\n                // the kernel and have to reboot due to dead\n                // hanging process\n                upload.request_id = event->value;\n\n                ioctl(fd, UI_BEGIN_FF_UPLOAD, &upload);\n                auto fg = util::fail_guard([&]() {\n                  upload.retval = 0;\n                  ioctl(fd, UI_END_FF_UPLOAD, &upload);\n                });\n\n                effect_it->upload(upload.effect);\n              } break;\n              case UI_FF_ERASE: {\n                uinput_ff_erase erase {};\n\n                // *VERY* important, without this you break\n                // the kernel and have to reboot due to dead\n                // hanging process\n                erase.request_id = event->value;\n\n                ioctl(fd, UI_BEGIN_FF_ERASE, &erase);\n                auto fg = util::fail_guard([&]() {\n                  erase.retval = 0;\n                  ioctl(fd, UI_END_FF_ERASE, &erase);\n                });\n\n                effect_it->erase(erase.effect_id);\n              } break;\n            }\n            break;\n          default:\n            BOOST_LOG(debug)\n              << util::hex(event->type).to_string_view() << \": \"sv\n              << util::hex(event->code).to_string_view() << \": \"sv\n              << event->value << \" aka \"sv << util::hex(event->value).to_string_view();\n        }\n      }\n    }\n  }\n\n  void\n  broadcastRumble(safe::queue_t<mail_evdev_t> &rumble_queue_queue) {\n    std::vector<effect_t> effects;\n    std::vector<pollfd_t> polls;\n\n    while (rumble_queue_queue.running()) {\n      while (rumble_queue_queue.peek()) {\n        auto dev_rumble_queue = rumble_queue_queue.pop();\n\n        if (!dev_rumble_queue) {\n          // rumble_queue_queue is no longer running\n          return;\n        }\n\n        auto gamepadnr = std::get<0>(*dev_rumble_queue);\n        auto dev = std::get<1>(*dev_rumble_queue);\n        auto &rumble_queue = std::get<2>(*dev_rumble_queue);\n        auto &pollfd = std::get<3>(*dev_rumble_queue);\n\n        {\n          auto effect_it = std::find_if(std::begin(effects), std::end(effects), [dev](auto &curr_effect) {\n            return dev == curr_effect.dev;\n          });\n\n          if (effect_it != std::end(effects)) {\n            auto poll_it = std::begin(polls) + (effect_it - std::begin(effects));\n\n            polls.erase(poll_it);\n            effects.erase(effect_it);\n\n            BOOST_LOG(debug) << \"Removed Gamepad device from notifications\"sv;\n\n            continue;\n          }\n\n          // There may be an attepmt to remove, that which not exists\n          if (!rumble_queue) {\n            BOOST_LOG(warning) << \"Attempting to remove a gamepad device from notifications that isn't already registered\"sv;\n            continue;\n          }\n        }\n\n        polls.emplace_back(std::move(pollfd));\n        effects.emplace_back(gamepadnr, dev, std::move(rumble_queue));\n\n        BOOST_LOG(debug) << \"Added Gamepad device to notifications\"sv;\n      }\n\n      if (polls.empty()) {\n        std::this_thread::sleep_for(250ms);\n      }\n      else {\n        rumbleIterate(effects, polls, 100ms);\n\n        auto now = std::chrono::steady_clock::now();\n        for (auto &effect : effects) {\n          TUPLE_2D(old_weak, old_strong, effect.old_rumble);\n          TUPLE_2D(weak, strong, effect.rumble(now));\n\n          if (old_weak != weak || old_strong != strong) {\n            BOOST_LOG(debug) << \"Sending haptic feedback: lowfreq [0x\"sv << util::hex(strong).to_string_view() << \"]: highfreq [0x\"sv << util::hex(weak).to_string_view() << ']';\n\n            effect.rumble_queue->raise(gamepad_feedback_msg_t::make_rumble(effect.gamepadnr, strong, weak));\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * @brief XTest absolute mouse move.\n   * @param input The input_t instance to use.\n   * @param x Absolute x position.\n   * @param y Absolute y position.\n   * @examples\n   * x_abs_mouse(input, 0, 0);\n   * @examples_end\n   */\n  static void\n  x_abs_mouse(input_t &input, float x, float y) {\n#ifdef SUNSHINE_BUILD_X11\n    Display *xdisplay = ((input_raw_t *) input.get())->display;\n    if (!xdisplay) {\n      return;\n    }\n    x11::tst::FakeMotionEvent(xdisplay, -1, x, y, CurrentTime);\n    x11::Flush(xdisplay);\n#endif\n  }\n\n  util::point_t\n  get_mouse_loc(input_t &input) {\n#ifdef SUNSHINE_BUILD_X11\n    Display *xdisplay = ((input_raw_t *) input.get())->display;\n    if (!xdisplay) {\n      return util::point_t {};\n    }\n    Window root, root_return, child_return;\n    root = DefaultRootWindow(xdisplay);\n    int root_x, root_y;\n    int win_x, win_y;\n    unsigned int mask_return;\n\n    if (XQueryPointer(xdisplay, root, &root_return, &child_return, &root_x, &root_y, &win_x, &win_y, &mask_return)) {\n      BOOST_LOG(debug)\n        << \"Pointer is at:\"sv << std::endl\n        << \"  x: \" << root_x << std::endl\n        << \"  y: \" << root_y << std::endl;\n\n      return util::point_t { (double) root_x, (double) root_y };\n    }\n    else {\n      BOOST_LOG(debug) << \"Unable to query x11 pointer\"sv << std::endl;\n    }\n#else\n    BOOST_LOG(debug) << \"Unable to query wayland pointer\"sv << std::endl;\n#endif\n    return util::point_t {};\n  }\n\n  /**\n   * @brief Absolute mouse move.\n   * @param input The input_t instance to use.\n   * @param touch_port The touch_port instance to use.\n   * @param x Absolute x position.\n   * @param y Absolute y position.\n   * @examples\n   * abs_mouse(input, touch_port, 0, 0);\n   * @examples_end\n   */\n  void\n  abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {\n    auto raw = (input_raw_t *) input.get();\n    auto mouse_abs = raw->mouse_abs_input.get();\n    if (!mouse_abs) {\n      x_abs_mouse(input, x, y);\n      return;\n    }\n\n    auto scaled_x = (int) std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width));\n    auto scaled_y = (int) std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height));\n\n    libevdev_uinput_write_event(mouse_abs, EV_ABS, ABS_X, scaled_x);\n    libevdev_uinput_write_event(mouse_abs, EV_ABS, ABS_Y, scaled_y);\n    libevdev_uinput_write_event(mouse_abs, EV_SYN, SYN_REPORT, 0);\n\n    // Remember this was the last device we sent input on\n    raw->last_mouse_device_used = mouse_abs;\n    raw->last_mouse_device_buttons_down = &raw->mouse_abs_buttons_down;\n  }\n\n  /**\n   * @brief XTest relative mouse move.\n   * @param input The input_t instance to use.\n   * @param deltaX Relative x position.\n   * @param deltaY Relative y position.\n   * @examples\n   * x_move_mouse(input, 10, 10);  // Move mouse 10 pixels down and right\n   * @examples_end\n   */\n  static void\n  x_move_mouse(input_t &input, int deltaX, int deltaY) {\n#ifdef SUNSHINE_BUILD_X11\n    Display *xdisplay = ((input_raw_t *) input.get())->display;\n    if (!xdisplay) {\n      return;\n    }\n    x11::tst::FakeRelativeMotionEvent(xdisplay, deltaX, deltaY, CurrentTime);\n    x11::Flush(xdisplay);\n#endif\n  }\n\n  /**\n   * @brief Relative mouse move.\n   * @param input The input_t instance to use.\n   * @param deltaX Relative x position.\n   * @param deltaY Relative y position.\n   * @examples\n   * move_mouse(input, 10, 10); // Move mouse 10 pixels down and right\n   * @examples_end\n   */\n  void\n  set_mouse_mode(int mode) {\n    // Virtual mouse driver is Windows-only; no-op on Linux\n  }\n\n  void\n  move_mouse(input_t &input, int deltaX, int deltaY) {\n    auto raw = (input_raw_t *) input.get();\n    auto mouse_rel = raw->mouse_rel_input.get();\n    if (!mouse_rel) {\n      x_move_mouse(input, deltaX, deltaY);\n      return;\n    }\n\n    if (deltaX) {\n      libevdev_uinput_write_event(mouse_rel, EV_REL, REL_X, deltaX);\n    }\n\n    if (deltaY) {\n      libevdev_uinput_write_event(mouse_rel, EV_REL, REL_Y, deltaY);\n    }\n\n    libevdev_uinput_write_event(mouse_rel, EV_SYN, SYN_REPORT, 0);\n\n    // Remember this was the last device we sent input on\n    raw->last_mouse_device_used = mouse_rel;\n    raw->last_mouse_device_buttons_down = &raw->mouse_rel_buttons_down;\n  }\n\n  /**\n   * @brief XTest mouse button press/release.\n   * @param input The input_t instance to use.\n   * @param button Which mouse button to emulate.\n   * @param release Whether the event was a press (false) or a release (true)\n   * @examples\n   * x_button_mouse(input, 1, false); // Press left mouse button\n   * @examples_end\n   */\n  static void\n  x_button_mouse(input_t &input, int button, bool release) {\n#ifdef SUNSHINE_BUILD_X11\n    unsigned int x_button = 0;\n    switch (button) {\n      case BUTTON_LEFT:\n        x_button = 1;\n        break;\n      case BUTTON_MIDDLE:\n        x_button = 2;\n        break;\n      case BUTTON_RIGHT:\n        x_button = 3;\n        break;\n      default:\n        x_button = (button - 4) + 8;  // Button 4 (Moonlight) starts at index 8 (X11)\n        break;\n    }\n\n    if (x_button < 1 || x_button > 31) {\n      return;\n    }\n\n    Display *xdisplay = ((input_raw_t *) input.get())->display;\n    if (!xdisplay) {\n      return;\n    }\n    x11::tst::FakeButtonEvent(xdisplay, x_button, !release, CurrentTime);\n    x11::Flush(xdisplay);\n#endif\n  }\n\n  /**\n   * @brief Mouse button press/release.\n   * @param input The input_t instance to use.\n   * @param button Which mouse button to emulate.\n   * @param release Whether the event was a press (false) or a release (true)\n   * @examples\n   * button_mouse(input, 1, false);  // Press left mouse button\n   * @examples_end\n   */\n  void\n  button_mouse(input_t &input, int button, bool release) {\n    auto raw = (input_raw_t *) input.get();\n\n    // We mimic the Linux vmmouse driver here and prefer to send buttons\n    // on the last mouse device we used. However, we make an exception\n    // if it's a release event and the button is down on the other device.\n    uinput_t::pointer chosen_mouse_dev = nullptr;\n    uint8_t *chosen_mouse_dev_buttons_down = nullptr;\n    if (release) {\n      // Prefer to send the release on the mouse with the button down\n      if (raw->mouse_rel_buttons_down & (1 << button)) {\n        chosen_mouse_dev = raw->mouse_rel_input.get();\n        chosen_mouse_dev_buttons_down = &raw->mouse_rel_buttons_down;\n      }\n      else if (raw->mouse_abs_buttons_down & (1 << button)) {\n        chosen_mouse_dev = raw->mouse_abs_input.get();\n        chosen_mouse_dev_buttons_down = &raw->mouse_abs_buttons_down;\n      }\n    }\n\n    if (!chosen_mouse_dev) {\n      if (raw->last_mouse_device_used) {\n        // Prefer to use the last device we sent motion\n        chosen_mouse_dev = raw->last_mouse_device_used;\n        chosen_mouse_dev_buttons_down = raw->last_mouse_device_buttons_down;\n      }\n      else {\n        // Send on the relative device if we have no preference yet\n        chosen_mouse_dev = raw->mouse_rel_input.get();\n        chosen_mouse_dev_buttons_down = &raw->mouse_rel_buttons_down;\n      }\n    }\n\n    if (!chosen_mouse_dev) {\n      x_button_mouse(input, button, release);\n      return;\n    }\n\n    int btn_type;\n    int scan;\n\n    if (button == 1) {\n      btn_type = BTN_LEFT;\n      scan = 90001;\n    }\n    else if (button == 2) {\n      btn_type = BTN_MIDDLE;\n      scan = 90003;\n    }\n    else if (button == 3) {\n      btn_type = BTN_RIGHT;\n      scan = 90002;\n    }\n    else if (button == 4) {\n      btn_type = BTN_SIDE;\n      scan = 90004;\n    }\n    else {\n      btn_type = BTN_EXTRA;\n      scan = 90005;\n    }\n\n    libevdev_uinput_write_event(chosen_mouse_dev, EV_MSC, MSC_SCAN, scan);\n    libevdev_uinput_write_event(chosen_mouse_dev, EV_KEY, btn_type, release ? 0 : 1);\n    libevdev_uinput_write_event(chosen_mouse_dev, EV_SYN, SYN_REPORT, 0);\n\n    if (release) {\n      *chosen_mouse_dev_buttons_down &= ~(1 << button);\n    }\n    else {\n      *chosen_mouse_dev_buttons_down |= (1 << button);\n    }\n  }\n\n  /**\n   * @brief XTest mouse scroll.\n   * @param input The input_t instance to use.\n   * @param distance How far to scroll.\n   * @param button_pos Which mouse button to emulate for positive scroll.\n   * @param button_neg Which mouse button to emulate for negative scroll.\n   * @examples\n   * x_scroll(input, 10, 4, 5);\n   * @examples_end\n   */\n  static void\n  x_scroll(input_t &input, int distance, int button_pos, int button_neg) {\n#ifdef SUNSHINE_BUILD_X11\n    Display *xdisplay = ((input_raw_t *) input.get())->display;\n    if (!xdisplay) {\n      return;\n    }\n\n    const int button = distance > 0 ? button_pos : button_neg;\n    for (int i = 0; i < abs(distance); i++) {\n      x11::tst::FakeButtonEvent(xdisplay, button, true, CurrentTime);\n      x11::tst::FakeButtonEvent(xdisplay, button, false, CurrentTime);\n    }\n    x11::Flush(xdisplay);\n#endif\n  }\n\n  /**\n   * @brief Vertical mouse scroll.\n   * @param input The input_t instance to use.\n   * @param high_res_distance How far to scroll.\n   * @examples\n   * scroll(input, 1200);\n   * @examples_end\n   */\n  void\n  scroll(input_t &input, int high_res_distance) {\n    auto raw = ((input_raw_t *) input.get());\n\n    raw->accumulated_vscroll_delta += high_res_distance;\n    int full_ticks = raw->accumulated_vscroll_delta / 120;\n\n    // We mimic the Linux vmmouse driver and always send scroll events\n    // via the relative pointing device for Xorg compatibility.\n    auto mouse = raw->mouse_rel_input.get();\n    if (mouse) {\n      if (full_ticks) {\n        libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL, full_ticks);\n      }\n      libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL_HI_RES, high_res_distance);\n      libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0);\n    }\n    else if (full_ticks) {\n      x_scroll(input, full_ticks, 4, 5);\n    }\n\n    raw->accumulated_vscroll_delta -= full_ticks * 120;\n  }\n\n  /**\n   * @brief Horizontal mouse scroll.\n   * @param input The input_t instance to use.\n   * @param high_res_distance How far to scroll.\n   * @examples\n   * hscroll(input, 1200);\n   * @examples_end\n   */\n  void\n  hscroll(input_t &input, int high_res_distance) {\n    auto raw = ((input_raw_t *) input.get());\n\n    raw->accumulated_hscroll_delta += high_res_distance;\n    int full_ticks = raw->accumulated_hscroll_delta / 120;\n\n    // We mimic the Linux vmmouse driver and always send scroll events\n    // via the relative pointing device for Xorg compatibility.\n    auto mouse_rel = raw->mouse_rel_input.get();\n    if (mouse_rel) {\n      if (full_ticks) {\n        libevdev_uinput_write_event(mouse_rel, EV_REL, REL_HWHEEL, full_ticks);\n      }\n      libevdev_uinput_write_event(mouse_rel, EV_REL, REL_HWHEEL_HI_RES, high_res_distance);\n      libevdev_uinput_write_event(mouse_rel, EV_SYN, SYN_REPORT, 0);\n    }\n    else if (full_ticks) {\n      x_scroll(input, full_ticks, 6, 7);\n    }\n\n    raw->accumulated_hscroll_delta -= full_ticks * 120;\n  }\n\n  static keycode_t\n  keysym(std::uint16_t modcode) {\n    if (modcode <= keycodes.size()) {\n      return keycodes[modcode];\n    }\n\n    return {};\n  }\n\n  /**\n   * @brief XTest keyboard emulation.\n   * @param input The input_t instance to use.\n   * @param modcode The moonlight key code.\n   * @param release Whether the event was a press (false) or a release (true).\n   * @param flags SS_KBE_FLAG_* values.\n   * @examples\n   * x_keyboard(input, 0x5A, false, 0);  // Press Z\n   * @examples_end\n   */\n  static void\n  x_keyboard(input_t &input, uint16_t modcode, bool release, uint8_t flags) {\n#ifdef SUNSHINE_BUILD_X11\n    auto keycode = keysym(modcode);\n    if (keycode.keysym == UNKNOWN) {\n      return;\n    }\n\n    Display *xdisplay = ((input_raw_t *) input.get())->display;\n    if (!xdisplay) {\n      return;\n    }\n\n    const auto keycode_x = XKeysymToKeycode(xdisplay, keycode.keysym);\n    if (keycode_x == 0) {\n      return;\n    }\n\n    x11::tst::FakeKeyEvent(xdisplay, keycode_x, !release, CurrentTime);\n    x11::Flush(xdisplay);\n#endif\n  }\n\n  /**\n   * @brief Keyboard emulation.\n   * @param input The input_t instance to use.\n   * @param modcode The moonlight key code.\n   * @param release Whether the event was a press (false) or a release (true).\n   * @param flags SS_KBE_FLAG_* values.\n   * @examples\n   * keyboard(input, 0x5A, false, 0);  // Press Z\n   * @examples_end\n   */\n  void\n  keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) {\n    auto keyboard = ((input_raw_t *) input.get())->keyboard_input.get();\n    if (!keyboard) {\n      x_keyboard(input, modcode, release, flags);\n      return;\n    }\n\n    auto keycode = keysym(modcode);\n    if (keycode.keycode == UNKNOWN) {\n      return;\n    }\n\n    if (keycode.scancode != UNKNOWN) {\n      libevdev_uinput_write_event(keyboard, EV_MSC, MSC_SCAN, keycode.scancode);\n    }\n\n    libevdev_uinput_write_event(keyboard, EV_KEY, keycode.keycode, release ? 0 : 1);\n    libevdev_uinput_write_event(keyboard, EV_SYN, SYN_REPORT, 0);\n  }\n\n  void\n  keyboard_ev(libevdev_uinput *keyboard, int linux_code, int event_code = 1) {\n    libevdev_uinput_write_event(keyboard, EV_KEY, linux_code, event_code);\n    libevdev_uinput_write_event(keyboard, EV_SYN, SYN_REPORT, 0);\n  }\n\n  /**\n   * Takes an UTF-32 encoded string and returns a hex string representation of the bytes (uppercase)\n   *\n   * ex: ['👱'] = \"1F471\" // see UTF encoding at https://www.compart.com/en/unicode/U+1F471\n   *\n   * adapted from: https://stackoverflow.com/a/7639754\n   */\n  std::string\n  to_hex(const std::basic_string<char32_t> &str) {\n    std::stringstream ss;\n    ss << std::hex << std::setfill('0');\n    for (const auto &ch : str) {\n      ss << static_cast<uint_least32_t>(ch);\n    }\n\n    std::string hex_unicode(ss.str());\n    std::transform(hex_unicode.begin(), hex_unicode.end(), hex_unicode.begin(), ::toupper);\n    return hex_unicode;\n  }\n\n  /**\n   * Here we receive a single UTF-8 encoded char at a time,\n   * the trick is to convert it to UTF-32 then send CTRL+SHIFT+U+{HEXCODE} in order to produce any\n   * unicode character, see: https://en.wikipedia.org/wiki/Unicode_input\n   *\n   * ex:\n   * - when receiving UTF-8 [0xF0 0x9F 0x91 0xB1] (which is '👱')\n   * - we'll convert it to UTF-32 [0x1F471]\n   * - then type: CTRL+SHIFT+U+1F471\n   * see the conversion at: https://www.compart.com/en/unicode/U+1F471\n   */\n  void\n  unicode(input_t &input, char *utf8, int size) {\n    auto kb = ((input_raw_t *) input.get())->keyboard_input.get();\n    if (!kb) {\n      return;\n    }\n\n    /* Reading input text as UTF-8 */\n    auto utf8_str = boost::locale::conv::to_utf<wchar_t>(utf8, utf8 + size, \"UTF-8\");\n    /* Converting to UTF-32 */\n    auto utf32_str = boost::locale::conv::utf_to_utf<char32_t>(utf8_str);\n    /* To HEX string */\n    auto hex_unicode = to_hex(utf32_str);\n    BOOST_LOG(debug) << \"Unicode, typing U+\"sv << hex_unicode;\n\n    /* pressing <CTRL> + <SHIFT> + U */\n    keyboard_ev(kb, KEY_LEFTCTRL, 1);\n    keyboard_ev(kb, KEY_LEFTSHIFT, 1);\n    keyboard_ev(kb, KEY_U, 1);\n    keyboard_ev(kb, KEY_U, 0);\n\n    /* input each HEX character */\n    for (auto &ch : hex_unicode) {\n      auto key_str = \"KEY_\"s + ch;\n      auto keycode = libevdev_event_code_from_name(EV_KEY, key_str.c_str());\n      if (keycode == -1) {\n        BOOST_LOG(warning) << \"Unicode, unable to find keycode for: \"sv << ch;\n      }\n      else {\n        keyboard_ev(kb, keycode, 1);\n        keyboard_ev(kb, keycode, 0);\n      }\n    }\n\n    /* releasing <SHIFT> and <CTRL> */\n    keyboard_ev(kb, KEY_LEFTSHIFT, 0);\n    keyboard_ev(kb, KEY_LEFTCTRL, 0);\n  }\n\n  int\n  alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {\n    return ((input_raw_t *) input.get())->alloc_gamepad(id, metadata, std::move(feedback_queue));\n  }\n\n  void\n  free_gamepad(input_t &input, int nr) {\n    ((input_raw_t *) input.get())->clear_gamepad(nr);\n  }\n\n  void\n  gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) {\n    TUPLE_2D_REF(uinput, gamepad_state_old, ((input_raw_t *) input.get())->gamepads[nr]);\n\n    auto bf = gamepad_state.buttonFlags ^ gamepad_state_old.buttonFlags;\n    auto bf_new = gamepad_state.buttonFlags;\n\n    if (bf) {\n      // up pressed == -1, down pressed == 1, else 0\n      if ((DPAD_UP | DPAD_DOWN) & bf) {\n        int button_state = bf_new & DPAD_UP ? -1 : (bf_new & DPAD_DOWN ? 1 : 0);\n\n        libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0Y, button_state);\n      }\n\n      if ((DPAD_LEFT | DPAD_RIGHT) & bf) {\n        int button_state = bf_new & DPAD_LEFT ? -1 : (bf_new & DPAD_RIGHT ? 1 : 0);\n\n        libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_HAT0X, button_state);\n      }\n\n      if (START & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_START, bf_new & START ? 1 : 0);\n      if (BACK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SELECT, bf_new & BACK ? 1 : 0);\n      if (LEFT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBL, bf_new & LEFT_STICK ? 1 : 0);\n      if (RIGHT_STICK & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_THUMBR, bf_new & RIGHT_STICK ? 1 : 0);\n      if (LEFT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TL, bf_new & LEFT_BUTTON ? 1 : 0);\n      if (RIGHT_BUTTON & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_TR, bf_new & RIGHT_BUTTON ? 1 : 0);\n      if ((HOME | MISC_BUTTON) & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_MODE, bf_new & (HOME | MISC_BUTTON) ? 1 : 0);\n      if (A & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_SOUTH, bf_new & A ? 1 : 0);\n      if (B & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_EAST, bf_new & B ? 1 : 0);\n      if (X & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_NORTH, bf_new & X ? 1 : 0);\n      if (Y & bf) libevdev_uinput_write_event(uinput.get(), EV_KEY, BTN_WEST, bf_new & Y ? 1 : 0);\n    }\n\n    if (gamepad_state_old.lt != gamepad_state.lt) {\n      libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_Z, gamepad_state.lt);\n    }\n\n    if (gamepad_state_old.rt != gamepad_state.rt) {\n      libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RZ, gamepad_state.rt);\n    }\n\n    if (gamepad_state_old.lsX != gamepad_state.lsX) {\n      libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_X, gamepad_state.lsX);\n    }\n\n    if (gamepad_state_old.lsY != gamepad_state.lsY) {\n      libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_Y, -gamepad_state.lsY);\n    }\n\n    if (gamepad_state_old.rsX != gamepad_state.rsX) {\n      libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RX, gamepad_state.rsX);\n    }\n\n    if (gamepad_state_old.rsY != gamepad_state.rsY) {\n      libevdev_uinput_write_event(uinput.get(), EV_ABS, ABS_RY, -gamepad_state.rsY);\n    }\n\n    gamepad_state_old = gamepad_state;\n    libevdev_uinput_write_event(uinput.get(), EV_SYN, SYN_REPORT, 0);\n  }\n\n  constexpr auto NUM_TOUCH_SLOTS = 10;\n  constexpr auto DISTANCE_MAX = 1024;\n  constexpr auto PRESSURE_MAX = 4096;\n  constexpr int64_t INVALID_TRACKING_ID = -1;\n\n  // HACK: Contacts with very small pressure values get discarded by libinput, but\n  // we assume that the client has already excluded such errant touches. We enforce\n  // a minimum pressure value to prevent our touches from being discarded.\n  constexpr auto PRESSURE_MIN = 0.10f;\n\n  struct client_input_raw_t: public client_input_t {\n    client_input_raw_t(input_t &input) {\n      global = (input_raw_t *) input.get();\n      touch_slots.fill(INVALID_TRACKING_ID);\n    }\n\n    input_raw_t *global;\n\n    // Device state and handles for pen and touch input must be stored in the per-client\n    // input context, because each connected client may be sending their own independent\n    // pen/touch events. To maintain separation, we expose separate pen and touch devices\n    // for each client.\n\n    // Mapping of ABS_MT_SLOT/ABS_MT_TRACKING_ID -> pointerId\n    std::array<int64_t, NUM_TOUCH_SLOTS> touch_slots;\n    uinput_t touch_input;\n    uinput_t pen_input;\n  };\n\n  /**\n   * @brief Allocates a context to store per-client input data.\n   * @param input The global input context.\n   * @return A unique pointer to a per-client input data context.\n   */\n  std::unique_ptr<client_input_t>\n  allocate_client_input_context(input_t &input) {\n    return std::make_unique<client_input_raw_t>(input);\n  }\n\n  /**\n   * @brief Retrieves the slot index for a given pointer ID.\n   * @param input The client-specific input context.\n   * @param pointerId The pointer ID sent from the client.\n   * @return Slot index or -1 if not found.\n   */\n  int\n  slot_index_by_pointer_id(client_input_raw_t *input, uint32_t pointerId) {\n    for (int i = 0; i < input->touch_slots.size(); i++) {\n      if (input->touch_slots[i] == pointerId) {\n        return i;\n      }\n    }\n    return -1;\n  }\n\n  /**\n   * @brief Reserves a slot index for a new pointer ID.\n   * @param input The client-specific input context.\n   * @param pointerId The pointer ID sent from the client.\n   * @return Slot index or -1 if no unallocated slots remain.\n   */\n  int\n  allocate_slot_index_for_pointer_id(client_input_raw_t *input, uint32_t pointerId) {\n    int i = slot_index_by_pointer_id(input, pointerId);\n    if (i >= 0) {\n      BOOST_LOG(warning) << \"Pointer \"sv << pointerId << \" already down. Did the client drop an up/cancel event?\"sv;\n      return i;\n    }\n\n    for (int i = 0; i < input->touch_slots.size(); i++) {\n      if (input->touch_slots[i] == INVALID_TRACKING_ID) {\n        input->touch_slots[i] = pointerId;\n        return i;\n      }\n    }\n\n    return -1;\n  }\n\n  /**\n   * @brief Sends a touch event to the OS.\n   * @param input The client-specific input context.\n   * @param touch_port The current viewport for translating to screen coordinates.\n   * @param touch The touch event.\n   */\n  void\n  touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) {\n    auto raw = (client_input_raw_t *) input;\n\n    if (!raw->touch_input) {\n      int err = libevdev_uinput_create_from_device(raw->global->touchscreen_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &raw->touch_input);\n      if (err) {\n        BOOST_LOG(error) << \"Could not create Sunshine Touchscreen: \"sv << strerror(-err);\n        return;\n      }\n    }\n\n    auto touch_input = raw->touch_input.get();\n\n    float pressure = std::max(PRESSURE_MIN, touch.pressureOrDistance);\n\n    if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) {\n      for (int i = 0; i < raw->touch_slots.size(); i++) {\n        libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, i);\n        libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1);\n      }\n      raw->touch_slots.fill(INVALID_TRACKING_ID);\n\n      libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0);\n      libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0);\n      libevdev_uinput_write_event(touch_input, EV_SYN, SYN_REPORT, 0);\n      return;\n    }\n\n    if (touch.eventType == LI_TOUCH_EVENT_CANCEL) {\n      // Stop tracking this slot\n      auto slot_index = slot_index_by_pointer_id(raw, touch.pointerId);\n      if (slot_index >= 0) {\n        libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, slot_index);\n        libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1);\n\n        raw->touch_slots[slot_index] = INVALID_TRACKING_ID;\n\n        // Raise BTN_TOUCH if no touches are down\n        if (std::all_of(raw->touch_slots.cbegin(), raw->touch_slots.cend(),\n              [](uint64_t pointer_id) { return pointer_id == INVALID_TRACKING_ID; })) {\n          libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0);\n\n          // This may have been the final slot down which was also being emulated\n          // through the single-touch axes. Reset ABS_PRESSURE to ensure code that\n          // uses ABS_PRESSURE instead of BTN_TOUCH will work properly.\n          libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0);\n        }\n      }\n    }\n    else if (touch.eventType == LI_TOUCH_EVENT_DOWN ||\n             touch.eventType == LI_TOUCH_EVENT_MOVE ||\n             touch.eventType == LI_TOUCH_EVENT_UP) {\n      int slot_index;\n      if (touch.eventType == LI_TOUCH_EVENT_DOWN) {\n        // Allocate a new slot for this new touch\n        slot_index = allocate_slot_index_for_pointer_id(raw, touch.pointerId);\n        if (slot_index < 0) {\n          BOOST_LOG(error) << \"No unused pointer entries! Cancelling all active touches!\"sv;\n\n          for (int i = 0; i < raw->touch_slots.size(); i++) {\n            libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, i);\n            libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1);\n          }\n          raw->touch_slots.fill(INVALID_TRACKING_ID);\n\n          libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0);\n          libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0);\n          libevdev_uinput_write_event(touch_input, EV_SYN, SYN_REPORT, 0);\n\n          // All slots are clear, so this should never fail on the second try\n          slot_index = allocate_slot_index_for_pointer_id(raw, touch.pointerId);\n          assert(slot_index >= 0);\n        }\n      }\n      else {\n        // Lookup the slot of the previous touch with this pointer ID\n        slot_index = slot_index_by_pointer_id(raw, touch.pointerId);\n        if (slot_index < 0) {\n          BOOST_LOG(warning) << \"Pointer \"sv << touch.pointerId << \" is not down. Did the client drop a down event?\"sv;\n          return;\n        }\n      }\n\n      libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_SLOT, slot_index);\n\n      if (touch.eventType == LI_TOUCH_EVENT_UP) {\n        // Stop tracking this touch\n        libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, -1);\n        raw->touch_slots[slot_index] = INVALID_TRACKING_ID;\n\n        // Raise BTN_TOUCH if no touches are down\n        if (std::all_of(raw->touch_slots.cbegin(), raw->touch_slots.cend(),\n              [](uint64_t pointer_id) { return pointer_id == INVALID_TRACKING_ID; })) {\n          libevdev_uinput_write_event(touch_input, EV_KEY, BTN_TOUCH, 0);\n\n          // This may have been the final slot down which was also being emulated\n          // through the single-touch axes. Reset ABS_PRESSURE to ensure code that\n          // uses ABS_PRESSURE instead of BTN_TOUCH will work properly.\n          libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, 0);\n        }\n      }\n      else {\n        float x = touch.x * touch_port.width;\n        float y = touch.y * touch_port.height;\n\n        auto scaled_x = (int) std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width));\n        auto scaled_y = (int) std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height));\n\n        libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TRACKING_ID, slot_index);\n        libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_POSITION_X, scaled_x);\n        libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_POSITION_Y, scaled_y);\n\n        if (touch.pressureOrDistance) {\n          libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_PRESSURE, PRESSURE_MAX * pressure);\n        }\n        else if (touch.eventType == LI_TOUCH_EVENT_DOWN) {\n          // Always report some moderate pressure value when down\n          libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_PRESSURE, PRESSURE_MAX / 2);\n        }\n\n        if (touch.rotation != LI_ROT_UNKNOWN) {\n          // Convert our 0..360 range to -90..90 relative to Y axis\n          int adjusted_angle = touch.rotation;\n\n          if (touch.rotation > 90 && touch.rotation < 270) {\n            // Lower hemisphere\n            adjusted_angle = 180 - adjusted_angle;\n          }\n\n          // Wrap the value if it's out of range\n          if (adjusted_angle > 90) {\n            adjusted_angle -= 360;\n          }\n          else if (adjusted_angle < -90) {\n            adjusted_angle += 360;\n          }\n\n          libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_ORIENTATION, adjusted_angle);\n        }\n\n        if (touch.contactAreaMajor) {\n          // Contact area comes from the input core scaled to the provided touch_port,\n          // however we need it rescaled to target_touch_port instead.\n          auto target_scaled_contact_area = input::scale_client_contact_area(\n            { touch.contactAreaMajor * 65535.f, touch.contactAreaMinor * 65535.f },\n            touch.rotation,\n            { target_touch_port.width / (touch_port.width * 65535.f),\n              target_touch_port.height / (touch_port.height * 65535.f) });\n\n          libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TOUCH_MAJOR, target_scaled_contact_area.first);\n\n          // scale_client_contact_area() will treat the contact area as circular (major == minor)\n          // if the minor axis wasn't specified, so we unconditionally report ABS_MT_TOUCH_MINOR.\n          libevdev_uinput_write_event(touch_input, EV_ABS, ABS_MT_TOUCH_MINOR, target_scaled_contact_area.second);\n        }\n\n        // If this slot is the first active one, send our data through the single touch axes as well\n        for (int i = 0; i <= slot_index; i++) {\n          if (raw->touch_slots[i] != INVALID_TRACKING_ID) {\n            if (i == slot_index) {\n              libevdev_uinput_write_event(touch_input, EV_ABS, ABS_X, scaled_x);\n              libevdev_uinput_write_event(touch_input, EV_ABS, ABS_Y, scaled_y);\n              if (touch.pressureOrDistance) {\n                libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX * pressure);\n              }\n              else if (touch.eventType == LI_TOUCH_EVENT_DOWN) {\n                libevdev_uinput_write_event(touch_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX / 2);\n              }\n            }\n            break;\n          }\n        }\n      }\n\n      libevdev_uinput_write_event(touch_input, EV_SYN, SYN_REPORT, 0);\n    }\n  }\n\n  /**\n   * @brief Sends a pen event to the OS.\n   * @param input The client-specific input context.\n   * @param touch_port The current viewport for translating to screen coordinates.\n   * @param pen The pen event.\n   */\n  void\n  pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) {\n    auto raw = (client_input_raw_t *) input;\n\n    if (!raw->pen_input) {\n      int err = libevdev_uinput_create_from_device(raw->global->pen_dev.get(), LIBEVDEV_UINPUT_OPEN_MANAGED, &raw->pen_input);\n      if (err) {\n        BOOST_LOG(error) << \"Could not create Sunshine Pen: \"sv << strerror(-err);\n        return;\n      }\n    }\n\n    auto pen_input = raw->pen_input.get();\n\n    float x = pen.x * touch_port.width;\n    float y = pen.y * touch_port.height;\n    float pressure = std::max(PRESSURE_MIN, pen.pressureOrDistance);\n\n    auto scaled_x = (int) std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width));\n    auto scaled_y = (int) std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height));\n\n    // First, process location updates for applicable events\n    switch (pen.eventType) {\n      case LI_TOUCH_EVENT_HOVER:\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x);\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y);\n\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, 0);\n        if (pen.pressureOrDistance) {\n          libevdev_uinput_write_event(pen_input, EV_ABS, ABS_DISTANCE, DISTANCE_MAX * pen.pressureOrDistance);\n        }\n        else {\n          // Always report some moderate distance value when hovering to ensure hovering\n          // can be detected properly by code that uses ABS_DISTANCE.\n          libevdev_uinput_write_event(pen_input, EV_ABS, ABS_DISTANCE, DISTANCE_MAX / 2);\n        }\n        break;\n\n      case LI_TOUCH_EVENT_DOWN:\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x);\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y);\n\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_DISTANCE, 0);\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX * pressure);\n        break;\n\n      case LI_TOUCH_EVENT_UP:\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x);\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y);\n\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, 0);\n        break;\n\n      case LI_TOUCH_EVENT_MOVE:\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_X, scaled_x);\n        libevdev_uinput_write_event(pen_input, EV_ABS, ABS_Y, scaled_y);\n\n        // Update the pressure value if it's present, otherwise leave the default/previous value alone\n        if (pen.pressureOrDistance) {\n          libevdev_uinput_write_event(pen_input, EV_ABS, ABS_PRESSURE, PRESSURE_MAX * pressure);\n        }\n        break;\n    }\n\n    if (pen.contactAreaMajor) {\n      // Contact area comes from the input core scaled to the provided touch_port,\n      // however we need it rescaled to target_touch_port instead.\n      auto target_scaled_contact_area = input::scale_client_contact_area(\n        { pen.contactAreaMajor * 65535.f, pen.contactAreaMinor * 65535.f },\n        pen.rotation,\n        { target_touch_port.width / (touch_port.width * 65535.f),\n          target_touch_port.height / (touch_port.height * 65535.f) });\n\n      // ABS_TOOL_WIDTH assumes a circular tool, so we just report the major axis\n      libevdev_uinput_write_event(pen_input, EV_ABS, ABS_TOOL_WIDTH, target_scaled_contact_area.first);\n    }\n\n    // We require rotation and tilt to perform the conversion to X and Y tilt angles\n    if (pen.tilt != LI_TILT_UNKNOWN && pen.rotation != LI_ROT_UNKNOWN) {\n      auto rotation_rads = pen.rotation * (M_PI / 180.f);\n      auto tilt_rads = pen.tilt * (M_PI / 180.f);\n      auto r = std::sin(tilt_rads);\n      auto z = std::cos(tilt_rads);\n\n      // Convert polar coordinates into X and Y tilt angles\n      libevdev_uinput_write_event(pen_input, EV_ABS, ABS_TILT_X, std::atan2(std::sin(-rotation_rads) * r, z) * 180.f / M_PI);\n      libevdev_uinput_write_event(pen_input, EV_ABS, ABS_TILT_Y, std::atan2(std::cos(-rotation_rads) * r, z) * 180.f / M_PI);\n    }\n\n    // Don't update tool type if we're cancelling or ending a touch/hover\n    if (pen.eventType != LI_TOUCH_EVENT_CANCEL &&\n        pen.eventType != LI_TOUCH_EVENT_CANCEL_ALL &&\n        pen.eventType != LI_TOUCH_EVENT_HOVER_LEAVE &&\n        pen.eventType != LI_TOUCH_EVENT_UP) {\n      // Update the tool type if it is known\n      switch (pen.toolType) {\n        default:\n          // We need to have _some_ tool type set, otherwise there's no way to know a tool is in\n          // range when hovering. If we don't know the type of tool, let's assume it's a pen.\n          if (pen.eventType != LI_TOUCH_EVENT_DOWN && pen.eventType != LI_TOUCH_EVENT_HOVER) {\n            break;\n          }\n          // fall-through\n        case LI_TOOL_TYPE_PEN:\n          libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_RUBBER, 0);\n          libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_PEN, 1);\n          break;\n        case LI_TOOL_TYPE_ERASER:\n          libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_PEN, 0);\n          libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_RUBBER, 1);\n          break;\n      }\n    }\n\n    // Next, process touch state changes\n    switch (pen.eventType) {\n      case LI_TOUCH_EVENT_CANCEL:\n      case LI_TOUCH_EVENT_CANCEL_ALL:\n      case LI_TOUCH_EVENT_HOVER_LEAVE:\n      case LI_TOUCH_EVENT_UP:\n        libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOUCH, 0);\n\n        // Leaving hover range is detected by all BTN_TOOL_* being cleared\n        libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_PEN, 0);\n        libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOOL_RUBBER, 0);\n        break;\n\n      case LI_TOUCH_EVENT_DOWN:\n        libevdev_uinput_write_event(pen_input, EV_KEY, BTN_TOUCH, 1);\n        break;\n    }\n\n    // Finally, process pen buttons\n    libevdev_uinput_write_event(pen_input, EV_KEY, BTN_STYLUS, !!(pen.penButtons & LI_PEN_BUTTON_PRIMARY));\n    libevdev_uinput_write_event(pen_input, EV_KEY, BTN_STYLUS2, !!(pen.penButtons & LI_PEN_BUTTON_SECONDARY));\n    libevdev_uinput_write_event(pen_input, EV_KEY, BTN_STYLUS3, !!(pen.penButtons & LI_PEN_BUTTON_TERTIARY));\n\n    libevdev_uinput_write_event(pen_input, EV_SYN, SYN_REPORT, 0);\n  }\n\n  /**\n   * @brief Sends a gamepad touch event to the OS.\n   * @param input The global input context.\n   * @param touch The touch event.\n   */\n  void\n  gamepad_touch(input_t &input, const gamepad_touch_t &touch) {\n    // Unimplemented feature - platform_caps::controller_touch\n  }\n\n  /**\n   * @brief Sends a gamepad motion event to the OS.\n   * @param input The global input context.\n   * @param motion The motion event.\n   */\n  void\n  gamepad_motion(input_t &input, const gamepad_motion_t &motion) {\n    // Unimplemented\n  }\n\n  /**\n   * @brief Sends a gamepad battery event to the OS.\n   * @param input The global input context.\n   * @param battery The battery event.\n   */\n  void\n  gamepad_battery(input_t &input, const gamepad_battery_t &battery) {\n    // Unimplemented\n  }\n\n  /**\n   * @brief Initialize a new keyboard and return it.\n   * @examples\n   * auto my_keyboard = keyboard();\n   * @examples_end\n   */\n  evdev_t\n  keyboard() {\n    evdev_t dev { libevdev_new() };\n\n    libevdev_set_uniq(dev.get(), \"Sunshine Keyboard\");\n    libevdev_set_id_product(dev.get(), 0xDEAD);\n    libevdev_set_id_vendor(dev.get(), 0xBEEF);\n    libevdev_set_id_bustype(dev.get(), 0x3);\n    libevdev_set_id_version(dev.get(), 0x111);\n    libevdev_set_name(dev.get(), \"Keyboard passthrough\");\n\n    libevdev_enable_event_type(dev.get(), EV_KEY);\n    for (const auto &keycode : keycodes) {\n      libevdev_enable_event_code(dev.get(), EV_KEY, keycode.keycode, nullptr);\n    }\n    libevdev_enable_event_type(dev.get(), EV_MSC);\n    libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr);\n\n    return dev;\n  }\n\n  /**\n   * @brief Initialize a new `uinput` virtual relative mouse and return it.\n   * @examples\n   * auto my_mouse = mouse_rel();\n   * @examples_end\n   */\n  evdev_t\n  mouse_rel() {\n    evdev_t dev { libevdev_new() };\n\n    libevdev_set_uniq(dev.get(), \"Sunshine Mouse (Rel)\");\n    libevdev_set_id_product(dev.get(), 0x4038);\n    libevdev_set_id_vendor(dev.get(), 0x46D);\n    libevdev_set_id_bustype(dev.get(), 0x3);\n    libevdev_set_id_version(dev.get(), 0x111);\n    libevdev_set_name(dev.get(), \"Logitech Wireless Mouse PID:4038\");\n\n    libevdev_enable_event_type(dev.get(), EV_KEY);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_LEFT, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_RIGHT, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MIDDLE, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SIDE, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EXTRA, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_FORWARD, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_BACK, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TASK, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, 280, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, 281, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, 282, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, 283, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, 284, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, 285, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, 286, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, 287, nullptr);\n\n    libevdev_enable_event_type(dev.get(), EV_REL);\n    libevdev_enable_event_code(dev.get(), EV_REL, REL_X, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_REL, REL_Y, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_REL, REL_WHEEL, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_REL, REL_WHEEL_HI_RES, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_REL, REL_HWHEEL, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_REL, REL_HWHEEL_HI_RES, nullptr);\n\n    libevdev_enable_event_type(dev.get(), EV_MSC);\n    libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr);\n\n    return dev;\n  }\n\n  /**\n   * @brief Initialize a new `uinput` virtual absolute mouse and return it.\n   * @examples\n   * auto my_mouse = mouse_abs();\n   * @examples_end\n   */\n  evdev_t\n  mouse_abs() {\n    evdev_t dev { libevdev_new() };\n\n    libevdev_set_uniq(dev.get(), \"Sunshine Mouse (Abs)\");\n    libevdev_set_id_product(dev.get(), 0xDEAD);\n    libevdev_set_id_vendor(dev.get(), 0xBEEF);\n    libevdev_set_id_bustype(dev.get(), 0x3);\n    libevdev_set_id_version(dev.get(), 0x111);\n    libevdev_set_name(dev.get(), \"Mouse passthrough\");\n\n    libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT);\n\n    libevdev_enable_event_type(dev.get(), EV_KEY);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_LEFT, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_RIGHT, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MIDDLE, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SIDE, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EXTRA, nullptr);\n\n    libevdev_enable_event_type(dev.get(), EV_MSC);\n    libevdev_enable_event_code(dev.get(), EV_MSC, MSC_SCAN, nullptr);\n\n    input_absinfo absx {\n      0,\n      0,\n      target_touch_port.width,\n      1,\n      0,\n      28\n    };\n\n    input_absinfo absy {\n      0,\n      0,\n      target_touch_port.height,\n      1,\n      0,\n      28\n    };\n    libevdev_enable_event_type(dev.get(), EV_ABS);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &absx);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &absy);\n\n    return dev;\n  }\n\n  /**\n   * @brief Initialize a new `uinput` virtual touchscreen and return it.\n   * @examples\n   * auto my_touchscreen = touchscreen();\n   * @examples_end\n   */\n  evdev_t\n  touchscreen() {\n    evdev_t dev { libevdev_new() };\n\n    libevdev_set_uniq(dev.get(), \"Sunshine Touchscreen\");\n    libevdev_set_id_product(dev.get(), 0xDEAD);\n    libevdev_set_id_vendor(dev.get(), 0xBEEF);\n    libevdev_set_id_bustype(dev.get(), 0x3);\n    libevdev_set_id_version(dev.get(), 0x111);\n    libevdev_set_name(dev.get(), \"Touch passthrough\");\n\n    libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT);\n\n    constexpr auto RESOLUTION = 28;\n\n    input_absinfo abs_slot {\n      0,\n      0,\n      NUM_TOUCH_SLOTS - 1,\n      0,\n      0,\n      0\n    };\n\n    input_absinfo abs_tracking_id {\n      0,\n      0,\n      NUM_TOUCH_SLOTS - 1,\n      0,\n      0,\n      0\n    };\n\n    input_absinfo abs_x {\n      0,\n      0,\n      target_touch_port.width,\n      1,\n      0,\n      RESOLUTION\n    };\n\n    input_absinfo abs_y {\n      0,\n      0,\n      target_touch_port.height,\n      1,\n      0,\n      RESOLUTION\n    };\n\n    input_absinfo abs_pressure {\n      0,\n      0,\n      PRESSURE_MAX,\n      0,\n      0,\n      0\n    };\n\n    // Degrees of a half revolution\n    input_absinfo abs_orientation {\n      0,\n      -90,\n      90,\n      0,\n      0,\n      0\n    };\n\n    // Fractions of the full diagonal\n    input_absinfo abs_contact_area {\n      0,\n      0,\n      (__s32) std::sqrt(std::pow(target_touch_port.width, 2) + std::pow(target_touch_port.height, 2)),\n      1,\n      0,\n      RESOLUTION\n    };\n\n    libevdev_enable_event_type(dev.get(), EV_ABS);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &abs_x);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &abs_y);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_PRESSURE, &abs_pressure);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_SLOT, &abs_slot);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_TRACKING_ID, &abs_tracking_id);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_POSITION_X, &abs_x);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_POSITION_Y, &abs_y);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_PRESSURE, &abs_pressure);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_ORIENTATION, &abs_orientation);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_TOUCH_MAJOR, &abs_contact_area);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_MT_TOUCH_MINOR, &abs_contact_area);\n\n    libevdev_enable_event_type(dev.get(), EV_KEY);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr);\n\n    return dev;\n  }\n\n  /**\n   * @brief Initialize a new `uinput` virtual pen pad and return it.\n   * @examples\n   * auto my_penpad = penpad();\n   * @examples_end\n   */\n  evdev_t\n  penpad() {\n    evdev_t dev { libevdev_new() };\n\n    libevdev_set_uniq(dev.get(), \"Sunshine Pen\");\n    libevdev_set_id_product(dev.get(), 0xDEAD);\n    libevdev_set_id_vendor(dev.get(), 0xBEEF);\n    libevdev_set_id_bustype(dev.get(), 0x3);\n    libevdev_set_id_version(dev.get(), 0x111);\n    libevdev_set_name(dev.get(), \"Pen passthrough\");\n\n    libevdev_enable_property(dev.get(), INPUT_PROP_DIRECT);\n\n    constexpr auto RESOLUTION = 28;\n\n    input_absinfo abs_x {\n      0,\n      0,\n      target_touch_port.width,\n      1,\n      0,\n      RESOLUTION\n    };\n\n    input_absinfo abs_y {\n      0,\n      0,\n      target_touch_port.height,\n      1,\n      0,\n      RESOLUTION\n    };\n\n    input_absinfo abs_pressure {\n      0,\n      0,\n      PRESSURE_MAX,\n      0,\n      0,\n      0\n    };\n\n    input_absinfo abs_distance {\n      0,\n      0,\n      DISTANCE_MAX,\n      0,\n      0,\n      0\n    };\n\n    // Degrees of tilt\n    input_absinfo abs_tilt {\n      0,\n      -90,\n      90,\n      0,\n      0,\n      0\n    };\n\n    // Fractions of the full diagonal\n    input_absinfo abs_contact_area {\n      0,\n      0,\n      (__s32) std::sqrt(std::pow(target_touch_port.width, 2) + std::pow(target_touch_port.height, 2)),\n      1,\n      0,\n      RESOLUTION\n    };\n\n    libevdev_enable_event_type(dev.get(), EV_ABS);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &abs_x);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &abs_y);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_PRESSURE, &abs_pressure);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_DISTANCE, &abs_distance);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_TILT_X, &abs_tilt);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_TILT_Y, &abs_tilt);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_TOOL_WIDTH, &abs_contact_area);\n\n    libevdev_enable_event_type(dev.get(), EV_KEY);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOUCH, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_PEN, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TOOL_RUBBER, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_STYLUS, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_STYLUS2, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_STYLUS3, nullptr);\n\n    return dev;\n  }\n\n  /**\n   * @brief Initialize a new `uinput` virtual X360 gamepad and return it.\n   * @examples\n   * auto my_x360 = x360();\n   * @examples_end\n   */\n  evdev_t\n  x360() {\n    evdev_t dev { libevdev_new() };\n\n    input_absinfo stick {\n      0,\n      -32768, 32767,\n      16,\n      128,\n      0\n    };\n\n    input_absinfo trigger {\n      0,\n      0, 255,\n      0,\n      0,\n      0\n    };\n\n    input_absinfo dpad {\n      0,\n      -1, 1,\n      0,\n      0,\n      0\n    };\n\n    libevdev_set_uniq(dev.get(), \"Sunshine Gamepad\");\n    libevdev_set_id_product(dev.get(), 0x28E);\n    libevdev_set_id_vendor(dev.get(), 0x45E);\n    libevdev_set_id_bustype(dev.get(), 0x3);\n    libevdev_set_id_version(dev.get(), 0x110);\n    libevdev_set_name(dev.get(), \"Microsoft X-Box 360 pad\");\n\n    libevdev_enable_event_type(dev.get(), EV_KEY);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_WEST, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_EAST, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_NORTH, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SOUTH, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_THUMBL, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_THUMBR, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TR, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_TL, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_SELECT, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_MODE, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_KEY, BTN_START, nullptr);\n\n    libevdev_enable_event_type(dev.get(), EV_ABS);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_HAT0Y, &dpad);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_HAT0X, &dpad);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Z, &trigger);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RZ, &trigger);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_X, &stick);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RX, &stick);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_Y, &stick);\n    libevdev_enable_event_code(dev.get(), EV_ABS, ABS_RY, &stick);\n\n    libevdev_enable_event_type(dev.get(), EV_FF);\n    libevdev_enable_event_code(dev.get(), EV_FF, FF_RUMBLE, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_FF, FF_CONSTANT, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_FF, FF_PERIODIC, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_FF, FF_SINE, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_FF, FF_RAMP, nullptr);\n    libevdev_enable_event_code(dev.get(), EV_FF, FF_GAIN, nullptr);\n\n    return dev;\n  }\n\n  /**\n   * @brief Initialize the input system and return it.\n   * @examples\n   * auto my_input = input();\n   * @examples_end\n   */\n  input_t\n  input() {\n    input_t result { new input_raw_t() };\n    auto &gp = *(input_raw_t *) result.get();\n\n    gp.rumble_ctx = notifications.ref();\n\n    gp.gamepads.resize(MAX_GAMEPADS);\n\n    // Ensure starting from clean slate\n    gp.clear();\n    gp.keyboard_dev = keyboard();\n    gp.mouse_rel_dev = mouse_rel();\n    gp.mouse_abs_dev = mouse_abs();\n    gp.touchscreen_dev = touchscreen();\n    gp.pen_dev = penpad();\n    gp.gamepad_dev = x360();\n\n    gp.create_mouse_rel();\n    gp.create_mouse_abs();\n    gp.create_keyboard();\n\n    // If we do not have a keyboard or mouse, fall back to XTest\n    if (!gp.mouse_rel_input || !gp.mouse_abs_input || !gp.keyboard_input) {\n#ifdef SUNSHINE_BUILD_X11\n      if (x11::init() || x11::tst::init()) {\n        BOOST_LOG(fatal) << \"Unable to create virtual input devices or use XTest fallback! Are you a member of the 'input' group?\"sv;\n      }\n      else {\n        BOOST_LOG(error) << \"Falling back to XTest for virtual input! Are you a member of the 'input' group?\"sv;\n        x11::InitThreads();\n        gp.display = x11::OpenDisplay(NULL);\n      }\n#else\n      BOOST_LOG(fatal) << \"Unable to create virtual input devices! Are you a member of the 'input' group?\"sv;\n#endif\n    }\n    else {\n      has_uinput = true;\n    }\n\n    return result;\n  }\n\n  void\n  freeInput(void *p) {\n    auto *input = (input_raw_t *) p;\n    delete input;\n  }\n\n  std::vector<supported_gamepad_t> &\n  supported_gamepads(input_t *input) {\n    static std::vector gamepads {\n      supported_gamepad_t { \"x360\", true, \"\" },\n    };\n\n    return gamepads;\n  }\n\n  /**\n   * @brief Returns the supported platform capabilities to advertise to the client.\n   * @return Capability flags.\n   */\n  platform_caps::caps_t\n  get_capabilities() {\n    platform_caps::caps_t caps = 0;\n\n    // Pen and touch emulation requires uinput\n    if (has_uinput && config::input.native_pen_touch) {\n      caps |= platform_caps::pen_touch;\n    }\n\n    return caps;\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/linux/kmsgrab.cpp",
    "content": "/**\n * @file src/platform/linux/kmsgrab.cpp\n * @brief Definitions for KMS screen capture.\n */\n#include <drm_fourcc.h>\n#include <errno.h>\n#include <fcntl.h>\n#include <linux/dma-buf.h>\n#include <sys/capability.h>\n#include <sys/mman.h>\n#include <unistd.h>\n#include <xf86drm.h>\n#include <xf86drmMode.h>\n\n#include <filesystem>\n#include <thread>\n\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/round_robin.h\"\n#include \"src/utility.h\"\n#include \"src/video.h\"\n\n#include \"cuda.h\"\n#include \"graphics.h\"\n#include \"vaapi.h\"\n#include \"wayland.h\"\n\nusing namespace std::literals;\nnamespace fs = std::filesystem;\n\nnamespace platf {\n\n  namespace kms {\n\n    class cap_sys_admin {\n    public:\n      cap_sys_admin() {\n        caps = cap_get_proc();\n\n        cap_value_t sys_admin = CAP_SYS_ADMIN;\n        if (cap_set_flag(caps, CAP_EFFECTIVE, 1, &sys_admin, CAP_SET) || cap_set_proc(caps)) {\n          BOOST_LOG(error) << \"Failed to gain CAP_SYS_ADMIN\";\n        }\n      }\n\n      ~cap_sys_admin() {\n        cap_value_t sys_admin = CAP_SYS_ADMIN;\n        if (cap_set_flag(caps, CAP_EFFECTIVE, 1, &sys_admin, CAP_CLEAR) || cap_set_proc(caps)) {\n          BOOST_LOG(error) << \"Failed to drop CAP_SYS_ADMIN\";\n        }\n        cap_free(caps);\n      }\n\n      cap_t caps;\n    };\n\n    class wrapper_fb {\n    public:\n      wrapper_fb(drmModeFB *fb):\n          fb { fb }, fb_id { fb->fb_id }, width { fb->width }, height { fb->height } {\n        pixel_format = DRM_FORMAT_XRGB8888;\n        modifier = DRM_FORMAT_MOD_INVALID;\n        std::fill_n(handles, 4, 0);\n        std::fill_n(pitches, 4, 0);\n        std::fill_n(offsets, 4, 0);\n        handles[0] = fb->handle;\n        pitches[0] = fb->pitch;\n      }\n\n      wrapper_fb(drmModeFB2 *fb2):\n          fb2 { fb2 }, fb_id { fb2->fb_id }, width { fb2->width }, height { fb2->height } {\n        pixel_format = fb2->pixel_format;\n        modifier = (fb2->flags & DRM_MODE_FB_MODIFIERS) ? fb2->modifier : DRM_FORMAT_MOD_INVALID;\n\n        memcpy(handles, fb2->handles, sizeof(handles));\n        memcpy(pitches, fb2->pitches, sizeof(pitches));\n        memcpy(offsets, fb2->offsets, sizeof(offsets));\n      }\n\n      ~wrapper_fb() {\n        if (fb) {\n          drmModeFreeFB(fb);\n        }\n        else if (fb2) {\n          drmModeFreeFB2(fb2);\n        }\n      }\n\n      drmModeFB *fb = nullptr;\n      drmModeFB2 *fb2 = nullptr;\n      uint32_t fb_id;\n      uint32_t width;\n      uint32_t height;\n      uint32_t pixel_format;\n      uint64_t modifier;\n      uint32_t handles[4];\n      uint32_t pitches[4];\n      uint32_t offsets[4];\n    };\n\n    using plane_res_t = util::safe_ptr<drmModePlaneRes, drmModeFreePlaneResources>;\n    using encoder_t = util::safe_ptr<drmModeEncoder, drmModeFreeEncoder>;\n    using res_t = util::safe_ptr<drmModeRes, drmModeFreeResources>;\n    using plane_t = util::safe_ptr<drmModePlane, drmModeFreePlane>;\n    using fb_t = std::unique_ptr<wrapper_fb>;\n    using crtc_t = util::safe_ptr<drmModeCrtc, drmModeFreeCrtc>;\n    using obj_prop_t = util::safe_ptr<drmModeObjectProperties, drmModeFreeObjectProperties>;\n    using prop_t = util::safe_ptr<drmModePropertyRes, drmModeFreeProperty>;\n    using prop_blob_t = util::safe_ptr<drmModePropertyBlobRes, drmModeFreePropertyBlob>;\n    using version_t = util::safe_ptr<drmVersion, drmFreeVersion>;\n\n    using conn_type_count_t = std::map<std::uint32_t, std::uint32_t>;\n\n    static int env_width;\n    static int env_height;\n\n    std::string_view\n    plane_type(std::uint64_t val) {\n      switch (val) {\n        case DRM_PLANE_TYPE_OVERLAY:\n          return \"DRM_PLANE_TYPE_OVERLAY\"sv;\n        case DRM_PLANE_TYPE_PRIMARY:\n          return \"DRM_PLANE_TYPE_PRIMARY\"sv;\n        case DRM_PLANE_TYPE_CURSOR:\n          return \"DRM_PLANE_TYPE_CURSOR\"sv;\n      }\n\n      return \"UNKNOWN\"sv;\n    }\n\n    struct connector_t {\n      // For example: HDMI-A or HDMI\n      std::uint32_t type;\n\n      // Equals zero if not applicable\n      std::uint32_t crtc_id;\n\n      // For example HDMI-A-{index} or HDMI-{index}\n      std::uint32_t index;\n\n      // ID of the connector\n      std::uint32_t connector_id;\n\n      bool connected;\n    };\n\n    struct monitor_t {\n      // Connector attributes\n      std::uint32_t type;\n      std::uint32_t index;\n\n      // Monitor index in the global list\n      std::uint32_t monitor_index;\n\n      platf::touch_port_t viewport;\n    };\n\n    struct card_descriptor_t {\n      std::string path;\n\n      std::map<std::uint32_t, monitor_t> crtc_to_monitor;\n    };\n\n    static std::vector<card_descriptor_t> card_descriptors;\n\n    static std::uint32_t\n    from_view(const std::string_view &string) {\n#define _CONVERT(x, y) \\\n  if (string == x) return DRM_MODE_CONNECTOR_##y\n\n      // This list was created from the following sources:\n      // https://gitlab.freedesktop.org/mesa/drm/-/blob/main/xf86drmMode.c (drmModeGetConnectorTypeName)\n      // https://gitlab.freedesktop.org/wayland/weston/-/blob/e74f2897b9408b6356a555a0ce59146836307ff5/libweston/backend-drm/drm.c#L1458-1477\n      // https://github.com/GNOME/mutter/blob/65d481594227ea7188c0416e8e00b57caeea214f/src/backends/meta-monitor-manager.c#L1618-L1639\n      _CONVERT(\"VGA\"sv, VGA);\n      _CONVERT(\"DVII\"sv, DVII);\n      _CONVERT(\"DVI-I\"sv, DVII);\n      _CONVERT(\"DVID\"sv, DVID);\n      _CONVERT(\"DVI-D\"sv, DVID);\n      _CONVERT(\"DVIA\"sv, DVIA);\n      _CONVERT(\"DVI-A\"sv, DVIA);\n      _CONVERT(\"Composite\"sv, Composite);\n      _CONVERT(\"SVIDEO\"sv, SVIDEO);\n      _CONVERT(\"S-Video\"sv, SVIDEO);\n      _CONVERT(\"LVDS\"sv, LVDS);\n      _CONVERT(\"Component\"sv, Component);\n      _CONVERT(\"9PinDIN\"sv, 9PinDIN);\n      _CONVERT(\"DIN\"sv, 9PinDIN);\n      _CONVERT(\"DisplayPort\"sv, DisplayPort);\n      _CONVERT(\"DP\"sv, DisplayPort);\n      _CONVERT(\"HDMIA\"sv, HDMIA);\n      _CONVERT(\"HDMI-A\"sv, HDMIA);\n      _CONVERT(\"HDMI\"sv, HDMIA);\n      _CONVERT(\"HDMIB\"sv, HDMIB);\n      _CONVERT(\"HDMI-B\"sv, HDMIB);\n      _CONVERT(\"TV\"sv, TV);\n      _CONVERT(\"eDP\"sv, eDP);\n      _CONVERT(\"VIRTUAL\"sv, VIRTUAL);\n      _CONVERT(\"Virtual\"sv, VIRTUAL);\n      _CONVERT(\"DSI\"sv, DSI);\n      _CONVERT(\"DPI\"sv, DPI);\n      _CONVERT(\"WRITEBACK\"sv, WRITEBACK);\n      _CONVERT(\"Writeback\"sv, WRITEBACK);\n      _CONVERT(\"SPI\"sv, SPI);\n#ifdef DRM_MODE_CONNECTOR_USB\n      _CONVERT(\"USB\"sv, USB);\n#endif\n\n      // If the string starts with \"Unknown\", it may have the raw type\n      // value appended to the string. Let's try to read it.\n      if (string.find(\"Unknown\"sv) == 0) {\n        std::uint32_t type;\n        std::string null_terminated_string { string };\n        if (std::sscanf(null_terminated_string.c_str(), \"Unknown%u\", &type) == 1) {\n          return type;\n        }\n      }\n\n      BOOST_LOG(error) << \"Unknown Monitor connector type [\"sv << string << \"]: Please report this to the GitHub issue tracker\"sv;\n      return DRM_MODE_CONNECTOR_Unknown;\n    }\n\n    class plane_it_t: public round_robin_util::it_wrap_t<plane_t::element_type, plane_it_t> {\n    public:\n      plane_it_t(int fd, std::uint32_t *plane_p, std::uint32_t *end):\n          fd { fd }, plane_p { plane_p }, end { end } {\n        load_next_valid_plane();\n      }\n\n      plane_it_t(int fd, std::uint32_t *end):\n          fd { fd }, plane_p { end }, end { end } {}\n\n      void\n      load_next_valid_plane() {\n        this->plane.reset();\n\n        for (; plane_p != end; ++plane_p) {\n          plane_t plane = drmModeGetPlane(fd, *plane_p);\n          if (!plane) {\n            BOOST_LOG(error) << \"Couldn't get drm plane [\"sv << (end - plane_p) << \"]: \"sv << strerror(errno);\n            continue;\n          }\n\n          this->plane = util::make_shared<plane_t>(plane.release());\n          break;\n        }\n      }\n\n      void\n      inc() {\n        ++plane_p;\n        load_next_valid_plane();\n      }\n\n      bool\n      eq(const plane_it_t &other) const {\n        return plane_p == other.plane_p;\n      }\n\n      plane_t::pointer\n      get() {\n        return plane.get();\n      }\n\n      int fd;\n      std::uint32_t *plane_p;\n      std::uint32_t *end;\n\n      util::shared_t<plane_t> plane;\n    };\n\n    struct cursor_t {\n      // Public properties used during blending\n      bool visible = false;\n      std::int32_t x, y;\n      std::uint32_t dst_w, dst_h;\n      std::uint32_t src_w, src_h;\n      std::vector<std::uint8_t> pixels;\n      unsigned long serial;\n\n      // Private properties used for tracking cursor changes\n      std::uint64_t prop_src_x, prop_src_y, prop_src_w, prop_src_h;\n      std::uint32_t fb_id;\n    };\n\n    class card_t {\n    public:\n      using connector_interal_t = util::safe_ptr<drmModeConnector, drmModeFreeConnector>;\n\n      int\n      init(const char *path) {\n        cap_sys_admin admin;\n        fd.el = open(path, O_RDWR);\n\n        if (fd.el < 0) {\n          BOOST_LOG(error) << \"Couldn't open: \"sv << path << \": \"sv << strerror(errno);\n          return -1;\n        }\n\n        version_t ver { drmGetVersion(fd.el) };\n        BOOST_LOG(info) << path << \" -> \"sv << ((ver && ver->name) ? ver->name : \"UNKNOWN\");\n\n        // Open the render node for this card to share with libva.\n        // If it fails, we'll just share the primary node instead.\n        char *rendernode_path = drmGetRenderDeviceNameFromFd(fd.el);\n        if (rendernode_path) {\n          BOOST_LOG(debug) << \"Opening render node: \"sv << rendernode_path;\n          render_fd.el = open(rendernode_path, O_RDWR);\n          if (render_fd.el < 0) {\n            BOOST_LOG(warning) << \"Couldn't open render node: \"sv << rendernode_path << \": \"sv << strerror(errno);\n            render_fd.el = dup(fd.el);\n          }\n          free(rendernode_path);\n        }\n        else {\n          BOOST_LOG(warning) << \"No render device name for: \"sv << path;\n          render_fd.el = dup(fd.el);\n        }\n\n        if (drmSetClientCap(fd.el, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1)) {\n          BOOST_LOG(error) << \"GPU driver doesn't support universal planes: \"sv << path;\n          return -1;\n        }\n\n        if (drmSetClientCap(fd.el, DRM_CLIENT_CAP_ATOMIC, 1)) {\n          BOOST_LOG(warning) << \"GPU driver doesn't support atomic mode-setting: \"sv << path;\n#if defined(SUNSHINE_BUILD_X11)\n          // We won't be able to capture the mouse cursor with KMS on non-atomic drivers,\n          // so fall back to X11 if it's available and the user didn't explicitly force KMS.\n          if (window_system == window_system_e::X11 && config::video.capture != \"kms\") {\n            BOOST_LOG(info) << \"Avoiding KMS capture under X11 due to lack of atomic mode-setting\"sv;\n            return -1;\n          }\n#endif\n          BOOST_LOG(warning) << \"Cursor capture may fail without atomic mode-setting support!\"sv;\n        }\n\n        plane_res.reset(drmModeGetPlaneResources(fd.el));\n        if (!plane_res) {\n          BOOST_LOG(error) << \"Couldn't get drm plane resources\"sv;\n          return -1;\n        }\n\n        return 0;\n      }\n\n      fb_t\n      fb(plane_t::pointer plane) {\n        cap_sys_admin admin;\n\n        auto fb2 = drmModeGetFB2(fd.el, plane->fb_id);\n        if (fb2) {\n          return std::make_unique<wrapper_fb>(fb2);\n        }\n\n        auto fb = drmModeGetFB(fd.el, plane->fb_id);\n        if (fb) {\n          return std::make_unique<wrapper_fb>(fb);\n        }\n\n        return nullptr;\n      }\n\n      crtc_t\n      crtc(std::uint32_t id) {\n        return drmModeGetCrtc(fd.el, id);\n      }\n\n      encoder_t\n      encoder(std::uint32_t id) {\n        return drmModeGetEncoder(fd.el, id);\n      }\n\n      res_t\n      res() {\n        return drmModeGetResources(fd.el);\n      }\n\n      bool\n      is_nvidia() {\n        version_t ver { drmGetVersion(fd.el) };\n        return ver && ver->name && strncmp(ver->name, \"nvidia-drm\", 10) == 0;\n      }\n\n      bool\n      is_cursor(std::uint32_t plane_id) {\n        auto props = plane_props(plane_id);\n        for (auto &[prop, val] : props) {\n          if (prop->name == \"type\"sv) {\n            if (val == DRM_PLANE_TYPE_CURSOR) {\n              return true;\n            }\n            else {\n              return false;\n            }\n          }\n        }\n\n        return false;\n      }\n\n      std::optional<std::uint64_t>\n      prop_value_by_name(const std::vector<std::pair<prop_t, std::uint64_t>> &props, std::string_view name) {\n        for (auto &[prop, val] : props) {\n          if (prop->name == name) {\n            return val;\n          }\n        }\n        return std::nullopt;\n      }\n\n      std::uint32_t\n      get_panel_orientation(std::uint32_t plane_id) {\n        auto props = plane_props(plane_id);\n        auto value = prop_value_by_name(props, \"rotation\"sv);\n        if (value) {\n          return *value;\n        }\n\n        BOOST_LOG(error) << \"Failed to determine panel orientation, defaulting to landscape.\";\n        return DRM_MODE_ROTATE_0;\n      }\n\n      int\n      get_crtc_index_by_id(std::uint32_t crtc_id) {\n        auto resources = res();\n        for (int i = 0; i < resources->count_crtcs; i++) {\n          if (resources->crtcs[i] == crtc_id) {\n            return i;\n          }\n        }\n        return -1;\n      }\n\n      connector_interal_t\n      connector(std::uint32_t id) {\n        return drmModeGetConnector(fd.el, id);\n      }\n\n      std::vector<connector_t>\n      monitors(conn_type_count_t &conn_type_count) {\n        auto resources = res();\n        if (!resources) {\n          BOOST_LOG(error) << \"Couldn't get connector resources\"sv;\n          return {};\n        }\n\n        std::vector<connector_t> monitors;\n        std::for_each_n(resources->connectors, resources->count_connectors, [this, &conn_type_count, &monitors](std::uint32_t id) {\n          auto conn = connector(id);\n\n          std::uint32_t crtc_id = 0;\n\n          if (conn->encoder_id) {\n            auto enc = encoder(conn->encoder_id);\n            if (enc) {\n              crtc_id = enc->crtc_id;\n            }\n          }\n\n          auto index = ++conn_type_count[conn->connector_type];\n\n          monitors.emplace_back(connector_t {\n            conn->connector_type,\n            crtc_id,\n            index,\n            conn->connector_id,\n            conn->connection == DRM_MODE_CONNECTED,\n          });\n        });\n\n        return monitors;\n      }\n\n      file_t\n      handleFD(std::uint32_t handle) {\n        file_t fb_fd;\n\n        auto status = drmPrimeHandleToFD(fd.el, handle, 0 /* flags */, &fb_fd.el);\n        if (status) {\n          return {};\n        }\n\n        return fb_fd;\n      }\n\n      std::vector<std::pair<prop_t, std::uint64_t>>\n      props(std::uint32_t id, std::uint32_t type) {\n        obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type);\n        if (!obj_prop) {\n          return {};\n        }\n\n        std::vector<std::pair<prop_t, std::uint64_t>> props;\n        props.reserve(obj_prop->count_props);\n\n        for (auto x = 0; x < obj_prop->count_props; ++x) {\n          props.emplace_back(drmModeGetProperty(fd.el, obj_prop->props[x]), obj_prop->prop_values[x]);\n        }\n\n        return props;\n      }\n\n      std::vector<std::pair<prop_t, std::uint64_t>>\n      plane_props(std::uint32_t id) {\n        return props(id, DRM_MODE_OBJECT_PLANE);\n      }\n\n      std::vector<std::pair<prop_t, std::uint64_t>>\n      crtc_props(std::uint32_t id) {\n        return props(id, DRM_MODE_OBJECT_CRTC);\n      }\n\n      std::vector<std::pair<prop_t, std::uint64_t>>\n      connector_props(std::uint32_t id) {\n        return props(id, DRM_MODE_OBJECT_CONNECTOR);\n      }\n\n      plane_t\n      operator[](std::uint32_t index) {\n        return drmModeGetPlane(fd.el, plane_res->planes[index]);\n      }\n\n      std::uint32_t\n      count() {\n        return plane_res->count_planes;\n      }\n\n      plane_it_t\n      begin() const {\n        return plane_it_t { fd.el, plane_res->planes, plane_res->planes + plane_res->count_planes };\n      }\n\n      plane_it_t\n      end() const {\n        return plane_it_t { fd.el, plane_res->planes + plane_res->count_planes };\n      }\n\n      file_t fd;\n      file_t render_fd;\n      plane_res_t plane_res;\n    };\n\n    std::map<std::uint32_t, monitor_t>\n    map_crtc_to_monitor(const std::vector<connector_t> &connectors) {\n      std::map<std::uint32_t, monitor_t> result;\n\n      for (auto &connector : connectors) {\n        result.emplace(connector.crtc_id,\n          monitor_t {\n            connector.type,\n            connector.index,\n          });\n      }\n\n      return result;\n    }\n\n    struct kms_img_t: public img_t {\n      ~kms_img_t() override {\n        delete[] data;\n        data = nullptr;\n      }\n    };\n\n    void\n    print(plane_t::pointer plane, fb_t::pointer fb, crtc_t::pointer crtc) {\n      if (crtc) {\n        BOOST_LOG(debug) << \"crtc(\"sv << crtc->x << \", \"sv << crtc->y << ')';\n        BOOST_LOG(debug) << \"crtc(\"sv << crtc->width << \", \"sv << crtc->height << ')';\n        BOOST_LOG(debug) << \"plane->possible_crtcs == \"sv << plane->possible_crtcs;\n      }\n\n      BOOST_LOG(debug)\n        << \"x(\"sv << plane->x\n        << \") y(\"sv << plane->y\n        << \") crtc_x(\"sv << plane->crtc_x\n        << \") crtc_y(\"sv << plane->crtc_y\n        << \") crtc_id(\"sv << plane->crtc_id\n        << ')';\n\n      BOOST_LOG(debug)\n        << \"Resolution: \"sv << fb->width << 'x' << fb->height\n        << \": Pitch: \"sv << fb->pitches[0]\n        << \": Offset: \"sv << fb->offsets[0];\n\n      std::stringstream ss;\n\n      ss << \"Format [\"sv;\n      std::for_each_n(plane->formats, plane->count_formats - 1, [&ss](auto format) {\n        ss << util::view(format) << \", \"sv;\n      });\n\n      ss << util::view(plane->formats[plane->count_formats - 1]) << ']';\n\n      BOOST_LOG(debug) << ss.str();\n    }\n\n    class display_t: public platf::display_t {\n    public:\n      display_t(mem_type_e mem_type):\n          platf::display_t(), mem_type { mem_type } {}\n\n      int\n      init(const std::string &display_name, const ::video::config_t &config) {\n        delay = std::chrono::nanoseconds { 1s } / config.framerate;\n\n        int monitor_index = util::from_view(display_name);\n        int monitor = 0;\n\n        fs::path card_dir { \"/dev/dri\"sv };\n        for (auto &entry : fs::directory_iterator { card_dir }) {\n          auto file = entry.path().filename();\n\n          auto filestring = file.generic_string();\n          if (filestring.size() < 4 || std::string_view { filestring }.substr(0, 4) != \"card\"sv) {\n            continue;\n          }\n\n          kms::card_t card;\n          if (card.init(entry.path().c_str())) {\n            continue;\n          }\n\n          // Skip non-Nvidia cards if we're looking for CUDA devices\n          // unless NVENC is selected manually by the user\n          if (mem_type == mem_type_e::cuda && !card.is_nvidia()) {\n            BOOST_LOG(debug) << file << \" is not a CUDA device\"sv;\n            if (config::video.encoder != \"nvenc\") {\n              continue;\n            }\n          }\n\n          auto end = std::end(card);\n          for (auto plane = std::begin(card); plane != end; ++plane) {\n            // Skip unused planes\n            if (!plane->fb_id) {\n              continue;\n            }\n\n            if (card.is_cursor(plane->plane_id)) {\n              continue;\n            }\n\n            if (monitor != monitor_index) {\n              ++monitor;\n              continue;\n            }\n\n            auto fb = card.fb(plane.get());\n            if (!fb) {\n              BOOST_LOG(error) << \"Couldn't get drm fb for plane [\"sv << plane->fb_id << \"]: \"sv << strerror(errno);\n              return -1;\n            }\n\n            if (!fb->handles[0]) {\n              BOOST_LOG(error) << \"Couldn't get handle for DRM Framebuffer [\"sv << plane->fb_id << \"]: Probably not permitted\"sv;\n              return -1;\n            }\n\n            for (int i = 0; i < 4; ++i) {\n              if (!fb->handles[i]) {\n                break;\n              }\n\n              auto fb_fd = card.handleFD(fb->handles[i]);\n              if (fb_fd.el < 0) {\n                BOOST_LOG(error) << \"Couldn't get primary file descriptor for Framebuffer [\"sv << fb->fb_id << \"]: \"sv << strerror(errno);\n                continue;\n              }\n            }\n\n            auto crtc = card.crtc(plane->crtc_id);\n            if (!crtc) {\n              BOOST_LOG(error) << \"Couldn't get CRTC info: \"sv << strerror(errno);\n              continue;\n            }\n\n            BOOST_LOG(info) << \"Found monitor for DRM screencasting\"sv;\n\n            // We need to find the correct /dev/dri/card{nr} to correlate the crtc_id with the monitor descriptor\n            auto pos = std::find_if(std::begin(card_descriptors), std::end(card_descriptors), [&](card_descriptor_t &cd) {\n              return cd.path == filestring;\n            });\n\n            if (pos == std::end(card_descriptors)) {\n              // This code path shouldn't happen, but it's there just in case.\n              // card_descriptors is part of the guesswork after all.\n              BOOST_LOG(error) << \"Couldn't find [\"sv << entry.path() << \"]: This shouldn't have happened :/\"sv;\n              return -1;\n            }\n\n            // TODO: surf_sd = fb->to_sd();\n\n            kms::print(plane.get(), fb.get(), crtc.get());\n\n            img_width = fb->width;\n            img_height = fb->height;\n            img_offset_x = crtc->x;\n            img_offset_y = crtc->y;\n\n            this->env_width = ::platf::kms::env_width;\n            this->env_height = ::platf::kms::env_height;\n\n            auto monitor = pos->crtc_to_monitor.find(plane->crtc_id);\n            if (monitor != std::end(pos->crtc_to_monitor)) {\n              auto &viewport = monitor->second.viewport;\n\n              width = viewport.width;\n              height = viewport.height;\n\n              switch (card.get_panel_orientation(plane->plane_id)) {\n                case DRM_MODE_ROTATE_270:\n                  BOOST_LOG(debug) << \"Detected panel orientation at 90, swapping width and height.\";\n                  width = viewport.height;\n                  height = viewport.width;\n                  break;\n                case DRM_MODE_ROTATE_90:\n                case DRM_MODE_ROTATE_180:\n                  BOOST_LOG(warning) << \"Panel orientation is unsupported, screen capture may not work correctly.\";\n                  break;\n              }\n\n              offset_x = viewport.offset_x;\n              offset_y = viewport.offset_y;\n            }\n\n            // This code path shouldn't happen, but it's there just in case.\n            // crtc_to_monitor is part of the guesswork after all.\n            else {\n              BOOST_LOG(warning) << \"Couldn't find crtc_id, this shouldn't have happened :\\\\\"sv;\n              width = crtc->width;\n              height = crtc->height;\n              offset_x = crtc->x;\n              offset_y = crtc->y;\n            }\n\n            plane_id = plane->plane_id;\n            crtc_id = plane->crtc_id;\n            crtc_index = card.get_crtc_index_by_id(plane->crtc_id);\n\n            // Find the connector for this CRTC\n            kms::conn_type_count_t conn_type_count;\n            for (auto &connector : card.monitors(conn_type_count)) {\n              if (connector.crtc_id == crtc_id) {\n                BOOST_LOG(info) << \"Found connector ID [\"sv << connector.connector_id << ']';\n\n                connector_id = connector.connector_id;\n\n                auto connector_props = card.connector_props(*connector_id);\n                hdr_metadata_blob_id = card.prop_value_by_name(connector_props, \"HDR_OUTPUT_METADATA\"sv);\n              }\n            }\n\n            this->card = std::move(card);\n            goto break_loop;\n          }\n        }\n\n        BOOST_LOG(error) << \"Couldn't find monitor [\"sv << monitor_index << ']';\n        return -1;\n\n      // Neatly break from nested for loop\n      break_loop:\n\n        // Look for the cursor plane for this CRTC\n        cursor_plane_id = -1;\n        auto end = std::end(card);\n        for (auto plane = std::begin(card); plane != end; ++plane) {\n          if (!card.is_cursor(plane->plane_id)) {\n            continue;\n          }\n\n          // NB: We do not skip unused planes here because cursor planes\n          // will look unused if the cursor is currently hidden.\n\n          if (!(plane->possible_crtcs & (1 << crtc_index))) {\n            // Skip cursor planes for other CRTCs\n            continue;\n          }\n          else if (plane->possible_crtcs != (1 << crtc_index)) {\n            // We assume a 1:1 mapping between cursor planes and CRTCs, which seems to\n            // match the behavior of drivers in the real world. If it's violated, we'll\n            // proceed anyway but print a warning in the log.\n            BOOST_LOG(warning) << \"Cursor plane spans multiple CRTCs!\"sv;\n          }\n\n          BOOST_LOG(info) << \"Found cursor plane [\"sv << plane->plane_id << ']';\n          cursor_plane_id = plane->plane_id;\n          break;\n        }\n\n        if (cursor_plane_id < 0) {\n          BOOST_LOG(warning) << \"No KMS cursor plane found. Cursor may not be displayed while streaming!\"sv;\n        }\n\n        return 0;\n      }\n\n      bool\n      is_hdr() {\n        if (!hdr_metadata_blob_id || *hdr_metadata_blob_id == 0) {\n          return false;\n        }\n\n        prop_blob_t hdr_metadata_blob = drmModeGetPropertyBlob(card.fd.el, *hdr_metadata_blob_id);\n        if (hdr_metadata_blob == nullptr) {\n          BOOST_LOG(error) << \"Unable to get HDR metadata blob: \"sv << strerror(errno);\n          return false;\n        }\n\n        if (hdr_metadata_blob->length < sizeof(uint32_t) + sizeof(hdr_metadata_infoframe)) {\n          BOOST_LOG(error) << \"HDR metadata blob is too small: \"sv << hdr_metadata_blob->length;\n          return false;\n        }\n\n        auto raw_metadata = (hdr_output_metadata *) hdr_metadata_blob->data;\n        if (raw_metadata->metadata_type != 0) {  // HDMI_STATIC_METADATA_TYPE1\n          BOOST_LOG(error) << \"Unknown HDMI_STATIC_METADATA_TYPE value: \"sv << raw_metadata->metadata_type;\n          return false;\n        }\n\n        if (raw_metadata->hdmi_metadata_type1.metadata_type != 0) {  // Static Metadata Type 1\n          BOOST_LOG(error) << \"Unknown secondary metadata type value: \"sv << raw_metadata->hdmi_metadata_type1.metadata_type;\n          return false;\n        }\n\n        // We only support Traditional Gamma SDR or SMPTE 2084 PQ HDR EOTFs.\n        // Print a warning if we encounter any others.\n        switch (raw_metadata->hdmi_metadata_type1.eotf) {\n          case 0:  // HDMI_EOTF_TRADITIONAL_GAMMA_SDR\n            return false;\n          case 1:  // HDMI_EOTF_TRADITIONAL_GAMMA_HDR\n            BOOST_LOG(warning) << \"Unsupported HDR EOTF: Traditional Gamma\"sv;\n            return true;\n          case 2:  // HDMI_EOTF_SMPTE_ST2084\n            return true;\n          case 3:  // HDMI_EOTF_BT_2100_HLG\n            BOOST_LOG(warning) << \"Unsupported HDR EOTF: HLG\"sv;\n            return true;\n          default:\n            BOOST_LOG(warning) << \"Unsupported HDR EOTF: \"sv << raw_metadata->hdmi_metadata_type1.eotf;\n            return true;\n        }\n      }\n\n      bool\n      get_hdr_metadata(SS_HDR_METADATA &metadata) {\n        // This performs all the metadata validation\n        if (!is_hdr()) {\n          return false;\n        }\n\n        prop_blob_t hdr_metadata_blob = drmModeGetPropertyBlob(card.fd.el, *hdr_metadata_blob_id);\n        if (hdr_metadata_blob == nullptr) {\n          BOOST_LOG(error) << \"Unable to get HDR metadata blob: \"sv << strerror(errno);\n          return false;\n        }\n\n        auto raw_metadata = (hdr_output_metadata *) hdr_metadata_blob->data;\n\n        for (int i = 0; i < 3; i++) {\n          metadata.displayPrimaries[i].x = raw_metadata->hdmi_metadata_type1.display_primaries[i].x;\n          metadata.displayPrimaries[i].y = raw_metadata->hdmi_metadata_type1.display_primaries[i].y;\n        }\n\n        metadata.whitePoint.x = raw_metadata->hdmi_metadata_type1.white_point.x;\n        metadata.whitePoint.y = raw_metadata->hdmi_metadata_type1.white_point.y;\n        metadata.maxDisplayLuminance = raw_metadata->hdmi_metadata_type1.max_display_mastering_luminance;\n        metadata.minDisplayLuminance = raw_metadata->hdmi_metadata_type1.min_display_mastering_luminance;\n        metadata.maxContentLightLevel = raw_metadata->hdmi_metadata_type1.max_cll;\n        metadata.maxFrameAverageLightLevel = raw_metadata->hdmi_metadata_type1.max_fall;\n\n        return true;\n      }\n\n      void\n      update_cursor() {\n        if (cursor_plane_id < 0) {\n          return;\n        }\n\n        plane_t plane = drmModeGetPlane(card.fd.el, cursor_plane_id);\n\n        std::optional<std::int32_t> prop_crtc_x;\n        std::optional<std::int32_t> prop_crtc_y;\n        std::optional<std::uint32_t> prop_crtc_w;\n        std::optional<std::uint32_t> prop_crtc_h;\n\n        std::optional<std::uint64_t> prop_src_x;\n        std::optional<std::uint64_t> prop_src_y;\n        std::optional<std::uint64_t> prop_src_w;\n        std::optional<std::uint64_t> prop_src_h;\n\n        auto props = card.plane_props(cursor_plane_id);\n        for (auto &[prop, val] : props) {\n          if (prop->name == \"CRTC_X\"sv) {\n            prop_crtc_x = val;\n          }\n          else if (prop->name == \"CRTC_Y\"sv) {\n            prop_crtc_y = val;\n          }\n          else if (prop->name == \"CRTC_W\"sv) {\n            prop_crtc_w = val;\n          }\n          else if (prop->name == \"CRTC_H\"sv) {\n            prop_crtc_h = val;\n          }\n          else if (prop->name == \"SRC_X\"sv) {\n            prop_src_x = val;\n          }\n          else if (prop->name == \"SRC_Y\"sv) {\n            prop_src_y = val;\n          }\n          else if (prop->name == \"SRC_W\"sv) {\n            prop_src_w = val;\n          }\n          else if (prop->name == \"SRC_H\"sv) {\n            prop_src_h = val;\n          }\n        }\n\n        if (!prop_crtc_w || !prop_crtc_h || !prop_crtc_x || !prop_crtc_y) {\n          BOOST_LOG(error) << \"Cursor plane is missing required plane CRTC properties!\"sv;\n          BOOST_LOG(error) << \"Atomic mode-setting must be enabled to capture the cursor!\"sv;\n          cursor_plane_id = -1;\n          captured_cursor.visible = false;\n          return;\n        }\n        if (!prop_src_x || !prop_src_y || !prop_src_w || !prop_src_h) {\n          BOOST_LOG(error) << \"Cursor plane is missing required plane SRC properties!\"sv;\n          BOOST_LOG(error) << \"Atomic mode-setting must be enabled to capture the cursor!\"sv;\n          cursor_plane_id = -1;\n          captured_cursor.visible = false;\n          return;\n        }\n\n        // Update the cursor position and size unconditionally\n        captured_cursor.x = *prop_crtc_x;\n        captured_cursor.y = *prop_crtc_y;\n        captured_cursor.dst_w = *prop_crtc_w;\n        captured_cursor.dst_h = *prop_crtc_h;\n\n        // We're technically cheating a bit here by assuming that we can detect\n        // changes to the cursor plane via property adjustments. If this isn't\n        // true, we'll really have to mmap() the dmabuf and draw that every time.\n        bool cursor_dirty = false;\n\n        if (!plane->fb_id) {\n          captured_cursor.visible = false;\n          captured_cursor.fb_id = 0;\n        }\n        else if (plane->fb_id != captured_cursor.fb_id) {\n          BOOST_LOG(debug) << \"Refreshing cursor image after FB changed\"sv;\n          cursor_dirty = true;\n        }\n        else if (*prop_src_x != captured_cursor.prop_src_x ||\n                 *prop_src_y != captured_cursor.prop_src_y ||\n                 *prop_src_w != captured_cursor.prop_src_w ||\n                 *prop_src_h != captured_cursor.prop_src_h) {\n          BOOST_LOG(debug) << \"Refreshing cursor image after source dimensions changed\"sv;\n          cursor_dirty = true;\n        }\n\n        // If the cursor is dirty, map it so we can download the new image\n        if (cursor_dirty) {\n          auto fb = card.fb(plane.get());\n          if (!fb || !fb->handles[0]) {\n            // This means the cursor is not currently visible\n            captured_cursor.visible = false;\n            return;\n          }\n\n          // All known cursor planes in the wild are ARGB8888\n          if (fb->pixel_format != DRM_FORMAT_ARGB8888) {\n            BOOST_LOG(error) << \"Unsupported non-ARGB8888 cursor format: \"sv << fb->pixel_format;\n            captured_cursor.visible = false;\n            cursor_plane_id = -1;\n            return;\n          }\n\n          // All known cursor planes in the wild require linear buffers\n          if (fb->modifier != DRM_FORMAT_MOD_LINEAR && fb->modifier != DRM_FORMAT_MOD_INVALID) {\n            BOOST_LOG(error) << \"Unsupported non-linear cursor modifier: \"sv << fb->modifier;\n            captured_cursor.visible = false;\n            cursor_plane_id = -1;\n            return;\n          }\n\n          // The SRC_* properties are in Q16.16 fixed point, so convert to integers\n          auto src_x = *prop_src_x >> 16;\n          auto src_y = *prop_src_y >> 16;\n          auto src_w = *prop_src_w >> 16;\n          auto src_h = *prop_src_h >> 16;\n\n          // Check for a legal source rectangle\n          if (src_x + src_w > fb->width || src_y + src_h > fb->height) {\n            BOOST_LOG(error) << \"Illegal source size: [\"sv << src_x + src_w << ',' << src_y + src_h << \"] > [\"sv << fb->width << ',' << fb->height << ']';\n            captured_cursor.visible = false;\n            return;\n          }\n\n          file_t plane_fd = card.handleFD(fb->handles[0]);\n          if (plane_fd.el < 0) {\n            captured_cursor.visible = false;\n            return;\n          }\n\n          // We will map the entire region, but only copy what the source rectangle specifies\n          size_t mapped_size = ((size_t) fb->pitches[0]) * fb->height;\n          void *mapped_data = mmap(nullptr, mapped_size, PROT_READ, MAP_SHARED, plane_fd.el, fb->offsets[0]);\n\n          // If we got ENOSYS back, let's try to map it as a dumb buffer instead (required for Nvidia GPUs)\n          if (mapped_data == MAP_FAILED && errno == ENOSYS) {\n            drm_mode_map_dumb map = {};\n            map.handle = fb->handles[0];\n            if (drmIoctl(card.fd.el, DRM_IOCTL_MODE_MAP_DUMB, &map) < 0) {\n              BOOST_LOG(error) << \"Failed to map cursor FB as dumb buffer: \"sv << strerror(errno);\n              captured_cursor.visible = false;\n              return;\n            }\n\n            mapped_data = mmap(nullptr, mapped_size, PROT_READ, MAP_SHARED, card.fd.el, map.offset);\n          }\n\n          if (mapped_data == MAP_FAILED) {\n            BOOST_LOG(error) << \"Failed to mmap cursor FB: \"sv << strerror(errno);\n            captured_cursor.visible = false;\n            return;\n          }\n\n          captured_cursor.pixels.resize(src_w * src_h * 4);\n\n          // Prepare to read the dmabuf from the CPU\n          struct dma_buf_sync sync;\n          sync.flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ;\n          drmIoctl(plane_fd.el, DMA_BUF_IOCTL_SYNC, &sync);\n\n          // If the image is tightly packed, copy it in one shot\n          if (fb->pitches[0] == src_w * 4 && src_x == 0) {\n            memcpy(captured_cursor.pixels.data(), &((std::uint8_t *) mapped_data)[src_y * fb->pitches[0]], src_h * fb->pitches[0]);\n          }\n          else {\n            // Copy row by row to deal with mismatched pitch or an X offset\n            auto pixel_dst = captured_cursor.pixels.data();\n            for (int y = 0; y < src_h; y++) {\n              memcpy(&pixel_dst[y * (src_w * 4)], &((std::uint8_t *) mapped_data)[(y + src_y) * fb->pitches[0] + (src_x * 4)], src_w * 4);\n            }\n          }\n\n          // End the CPU read and unmap the dmabuf\n          sync.flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ;\n          drmIoctl(plane_fd.el, DMA_BUF_IOCTL_SYNC, &sync);\n\n          munmap(mapped_data, mapped_size);\n\n          captured_cursor.visible = true;\n          captured_cursor.src_w = src_w;\n          captured_cursor.src_h = src_h;\n          captured_cursor.prop_src_x = *prop_src_x;\n          captured_cursor.prop_src_y = *prop_src_y;\n          captured_cursor.prop_src_w = *prop_src_w;\n          captured_cursor.prop_src_h = *prop_src_h;\n          captured_cursor.fb_id = plane->fb_id;\n          ++captured_cursor.serial;\n        }\n      }\n\n      inline capture_e\n      refresh(file_t *file, egl::surface_descriptor_t *sd, std::optional<std::chrono::steady_clock::time_point> &frame_timestamp) {\n        // Check for a change in HDR metadata\n        if (connector_id) {\n          auto connector_props = card.connector_props(*connector_id);\n          if (hdr_metadata_blob_id != card.prop_value_by_name(connector_props, \"HDR_OUTPUT_METADATA\"sv)) {\n            BOOST_LOG(info) << \"Reinitializing capture after HDR metadata change\"sv;\n            return capture_e::reinit;\n          }\n        }\n\n        plane_t plane = drmModeGetPlane(card.fd.el, plane_id);\n        frame_timestamp = std::chrono::steady_clock::now();\n\n        auto fb = card.fb(plane.get());\n        if (!fb) {\n          // This can happen if the display is being reconfigured while streaming\n          BOOST_LOG(warning) << \"Couldn't get drm fb for plane [\"sv << plane->fb_id << \"]: \"sv << strerror(errno);\n          return capture_e::timeout;\n        }\n\n        if (!fb->handles[0]) {\n          BOOST_LOG(error) << \"Couldn't get handle for DRM Framebuffer [\"sv << plane->fb_id << \"]: Probably not permitted\"sv;\n          return capture_e::error;\n        }\n\n        for (int y = 0; y < 4; ++y) {\n          if (!fb->handles[y]) {\n            // setting sd->fds[y] to a negative value indicates that sd->offsets[y] and sd->pitches[y]\n            // are uninitialized and contain invalid values.\n            sd->fds[y] = -1;\n            // It's not clear whether there could still be valid handles left.\n            // So, continue anyway.\n            // TODO: Is this redundant?\n            continue;\n          }\n\n          file[y] = card.handleFD(fb->handles[y]);\n          if (file[y].el < 0) {\n            BOOST_LOG(error) << \"Couldn't get primary file descriptor for Framebuffer [\"sv << fb->fb_id << \"]: \"sv << strerror(errno);\n            return capture_e::error;\n          }\n\n          sd->fds[y] = file[y].el;\n          sd->offsets[y] = fb->offsets[y];\n          sd->pitches[y] = fb->pitches[y];\n        }\n\n        sd->width = fb->width;\n        sd->height = fb->height;\n        sd->modifier = fb->modifier;\n        sd->fourcc = fb->pixel_format;\n\n        if (\n          fb->width != img_width ||\n          fb->height != img_height) {\n          return capture_e::reinit;\n        }\n\n        update_cursor();\n\n        return capture_e::ok;\n      }\n\n      mem_type_e mem_type;\n\n      std::chrono::nanoseconds delay;\n\n      int img_width, img_height;\n      int img_offset_x, img_offset_y;\n\n      int plane_id;\n      int crtc_id;\n      int crtc_index;\n\n      std::optional<uint32_t> connector_id;\n      std::optional<uint64_t> hdr_metadata_blob_id;\n\n      int cursor_plane_id;\n      cursor_t captured_cursor {};\n\n      card_t card;\n    };\n\n    class display_ram_t: public display_t {\n    public:\n      display_ram_t(mem_type_e mem_type):\n          display_t(mem_type) {}\n\n      int\n      init(const std::string &display_name, const ::video::config_t &config) {\n        if (!gbm::create_device) {\n          BOOST_LOG(warning) << \"libgbm not initialized\"sv;\n          return -1;\n        }\n\n        if (display_t::init(display_name, config)) {\n          return -1;\n        }\n\n        gbm.reset(gbm::create_device(card.fd.el));\n        if (!gbm) {\n          BOOST_LOG(error) << \"Couldn't create GBM device: [\"sv << util::hex(eglGetError()).to_string_view() << ']';\n          return -1;\n        }\n\n        display = egl::make_display(gbm.get());\n        if (!display) {\n          return -1;\n        }\n\n        auto ctx_opt = egl::make_ctx(display.get());\n        if (!ctx_opt) {\n          return -1;\n        }\n\n        ctx = std::move(*ctx_opt);\n\n        return 0;\n      }\n\n      capture_e\n      capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {\n        auto next_frame = std::chrono::steady_clock::now();\n\n        sleep_overshoot_logger.reset();\n\n        while (true) {\n          auto now = std::chrono::steady_clock::now();\n\n          if (next_frame > now) {\n            std::this_thread::sleep_for(next_frame - now);\n            sleep_overshoot_logger.first_point(next_frame);\n            sleep_overshoot_logger.second_point_now_and_log();\n          }\n\n          next_frame += delay;\n          if (next_frame < now) {  // some major slowdown happened; we couldn't keep up\n            next_frame = now + delay;\n          }\n\n          std::shared_ptr<platf::img_t> img_out;\n          auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);\n          switch (status) {\n            case platf::capture_e::reinit:\n            case platf::capture_e::error:\n            case platf::capture_e::interrupted:\n              return status;\n            case platf::capture_e::timeout:\n              if (!push_captured_image_cb(std::move(img_out), false)) {\n                return platf::capture_e::ok;\n              }\n              break;\n            case platf::capture_e::ok:\n              if (!push_captured_image_cb(std::move(img_out), true)) {\n                return platf::capture_e::ok;\n              }\n              break;\n            default:\n              BOOST_LOG(error) << \"Unrecognized capture status [\"sv << (int) status << ']';\n              return status;\n          }\n        }\n\n        return capture_e::ok;\n      }\n\n      std::unique_ptr<avcodec_encode_device_t>\n      make_avcodec_encode_device(pix_fmt_e pix_fmt) override {\n#ifdef SUNSHINE_BUILD_VAAPI\n        if (mem_type == mem_type_e::vaapi) {\n          return va::make_avcodec_encode_device(width, height, false);\n        }\n#endif\n\n#ifdef SUNSHINE_BUILD_CUDA\n        if (mem_type == mem_type_e::cuda) {\n          return cuda::make_avcodec_encode_device(width, height, false);\n        }\n#endif\n\n        return std::make_unique<avcodec_encode_device_t>();\n      }\n\n      void\n      blend_cursor(img_t &img) {\n        // TODO: Cursor scaling is not supported in this codepath.\n        // We always draw the cursor at the source size.\n        auto pixels = (int *) img.data;\n\n        int32_t screen_height = img.height;\n        int32_t screen_width = img.width;\n\n        // This is the position in the target that we will start drawing the cursor\n        auto cursor_x = std::max<int32_t>(0, captured_cursor.x - img_offset_x);\n        auto cursor_y = std::max<int32_t>(0, captured_cursor.y - img_offset_y);\n\n        // If the cursor is partially off screen, the coordinates may be negative\n        // which means we will draw the top-right visible portion of the cursor only.\n        auto cursor_delta_x = cursor_x - std::max<int32_t>(-captured_cursor.src_w, captured_cursor.x - img_offset_x);\n        auto cursor_delta_y = cursor_y - std::max<int32_t>(-captured_cursor.src_h, captured_cursor.y - img_offset_y);\n\n        auto delta_height = std::min<uint32_t>(captured_cursor.src_h, std::max<int32_t>(0, screen_height - cursor_y)) - cursor_delta_y;\n        auto delta_width = std::min<uint32_t>(captured_cursor.src_w, std::max<int32_t>(0, screen_width - cursor_x)) - cursor_delta_x;\n        for (auto y = 0; y < delta_height; ++y) {\n          // Offset into the cursor image to skip drawing the parts of the cursor image that are off screen\n          //\n          // NB: We must access the elements via the data() function because cursor_end may point to the\n          // the first element beyond the valid range of the vector. Using vector's [] operator in that\n          // manner is undefined behavior (and triggers errors when using debug libc++), while doing the\n          // same with an array is fine.\n          auto cursor_begin = (uint32_t *) &captured_cursor.pixels.data()[((y + cursor_delta_y) * captured_cursor.src_w + cursor_delta_x) * 4];\n          auto cursor_end = (uint32_t *) &captured_cursor.pixels.data()[((y + cursor_delta_y) * captured_cursor.src_w + delta_width + cursor_delta_x) * 4];\n\n          auto pixels_begin = &pixels[(y + cursor_y) * (img.row_pitch / img.pixel_pitch) + cursor_x];\n\n          std::for_each(cursor_begin, cursor_end, [&](uint32_t cursor_pixel) {\n            auto colors_in = (uint8_t *) pixels_begin;\n\n            auto alpha = (*(uint *) &cursor_pixel) >> 24u;\n            if (alpha == 255) {\n              *pixels_begin = cursor_pixel;\n            }\n            else {\n              auto colors_out = (uint8_t *) &cursor_pixel;\n              colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255;\n              colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255;\n              colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255;\n            }\n            ++pixels_begin;\n          });\n        }\n      }\n\n      capture_e\n      snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {\n        file_t fb_fd[4];\n\n        egl::surface_descriptor_t sd;\n\n        std::optional<std::chrono::steady_clock::time_point> frame_timestamp;\n        auto status = refresh(fb_fd, &sd, frame_timestamp);\n        if (status != capture_e::ok) {\n          return status;\n        }\n\n        auto rgb_opt = egl::import_source(display.get(), sd);\n\n        if (!rgb_opt) {\n          return capture_e::error;\n        }\n\n        auto &rgb = *rgb_opt;\n\n        gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);\n\n        // Don't remove these lines, see https://github.com/LizardByte/Sunshine/issues/453\n        int w, h;\n        gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w);\n        gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h);\n        BOOST_LOG(debug) << \"width and height: w \"sv << w << \" h \"sv << h;\n\n        if (!pull_free_image_cb(img_out)) {\n          return platf::capture_e::interrupted;\n        }\n\n        gl::ctx.GetTextureSubImage(rgb->tex[0], 0, img_offset_x, img_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data);\n\n        img_out->frame_timestamp = frame_timestamp;\n\n        if (cursor && captured_cursor.visible) {\n          blend_cursor(*img_out);\n        }\n\n        return capture_e::ok;\n      }\n\n      std::shared_ptr<img_t>\n      alloc_img() override {\n        auto img = std::make_shared<kms_img_t>();\n        img->width = width;\n        img->height = height;\n        img->pixel_pitch = 4;\n        img->row_pitch = img->pixel_pitch * width;\n        img->data = new std::uint8_t[height * img->row_pitch];\n\n        return img;\n      }\n\n      int\n      dummy_img(platf::img_t *img) override {\n        return 0;\n      }\n\n      gbm::gbm_t gbm;\n      egl::display_t display;\n      egl::ctx_t ctx;\n    };\n\n    class display_vram_t: public display_t {\n    public:\n      display_vram_t(mem_type_e mem_type):\n          display_t(mem_type) {}\n\n      std::unique_ptr<avcodec_encode_device_t>\n      make_avcodec_encode_device(pix_fmt_e pix_fmt) override {\n#ifdef SUNSHINE_BUILD_VAAPI\n        if (mem_type == mem_type_e::vaapi) {\n          return va::make_avcodec_encode_device(width, height, dup(card.render_fd.el), img_offset_x, img_offset_y, true);\n        }\n#endif\n\n#ifdef SUNSHINE_BUILD_CUDA\n        if (mem_type == mem_type_e::cuda) {\n          return cuda::make_avcodec_gl_encode_device(width, height, img_offset_x, img_offset_y);\n        }\n#endif\n\n        BOOST_LOG(error) << \"Unsupported pixel format for egl::display_vram_t: \"sv << platf::from_pix_fmt(pix_fmt);\n        return nullptr;\n      }\n\n      std::shared_ptr<img_t>\n      alloc_img() override {\n        auto img = std::make_shared<egl::img_descriptor_t>();\n\n        img->width = width;\n        img->height = height;\n        img->serial = std::numeric_limits<decltype(img->serial)>::max();\n        img->data = nullptr;\n        img->pixel_pitch = 4;\n\n        img->sequence = 0;\n        std::fill_n(img->sd.fds, 4, -1);\n\n        return img;\n      }\n\n      int\n      dummy_img(platf::img_t *img) override {\n        // Empty images are recognized as dummies by the zero sequence number\n        return 0;\n      }\n\n      capture_e\n      capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) {\n        auto next_frame = std::chrono::steady_clock::now();\n\n        sleep_overshoot_logger.reset();\n\n        while (true) {\n          auto now = std::chrono::steady_clock::now();\n\n          if (next_frame > now) {\n            std::this_thread::sleep_for(next_frame - now);\n            sleep_overshoot_logger.first_point(next_frame);\n            sleep_overshoot_logger.second_point_now_and_log();\n          }\n\n          next_frame += delay;\n          if (next_frame < now) {  // some major slowdown happened; we couldn't keep up\n            next_frame = now + delay;\n          }\n\n          std::shared_ptr<platf::img_t> img_out;\n          auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);\n          switch (status) {\n            case platf::capture_e::reinit:\n            case platf::capture_e::error:\n            case platf::capture_e::interrupted:\n              return status;\n            case platf::capture_e::timeout:\n              if (!push_captured_image_cb(std::move(img_out), false)) {\n                return platf::capture_e::ok;\n              }\n              break;\n            case platf::capture_e::ok:\n              if (!push_captured_image_cb(std::move(img_out), true)) {\n                return platf::capture_e::ok;\n              }\n              break;\n            default:\n              BOOST_LOG(error) << \"Unrecognized capture status [\"sv << (int) status << ']';\n              return status;\n          }\n        }\n\n        return capture_e::ok;\n      }\n\n      capture_e\n      snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds /* timeout */, bool cursor) {\n        file_t fb_fd[4];\n\n        if (!pull_free_image_cb(img_out)) {\n          return platf::capture_e::interrupted;\n        }\n        auto img = (egl::img_descriptor_t *) img_out.get();\n        img->reset();\n\n        auto status = refresh(fb_fd, &img->sd, img->frame_timestamp);\n        if (status != capture_e::ok) {\n          return status;\n        }\n\n        img->sequence = ++sequence;\n\n        if (cursor && captured_cursor.visible) {\n          // Copy new cursor pixel data if it's been updated\n          if (img->serial != captured_cursor.serial) {\n            img->buffer = captured_cursor.pixels;\n            img->serial = captured_cursor.serial;\n          }\n\n          img->x = captured_cursor.x;\n          img->y = captured_cursor.y;\n          img->src_w = captured_cursor.src_w;\n          img->src_h = captured_cursor.src_h;\n          img->width = captured_cursor.dst_w;\n          img->height = captured_cursor.dst_h;\n          img->pixel_pitch = 4;\n          img->row_pitch = img->pixel_pitch * img->width;\n          img->data = img->buffer.data();\n        }\n        else {\n          img->data = nullptr;\n        }\n\n        for (auto x = 0; x < 4; ++x) {\n          fb_fd[x].release();\n        }\n        return capture_e::ok;\n      }\n\n      int\n      init(const std::string &display_name, const ::video::config_t &config) {\n        if (display_t::init(display_name, config)) {\n          return -1;\n        }\n\n#ifdef SUNSHINE_BUILD_VAAPI\n        if (mem_type == mem_type_e::vaapi && !va::validate(card.render_fd.el)) {\n          BOOST_LOG(warning) << \"Monitor \"sv << display_name << \" doesn't support hardware encoding. Reverting back to GPU -> RAM -> GPU\"sv;\n          return -1;\n        }\n#endif\n\n#ifndef SUNSHINE_BUILD_CUDA\n        if (mem_type == mem_type_e::cuda) {\n          BOOST_LOG(warning) << \"Attempting to use NVENC without CUDA support. Reverting back to GPU -> RAM -> GPU\"sv;\n          return -1;\n        }\n#endif\n\n        return 0;\n      }\n\n      std::uint64_t sequence {};\n    };\n\n  }  // namespace kms\n\n  std::shared_ptr<display_t>\n  kms_display(mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {\n    if (hwdevice_type == mem_type_e::vaapi || hwdevice_type == mem_type_e::cuda) {\n      auto disp = std::make_shared<kms::display_vram_t>(hwdevice_type);\n\n      if (!disp->init(display_name, config)) {\n        return disp;\n      }\n\n      // In the case of failure, attempt the old method for VAAPI\n    }\n\n    auto disp = std::make_shared<kms::display_ram_t>(hwdevice_type);\n\n    if (disp->init(display_name, config)) {\n      return nullptr;\n    }\n\n    return disp;\n  }\n\n  /**\n   * On Wayland, it's not possible to determine the position of the monitor on the desktop with KMS.\n   * Wayland does allow applications to query attached monitors on the desktop,\n   * however, the naming scheme is not standardized across implementations.\n   *\n   * As a result, correlating the KMS output to the wayland outputs is guess work at best.\n   * But, it's necessary for absolute mouse coordinates to work.\n   *\n   * This is an ugly hack :(\n   */\n  void\n  correlate_to_wayland(std::vector<kms::card_descriptor_t> &cds) {\n    auto monitors = wl::monitors();\n\n    BOOST_LOG(info) << \"-------- Start of KMS monitor list --------\"sv;\n\n    for (auto &monitor : monitors) {\n      std::string_view name = monitor->name;\n\n      // Try to convert names in the format:\n      // {type}-{index}\n      // {index} is n'th occurrence of {type}\n      auto index_begin = name.find_last_of('-');\n\n      std::uint32_t index;\n      if (index_begin == std::string_view::npos) {\n        index = 1;\n      }\n      else {\n        index = std::max<int64_t>(1, util::from_view(name.substr(index_begin + 1)));\n      }\n\n      auto type = kms::from_view(name.substr(0, index_begin));\n\n      for (auto &card_descriptor : cds) {\n        for (auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) {\n          if (monitor_descriptor.index == index && monitor_descriptor.type == type) {\n            monitor_descriptor.viewport.offset_x = monitor->viewport.offset_x;\n            monitor_descriptor.viewport.offset_y = monitor->viewport.offset_y;\n\n            // A sanity check, it's guesswork after all.\n            if (\n              monitor_descriptor.viewport.width != monitor->viewport.width ||\n              monitor_descriptor.viewport.height != monitor->viewport.height) {\n              BOOST_LOG(warning)\n                << \"Mismatch on expected Resolution compared to actual resolution: \"sv\n                << monitor_descriptor.viewport.width << 'x' << monitor_descriptor.viewport.height\n                << \" vs \"sv\n                << monitor->viewport.width << 'x' << monitor->viewport.height;\n            }\n\n            BOOST_LOG(info) << \"Monitor \" << monitor_descriptor.monitor_index << \" is \"sv << name << \": \"sv << monitor->description;\n            goto break_for_loop;\n          }\n        }\n      }\n    break_for_loop:\n\n      BOOST_LOG(verbose) << \"Reduced to name: \"sv << name << \": \"sv << index;\n    }\n\n    BOOST_LOG(info) << \"--------- End of KMS monitor list ---------\"sv;\n  }\n\n  // A list of names of displays accepted as display_name\n  std::vector<std::string>\n  kms_display_names(mem_type_e hwdevice_type) {\n    int count = 0;\n\n    if (!fs::exists(\"/dev/dri\")) {\n      BOOST_LOG(warning) << \"Couldn't find /dev/dri, kmsgrab won't be enabled\"sv;\n      return {};\n    }\n\n    if (!gbm::create_device) {\n      BOOST_LOG(warning) << \"libgbm not initialized\"sv;\n      return {};\n    }\n\n    kms::conn_type_count_t conn_type_count;\n\n    std::vector<kms::card_descriptor_t> cds;\n    std::vector<std::string> display_names;\n\n    fs::path card_dir { \"/dev/dri\"sv };\n    for (auto &entry : fs::directory_iterator { card_dir }) {\n      auto file = entry.path().filename();\n\n      auto filestring = file.generic_string();\n      if (std::string_view { filestring }.substr(0, 4) != \"card\"sv) {\n        continue;\n      }\n\n      kms::card_t card;\n      if (card.init(entry.path().c_str())) {\n        continue;\n      }\n\n      // Skip non-Nvidia cards if we're looking for CUDA devices\n      // unless NVENC is selected manually by the user\n      if (hwdevice_type == mem_type_e::cuda && !card.is_nvidia()) {\n        BOOST_LOG(debug) << file << \" is not a CUDA device\"sv;\n        if (config::video.encoder == \"nvenc\") {\n          BOOST_LOG(warning) << \"Using NVENC with your display connected to a different GPU may not work properly!\"sv;\n        }\n        else {\n          continue;\n        }\n      }\n\n      auto crtc_to_monitor = kms::map_crtc_to_monitor(card.monitors(conn_type_count));\n\n      auto end = std::end(card);\n      for (auto plane = std::begin(card); plane != end; ++plane) {\n        // Skip unused planes\n        if (!plane->fb_id) {\n          continue;\n        }\n\n        if (card.is_cursor(plane->plane_id)) {\n          continue;\n        }\n\n        auto fb = card.fb(plane.get());\n        if (!fb) {\n          BOOST_LOG(error) << \"Couldn't get drm fb for plane [\"sv << plane->fb_id << \"]: \"sv << strerror(errno);\n          continue;\n        }\n\n        if (!fb->handles[0]) {\n          BOOST_LOG(error) << \"Couldn't get handle for DRM Framebuffer [\"sv << plane->fb_id << \"]: Probably not permitted\"sv;\n          BOOST_LOG((window_system != window_system_e::X11 || config::video.capture == \"kms\") ? fatal : error)\n            << \"You must run [sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))] for KMS display capture to work!\\n\"sv\n            << \"If you installed from AppImage or Flatpak, please refer to the official documentation:\\n\"sv\n            << \"https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/setup.html#install\"sv;\n          break;\n        }\n\n        // This appears to return the offset of the monitor\n        auto crtc = card.crtc(plane->crtc_id);\n        if (!crtc) {\n          BOOST_LOG(error) << \"Couldn't get CRTC info: \"sv << strerror(errno);\n          continue;\n        }\n\n        auto it = crtc_to_monitor.find(plane->crtc_id);\n        if (it != std::end(crtc_to_monitor)) {\n          it->second.viewport = platf::touch_port_t {\n            (int) crtc->x,\n            (int) crtc->y,\n            (int) crtc->width,\n            (int) crtc->height,\n          };\n          it->second.monitor_index = count;\n        }\n\n        kms::env_width = std::max(kms::env_width, (int) (crtc->x + crtc->width));\n        kms::env_height = std::max(kms::env_height, (int) (crtc->y + crtc->height));\n\n        kms::print(plane.get(), fb.get(), crtc.get());\n\n        display_names.emplace_back(std::to_string(count++));\n      }\n\n      cds.emplace_back(kms::card_descriptor_t {\n        std::move(file),\n        std::move(crtc_to_monitor),\n      });\n    }\n\n    if (!wl::init()) {\n      correlate_to_wayland(cds);\n    }\n\n    // Deduce the full virtual desktop size\n    kms::env_width = 0;\n    kms::env_height = 0;\n\n    for (auto &card_descriptor : cds) {\n      for (auto &[_, monitor_descriptor] : card_descriptor.crtc_to_monitor) {\n        BOOST_LOG(debug) << \"Monitor description\"sv;\n        BOOST_LOG(debug) << \"Resolution: \"sv << monitor_descriptor.viewport.width << 'x' << monitor_descriptor.viewport.height;\n        BOOST_LOG(debug) << \"Offset: \"sv << monitor_descriptor.viewport.offset_x << 'x' << monitor_descriptor.viewport.offset_y;\n\n        kms::env_width = std::max(kms::env_width, (int) (monitor_descriptor.viewport.offset_x + monitor_descriptor.viewport.width));\n        kms::env_height = std::max(kms::env_height, (int) (monitor_descriptor.viewport.offset_y + monitor_descriptor.viewport.height));\n      }\n    }\n\n    BOOST_LOG(debug) << \"Desktop resolution: \"sv << kms::env_width << 'x' << kms::env_height;\n\n    kms::card_descriptors = std::move(cds);\n\n    return display_names;\n  }\n\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/linux/misc.cpp",
    "content": "/**\n * @file src/platform/linux/misc.cpp\n * @brief Miscellaneous definitions for Linux.\n */\n\n// Required for in6_pktinfo with glibc headers\n#ifndef _GNU_SOURCE\n  #define _GNU_SOURCE 1\n#endif\n\n// standard includes\n#include <fstream>\n#include <iostream>\n\n// lib includes\n#include <arpa/inet.h>\n#include <boost/asio/ip/address.hpp>\n#include <boost/asio/ip/host_name.hpp>\n#include <boost/process/v1.hpp>\n#include <dlfcn.h>\n#include <fcntl.h>\n#include <ifaddrs.h>\n#include <netinet/udp.h>\n#include <pwd.h>\n#include <unistd.h>\n\n// local includes\n#include \"graphics.h\"\n#include \"misc.h\"\n#include \"src/config.h\"\n#include \"src/entry_handler.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/platform/run_command.h\"\n#include \"vaapi.h\"\n\n#ifdef __GNUC__\n  #define SUNSHINE_GNUC_EXTENSION __extension__\n#else\n  #define SUNSHINE_GNUC_EXTENSION\n#endif\n\nusing namespace std::literals;\nnamespace fs = std::filesystem;\nnamespace bp = boost::process::v1;\n\nwindow_system_e window_system;\n\nnamespace dyn {\n  void *\n  handle(const std::vector<const char *> &libs) {\n    void *handle;\n\n    for (auto lib : libs) {\n      handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL);\n      if (handle) {\n        return handle;\n      }\n    }\n\n    std::stringstream ss;\n    ss << \"Couldn't find any of the following libraries: [\"sv << libs.front();\n    std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) {\n      ss << \", \"sv << lib;\n    });\n\n    ss << ']';\n\n    BOOST_LOG(error) << ss.str();\n\n    return nullptr;\n  }\n\n  int\n  load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict) {\n    int err = 0;\n    for (auto &func : funcs) {\n      TUPLE_2D_REF(fn, name, func);\n\n      *fn = SUNSHINE_GNUC_EXTENSION(apiproc) dlsym(handle, name);\n\n      if (!*fn && strict) {\n        BOOST_LOG(error) << \"Couldn't find function: \"sv << name;\n\n        err = -1;\n      }\n    }\n\n    return err;\n  }\n}  // namespace dyn\nnamespace platf {\n  using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;\n\n  ifaddr_t\n  get_ifaddrs() {\n    ifaddrs *p { nullptr };\n\n    getifaddrs(&p);\n\n    return ifaddr_t { p };\n  }\n\n  /**\n   * @brief Performs migration if necessary, then returns the appdata directory.\n   * @details This is used for the log directory, so it cannot invoke Boost logging!\n   * @return The path of the appdata directory that should be used.\n   */\n  fs::path\n  appdata() {\n    static std::once_flag migration_flag;\n    static fs::path config_path;\n\n    // Ensure migration is only attempted once\n    std::call_once(migration_flag, []() {\n      bool found = false;\n      bool migrate_config = true;\n      const char *dir;\n      const char *homedir;\n      const char *migrate_envvar;\n\n      // Get the home directory\n      if ((homedir = getenv(\"HOME\")) == nullptr || strlen(homedir) == 0) {\n        // If HOME is empty or not set, use the current user's home directory\n        homedir = getpwuid(geteuid())->pw_dir;\n      }\n\n      // May be set if running under a systemd service with the ConfigurationDirectory= option set.\n      if ((dir = getenv(\"CONFIGURATION_DIRECTORY\")) != nullptr && strlen(dir) > 0) {\n        found = true;\n        config_path = fs::path(dir) / \"sunshine\"sv;\n      }\n      // Otherwise, follow the XDG base directory specification:\n      // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html\n      if (!found && (dir = getenv(\"XDG_CONFIG_HOME\")) != nullptr && strlen(dir) > 0) {\n        found = true;\n        config_path = fs::path(dir) / \"sunshine\"sv;\n      }\n      // As a last resort, use the home directory\n      if (!found) {\n        migrate_config = false;\n        config_path = fs::path(homedir) / \".config/sunshine\"sv;\n      }\n\n      // migrate from the old config location if necessary\n      migrate_envvar = getenv(\"SUNSHINE_MIGRATE_CONFIG\");\n      if (migrate_config && found && migrate_envvar && strcmp(migrate_envvar, \"1\") == 0) {\n        std::error_code ec;\n        fs::path old_config_path = fs::path(homedir) / \".config/sunshine\"sv;\n        if (old_config_path != config_path && fs::exists(old_config_path, ec)) {\n          if (!fs::exists(config_path, ec)) {\n            std::cout << \"Migrating config from \"sv << old_config_path << \" to \"sv << config_path << std::endl;\n            if (!ec) {\n              // Create the new directory tree if it doesn't already exist\n              fs::create_directories(config_path, ec);\n            }\n            if (!ec) {\n              // Copy the old directory into the new location\n              // NB: We use a copy instead of a move so that cross-volume migrations work\n              fs::copy(old_config_path, config_path, fs::copy_options::recursive | fs::copy_options::copy_symlinks, ec);\n            }\n            if (!ec) {\n              // If the copy was successful, delete the original directory\n              fs::remove_all(old_config_path, ec);\n              if (ec) {\n                std::cerr << \"Failed to clean up old config directory: \" << ec.message() << std::endl;\n\n                // This is not fatal. Next time we start, we'll warn the user to delete the old one.\n                ec.clear();\n              }\n            }\n            if (ec) {\n              std::cerr << \"Migration failed: \" << ec.message() << std::endl;\n              config_path = old_config_path;\n            }\n          }\n          else {\n            // We cannot use Boost logging because it hasn't been initialized yet!\n            std::cerr << \"Config exists in both \"sv << old_config_path << \" and \"sv << config_path << \". Using \"sv << config_path << \" for config\" << std::endl;\n            std::cerr << \"It is recommended to remove \"sv << old_config_path << std::endl;\n          }\n        }\n      }\n    });\n\n    return config_path;\n  }\n\n  std::string\n  from_sockaddr(const sockaddr *const ip_addr) {\n    char data[INET6_ADDRSTRLEN] = {};\n\n    auto family = ip_addr->sa_family;\n    if (family == AF_INET6) {\n      inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data,\n        INET6_ADDRSTRLEN);\n    }\n    else if (family == AF_INET) {\n      inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data,\n        INET_ADDRSTRLEN);\n    }\n\n    return std::string { data };\n  }\n\n  std::pair<std::uint16_t, std::string>\n  from_sockaddr_ex(const sockaddr *const ip_addr) {\n    char data[INET6_ADDRSTRLEN] = {};\n\n    auto family = ip_addr->sa_family;\n    std::uint16_t port = 0;\n    if (family == AF_INET6) {\n      inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data,\n        INET6_ADDRSTRLEN);\n      port = ((sockaddr_in6 *) ip_addr)->sin6_port;\n    }\n    else if (family == AF_INET) {\n      inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data,\n        INET_ADDRSTRLEN);\n      port = ((sockaddr_in *) ip_addr)->sin_port;\n    }\n\n    return { port, std::string { data } };\n  }\n\n  std::string\n  get_mac_address(const std::string_view &address) {\n    auto ifaddrs = get_ifaddrs();\n    for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {\n      if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {\n        std::ifstream mac_file(\"/sys/class/net/\"s + pos->ifa_name + \"/address\");\n        if (mac_file.good()) {\n          std::string mac_address;\n          std::getline(mac_file, mac_address);\n          return mac_address;\n        }\n      }\n    }\n\n    BOOST_LOG(warning) << \"Unable to find MAC address for \"sv << address;\n    return \"00:00:00:00:00:00\"s;\n  }\n\n  bp::child\n  run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {\n    // clang-format off\n    if (!group) {\n      if (!file) {\n        return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_in < bp::null, bp::std_out > bp::null, bp::std_err > bp::null, bp::limit_handles, ec);\n      }\n      else {\n        return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_in < bp::null, bp::std_out > file, bp::std_err > file, bp::limit_handles, ec);\n      }\n    }\n    else {\n      if (!file) {\n        return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_in < bp::null, bp::std_out > bp::null, bp::std_err > bp::null, bp::limit_handles, ec, *group);\n      }\n      else {\n        return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_in < bp::null, bp::std_out > file, bp::std_err > file, bp::limit_handles, ec, *group);\n      }\n    }\n    // clang-format on\n  }\n\n  /**\n   * @brief Open a url in the default web browser.\n   * @param url The url to open.\n   */\n  void\n  open_url(const std::string &url) {\n    // set working dir to user home directory\n    auto working_dir = boost::filesystem::path(std::getenv(\"HOME\"));\n    std::string cmd = R\"(xdg-open \")\" + url + R\"(\")\";\n\n    boost::process::v1::environment _env = boost::this_process::environment();\n    std::error_code ec;\n    auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr);\n    if (ec) {\n      BOOST_LOG(warning) << \"Couldn't open url [\"sv << url << \"]: System: \"sv << ec.message();\n    }\n    else {\n      BOOST_LOG(debug) << \"Opened url [\"sv << url << \"]\"sv;\n      child.detach();\n    }\n  }\n\n  /**\n   * @brief Open a url directly in the system default browser.\n   * @param url The url to open.\n   */\n  void\n  open_url_in_browser(const std::string &url) {\n    // On Linux, xdg-open handles this correctly\n    open_url(url);\n  }\n\n  void\n  adjust_thread_priority(thread_priority_e priority) {\n    // Unimplemented\n  }\n\n  void\n  streaming_will_start() {\n    // Nothing to do\n  }\n\n  void\n  streaming_will_stop() {\n    // Nothing to do\n  }\n\n  void\n  enter_away_mode() {\n    // TODO: Linux implementation could use DPMS to turn off display\n    // and inhibit systemd sleep via org.freedesktop.login1.Manager.Inhibit\n    BOOST_LOG(info) << \"Away Mode is not yet implemented on Linux\"sv;\n  }\n\n  void\n  exit_away_mode() {\n    // No-op on Linux for now\n  }\n\n  bool\n  is_away_mode_active() {\n    return false;\n  }\n\n  bool\n  system_sleep() {\n    // Use systemd: systemctl suspend\n    auto ret = std::system(\"systemctl suspend\");\n    if (ret != 0) {\n      BOOST_LOG(error) << \"systemctl suspend failed with code: \"sv << ret;\n      return false;\n    }\n    return true;\n  }\n\n  bool\n  system_hibernate() {\n    auto ret = std::system(\"systemctl hibernate\");\n    if (ret != 0) {\n      BOOST_LOG(error) << \"systemctl hibernate failed with code: \"sv << ret;\n      return false;\n    }\n    return true;\n  }\n\n  void\n  restart_on_exit() {\n    char executable[PATH_MAX];\n    ssize_t len = readlink(\"/proc/self/exe\", executable, PATH_MAX - 1);\n    if (len == -1) {\n      BOOST_LOG(fatal) << \"readlink() failed: \"sv << errno;\n      return;\n    }\n    executable[len] = '\\0';\n\n    // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves\n    int openmax = (int) sysconf(_SC_OPEN_MAX);\n    for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) {\n      close(fd);\n    }\n\n    // Re-exec ourselves with the same arguments\n    if (execv(executable, lifetime::get_argv()) < 0) {\n      BOOST_LOG(fatal) << \"execv() failed: \"sv << errno;\n      return;\n    }\n  }\n\n  void\n  restart() {\n    // Gracefully clean up and restart ourselves instead of exiting\n    atexit(restart_on_exit);\n    lifetime::exit_sunshine(0, true);\n  }\n\n  int\n  set_env(const std::string &name, const std::string &value) {\n    return setenv(name.c_str(), value.c_str(), 1);\n  }\n\n  int\n  unset_env(const std::string &name) {\n    return unsetenv(name.c_str());\n  }\n\n  bool\n  request_process_group_exit(std::uintptr_t native_handle) {\n    if (kill(-((pid_t) native_handle), SIGTERM) == 0 || errno == ESRCH) {\n      BOOST_LOG(debug) << \"Successfully sent SIGTERM to process group: \"sv << native_handle;\n      return true;\n    }\n    else {\n      BOOST_LOG(warning) << \"Unable to send SIGTERM to process group [\"sv << native_handle << \"]: \"sv << errno;\n      return false;\n    }\n  }\n\n  bool\n  process_group_running(std::uintptr_t native_handle) {\n    return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0;\n  }\n\n  struct sockaddr_in\n  to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) {\n    struct sockaddr_in saddr_v4 = {};\n\n    saddr_v4.sin_family = AF_INET;\n    saddr_v4.sin_port = htons(port);\n\n    auto addr_bytes = address.to_bytes();\n    memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr));\n\n    return saddr_v4;\n  }\n\n  struct sockaddr_in6\n  to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) {\n    struct sockaddr_in6 saddr_v6 = {};\n\n    saddr_v6.sin6_family = AF_INET6;\n    saddr_v6.sin6_port = htons(port);\n    saddr_v6.sin6_scope_id = address.scope_id();\n\n    auto addr_bytes = address.to_bytes();\n    memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr));\n\n    return saddr_v6;\n  }\n\n  bool\n  send_batch(batched_send_info_t &send_info) {\n    auto sockfd = (int) send_info.native_socket;\n    struct msghdr msg = {};\n\n    // Convert the target address into a sockaddr\n    struct sockaddr_in taddr_v4 = {};\n    struct sockaddr_in6 taddr_v6 = {};\n    if (send_info.target_address.is_v6()) {\n      taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port);\n\n      msg.msg_name = (struct sockaddr *) &taddr_v6;\n      msg.msg_namelen = sizeof(taddr_v6);\n    }\n    else {\n      taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port);\n\n      msg.msg_name = (struct sockaddr *) &taddr_v4;\n      msg.msg_namelen = sizeof(taddr_v4);\n    }\n\n    union {\n      char buf[CMSG_SPACE(sizeof(uint16_t)) +\n               std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))];\n      struct cmsghdr alignment;\n    } cmbuf = {};  // Must be zeroed for CMSG_NXTHDR()\n    socklen_t cmbuflen = 0;\n\n    msg.msg_control = cmbuf.buf;\n    msg.msg_controllen = sizeof(cmbuf.buf);\n\n    // The PKTINFO option will always be first, then we will conditionally\n    // append the UDP_SEGMENT option next if applicable.\n    auto pktinfo_cm = CMSG_FIRSTHDR(&msg);\n    if (send_info.source_address.is_v6()) {\n      struct in6_pktinfo pktInfo;\n\n      struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0);\n      pktInfo.ipi6_addr = saddr_v6.sin6_addr;\n      pktInfo.ipi6_ifindex = 0;\n\n      cmbuflen += CMSG_SPACE(sizeof(pktInfo));\n\n      pktinfo_cm->cmsg_level = IPPROTO_IPV6;\n      pktinfo_cm->cmsg_type = IPV6_PKTINFO;\n      pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo));\n      memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo));\n    }\n    else {\n      struct in_pktinfo pktInfo;\n\n      struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0);\n      pktInfo.ipi_spec_dst = saddr_v4.sin_addr;\n      pktInfo.ipi_ifindex = 0;\n\n      cmbuflen += CMSG_SPACE(sizeof(pktInfo));\n\n      pktinfo_cm->cmsg_level = IPPROTO_IP;\n      pktinfo_cm->cmsg_type = IP_PKTINFO;\n      pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo));\n      memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo));\n    }\n\n    auto const max_iovs_per_msg = send_info.payload_buffers.size() + (send_info.headers ? 1 : 0);\n\n#ifdef UDP_SEGMENT\n    {\n      // UDP GSO on Linux currently only supports sending 64K or 64 segments at a time\n      size_t seg_index = 0;\n      const size_t seg_max = 65536 / 1500;\n      struct iovec iovs[(send_info.headers ? std::min(seg_max, send_info.block_count) : 1) * max_iovs_per_msg] = {};\n      auto msg_size = send_info.header_size + send_info.payload_size;\n      while (seg_index < send_info.block_count) {\n        int iovlen = 0;\n        auto segs_in_batch = std::min(send_info.block_count - seg_index, seg_max);\n        if (send_info.headers) {\n          // Interleave iovs for headers and payloads\n          for (auto i = 0; i < segs_in_batch; i++) {\n            iovs[iovlen].iov_base = (void *) &send_info.headers[(send_info.block_offset + seg_index + i) * send_info.header_size];\n            iovs[iovlen].iov_len = send_info.header_size;\n            iovlen++;\n            auto payload_desc = send_info.buffer_for_payload_offset((send_info.block_offset + seg_index + i) * send_info.payload_size);\n            iovs[iovlen].iov_base = (void *) payload_desc.buffer;\n            iovs[iovlen].iov_len = send_info.payload_size;\n            iovlen++;\n          }\n        }\n        else {\n          // Translate buffer descriptors into iovs\n          auto payload_offset = (send_info.block_offset + seg_index) * send_info.payload_size;\n          auto payload_length = payload_offset + (segs_in_batch * send_info.payload_size);\n          while (payload_offset < payload_length) {\n            auto payload_desc = send_info.buffer_for_payload_offset(payload_offset);\n            iovs[iovlen].iov_base = (void *) payload_desc.buffer;\n            iovs[iovlen].iov_len = std::min(payload_desc.size, payload_length - payload_offset);\n            payload_offset += iovs[iovlen].iov_len;\n            iovlen++;\n          }\n        }\n\n        msg.msg_iov = iovs;\n        msg.msg_iovlen = iovlen;\n\n        // We should not use GSO if the data is <= one full block size\n        if (segs_in_batch > 1) {\n          msg.msg_controllen = cmbuflen + CMSG_SPACE(sizeof(uint16_t));\n\n          // Enable GSO to perform segmentation of our buffer for us\n          auto cm = CMSG_NXTHDR(&msg, pktinfo_cm);\n          cm->cmsg_level = SOL_UDP;\n          cm->cmsg_type = UDP_SEGMENT;\n          cm->cmsg_len = CMSG_LEN(sizeof(uint16_t));\n          *((uint16_t *) CMSG_DATA(cm)) = msg_size;\n        }\n        else {\n          msg.msg_controllen = cmbuflen;\n        }\n\n        // This will fail if GSO is not available, so we will fall back to non-GSO if\n        // it's the first sendmsg() call. On subsequent calls, we will treat errors as\n        // actual failures and return to the caller.\n        auto bytes_sent = sendmsg(sockfd, &msg, 0);\n        if (bytes_sent < 0) {\n          // If there's no send buffer space, wait for some to be available\n          if (errno == EAGAIN) {\n            struct pollfd pfd;\n\n            pfd.fd = sockfd;\n            pfd.events = POLLOUT;\n\n            if (poll(&pfd, 1, -1) != 1) {\n              BOOST_LOG(warning) << \"poll() failed: \"sv << errno;\n              break;\n            }\n\n            // Try to send again\n            continue;\n          }\n\n          BOOST_LOG(verbose) << \"sendmsg() failed: \"sv << errno;\n          break;\n        }\n\n        seg_index += bytes_sent / msg_size;\n      }\n\n      // If we sent something, return the status and don't fall back to the non-GSO path.\n      if (seg_index != 0) {\n        return seg_index >= send_info.block_count;\n      }\n    }\n#endif\n\n    {\n      // If GSO is not supported, use sendmmsg() instead.\n      struct mmsghdr msgs[send_info.block_count] = {};\n      struct iovec iovs[send_info.block_count * (send_info.headers ? 2 : 1)] = {};\n      int iov_idx = 0;\n      for (size_t i = 0; i < send_info.block_count; i++) {\n        msgs[i].msg_hdr.msg_iov = &iovs[iov_idx];\n        msgs[i].msg_hdr.msg_iovlen = send_info.headers ? 2 : 1;\n\n        if (send_info.headers) {\n          iovs[iov_idx].iov_base = (void *) &send_info.headers[(send_info.block_offset + i) * send_info.header_size];\n          iovs[iov_idx].iov_len = send_info.header_size;\n          iov_idx++;\n        }\n        auto payload_desc = send_info.buffer_for_payload_offset((send_info.block_offset + i) * send_info.payload_size);\n        iovs[iov_idx].iov_base = (void *) payload_desc.buffer;\n        iovs[iov_idx].iov_len = send_info.payload_size;\n        iov_idx++;\n\n        msgs[i].msg_hdr.msg_name = msg.msg_name;\n        msgs[i].msg_hdr.msg_namelen = msg.msg_namelen;\n        msgs[i].msg_hdr.msg_control = cmbuf.buf;\n        msgs[i].msg_hdr.msg_controllen = cmbuflen;\n      }\n\n      // Call sendmmsg() until all messages are sent\n      size_t blocks_sent = 0;\n      while (blocks_sent < send_info.block_count) {\n        int msgs_sent = sendmmsg(sockfd, &msgs[blocks_sent], send_info.block_count - blocks_sent, 0);\n        if (msgs_sent < 0) {\n          // If there's no send buffer space, wait for some to be available\n          if (errno == EAGAIN) {\n            struct pollfd pfd;\n\n            pfd.fd = sockfd;\n            pfd.events = POLLOUT;\n\n            if (poll(&pfd, 1, -1) != 1) {\n              BOOST_LOG(warning) << \"poll() failed: \"sv << errno;\n              break;\n            }\n\n            // Try to send again\n            continue;\n          }\n\n          BOOST_LOG(warning) << \"sendmmsg() failed: \"sv << errno;\n          return false;\n        }\n\n        blocks_sent += msgs_sent;\n      }\n\n      return true;\n    }\n  }\n\n  bool\n  send(send_info_t &send_info) {\n    auto sockfd = (int) send_info.native_socket;\n    struct msghdr msg = {};\n\n    // Convert the target address into a sockaddr\n    struct sockaddr_in taddr_v4 = {};\n    struct sockaddr_in6 taddr_v6 = {};\n    if (send_info.target_address.is_v6()) {\n      taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port);\n\n      msg.msg_name = (struct sockaddr *) &taddr_v6;\n      msg.msg_namelen = sizeof(taddr_v6);\n    }\n    else {\n      taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port);\n\n      msg.msg_name = (struct sockaddr *) &taddr_v4;\n      msg.msg_namelen = sizeof(taddr_v4);\n    }\n\n    union {\n      char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))];\n      struct cmsghdr alignment;\n    } cmbuf;\n    socklen_t cmbuflen = 0;\n\n    msg.msg_control = cmbuf.buf;\n    msg.msg_controllen = sizeof(cmbuf.buf);\n\n    auto pktinfo_cm = CMSG_FIRSTHDR(&msg);\n    if (send_info.source_address.is_v6()) {\n      struct in6_pktinfo pktInfo;\n\n      struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0);\n      pktInfo.ipi6_addr = saddr_v6.sin6_addr;\n      pktInfo.ipi6_ifindex = 0;\n\n      cmbuflen += CMSG_SPACE(sizeof(pktInfo));\n\n      pktinfo_cm->cmsg_level = IPPROTO_IPV6;\n      pktinfo_cm->cmsg_type = IPV6_PKTINFO;\n      pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo));\n      memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo));\n    }\n    else {\n      struct in_pktinfo pktInfo;\n\n      struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0);\n      pktInfo.ipi_spec_dst = saddr_v4.sin_addr;\n      pktInfo.ipi_ifindex = 0;\n\n      cmbuflen += CMSG_SPACE(sizeof(pktInfo));\n\n      pktinfo_cm->cmsg_level = IPPROTO_IP;\n      pktinfo_cm->cmsg_type = IP_PKTINFO;\n      pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo));\n      memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo));\n    }\n\n    struct iovec iovs[2] = {};\n    int iovlen = 0;\n    if (send_info.header) {\n      iovs[iovlen].iov_base = (void *) send_info.header;\n      iovs[iovlen].iov_len = send_info.header_size;\n      iovlen++;\n    }\n    iovs[iovlen].iov_base = (void *) send_info.payload;\n    iovs[iovlen].iov_len = send_info.payload_size;\n    iovlen++;\n\n    msg.msg_iov = iovs;\n    msg.msg_iovlen = iovlen;\n\n    msg.msg_controllen = cmbuflen;\n\n    auto bytes_sent = sendmsg(sockfd, &msg, 0);\n\n    // If there's no send buffer space, wait for some to be available\n    while (bytes_sent < 0 && errno == EAGAIN) {\n      struct pollfd pfd;\n\n      pfd.fd = sockfd;\n      pfd.events = POLLOUT;\n\n      if (poll(&pfd, 1, -1) != 1) {\n        BOOST_LOG(warning) << \"poll() failed: \"sv << errno;\n        break;\n      }\n\n      // Try to send again\n      bytes_sent = sendmsg(sockfd, &msg, 0);\n    }\n\n    if (bytes_sent < 0) {\n      BOOST_LOG(warning) << \"sendmsg() failed: \"sv << errno;\n      return false;\n    }\n\n    return true;\n  }\n\n  // We can't track QoS state separately for each destination on this OS,\n  // so we keep a ref count to only disable QoS options when all clients\n  // are disconnected.\n  static std::atomic<int> qos_ref_count = 0;\n\n  class qos_t: public deinit_t {\n  public:\n    qos_t(int sockfd, std::vector<std::tuple<int, int, int>> options):\n        sockfd(sockfd), options(options) {\n      qos_ref_count++;\n    }\n\n    virtual ~qos_t() {\n      if (--qos_ref_count == 0) {\n        for (const auto &tuple : options) {\n          auto reset_val = std::get<2>(tuple);\n          if (setsockopt(sockfd, std::get<0>(tuple), std::get<1>(tuple), &reset_val, sizeof(reset_val)) < 0) {\n            BOOST_LOG(warning) << \"Failed to reset option: \"sv << errno;\n          }\n        }\n      }\n    }\n\n  private:\n    int sockfd;\n    std::vector<std::tuple<int, int, int>> options;\n  };\n\n  /**\n   * @brief Enables QoS on the given socket for traffic to the specified destination.\n   * @param native_socket The native socket handle.\n   * @param address The destination address for traffic sent on this socket.\n   * @param port The destination port for traffic sent on this socket.\n   * @param data_type The type of traffic sent on this socket.\n   * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic.\n   */\n  std::unique_ptr<deinit_t>\n  enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) {\n    int sockfd = (int) native_socket;\n    std::vector<std::tuple<int, int, int>> reset_options;\n\n    if (dscp_tagging) {\n      int level;\n      int option;\n\n      // With dual-stack sockets, Linux uses IPV6_TCLASS for IPv6 traffic\n      // and IP_TOS for IPv4 traffic.\n      if (address.is_v6() && !address.to_v6().is_v4_mapped()) {\n        level = SOL_IPV6;\n        option = IPV6_TCLASS;\n      }\n      else {\n        level = SOL_IP;\n        option = IP_TOS;\n      }\n\n      // The specific DSCP values here are chosen to be consistent with Windows,\n      // except that we use CS6 instead of CS7 for audio traffic.\n      int dscp = 0;\n      switch (data_type) {\n        case qos_data_type_e::video:\n          dscp = 40;\n          break;\n        case qos_data_type_e::audio:\n          dscp = 48;\n          break;\n        default:\n          BOOST_LOG(error) << \"Unknown traffic type: \"sv << (int) data_type;\n          break;\n      }\n\n      if (dscp) {\n        // Shift to put the DSCP value in the correct position in the TOS field\n        dscp <<= 2;\n\n        if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) {\n          // Reset TOS to -1 when QoS is disabled\n          reset_options.emplace_back(std::make_tuple(level, option, -1));\n        }\n        else {\n          BOOST_LOG(error) << \"Failed to set TOS/TCLASS: \"sv << errno;\n        }\n      }\n    }\n\n    // We can use SO_PRIORITY to set outgoing traffic priority without DSCP tagging.\n    //\n    // NB: We set this after IP_TOS/IPV6_TCLASS since setting TOS value seems to\n    // reset SO_PRIORITY back to 0.\n    //\n    // 6 is the highest priority that can be used without SYS_CAP_ADMIN.\n    int priority = data_type == qos_data_type_e::audio ? 6 : 5;\n    if (setsockopt(sockfd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority)) == 0) {\n      // Reset SO_PRIORITY to 0 when QoS is disabled\n      reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_PRIORITY, 0));\n    }\n    else {\n      BOOST_LOG(error) << \"Failed to set SO_PRIORITY: \"sv << errno;\n    }\n\n    return std::make_unique<qos_t>(sockfd, reset_options);\n  }\n\n  std::string\n  get_host_name() {\n    try {\n      return boost::asio::ip::host_name();\n    }\n    catch (boost::system::system_error &err) {\n      BOOST_LOG(error) << \"Failed to get hostname: \"sv << err.what();\n      return \"Sunshine\"s;\n    }\n  }\n\n  namespace source {\n    enum source_e : std::size_t {\n#ifdef SUNSHINE_BUILD_CUDA\n      NVFBC,  ///< NvFBC\n#endif\n#ifdef SUNSHINE_BUILD_WAYLAND\n      WAYLAND,  ///< Wayland\n#endif\n#ifdef SUNSHINE_BUILD_DRM\n      KMS,  ///< KMS\n#endif\n#ifdef SUNSHINE_BUILD_X11\n      X11,  ///< X11\n#endif\n      MAX_FLAGS  ///< The maximum number of flags\n    };\n  }  // namespace source\n\n  static std::bitset<source::MAX_FLAGS> sources;\n\n#ifdef SUNSHINE_BUILD_CUDA\n  std::vector<std::string>\n  nvfbc_display_names();\n  std::shared_ptr<display_t>\n  nvfbc_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config);\n\n  bool\n  verify_nvfbc() {\n    return !nvfbc_display_names().empty();\n  }\n#endif\n\n#ifdef SUNSHINE_BUILD_WAYLAND\n  std::vector<std::string>\n  wl_display_names();\n  std::shared_ptr<display_t>\n  wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config);\n\n  bool\n  verify_wl() {\n    return window_system == window_system_e::WAYLAND && !wl_display_names().empty();\n  }\n#endif\n\n#ifdef SUNSHINE_BUILD_DRM\n  std::vector<std::string>\n  kms_display_names(mem_type_e hwdevice_type);\n  std::shared_ptr<display_t>\n  kms_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config);\n\n  bool\n  verify_kms() {\n    return !kms_display_names(mem_type_e::unknown).empty();\n  }\n#endif\n\n#ifdef SUNSHINE_BUILD_X11\n  std::vector<std::string>\n  x11_display_names();\n  std::shared_ptr<display_t>\n  x11_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config);\n\n  bool\n  verify_x11() {\n    return window_system == window_system_e::X11 && !x11_display_names().empty();\n  }\n#endif\n\n  std::vector<std::string>\n  display_names(mem_type_e hwdevice_type) {\n#ifdef SUNSHINE_BUILD_CUDA\n    // display using NvFBC only supports mem_type_e::cuda\n    if (sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) return nvfbc_display_names();\n#endif\n#ifdef SUNSHINE_BUILD_WAYLAND\n    if (sources[source::WAYLAND]) return wl_display_names();\n#endif\n#ifdef SUNSHINE_BUILD_DRM\n    if (sources[source::KMS]) return kms_display_names(hwdevice_type);\n#endif\n#ifdef SUNSHINE_BUILD_X11\n    if (sources[source::X11]) return x11_display_names();\n#endif\n    return {};\n  }\n\n  /**\n   * @brief Returns if GPUs/drivers have changed since the last call to this function.\n   * @return `true` if a change has occurred or if it is unknown whether a change occurred.\n   */\n  bool\n  needs_encoder_reenumeration() {\n    // We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on Linux.\n    return true;\n  }\n\n  std::shared_ptr<display_t>\n  display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {\n#ifdef SUNSHINE_BUILD_CUDA\n    if (sources[source::NVFBC] && hwdevice_type == mem_type_e::cuda) {\n      BOOST_LOG(info) << \"Screencasting with NvFBC\"sv;\n      return nvfbc_display(hwdevice_type, display_name, config);\n    }\n#endif\n#ifdef SUNSHINE_BUILD_WAYLAND\n    if (sources[source::WAYLAND]) {\n      BOOST_LOG(info) << \"Screencasting with Wayland's protocol\"sv;\n      return wl_display(hwdevice_type, display_name, config);\n    }\n#endif\n#ifdef SUNSHINE_BUILD_DRM\n    if (sources[source::KMS]) {\n      BOOST_LOG(info) << \"Screencasting with KMS\"sv;\n      return kms_display(hwdevice_type, display_name, config);\n    }\n#endif\n#ifdef SUNSHINE_BUILD_X11\n    if (sources[source::X11]) {\n      BOOST_LOG(info) << \"Screencasting with X11\"sv;\n      return x11_display(hwdevice_type, display_name, config);\n    }\n#endif\n\n    return nullptr;\n  }\n\n  std::unique_ptr<deinit_t>\n  init() {\n    // enable low latency mode for AMD\n    // https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/30039\n    set_env(\"AMD_DEBUG\", \"lowlatency\");\n\n    // These are allowed to fail.\n    gbm::init();\n\n    window_system = window_system_e::NONE;\n#ifdef SUNSHINE_BUILD_WAYLAND\n    if (std::getenv(\"WAYLAND_DISPLAY\")) {\n      window_system = window_system_e::WAYLAND;\n    }\n#endif\n#if defined(SUNSHINE_BUILD_X11) || defined(SUNSHINE_BUILD_CUDA)\n    if (std::getenv(\"DISPLAY\") && window_system != window_system_e::WAYLAND) {\n      if (std::getenv(\"WAYLAND_DISPLAY\")) {\n        BOOST_LOG(warning) << \"Wayland detected, yet sunshine will use X11 for screencasting, screencasting will only work on XWayland applications\"sv;\n      }\n\n      window_system = window_system_e::X11;\n    }\n#endif\n\n#ifdef SUNSHINE_BUILD_CUDA\n    if ((config::video.capture.empty() && sources.none()) || config::video.capture == \"nvfbc\") {\n      if (verify_nvfbc()) {\n        sources[source::NVFBC] = true;\n      }\n    }\n#endif\n#ifdef SUNSHINE_BUILD_WAYLAND\n    if ((config::video.capture.empty() && sources.none()) || config::video.capture == \"wlr\") {\n      if (verify_wl()) {\n        sources[source::WAYLAND] = true;\n      }\n    }\n#endif\n#ifdef SUNSHINE_BUILD_DRM\n    if ((config::video.capture.empty() && sources.none()) || config::video.capture == \"kms\") {\n      if (verify_kms()) {\n        sources[source::KMS] = true;\n      }\n    }\n#endif\n#ifdef SUNSHINE_BUILD_X11\n    // We enumerate this capture backend regardless of other suitable sources,\n    // since it may be needed as a NvFBC fallback for software encoding on X11.\n    if (config::video.capture.empty() || config::video.capture == \"x11\") {\n      if (verify_x11()) {\n        sources[source::X11] = true;\n      }\n    }\n#endif\n\n    if (sources.none()) {\n      BOOST_LOG(error) << \"Unable to initialize capture method\"sv;\n      return nullptr;\n    }\n\n    if (!gladLoaderLoadEGL(EGL_NO_DISPLAY) || !eglGetPlatformDisplay) {\n      BOOST_LOG(warning) << \"Couldn't load EGL library\"sv;\n    }\n\n    return std::make_unique<deinit_t>();\n  }\n\n  class linux_high_precision_timer: public high_precision_timer {\n  public:\n    void\n    sleep_for(const std::chrono::nanoseconds &duration) override {\n      std::this_thread::sleep_for(duration);\n    }\n\n    operator bool() override {\n      return true;\n    }\n  };\n\n  std::unique_ptr<high_precision_timer>\n  create_high_precision_timer() {\n    return std::make_unique<linux_high_precision_timer>();\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/linux/misc.h",
    "content": "/**\n * @file src/platform/linux/misc.h\n * @brief Miscellaneous declarations for Linux.\n */\n#pragma once\n\n#include <unistd.h>\n#include <vector>\n\n#include \"src/utility.h\"\n\nKITTY_USING_MOVE_T(file_t, int, -1, {\n  if (el >= 0) {\n    close(el);\n  }\n});\n\nenum class window_system_e {\n  NONE,  ///< No window system\n  X11,  ///< X11\n  WAYLAND,  ///< Wayland\n};\n\nextern window_system_e window_system;\n\nnamespace dyn {\n  typedef void (*apiproc)(void);\n\n  int\n  load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict = true);\n  void *\n  handle(const std::vector<const char *> &libs);\n\n}  // namespace dyn\n"
  },
  {
    "path": "src/platform/linux/publish.cpp",
    "content": "/**\n * @file src/platform/linux/publish.cpp\n * @brief Definitions for publishing services on Linux.\n * @note Adapted from https://www.avahi.org/doxygen/html/client-publish-service_8c-example.html\n */\n#include <thread>\n\n#include \"misc.h\"\n#include \"src/logging.h\"\n#include \"src/network.h\"\n#include \"src/nvhttp.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\nusing namespace std::literals;\n\nnamespace avahi {\n\n  /**\n   * @brief Error codes used by avahi.\n   */\n  enum err_e {\n    OK = 0,  ///< OK\n    ERR_FAILURE = -1,  ///< Generic error code\n    ERR_BAD_STATE = -2,  ///< Object was in a bad state\n    ERR_INVALID_HOST_NAME = -3,  ///< Invalid host name\n    ERR_INVALID_DOMAIN_NAME = -4,  ///< Invalid domain name\n    ERR_NO_NETWORK = -5,  ///< No suitable network protocol available\n    ERR_INVALID_TTL = -6,  ///< Invalid DNS TTL\n    ERR_IS_PATTERN = -7,  ///< RR key is pattern\n    ERR_COLLISION = -8,  ///< Name collision\n    ERR_INVALID_RECORD = -9,  ///< Invalid RR\n\n    ERR_INVALID_SERVICE_NAME = -10,  ///< Invalid service name\n    ERR_INVALID_SERVICE_TYPE = -11,  ///< Invalid service type\n    ERR_INVALID_PORT = -12,  ///< Invalid port number\n    ERR_INVALID_KEY = -13,  ///< Invalid key\n    ERR_INVALID_ADDRESS = -14,  ///< Invalid address\n    ERR_TIMEOUT = -15,  ///< Timeout reached\n    ERR_TOO_MANY_CLIENTS = -16,  ///< Too many clients\n    ERR_TOO_MANY_OBJECTS = -17,  ///< Too many objects\n    ERR_TOO_MANY_ENTRIES = -18,  ///< Too many entries\n    ERR_OS = -19,  ///< OS error\n\n    ERR_ACCESS_DENIED = -20,  ///< Access denied\n    ERR_INVALID_OPERATION = -21,  ///< Invalid operation\n    ERR_DBUS_ERROR = -22,  ///< An unexpected D-Bus error occurred\n    ERR_DISCONNECTED = -23,  ///< Daemon connection failed\n    ERR_NO_MEMORY = -24,  ///< Memory exhausted\n    ERR_INVALID_OBJECT = -25,  ///< The object passed to this function was invalid\n    ERR_NO_DAEMON = -26,  ///< Daemon not running\n    ERR_INVALID_INTERFACE = -27,  ///< Invalid interface\n    ERR_INVALID_PROTOCOL = -28,  ///< Invalid protocol\n    ERR_INVALID_FLAGS = -29,  ///< Invalid flags\n\n    ERR_NOT_FOUND = -30,  ///< Not found\n    ERR_INVALID_CONFIG = -31,  ///< Configuration error\n    ERR_VERSION_MISMATCH = -32,  ///< Version mismatch\n    ERR_INVALID_SERVICE_SUBTYPE = -33,  ///< Invalid service subtype\n    ERR_INVALID_PACKET = -34,  ///< Invalid packet\n    ERR_INVALID_DNS_ERROR = -35,  ///< Invalid DNS return code\n    ERR_DNS_FORMERR = -36,  ///< DNS Error: Form error\n    ERR_DNS_SERVFAIL = -37,  ///< DNS Error: Server Failure\n    ERR_DNS_NXDOMAIN = -38,  ///< DNS Error: No such domain\n    ERR_DNS_NOTIMP = -39,  ///< DNS Error: Not implemented\n\n    ERR_DNS_REFUSED = -40,  ///< DNS Error: Operation refused\n    ERR_DNS_YXDOMAIN = -41,  ///< TODO\n    ERR_DNS_YXRRSET = -42,  ///< TODO\n    ERR_DNS_NXRRSET = -43,  ///< TODO\n    ERR_DNS_NOTAUTH = -44,  ///< DNS Error: Not authorized\n    ERR_DNS_NOTZONE = -45,  ///< TODO\n    ERR_INVALID_RDATA = -46,  ///< Invalid RDATA\n    ERR_INVALID_DNS_CLASS = -47,  ///< Invalid DNS class\n    ERR_INVALID_DNS_TYPE = -48,  ///< Invalid DNS type\n    ERR_NOT_SUPPORTED = -49,  ///< Not supported\n\n    ERR_NOT_PERMITTED = -50,  ///< Operation not permitted\n    ERR_INVALID_ARGUMENT = -51,  ///< Invalid argument\n    ERR_IS_EMPTY = -52,  ///< Is empty\n    ERR_NO_CHANGE = -53,  ///< The requested operation is invalid because it is redundant\n\n    ERR_MAX = -54  ///< TODO\n  };\n\n  constexpr auto IF_UNSPEC = -1;\n  enum proto {\n    PROTO_INET = 0,  ///< IPv4\n    PROTO_INET6 = 1,  ///< IPv6\n    PROTO_UNSPEC = -1  ///< Unspecified/all protocol(s)\n  };\n\n  enum ServerState {\n    SERVER_INVALID,  ///< Invalid state (initial)\n    SERVER_REGISTERING,  ///< Host RRs are being registered\n    SERVER_RUNNING,  ///< All host RRs have been established\n    SERVER_COLLISION,  ///< There is a collision with a host RR. All host RRs have been withdrawn, the user should set a new host name via avahi_server_set_host_name()\n    SERVER_FAILURE  ///< Some fatal failure happened, the server is unable to proceed\n  };\n\n  enum ClientState {\n    CLIENT_S_REGISTERING = SERVER_REGISTERING,  ///< Server state: REGISTERING\n    CLIENT_S_RUNNING = SERVER_RUNNING,  ///< Server state: RUNNING\n    CLIENT_S_COLLISION = SERVER_COLLISION,  ///< Server state: COLLISION\n    CLIENT_FAILURE = 100,  ///< Some kind of error happened on the client side\n    CLIENT_CONNECTING = 101  ///< We're still connecting. This state is only entered when AVAHI_CLIENT_NO_FAIL has been passed to avahi_client_new() and the daemon is not yet available.\n  };\n\n  enum EntryGroupState {\n    ENTRY_GROUP_UNCOMMITED,  ///< The group has not yet been committed, the user must still call avahi_entry_group_commit()\n    ENTRY_GROUP_REGISTERING,  ///< The entries of the group are currently being registered\n    ENTRY_GROUP_ESTABLISHED,  ///< The entries have successfully been established\n    ENTRY_GROUP_COLLISION,  ///< A name collision for one of the entries in the group has been detected, the entries have been withdrawn\n    ENTRY_GROUP_FAILURE  ///< Some kind of failure happened, the entries have been withdrawn\n  };\n\n  enum ClientFlags {\n    CLIENT_IGNORE_USER_CONFIG = 1,  ///< Don't read user configuration\n    CLIENT_NO_FAIL = 2  ///< Don't fail if the daemon is not available when avahi_client_new() is called, instead enter CLIENT_CONNECTING state and wait for the daemon to appear\n  };\n\n  /**\n   * @brief Flags for publishing functions.\n   */\n  enum PublishFlags {\n    PUBLISH_UNIQUE = 1,  ///< For raw records: The RRset is intended to be unique\n    PUBLISH_NO_PROBE = 2,  ///< For raw records: Though the RRset is intended to be unique no probes shall be sent\n    PUBLISH_NO_ANNOUNCE = 4,  ///< For raw records: Do not announce this RR to other hosts\n    PUBLISH_ALLOW_MULTIPLE = 8,  ///< For raw records: Allow multiple local records of this type, even if they are intended to be unique\n    PUBLISH_NO_REVERSE = 16,  ///< For address records: don't create a reverse (PTR) entry\n    PUBLISH_NO_COOKIE = 32,  ///< For service records: do not implicitly add the local service cookie to TXT data\n    PUBLISH_UPDATE = 64,  ///< Update existing records instead of adding new ones\n    PUBLISH_USE_WIDE_AREA = 128,  ///< Register the record using wide area DNS (i.e. unicast DNS update)\n    PUBLISH_USE_MULTICAST = 256  ///< Register the record using multicast DNS\n  };\n\n  using IfIndex = int;\n  using Protocol = int;\n\n  struct EntryGroup;\n  struct Poll;\n  struct SimplePoll;\n  struct Client;\n\n  typedef void (*ClientCallback)(Client *, ClientState, void *userdata);\n  typedef void (*EntryGroupCallback)(EntryGroup *g, EntryGroupState state, void *userdata);\n\n  typedef void (*free_fn)(void *);\n\n  typedef Client *(*client_new_fn)(const Poll *poll_api, ClientFlags flags, ClientCallback callback, void *userdata, int *error);\n  typedef void (*client_free_fn)(Client *);\n  typedef char *(*alternative_service_name_fn)(char *);\n\n  typedef Client *(*entry_group_get_client_fn)(EntryGroup *);\n\n  typedef EntryGroup *(*entry_group_new_fn)(Client *, EntryGroupCallback, void *userdata);\n  typedef int (*entry_group_add_service_fn)(\n    EntryGroup *group,\n    IfIndex interface,\n    Protocol protocol,\n    PublishFlags flags,\n    const char *name,\n    const char *type,\n    const char *domain,\n    const char *host,\n    uint16_t port,\n    ...);\n\n  typedef int (*entry_group_is_empty_fn)(EntryGroup *);\n  typedef int (*entry_group_reset_fn)(EntryGroup *);\n  typedef int (*entry_group_commit_fn)(EntryGroup *);\n\n  typedef char *(*strdup_fn)(const char *);\n  typedef char *(*strerror_fn)(int);\n  typedef int (*client_errno_fn)(Client *);\n\n  typedef Poll *(*simple_poll_get_fn)(SimplePoll *);\n  typedef int (*simple_poll_loop_fn)(SimplePoll *);\n  typedef void (*simple_poll_quit_fn)(SimplePoll *);\n  typedef SimplePoll *(*simple_poll_new_fn)();\n  typedef void (*simple_poll_free_fn)(SimplePoll *);\n\n  free_fn free;\n  client_new_fn client_new;\n  client_free_fn client_free;\n  alternative_service_name_fn alternative_service_name;\n  entry_group_get_client_fn entry_group_get_client;\n  entry_group_new_fn entry_group_new;\n  entry_group_add_service_fn entry_group_add_service;\n  entry_group_is_empty_fn entry_group_is_empty;\n  entry_group_reset_fn entry_group_reset;\n  entry_group_commit_fn entry_group_commit;\n  strdup_fn strdup;\n  strerror_fn strerror;\n  client_errno_fn client_errno;\n  simple_poll_get_fn simple_poll_get;\n  simple_poll_loop_fn simple_poll_loop;\n  simple_poll_quit_fn simple_poll_quit;\n  simple_poll_new_fn simple_poll_new;\n  simple_poll_free_fn simple_poll_free;\n\n  int\n  init_common() {\n    static void *handle { nullptr };\n    static bool funcs_loaded = false;\n\n    if (funcs_loaded) return 0;\n\n    if (!handle) {\n      handle = dyn::handle({ \"libavahi-common.so.3\", \"libavahi-common.so\" });\n      if (!handle) {\n        return -1;\n      }\n    }\n\n    std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {\n      { (dyn::apiproc *) &alternative_service_name, \"avahi_alternative_service_name\" },\n      { (dyn::apiproc *) &free, \"avahi_free\" },\n      { (dyn::apiproc *) &strdup, \"avahi_strdup\" },\n      { (dyn::apiproc *) &strerror, \"avahi_strerror\" },\n      { (dyn::apiproc *) &simple_poll_get, \"avahi_simple_poll_get\" },\n      { (dyn::apiproc *) &simple_poll_loop, \"avahi_simple_poll_loop\" },\n      { (dyn::apiproc *) &simple_poll_quit, \"avahi_simple_poll_quit\" },\n      { (dyn::apiproc *) &simple_poll_new, \"avahi_simple_poll_new\" },\n      { (dyn::apiproc *) &simple_poll_free, \"avahi_simple_poll_free\" },\n    };\n\n    if (dyn::load(handle, funcs)) {\n      return -1;\n    }\n\n    funcs_loaded = true;\n    return 0;\n  }\n\n  int\n  init_client() {\n    if (init_common()) {\n      return -1;\n    }\n\n    static void *handle { nullptr };\n    static bool funcs_loaded = false;\n\n    if (funcs_loaded) return 0;\n\n    if (!handle) {\n      handle = dyn::handle({ \"libavahi-client.so.3\", \"libavahi-client.so\" });\n      if (!handle) {\n        return -1;\n      }\n    }\n\n    std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {\n      { (dyn::apiproc *) &client_new, \"avahi_client_new\" },\n      { (dyn::apiproc *) &client_free, \"avahi_client_free\" },\n      { (dyn::apiproc *) &entry_group_get_client, \"avahi_entry_group_get_client\" },\n      { (dyn::apiproc *) &entry_group_new, \"avahi_entry_group_new\" },\n      { (dyn::apiproc *) &entry_group_add_service, \"avahi_entry_group_add_service\" },\n      { (dyn::apiproc *) &entry_group_is_empty, \"avahi_entry_group_is_empty\" },\n      { (dyn::apiproc *) &entry_group_reset, \"avahi_entry_group_reset\" },\n      { (dyn::apiproc *) &entry_group_commit, \"avahi_entry_group_commit\" },\n      { (dyn::apiproc *) &client_errno, \"avahi_client_errno\" },\n    };\n\n    if (dyn::load(handle, funcs)) {\n      return -1;\n    }\n\n    funcs_loaded = true;\n    return 0;\n  }\n}  // namespace avahi\n\nnamespace platf::publish {\n\n  template <class T>\n  void\n  free(T *p) {\n    avahi::free(p);\n  }\n\n  template <class T>\n  using ptr_t = util::safe_ptr<T, free<T>>;\n  using client_t = util::dyn_safe_ptr<avahi::Client, &avahi::client_free>;\n  using poll_t = util::dyn_safe_ptr<avahi::SimplePoll, &avahi::simple_poll_free>;\n\n  avahi::EntryGroup *group = nullptr;\n\n  poll_t poll;\n  client_t client;\n\n  ptr_t<char> name;\n\n  void\n  create_services(avahi::Client *c);\n\n  void\n  entry_group_callback(avahi::EntryGroup *g, avahi::EntryGroupState state, void *) {\n    group = g;\n\n    switch (state) {\n      case avahi::ENTRY_GROUP_ESTABLISHED:\n        BOOST_LOG(info) << \"Avahi service \" << name.get() << \" successfully established.\";\n        break;\n      case avahi::ENTRY_GROUP_COLLISION:\n        name.reset(avahi::alternative_service_name(name.get()));\n\n        BOOST_LOG(info) << \"Avahi service name collision, renaming service to \" << name.get();\n\n        create_services(avahi::entry_group_get_client(g));\n        break;\n      case avahi::ENTRY_GROUP_FAILURE:\n        BOOST_LOG(error) << \"Avahi entry group failure: \" << avahi::strerror(avahi::client_errno(avahi::entry_group_get_client(g)));\n        avahi::simple_poll_quit(poll.get());\n        break;\n      case avahi::ENTRY_GROUP_UNCOMMITED:\n      case avahi::ENTRY_GROUP_REGISTERING:;\n    }\n  }\n\n  void\n  create_services(avahi::Client *c) {\n    int ret;\n\n    auto fg = util::fail_guard([]() {\n      avahi::simple_poll_quit(poll.get());\n    });\n\n    if (!group) {\n      if (!(group = avahi::entry_group_new(c, entry_group_callback, nullptr))) {\n        BOOST_LOG(error) << \"avahi::entry_group_new() failed: \"sv << avahi::strerror(avahi::client_errno(c));\n        return;\n      }\n    }\n\n    if (avahi::entry_group_is_empty(group)) {\n      BOOST_LOG(info) << \"Adding avahi service \"sv << name.get();\n\n      ret = avahi::entry_group_add_service(\n        group,\n        avahi::IF_UNSPEC, avahi::PROTO_UNSPEC,\n        avahi::PublishFlags(0),\n        name.get(),\n        SERVICE_TYPE,\n        nullptr, nullptr,\n        net::map_port(nvhttp::PORT_HTTP),\n        nullptr);\n\n      if (ret < 0) {\n        if (ret == avahi::ERR_COLLISION) {\n          // A service name collision with a local service happened. Let's pick a new name\n          name.reset(avahi::alternative_service_name(name.get()));\n          BOOST_LOG(info) << \"Service name collision, renaming service to \"sv << name.get();\n\n          avahi::entry_group_reset(group);\n\n          create_services(c);\n\n          fg.disable();\n          return;\n        }\n\n        BOOST_LOG(error) << \"Failed to add \"sv << SERVICE_TYPE << \" service: \"sv << avahi::strerror(ret);\n        return;\n      }\n\n      ret = avahi::entry_group_commit(group);\n      if (ret < 0) {\n        BOOST_LOG(error) << \"Failed to commit entry group: \"sv << avahi::strerror(ret);\n        return;\n      }\n    }\n\n    fg.disable();\n  }\n\n  void\n  client_callback(avahi::Client *c, avahi::ClientState state, void *) {\n    switch (state) {\n      case avahi::CLIENT_S_RUNNING:\n        create_services(c);\n        break;\n      case avahi::CLIENT_FAILURE:\n        BOOST_LOG(error) << \"Client failure: \"sv << avahi::strerror(avahi::client_errno(c));\n        avahi::simple_poll_quit(poll.get());\n        break;\n      case avahi::CLIENT_S_COLLISION:\n      case avahi::CLIENT_S_REGISTERING:\n        if (group)\n          avahi::entry_group_reset(group);\n        break;\n      case avahi::CLIENT_CONNECTING:;\n    }\n  }\n\n  class deinit_t: public ::platf::deinit_t {\n  public:\n    std::thread poll_thread;\n\n    deinit_t(std::thread poll_thread):\n        poll_thread { std::move(poll_thread) } {}\n\n    ~deinit_t() override {\n      if (avahi::simple_poll_quit && poll) {\n        avahi::simple_poll_quit(poll.get());\n      }\n\n      if (poll_thread.joinable()) {\n        poll_thread.join();\n      }\n    }\n  };\n\n  [[nodiscard]] std::unique_ptr<::platf::deinit_t>\n  start() {\n    if (avahi::init_client()) {\n      return nullptr;\n    }\n\n    int avhi_error;\n\n    poll.reset(avahi::simple_poll_new());\n    if (!poll) {\n      BOOST_LOG(error) << \"Failed to create simple poll object.\"sv;\n      return nullptr;\n    }\n\n    auto instance_name = net::mdns_instance_name(platf::get_host_name());\n    name.reset(avahi::strdup(instance_name.c_str()));\n\n    client.reset(\n      avahi::client_new(avahi::simple_poll_get(poll.get()), avahi::ClientFlags(0), client_callback, nullptr, &avhi_error));\n\n    if (!client) {\n      BOOST_LOG(error) << \"Failed to create client: \"sv << avahi::strerror(avhi_error);\n      return nullptr;\n    }\n\n    return std::make_unique<deinit_t>(std::thread { avahi::simple_poll_loop, poll.get() });\n  }\n}  // namespace platf::publish\n"
  },
  {
    "path": "src/platform/linux/vaapi.cpp",
    "content": "/**\n * @file src/platform/linux/vaapi.cpp\n * @brief Definitions for VA-API hardware accelerated capture.\n */\n#include <sstream>\n#include <string>\n\n#include <fcntl.h>\n\nextern \"C\" {\n#include <libavcodec/avcodec.h>\n#include <va/va.h>\n#include <va/va_drm.h>\n#if !VA_CHECK_VERSION(1, 9, 0)\n// vaSyncBuffer stub allows Sunshine built against libva <2.9.0 to link against ffmpeg on libva 2.9.0 or later\nVAStatus\nvaSyncBuffer(\n  VADisplay dpy,\n  VABufferID buf_id,\n  uint64_t timeout_ns) {\n  return VA_STATUS_ERROR_UNIMPLEMENTED;\n}\n#endif\n#if !VA_CHECK_VERSION(1, 21, 0)\n  // vaMapBuffer2 stub allows Sunshine built against libva <2.21.0 to link against ffmpeg on libva 2.21.0 or later\n  VAStatus\n    vaMapBuffer2(\n      VADisplay dpy,\n      VABufferID buf_id,\n      void **pbuf,\n      uint32_t flags\n    ) {\n    return vaMapBuffer(dpy, buf_id, pbuf);\n  }\n#endif\n}\n\n#include \"graphics.h\"\n#include \"misc.h\"\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n#include \"src/video.h\"\n\nusing namespace std::literals;\n\nextern \"C\" struct AVBufferRef;\n\nnamespace va {\n  constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000;\n  constexpr auto EXPORT_SURFACE_WRITE_ONLY = 0x0002;\n  constexpr auto EXPORT_SURFACE_SEPARATE_LAYERS = 0x0004;\n\n  using VADisplay = void *;\n  using VAStatus = int;\n  using VAGenericID = unsigned int;\n  using VASurfaceID = VAGenericID;\n\n  struct DRMPRIMESurfaceDescriptor {\n    // VA Pixel format fourcc of the whole surface (VA_FOURCC_*).\n    uint32_t fourcc;\n\n    uint32_t width;\n    uint32_t height;\n\n    // Number of distinct DRM objects making up the surface.\n    uint32_t num_objects;\n\n    struct {\n      // DRM PRIME file descriptor for this object.\n      // Needs to be closed manually\n      int fd;\n\n      // Total size of this object (may include regions which are not part of the surface)\n      uint32_t size;\n      // Format modifier applied to this object, not sure what that means\n      uint64_t drm_format_modifier;\n    } objects[4];\n\n    // Number of layers making up the surface.\n    uint32_t num_layers;\n    struct {\n      // DRM format fourcc of this layer (DRM_FOURCC_*).\n      uint32_t drm_format;\n\n      // Number of planes in this layer.\n      uint32_t num_planes;\n\n      // references objects --> DRMPRIMESurfaceDescriptor.objects[object_index[0]]\n      uint32_t object_index[4];\n\n      // Offset within the object of each plane.\n      uint32_t offset[4];\n\n      // Pitch of each plane.\n      uint32_t pitch[4];\n    } layers[4];\n  };\n\n  using display_t = util::safe_ptr_v2<void, VAStatus, vaTerminate>;\n\n  int\n  vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf);\n\n  class va_t: public platf::avcodec_encode_device_t {\n  public:\n    int\n    init(int in_width, int in_height, file_t &&render_device) {\n      file = std::move(render_device);\n\n      if (!gbm::create_device) {\n        BOOST_LOG(warning) << \"libgbm not initialized\"sv;\n        return -1;\n      }\n\n      this->data = (void *) vaapi_init_avcodec_hardware_input_buffer;\n\n      gbm.reset(gbm::create_device(file.el));\n      if (!gbm) {\n        char string[1024];\n        BOOST_LOG(error) << \"Couldn't create GBM device: [\"sv << strerror_r(errno, string, sizeof(string)) << ']';\n        return -1;\n      }\n\n      display = egl::make_display(gbm.get());\n      if (!display) {\n        return -1;\n      }\n\n      auto ctx_opt = egl::make_ctx(display.get());\n      if (!ctx_opt) {\n        return -1;\n      }\n\n      ctx = std::move(*ctx_opt);\n\n      width = in_width;\n      height = in_height;\n\n      return 0;\n    }\n\n    /**\n     * @brief Finds a supported VA entrypoint for the given VA profile.\n     * @param profile The profile to match.\n     * @return A valid encoding entrypoint or 0 on failure.\n     */\n    VAEntrypoint select_va_entrypoint(VAProfile profile) {\n      std::vector<VAEntrypoint> entrypoints(vaMaxNumEntrypoints(va_display));\n      int num_eps;\n      auto status = vaQueryConfigEntrypoints(va_display, profile, entrypoints.data(), &num_eps);\n      if (status != VA_STATUS_SUCCESS) {\n        BOOST_LOG(error) << \"Failed to query VA entrypoints: \"sv << vaErrorStr(status);\n        return (VAEntrypoint) 0;\n      }\n      entrypoints.resize(num_eps);\n\n      // Sorted in order of descending preference\n      VAEntrypoint ep_preferences[] = {\n        VAEntrypointEncSliceLP,\n        VAEntrypointEncSlice,\n        VAEntrypointEncPicture\n      };\n      for (auto ep_pref : ep_preferences) {\n        if (std::find(entrypoints.begin(), entrypoints.end(), ep_pref) != entrypoints.end()) {\n          return ep_pref;\n        }\n      }\n\n      return (VAEntrypoint) 0;\n    }\n\n    /**\n     * @brief Determines if a given VA profile is supported.\n     * @param profile The profile to match.\n     * @return Boolean value indicating if the profile is supported.\n     */\n    bool is_va_profile_supported(VAProfile profile) {\n      std::vector<VAProfile> profiles(vaMaxNumProfiles(va_display));\n      int num_profs;\n      auto status = vaQueryConfigProfiles(va_display, profiles.data(), &num_profs);\n      if (status != VA_STATUS_SUCCESS) {\n        BOOST_LOG(error) << \"Failed to query VA profiles: \"sv << vaErrorStr(status);\n        return false;\n      }\n      profiles.resize(num_profs);\n\n      return std::find(profiles.begin(), profiles.end(), profile) != profiles.end();\n    }\n\n    /**\n     * @brief Determines the matching VA profile for the codec configuration.\n     * @param ctx The FFmpeg codec context.\n     * @return The matching VA profile or `VAProfileNone` on failure.\n     */\n    VAProfile get_va_profile(AVCodecContext *ctx) {\n      if (ctx->codec_id == AV_CODEC_ID_H264) {\n        // There's no VAAPI profile for H.264 4:4:4\n        return VAProfileH264High;\n      } else if (ctx->codec_id == AV_CODEC_ID_HEVC) {\n        switch (ctx->profile) {\n          case AV_PROFILE_HEVC_REXT:\n            switch (av_pix_fmt_desc_get(ctx->sw_pix_fmt)->comp[0].depth) {\n              case 10:\n                return VAProfileHEVCMain444_10;\n              case 8:\n                return VAProfileHEVCMain444;\n            }\n            break;\n          case AV_PROFILE_HEVC_MAIN_10:\n            return VAProfileHEVCMain10;\n          case AV_PROFILE_HEVC_MAIN:\n            return VAProfileHEVCMain;\n        }\n      } else if (ctx->codec_id == AV_CODEC_ID_AV1) {\n        switch (ctx->profile) {\n          case AV_PROFILE_AV1_HIGH:\n            return VAProfileAV1Profile1;\n          case AV_PROFILE_AV1_MAIN:\n            return VAProfileAV1Profile0;\n        }\n      }\n\n      BOOST_LOG(error) << \"Unknown encoder profile: \"sv << ctx->profile;\n      return VAProfileNone;\n    }\n\n    void init_codec_options(AVCodecContext *ctx, AVDictionary **options) override {\n      auto va_profile = get_va_profile(ctx);\n      if (va_profile == VAProfileNone || !is_va_profile_supported(va_profile)) {\n        // Don't bother doing anything if the profile isn't supported\n        return;\n      }\n\n      auto va_entrypoint = select_va_entrypoint(va_profile);\n      if (va_entrypoint == 0) {\n        // It's possible that only decoding is supported for this profile\n        return;\n      }\n\n      auto vendor = vaQueryVendorString(va_display);\n      if (ctx->codec_id != AV_CODEC_ID_H264 || (vendor && !strstr(vendor, \"Intel\"))) {\n        ctx->rc_buffer_size = ctx->bit_rate * ctx->framerate.den / ctx->framerate.num;\n      }\n    }\n\n    int\n    set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override {\n      this->hwframe.reset(frame);\n      this->frame = frame;\n\n      if (!frame->buf[0]) {\n        if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) {\n          BOOST_LOG(error) << \"Couldn't get hwframe for VAAPI\"sv;\n          return -1;\n        }\n      }\n\n      va::DRMPRIMESurfaceDescriptor prime;\n      va::VASurfaceID surface = (std::uintptr_t) frame->data[3];\n      auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data;\n\n      auto status = vaExportSurfaceHandle(\n        this->va_display,\n        surface,\n        va::SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,\n        va::EXPORT_SURFACE_WRITE_ONLY | va::EXPORT_SURFACE_SEPARATE_LAYERS,\n        &prime);\n      if (status) {\n        BOOST_LOG(error) << \"Couldn't export va surface handle: [\"sv << (int) surface << \"]: \"sv << vaErrorStr(status);\n\n        return -1;\n      }\n\n      // Keep track of file descriptors\n      std::array<file_t, egl::nv12_img_t::num_fds> fds;\n      for (int x = 0; x < prime.num_objects; ++x) {\n        fds[x] = prime.objects[x].fd;\n      }\n\n      if (prime.num_layers != 2) {\n        BOOST_LOG(error) << \"Invalid layer count for VA surface: expected 2, got \"sv << prime.num_layers;\n        return -1;\n      }\n\n      egl::surface_descriptor_t sds[2] = {};\n      for (int plane = 0; plane < 2; ++plane) {\n        auto &sd = sds[plane];\n        auto &layer = prime.layers[plane];\n\n        sd.fourcc = layer.drm_format;\n\n        // UV plane is subsampled\n        sd.width = prime.width / (plane == 0 ? 1 : 2);\n        sd.height = prime.height / (plane == 0 ? 1 : 2);\n\n        // The modifier must be the same for all planes\n        sd.modifier = prime.objects[layer.object_index[0]].drm_format_modifier;\n\n        std::fill_n(sd.fds, 4, -1);\n        for (int x = 0; x < layer.num_planes; ++x) {\n          sd.fds[x] = prime.objects[layer.object_index[x]].fd;\n          sd.pitches[x] = layer.pitch[x];\n          sd.offsets[x] = layer.offset[x];\n        }\n      }\n\n      auto nv12_opt = egl::import_target(display.get(), std::move(fds), sds[0], sds[1]);\n      if (!nv12_opt) {\n        return -1;\n      }\n\n      auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, hw_frames_ctx->sw_format);\n      if (!sws_opt) {\n        return -1;\n      }\n\n      this->sws = std::move(*sws_opt);\n      this->nv12 = std::move(*nv12_opt);\n\n      return 0;\n    }\n\n    void\n    apply_colorspace() override {\n      sws.apply_colorspace(colorspace);\n    }\n\n    va::display_t::pointer va_display;\n    file_t file;\n\n    gbm::gbm_t gbm;\n    egl::display_t display;\n    egl::ctx_t ctx;\n\n    // This must be destroyed before display_t to ensure the GPU\n    // driver is still loaded when vaDestroySurfaces() is called.\n    frame_t hwframe;\n\n    egl::sws_t sws;\n    egl::nv12_t nv12;\n\n    int width, height;\n  };\n\n  class va_ram_t: public va_t {\n  public:\n    int\n    convert(platf::img_t &img) override {\n      sws.load_ram(img);\n\n      sws.convert(nv12->buf);\n      return 0;\n    }\n  };\n\n  class va_vram_t: public va_t {\n  public:\n    int\n    convert(platf::img_t &img) override {\n      auto &descriptor = (egl::img_descriptor_t &) img;\n\n      if (descriptor.sequence == 0) {\n        // For dummy images, use a blank RGB texture instead of importing a DMA-BUF\n        rgb = egl::create_blank(img);\n      }\n      else if (descriptor.sequence > sequence) {\n        sequence = descriptor.sequence;\n\n        rgb = egl::rgb_t {};\n\n        auto rgb_opt = egl::import_source(display.get(), descriptor.sd);\n\n        if (!rgb_opt) {\n          return -1;\n        }\n\n        rgb = std::move(*rgb_opt);\n      }\n\n      sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0]);\n\n      sws.convert(nv12->buf);\n      return 0;\n    }\n\n    int\n    init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) {\n      if (va_t::init(in_width, in_height, std::move(render_device))) {\n        return -1;\n      }\n\n      sequence = 0;\n\n      this->offset_x = offset_x;\n      this->offset_y = offset_y;\n\n      return 0;\n    }\n\n    std::uint64_t sequence;\n    egl::rgb_t rgb;\n\n    int offset_x, offset_y;\n  };\n\n  /**\n   * This is a private structure of FFmpeg, I need this to manually create\n   * a VAAPI hardware context\n   *\n   * xdisplay will not be used internally by FFmpeg\n   */\n  typedef struct VAAPIDevicePriv {\n    union {\n      void *xdisplay;\n      int fd;\n    } drm;\n    int drm_fd;\n  } VAAPIDevicePriv;\n\n  /**\n   * VAAPI connection details.\n   *\n   * Allocated as AVHWDeviceContext.hwctx\n   */\n  typedef struct AVVAAPIDeviceContext {\n    /**\n     * The VADisplay handle, to be filled by the user.\n     */\n    va::VADisplay display;\n    /**\n     * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(),\n     * with reference to a table of known drivers, unless the\n     * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present.  The user\n     * may need to refer to this field when performing any later\n     * operations using VAAPI with the same VADisplay.\n     */\n    unsigned int driver_quirks;\n  } AVVAAPIDeviceContext;\n\n  static void\n  __log(void *level, const char *msg) {\n    BOOST_LOG(*(boost::log::sources::severity_logger<int> *) level) << msg;\n  }\n\n  static void\n  vaapi_hwdevice_ctx_free(AVHWDeviceContext *ctx) {\n    auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx;\n    auto priv = (VAAPIDevicePriv *) ctx->user_opaque;\n\n    vaTerminate(hwctx->display);\n    close(priv->drm_fd);\n    av_freep(&priv);\n  }\n\n  int\n  vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *base, AVBufferRef **hw_device_buf) {\n    auto va = (va::va_t *) base;\n    auto fd = dup(va->file.el);\n\n    auto *priv = (VAAPIDevicePriv *) av_mallocz(sizeof(VAAPIDevicePriv));\n    priv->drm_fd = fd;\n\n    auto fg = util::fail_guard([fd, priv]() {\n      close(fd);\n      av_free(priv);\n    });\n\n    va::display_t display { vaGetDisplayDRM(fd) };\n    if (!display) {\n      auto render_device = config::video.adapter_name.empty() ? \"/dev/dri/renderD128\" : config::video.adapter_name.c_str();\n\n      BOOST_LOG(error) << \"Couldn't open a va display from DRM with device: \"sv << render_device;\n      return -1;\n    }\n\n    va->va_display = display.get();\n\n    vaSetErrorCallback(display.get(), __log, &error);\n    vaSetErrorCallback(display.get(), __log, &info);\n\n    int major, minor;\n    auto status = vaInitialize(display.get(), &major, &minor);\n    if (status) {\n      BOOST_LOG(error) << \"Couldn't initialize va display: \"sv << vaErrorStr(status);\n      return -1;\n    }\n\n    BOOST_LOG(info) << \"vaapi vendor: \"sv << vaQueryVendorString(display.get());\n\n    *hw_device_buf = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);\n    auto ctx = (AVHWDeviceContext *) (*hw_device_buf)->data;\n    auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx;\n\n    // Ownership of the VADisplay and DRM fd is now ours to manage via the free() function\n    hwctx->display = display.release();\n    ctx->user_opaque = priv;\n    ctx->free = vaapi_hwdevice_ctx_free;\n    fg.disable();\n\n    auto err = av_hwdevice_ctx_init(*hw_device_buf);\n    if (err) {\n      char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n      BOOST_LOG(error) << \"Failed to create FFMpeg hardware device context: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);\n\n      return err;\n    }\n\n    return 0;\n  }\n\n  static bool\n  query(display_t::pointer display, VAProfile profile) {\n    std::vector<VAEntrypoint> entrypoints;\n    entrypoints.resize(vaMaxNumEntrypoints(display));\n\n    int count;\n    auto status = vaQueryConfigEntrypoints(display, profile, entrypoints.data(), &count);\n    if (status) {\n      BOOST_LOG(error) << \"Couldn't query entrypoints: \"sv << vaErrorStr(status);\n      return false;\n    }\n    entrypoints.resize(count);\n\n    for (auto entrypoint : entrypoints) {\n      if (entrypoint == VAEntrypointEncSlice || entrypoint == VAEntrypointEncSliceLP) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  bool\n  validate(int fd) {\n    va::display_t display { vaGetDisplayDRM(fd) };\n    if (!display) {\n      char string[1024];\n\n      auto bytes = readlink((\"/proc/self/fd/\" + std::to_string(fd)).c_str(), string, sizeof(string));\n\n      std::string_view render_device { string, (std::size_t) bytes };\n\n      BOOST_LOG(error) << \"Couldn't open a va display from DRM with device: \"sv << render_device;\n      return false;\n    }\n\n    int major, minor;\n    auto status = vaInitialize(display.get(), &major, &minor);\n    if (status) {\n      BOOST_LOG(error) << \"Couldn't initialize va display: \"sv << vaErrorStr(status);\n      return false;\n    }\n\n    if (!query(display.get(), VAProfileH264Main)) {\n      return false;\n    }\n\n    if (video::active_hevc_mode > 1 && !query(display.get(), VAProfileHEVCMain)) {\n      return false;\n    }\n\n    if (video::active_hevc_mode > 2 && !query(display.get(), VAProfileHEVCMain10)) {\n      return false;\n    }\n\n    return true;\n  }\n\n  std::unique_ptr<platf::avcodec_encode_device_t>\n  make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) {\n    if (vram) {\n      auto egl = std::make_unique<va::va_vram_t>();\n      if (egl->init(width, height, std::move(card), offset_x, offset_y)) {\n        return nullptr;\n      }\n\n      return egl;\n    }\n\n    else {\n      auto egl = std::make_unique<va::va_ram_t>();\n      if (egl->init(width, height, std::move(card))) {\n        return nullptr;\n      }\n\n      return egl;\n    }\n  }\n\n  std::unique_ptr<platf::avcodec_encode_device_t>\n  make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram) {\n    auto render_device = config::video.adapter_name.empty() ? \"/dev/dri/renderD128\" : config::video.adapter_name.c_str();\n\n    file_t file = open(render_device, O_RDWR);\n    if (file.el < 0) {\n      char string[1024];\n      BOOST_LOG(error) << \"Couldn't open \"sv << render_device << \": \" << strerror_r(errno, string, sizeof(string));\n\n      return nullptr;\n    }\n\n    return make_avcodec_encode_device(width, height, std::move(file), offset_x, offset_y, vram);\n  }\n\n  std::unique_ptr<platf::avcodec_encode_device_t>\n  make_avcodec_encode_device(int width, int height, bool vram) {\n    return make_avcodec_encode_device(width, height, 0, 0, vram);\n  }\n}  // namespace va\n"
  },
  {
    "path": "src/platform/linux/vaapi.h",
    "content": "/**\n * @file src/platform/linux/vaapi.h\n * @brief Declarations for VA-API hardware accelerated capture.\n */\n#pragma once\n\n#include \"misc.h\"\n#include \"src/platform/common.h\"\n\nnamespace egl {\n  struct surface_descriptor_t;\n}\nnamespace va {\n  /**\n   * Width --> Width of the image\n   * Height --> Height of the image\n   * offset_x --> Horizontal offset of the image in the texture\n   * offset_y --> Vertical offset of the image in the texture\n   * file_t card --> The file descriptor of the render device used for encoding\n   */\n  std::unique_ptr<platf::avcodec_encode_device_t>\n  make_avcodec_encode_device(int width, int height, bool vram);\n  std::unique_ptr<platf::avcodec_encode_device_t>\n  make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram);\n  std::unique_ptr<platf::avcodec_encode_device_t>\n  make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram);\n\n  // Ensure the render device pointed to by fd is capable of encoding h264 with the hevc_mode configured\n  bool\n  validate(int fd);\n}  // namespace va\n"
  },
  {
    "path": "src/platform/linux/wayland.cpp",
    "content": "/**\n * @file src/platform/linux/wayland.cpp\n * @brief Definitions for Wayland capture.\n */\n#include <poll.h>\n#include <wayland-client.h>\n#include <wayland-util.h>\n\n#include <cstdlib>\n\n#include \"graphics.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/round_robin.h\"\n#include \"src/utility.h\"\n#include \"wayland.h\"\n\nextern const wl_interface wl_output_interface;\n\nusing namespace std::literals;\n\n// Disable warning for converting incompatible functions\n#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored \"-Wpedantic\"\n#pragma GCC diagnostic ignored \"-Wpmf-conversions\"\n\nnamespace wl {\n\n  // Helper to call C++ method from wayland C callback\n  template <class T, class Method, Method m, class... Params>\n  static auto\n  classCall(void *data, Params... params) -> decltype(((*reinterpret_cast<T *>(data)).*m)(params...)) {\n    return ((*reinterpret_cast<T *>(data)).*m)(params...);\n  }\n\n#define CLASS_CALL(c, m) classCall<c, decltype(&c::m), &c::m>\n\n  int\n  display_t::init(const char *display_name) {\n    if (!display_name) {\n      display_name = std::getenv(\"WAYLAND_DISPLAY\");\n    }\n\n    if (!display_name) {\n      BOOST_LOG(error) << \"Environment variable WAYLAND_DISPLAY has not been defined\"sv;\n      return -1;\n    }\n\n    display_internal.reset(wl_display_connect(display_name));\n    if (!display_internal) {\n      BOOST_LOG(error) << \"Couldn't connect to Wayland display: \"sv << display_name;\n      return -1;\n    }\n\n    BOOST_LOG(info) << \"Found display [\"sv << display_name << ']';\n\n    return 0;\n  }\n\n  void\n  display_t::roundtrip() {\n    wl_display_roundtrip(display_internal.get());\n  }\n\n  /**\n   * @brief Waits up to the specified timeout to dispatch new events on the wl_display.\n   * @param timeout The timeout in milliseconds.\n   * @return `true` if new events were dispatched or `false` if the timeout expired.\n   */\n  bool\n  display_t::dispatch(std::chrono::milliseconds timeout) {\n    // Check if any events are queued already. If not, flush\n    // outgoing events, and prepare to wait for readability.\n    if (wl_display_prepare_read(display_internal.get()) == 0) {\n      wl_display_flush(display_internal.get());\n\n      // Wait for an event to come in\n      struct pollfd pfd = {};\n      pfd.fd = wl_display_get_fd(display_internal.get());\n      pfd.events = POLLIN;\n      if (poll(&pfd, 1, timeout.count()) == 1 && (pfd.revents & POLLIN)) {\n        // Read the new event(s)\n        wl_display_read_events(display_internal.get());\n      }\n      else {\n        // We timed out, so unlock the queue now\n        wl_display_cancel_read(display_internal.get());\n        return false;\n      }\n    }\n\n    // Dispatch any existing or new pending events\n    wl_display_dispatch_pending(display_internal.get());\n    return true;\n  }\n\n  wl_registry *\n  display_t::registry() {\n    return wl_display_get_registry(display_internal.get());\n  }\n\n  inline monitor_t::monitor_t(wl_output *output):\n      output { output },\n      wl_listener {\n        &CLASS_CALL(monitor_t, wl_geometry),\n        &CLASS_CALL(monitor_t, wl_mode),\n        &CLASS_CALL(monitor_t, wl_done),\n        &CLASS_CALL(monitor_t, wl_scale),\n      },\n      xdg_listener {\n        &CLASS_CALL(monitor_t, xdg_position),\n        &CLASS_CALL(monitor_t, xdg_size),\n        &CLASS_CALL(monitor_t, xdg_done),\n        &CLASS_CALL(monitor_t, xdg_name),\n        &CLASS_CALL(monitor_t, xdg_description)\n      } {}\n\n  inline void\n  monitor_t::xdg_name(zxdg_output_v1 *, const char *name) {\n    this->name = name;\n\n    BOOST_LOG(info) << \"Name: \"sv << this->name;\n  }\n\n  void\n  monitor_t::xdg_description(zxdg_output_v1 *, const char *description) {\n    this->description = description;\n\n    BOOST_LOG(info) << \"Found monitor: \"sv << this->description;\n  }\n\n  void\n  monitor_t::xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y) {\n    viewport.offset_x = x;\n    viewport.offset_y = y;\n\n    BOOST_LOG(info) << \"Offset: \"sv << x << 'x' << y;\n  }\n\n  void\n  monitor_t::xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height) {\n    BOOST_LOG(info) << \"Logical size: \"sv << width << 'x' << height;\n  }\n\n  void\n  monitor_t::wl_mode(wl_output *wl_output, std::uint32_t flags,\n    std::int32_t width, std::int32_t height, std::int32_t refresh) {\n    viewport.width = width;\n    viewport.height = height;\n\n    BOOST_LOG(info) << \"Resolution: \"sv << width << 'x' << height;\n  }\n\n  void\n  monitor_t::listen(zxdg_output_manager_v1 *output_manager) {\n    auto xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output);\n    zxdg_output_v1_add_listener(xdg_output, &xdg_listener, this);\n    wl_output_add_listener(output, &wl_listener, this);\n  }\n\n  interface_t::interface_t() noexcept\n      :\n      output_manager { nullptr },\n      listener {\n        &CLASS_CALL(interface_t, add_interface),\n        &CLASS_CALL(interface_t, del_interface)\n      } {}\n\n  void\n  interface_t::listen(wl_registry *registry) {\n    wl_registry_add_listener(registry, &listener, this);\n  }\n\n  void\n  interface_t::add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version) {\n    BOOST_LOG(debug) << \"Available interface: \"sv << interface << '(' << id << \") version \"sv << version;\n\n    if (!std::strcmp(interface, wl_output_interface.name)) {\n      BOOST_LOG(info) << \"Found interface: \"sv << interface << '(' << id << \") version \"sv << version;\n      monitors.emplace_back(\n        std::make_unique<monitor_t>(\n          (wl_output *) wl_registry_bind(registry, id, &wl_output_interface, 2)));\n    }\n    else if (!std::strcmp(interface, zxdg_output_manager_v1_interface.name)) {\n      BOOST_LOG(info) << \"Found interface: \"sv << interface << '(' << id << \") version \"sv << version;\n      output_manager = (zxdg_output_manager_v1 *) wl_registry_bind(registry, id, &zxdg_output_manager_v1_interface, version);\n\n      this->interface[XDG_OUTPUT] = true;\n    }\n    else if (!std::strcmp(interface, zwlr_export_dmabuf_manager_v1_interface.name)) {\n      BOOST_LOG(info) << \"Found interface: \"sv << interface << '(' << id << \") version \"sv << version;\n      dmabuf_manager = (zwlr_export_dmabuf_manager_v1 *) wl_registry_bind(registry, id, &zwlr_export_dmabuf_manager_v1_interface, version);\n\n      this->interface[WLR_EXPORT_DMABUF] = true;\n    }\n  }\n\n  void\n  interface_t::del_interface(wl_registry *registry, uint32_t id) {\n    BOOST_LOG(info) << \"Delete: \"sv << id;\n  }\n\n  dmabuf_t::dmabuf_t():\n      status { READY }, frames {}, current_frame { &frames[0] }, listener {\n        &CLASS_CALL(dmabuf_t, frame),\n        &CLASS_CALL(dmabuf_t, object),\n        &CLASS_CALL(dmabuf_t, ready),\n        &CLASS_CALL(dmabuf_t, cancel)\n      } {\n  }\n\n  void\n  dmabuf_t::listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor) {\n    auto frame = zwlr_export_dmabuf_manager_v1_capture_output(dmabuf_manager, blend_cursor, output);\n    zwlr_export_dmabuf_frame_v1_add_listener(frame, &listener, this);\n\n    status = WAITING;\n  }\n\n  dmabuf_t::~dmabuf_t() {\n    for (auto &frame : frames) {\n      frame.destroy();\n    }\n  }\n\n  void\n  dmabuf_t::frame(\n    zwlr_export_dmabuf_frame_v1 *frame,\n    std::uint32_t width, std::uint32_t height,\n    std::uint32_t x, std::uint32_t y,\n    std::uint32_t buffer_flags, std::uint32_t flags,\n    std::uint32_t format,\n    std::uint32_t high, std::uint32_t low,\n    std::uint32_t obj_count) {\n    auto next_frame = get_next_frame();\n\n    next_frame->sd.fourcc = format;\n    next_frame->sd.width = width;\n    next_frame->sd.height = height;\n    next_frame->sd.modifier = (((std::uint64_t) high) << 32) | low;\n  }\n\n  void\n  dmabuf_t::object(\n    zwlr_export_dmabuf_frame_v1 *frame,\n    std::uint32_t index,\n    std::int32_t fd,\n    std::uint32_t size,\n    std::uint32_t offset,\n    std::uint32_t stride,\n    std::uint32_t plane_index) {\n    auto next_frame = get_next_frame();\n\n    next_frame->sd.fds[plane_index] = fd;\n    next_frame->sd.pitches[plane_index] = stride;\n    next_frame->sd.offsets[plane_index] = offset;\n  }\n\n  void\n  dmabuf_t::ready(\n    zwlr_export_dmabuf_frame_v1 *frame,\n    std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec) {\n    zwlr_export_dmabuf_frame_v1_destroy(frame);\n\n    current_frame->destroy();\n    current_frame = get_next_frame();\n\n    status = READY;\n  }\n\n  void\n  dmabuf_t::cancel(\n    zwlr_export_dmabuf_frame_v1 *frame,\n    std::uint32_t reason) {\n    zwlr_export_dmabuf_frame_v1_destroy(frame);\n\n    auto next_frame = get_next_frame();\n    next_frame->destroy();\n\n    status = REINIT;\n  }\n\n  void\n  frame_t::destroy() {\n    for (auto x = 0; x < 4; ++x) {\n      if (sd.fds[x] >= 0) {\n        close(sd.fds[x]);\n\n        sd.fds[x] = -1;\n      }\n    }\n  }\n\n  frame_t::frame_t() {\n    // File descriptors aren't open\n    std::fill_n(sd.fds, 4, -1);\n  };\n\n  std::vector<std::unique_ptr<monitor_t>>\n  monitors(const char *display_name) {\n    display_t display;\n\n    if (display.init(display_name)) {\n      return {};\n    }\n\n    interface_t interface;\n    interface.listen(display.registry());\n\n    display.roundtrip();\n\n    if (!interface[interface_t::XDG_OUTPUT]) {\n      BOOST_LOG(error) << \"Missing Wayland wire XDG_OUTPUT\"sv;\n      return {};\n    }\n\n    for (auto &monitor : interface.monitors) {\n      monitor->listen(interface.output_manager);\n    }\n\n    display.roundtrip();\n\n    return std::move(interface.monitors);\n  }\n\n  static bool\n  validate() {\n    display_t display;\n\n    return display.init() == 0;\n  }\n\n  int\n  init() {\n    static bool validated = validate();\n\n    return !validated;\n  }\n\n}  // namespace wl\n\n#pragma GCC diagnostic pop"
  },
  {
    "path": "src/platform/linux/wayland.h",
    "content": "/**\n * @file src/platform/linux/wayland.h\n * @brief Declarations for Wayland capture.\n */\n#pragma once\n\n#include <bitset>\n\n#ifdef SUNSHINE_BUILD_WAYLAND\n  #include <wlr-export-dmabuf-unstable-v1.h>\n  #include <xdg-output-unstable-v1.h>\n#endif\n\n#include \"graphics.h\"\n\n/**\n * The classes defined in this macro block should only be used by\n * cpp files whose compilation depends on SUNSHINE_BUILD_WAYLAND\n */\n#ifdef SUNSHINE_BUILD_WAYLAND\n\nnamespace wl {\n  using display_internal_t = util::safe_ptr<wl_display, wl_display_disconnect>;\n\n  class frame_t {\n  public:\n    frame_t();\n    egl::surface_descriptor_t sd;\n\n    void\n    destroy();\n  };\n\n  class dmabuf_t {\n  public:\n    enum status_e {\n      WAITING,  ///< Waiting for a frame\n      READY,  ///< Frame is ready\n      REINIT,  ///< Reinitialize the frame\n    };\n\n    dmabuf_t(dmabuf_t &&) = delete;\n    dmabuf_t(const dmabuf_t &) = delete;\n\n    dmabuf_t &\n    operator=(const dmabuf_t &) = delete;\n    dmabuf_t &\n    operator=(dmabuf_t &&) = delete;\n\n    dmabuf_t();\n\n    void\n    listen(zwlr_export_dmabuf_manager_v1 *dmabuf_manager, wl_output *output, bool blend_cursor = false);\n\n    ~dmabuf_t();\n\n    void\n    frame(\n      zwlr_export_dmabuf_frame_v1 *frame,\n      std::uint32_t width, std::uint32_t height,\n      std::uint32_t x, std::uint32_t y,\n      std::uint32_t buffer_flags, std::uint32_t flags,\n      std::uint32_t format,\n      std::uint32_t high, std::uint32_t low,\n      std::uint32_t obj_count);\n\n    void\n    object(\n      zwlr_export_dmabuf_frame_v1 *frame,\n      std::uint32_t index,\n      std::int32_t fd,\n      std::uint32_t size,\n      std::uint32_t offset,\n      std::uint32_t stride,\n      std::uint32_t plane_index);\n\n    void\n    ready(\n      zwlr_export_dmabuf_frame_v1 *frame,\n      std::uint32_t tv_sec_hi, std::uint32_t tv_sec_lo, std::uint32_t tv_nsec);\n\n    void\n    cancel(\n      zwlr_export_dmabuf_frame_v1 *frame,\n      std::uint32_t reason);\n\n    inline frame_t *\n    get_next_frame() {\n      return current_frame == &frames[0] ? &frames[1] : &frames[0];\n    }\n\n    status_e status;\n\n    std::array<frame_t, 2> frames;\n    frame_t *current_frame;\n\n    zwlr_export_dmabuf_frame_v1_listener listener;\n  };\n\n  class monitor_t {\n  public:\n    monitor_t(monitor_t &&) = delete;\n    monitor_t(const monitor_t &) = delete;\n\n    monitor_t &\n    operator=(const monitor_t &) = delete;\n    monitor_t &\n    operator=(monitor_t &&) = delete;\n\n    monitor_t(wl_output *output);\n\n    void\n    xdg_name(zxdg_output_v1 *, const char *name);\n    void\n    xdg_description(zxdg_output_v1 *, const char *description);\n    void\n    xdg_position(zxdg_output_v1 *, std::int32_t x, std::int32_t y);\n    void\n    xdg_size(zxdg_output_v1 *, std::int32_t width, std::int32_t height);\n    void\n    xdg_done(zxdg_output_v1 *) {}\n\n    void\n    wl_geometry(wl_output *wl_output, std::int32_t x, std::int32_t y,\n      std::int32_t physical_width, std::int32_t physical_height, std::int32_t subpixel,\n      const char *make, const char *model, std::int32_t transform) {}\n    void\n    wl_mode(wl_output *wl_output, std::uint32_t flags,\n      std::int32_t width, std::int32_t height, std::int32_t refresh);\n    void\n    wl_done(wl_output *wl_output) {}\n    void\n    wl_scale(wl_output *wl_output, std::int32_t factor) {}\n\n    void\n    listen(zxdg_output_manager_v1 *output_manager);\n\n    wl_output *output;\n\n    std::string name;\n    std::string description;\n\n    platf::touch_port_t viewport;\n\n    wl_output_listener wl_listener;\n    zxdg_output_v1_listener xdg_listener;\n  };\n\n  class interface_t {\n    struct bind_t {\n      std::uint32_t id;\n      std::uint32_t version;\n    };\n\n  public:\n    enum interface_e {\n      XDG_OUTPUT,  ///< xdg-output\n      WLR_EXPORT_DMABUF,  ///< Export dmabuf\n      MAX_INTERFACES,  ///< Maximum number of interfaces\n    };\n\n    interface_t(interface_t &&) = delete;\n    interface_t(const interface_t &) = delete;\n\n    interface_t &\n    operator=(const interface_t &) = delete;\n    interface_t &\n    operator=(interface_t &&) = delete;\n\n    interface_t() noexcept;\n\n    void\n    listen(wl_registry *registry);\n\n    std::vector<std::unique_ptr<monitor_t>> monitors;\n\n    zwlr_export_dmabuf_manager_v1 *dmabuf_manager;\n    zxdg_output_manager_v1 *output_manager;\n\n    bool\n    operator[](interface_e bit) const {\n      return interface[bit];\n    }\n\n  private:\n    void\n    add_interface(wl_registry *registry, std::uint32_t id, const char *interface, std::uint32_t version);\n    void\n    del_interface(wl_registry *registry, uint32_t id);\n\n    std::bitset<MAX_INTERFACES> interface;\n\n    wl_registry_listener listener;\n  };\n\n  class display_t {\n  public:\n    /**\n     * @brief Initialize display.\n     * If display_name == nullptr -> display_name = std::getenv(\"WAYLAND_DISPLAY\")\n     * @param display_name The name of the display.\n     * @return 0 on success, -1 on failure.\n     */\n    int\n    init(const char *display_name = nullptr);\n\n    // Roundtrip with Wayland connection\n    void\n    roundtrip();\n\n    // Wait up to the timeout to read and dispatch new events\n    bool\n    dispatch(std::chrono::milliseconds timeout);\n\n    // Get the registry associated with the display\n    // No need to manually free the registry\n    wl_registry *\n    registry();\n\n    inline display_internal_t::pointer\n    get() {\n      return display_internal.get();\n    }\n\n  private:\n    display_internal_t display_internal;\n  };\n\n  std::vector<std::unique_ptr<monitor_t>>\n  monitors(const char *display_name = nullptr);\n\n  int\n  init();\n}  // namespace wl\n#else\n\nstruct wl_output;\nstruct zxdg_output_manager_v1;\n\nnamespace wl {\n  class monitor_t {\n  public:\n    monitor_t(monitor_t &&) = delete;\n    monitor_t(const monitor_t &) = delete;\n\n    monitor_t &\n    operator=(const monitor_t &) = delete;\n    monitor_t &\n    operator=(monitor_t &&) = delete;\n\n    monitor_t(wl_output *output);\n\n    void\n    listen(zxdg_output_manager_v1 *output_manager);\n\n    wl_output *output;\n\n    std::string name;\n    std::string description;\n\n    platf::touch_port_t viewport;\n  };\n\n  inline std::vector<std::unique_ptr<monitor_t>>\n  monitors(const char *display_name = nullptr) { return {}; }\n\n  inline int\n  init() { return -1; }\n}  // namespace wl\n#endif\n"
  },
  {
    "path": "src/platform/linux/wlgrab.cpp",
    "content": "/**\n * @file src/platform/linux/wlgrab.cpp\n * @brief Definitions for wlgrab capture.\n */\n#include <thread>\n\n#include \"src/platform/common.h\"\n\n#include \"src/logging.h\"\n#include \"src/video.h\"\n\n#include \"cuda.h\"\n#include \"vaapi.h\"\n#include \"wayland.h\"\n\nusing namespace std::literals;\nnamespace wl {\n  static int env_width;\n  static int env_height;\n\n  struct img_t: public platf::img_t {\n    ~img_t() override {\n      delete[] data;\n      data = nullptr;\n    }\n  };\n\n  class wlr_t: public platf::display_t {\n  public:\n    int\n    init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {\n      delay = std::chrono::nanoseconds { 1s } / config.framerate;\n      mem_type = hwdevice_type;\n\n      if (display.init()) {\n        return -1;\n      }\n\n      interface.listen(display.registry());\n\n      display.roundtrip();\n\n      if (!interface[wl::interface_t::XDG_OUTPUT]) {\n        BOOST_LOG(error) << \"Missing Wayland wire for xdg_output\"sv;\n        return -1;\n      }\n\n      if (!interface[wl::interface_t::WLR_EXPORT_DMABUF]) {\n        BOOST_LOG(error) << \"Missing Wayland wire for wlr-export-dmabuf\"sv;\n        return -1;\n      }\n\n      auto monitor = interface.monitors[0].get();\n\n      if (!display_name.empty()) {\n        auto streamedMonitor = util::from_view(display_name);\n\n        if (streamedMonitor >= 0 && streamedMonitor < interface.monitors.size()) {\n          monitor = interface.monitors[streamedMonitor].get();\n        }\n      }\n\n      monitor->listen(interface.output_manager);\n\n      display.roundtrip();\n\n      output = monitor->output;\n\n      offset_x = monitor->viewport.offset_x;\n      offset_y = monitor->viewport.offset_y;\n      width = monitor->viewport.width;\n      height = monitor->viewport.height;\n\n      this->env_width = ::wl::env_width;\n      this->env_height = ::wl::env_height;\n\n      BOOST_LOG(info) << \"Selected monitor [\"sv << monitor->description << \"] for streaming\"sv;\n      BOOST_LOG(debug) << \"Offset: \"sv << offset_x << 'x' << offset_y;\n      BOOST_LOG(debug) << \"Resolution: \"sv << width << 'x' << height;\n      BOOST_LOG(debug) << \"Desktop Resolution: \"sv << env_width << 'x' << env_height;\n\n      return 0;\n    }\n\n    int\n    dummy_img(platf::img_t *img) override {\n      return 0;\n    }\n\n    inline platf::capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {\n      auto to = std::chrono::steady_clock::now() + timeout;\n\n      // Dispatch events until we get a new frame or the timeout expires\n      dmabuf.listen(interface.dmabuf_manager, output, cursor);\n      do {\n        auto remaining_time_ms = std::chrono::duration_cast<std::chrono::milliseconds>(to - std::chrono::steady_clock::now());\n        if (remaining_time_ms.count() < 0 || !display.dispatch(remaining_time_ms)) {\n          return platf::capture_e::timeout;\n        }\n      } while (dmabuf.status == dmabuf_t::WAITING);\n\n      auto current_frame = dmabuf.current_frame;\n\n      if (\n        dmabuf.status == dmabuf_t::REINIT ||\n        current_frame->sd.width != width ||\n        current_frame->sd.height != height) {\n        return platf::capture_e::reinit;\n      }\n\n      return platf::capture_e::ok;\n    }\n\n    platf::mem_type_e mem_type;\n\n    std::chrono::nanoseconds delay;\n\n    wl::display_t display;\n    interface_t interface;\n    dmabuf_t dmabuf;\n\n    wl_output *output;\n  };\n\n  class wlr_ram_t: public wlr_t {\n  public:\n    platf::capture_e\n    capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {\n      auto next_frame = std::chrono::steady_clock::now();\n\n      sleep_overshoot_logger.reset();\n\n      while (true) {\n        auto now = std::chrono::steady_clock::now();\n\n        if (next_frame > now) {\n          std::this_thread::sleep_for(next_frame - now);\n          sleep_overshoot_logger.first_point(next_frame);\n          sleep_overshoot_logger.second_point_now_and_log();\n        }\n\n        next_frame += delay;\n        if (next_frame < now) {  // some major slowdown happened; we couldn't keep up\n          next_frame = now + delay;\n        }\n\n        std::shared_ptr<platf::img_t> img_out;\n        auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);\n        switch (status) {\n          case platf::capture_e::reinit:\n          case platf::capture_e::error:\n          case platf::capture_e::interrupted:\n            return status;\n          case platf::capture_e::timeout:\n            if (!push_captured_image_cb(std::move(img_out), false)) {\n              return platf::capture_e::ok;\n            }\n            break;\n          case platf::capture_e::ok:\n            if (!push_captured_image_cb(std::move(img_out), true)) {\n              return platf::capture_e::ok;\n            }\n            break;\n          default:\n            BOOST_LOG(error) << \"Unrecognized capture status [\"sv << (int) status << ']';\n            return status;\n        }\n      }\n\n      return platf::capture_e::ok;\n    }\n\n    platf::capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {\n      auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor);\n      if (status != platf::capture_e::ok) {\n        return status;\n      }\n\n      auto current_frame = dmabuf.current_frame;\n\n      auto rgb_opt = egl::import_source(egl_display.get(), current_frame->sd);\n\n      if (!rgb_opt) {\n        return platf::capture_e::reinit;\n      }\n\n      if (!pull_free_image_cb(img_out)) {\n        return platf::capture_e::interrupted;\n      }\n\n      gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]);\n\n      // Don't remove these lines, see https://github.com/LizardByte/Sunshine/issues/453\n      int w, h;\n      gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w);\n      gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h);\n      BOOST_LOG(debug) << \"width and height: w \"sv << w << \" h \"sv << h;\n\n      gl::ctx.GetTextureSubImage((*rgb_opt)->tex[0], 0, 0, 0, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data);\n      gl::ctx.BindTexture(GL_TEXTURE_2D, 0);\n\n      return platf::capture_e::ok;\n    }\n\n    int\n    init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {\n      if (wlr_t::init(hwdevice_type, display_name, config)) {\n        return -1;\n      }\n\n      egl_display = egl::make_display(display.get());\n      if (!egl_display) {\n        return -1;\n      }\n\n      auto ctx_opt = egl::make_ctx(egl_display.get());\n      if (!ctx_opt) {\n        return -1;\n      }\n\n      ctx = std::move(*ctx_opt);\n\n      return 0;\n    }\n\n    std::unique_ptr<platf::avcodec_encode_device_t>\n    make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override {\n#ifdef SUNSHINE_BUILD_VAAPI\n      if (mem_type == platf::mem_type_e::vaapi) {\n        return va::make_avcodec_encode_device(width, height, false);\n      }\n#endif\n\n#ifdef SUNSHINE_BUILD_CUDA\n      if (mem_type == platf::mem_type_e::cuda) {\n        return cuda::make_avcodec_encode_device(width, height, false);\n      }\n#endif\n\n      return std::make_unique<platf::avcodec_encode_device_t>();\n    }\n\n    std::shared_ptr<platf::img_t>\n    alloc_img() override {\n      auto img = std::make_shared<img_t>();\n      img->width = width;\n      img->height = height;\n      img->pixel_pitch = 4;\n      img->row_pitch = img->pixel_pitch * width;\n      img->data = new std::uint8_t[height * img->row_pitch];\n\n      return img;\n    }\n\n    egl::display_t egl_display;\n    egl::ctx_t ctx;\n  };\n\n  class wlr_vram_t: public wlr_t {\n  public:\n    platf::capture_e\n    capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {\n      auto next_frame = std::chrono::steady_clock::now();\n\n      sleep_overshoot_logger.reset();\n\n      while (true) {\n        auto now = std::chrono::steady_clock::now();\n\n        if (next_frame > now) {\n          std::this_thread::sleep_for(next_frame - now);\n          sleep_overshoot_logger.first_point(next_frame);\n          sleep_overshoot_logger.second_point_now_and_log();\n        }\n\n        next_frame += delay;\n        if (next_frame < now) {  // some major slowdown happened; we couldn't keep up\n          next_frame = now + delay;\n        }\n\n        std::shared_ptr<platf::img_t> img_out;\n        auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);\n        switch (status) {\n          case platf::capture_e::reinit:\n          case platf::capture_e::error:\n          case platf::capture_e::interrupted:\n            return status;\n          case platf::capture_e::timeout:\n            if (!push_captured_image_cb(std::move(img_out), false)) {\n              return platf::capture_e::ok;\n            }\n            break;\n          case platf::capture_e::ok:\n            if (!push_captured_image_cb(std::move(img_out), true)) {\n              return platf::capture_e::ok;\n            }\n            break;\n          default:\n            BOOST_LOG(error) << \"Unrecognized capture status [\"sv << (int) status << ']';\n            return status;\n        }\n      }\n\n      return platf::capture_e::ok;\n    }\n\n    platf::capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {\n      auto status = wlr_t::snapshot(pull_free_image_cb, img_out, timeout, cursor);\n      if (status != platf::capture_e::ok) {\n        return status;\n      }\n\n      if (!pull_free_image_cb(img_out)) {\n        return platf::capture_e::interrupted;\n      }\n      auto img = (egl::img_descriptor_t *) img_out.get();\n      img->reset();\n\n      auto current_frame = dmabuf.current_frame;\n\n      ++sequence;\n      img->sequence = sequence;\n\n      img->sd = current_frame->sd;\n\n      // Prevent dmabuf from closing the file descriptors.\n      std::fill_n(current_frame->sd.fds, 4, -1);\n\n      return platf::capture_e::ok;\n    }\n\n    std::shared_ptr<platf::img_t>\n    alloc_img() override {\n      auto img = std::make_shared<egl::img_descriptor_t>();\n\n      img->width = width;\n      img->height = height;\n      img->sequence = 0;\n      img->serial = std::numeric_limits<decltype(img->serial)>::max();\n      img->data = nullptr;\n\n      // File descriptors aren't open\n      std::fill_n(img->sd.fds, 4, -1);\n\n      return img;\n    }\n\n    std::unique_ptr<platf::avcodec_encode_device_t>\n    make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override {\n#ifdef SUNSHINE_BUILD_VAAPI\n      if (mem_type == platf::mem_type_e::vaapi) {\n        return va::make_avcodec_encode_device(width, height, 0, 0, true);\n      }\n#endif\n\n#ifdef SUNSHINE_BUILD_CUDA\n      if (mem_type == platf::mem_type_e::cuda) {\n        return cuda::make_avcodec_gl_encode_device(width, height, 0, 0);\n      }\n#endif\n\n      return std::make_unique<platf::avcodec_encode_device_t>();\n    }\n\n    int\n    dummy_img(platf::img_t *img) override {\n      // Empty images are recognized as dummies by the zero sequence number\n      return 0;\n    }\n\n    std::uint64_t sequence {};\n  };\n\n}  // namespace wl\n\nnamespace platf {\n  std::shared_ptr<display_t>\n  wl_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {\n    if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) {\n      BOOST_LOG(error) << \"Could not initialize display with the given hw device type.\"sv;\n      return nullptr;\n    }\n\n    if (hwdevice_type == platf::mem_type_e::vaapi || hwdevice_type == platf::mem_type_e::cuda) {\n      auto wlr = std::make_shared<wl::wlr_vram_t>();\n      if (wlr->init(hwdevice_type, display_name, config)) {\n        return nullptr;\n      }\n\n      return wlr;\n    }\n\n    auto wlr = std::make_shared<wl::wlr_ram_t>();\n    if (wlr->init(hwdevice_type, display_name, config)) {\n      return nullptr;\n    }\n\n    return wlr;\n  }\n\n  std::vector<std::string>\n  wl_display_names() {\n    std::vector<std::string> display_names;\n\n    wl::display_t display;\n    if (display.init()) {\n      return {};\n    }\n\n    wl::interface_t interface;\n    interface.listen(display.registry());\n\n    display.roundtrip();\n\n    if (!interface[wl::interface_t::XDG_OUTPUT]) {\n      BOOST_LOG(warning) << \"Missing Wayland wire for xdg_output\"sv;\n      return {};\n    }\n\n    if (!interface[wl::interface_t::WLR_EXPORT_DMABUF]) {\n      BOOST_LOG(warning) << \"Missing Wayland wire for wlr-export-dmabuf\"sv;\n      return {};\n    }\n\n    wl::env_width = 0;\n    wl::env_height = 0;\n\n    for (auto &monitor : interface.monitors) {\n      monitor->listen(interface.output_manager);\n    }\n\n    display.roundtrip();\n\n    BOOST_LOG(info) << \"-------- Start of Wayland monitor list --------\"sv;\n\n    for (int x = 0; x < interface.monitors.size(); ++x) {\n      auto monitor = interface.monitors[x].get();\n\n      wl::env_width = std::max(wl::env_width, (int) (monitor->viewport.offset_x + monitor->viewport.width));\n      wl::env_height = std::max(wl::env_height, (int) (monitor->viewport.offset_y + monitor->viewport.height));\n\n      BOOST_LOG(info) << \"Monitor \" << x << \" is \"sv << monitor->name << \": \"sv << monitor->description;\n\n      display_names.emplace_back(std::to_string(x));\n    }\n\n    BOOST_LOG(info) << \"--------- End of Wayland monitor list ---------\"sv;\n\n    return display_names;\n  }\n\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/linux/x11grab.cpp",
    "content": "/**\n * @file src/platform/linux/x11grab.cpp\n * @brief Definitions for x11 capture.\n */\n#include \"src/platform/common.h\"\n\n#include <fstream>\n#include <thread>\n\n#include <X11/X.h>\n#include <X11/Xlib.h>\n#include <X11/Xutil.h>\n#include <X11/extensions/Xfixes.h>\n#include <X11/extensions/Xrandr.h>\n#include <sys/ipc.h>\n#include <sys/shm.h>\n#include <xcb/shm.h>\n#include <xcb/xfixes.h>\n\n#include \"src/config.h\"\n#include \"src/globals.h\"\n#include \"src/logging.h\"\n#include \"src/task_pool.h\"\n#include \"src/video.h\"\n\n#include \"cuda.h\"\n#include \"graphics.h\"\n#include \"misc.h\"\n#include \"vaapi.h\"\n#include \"x11grab.h\"\n\nusing namespace std::literals;\n\nnamespace platf {\n  int\n  load_xcb();\n  int\n  load_x11();\n\n  namespace x11 {\n#define _FN(x, ret, args)    \\\n  typedef ret(*x##_fn) args; \\\n  static x##_fn x\n\n    _FN(GetImage, XImage *,\n      (\n        Display * display,\n        Drawable d,\n        int x, int y,\n        unsigned int width, unsigned int height,\n        unsigned long plane_mask,\n        int format));\n\n    _FN(OpenDisplay, Display *, (_Xconst char *display_name));\n    _FN(GetWindowAttributes, Status,\n      (\n        Display * display,\n        Window w,\n        XWindowAttributes *window_attributes_return));\n\n    _FN(CloseDisplay, int, (Display * display));\n    _FN(Free, int, (void *data));\n    _FN(InitThreads, Status, (void) );\n\n    namespace rr {\n      _FN(GetScreenResources, XRRScreenResources *, (Display * dpy, Window window));\n      _FN(GetOutputInfo, XRROutputInfo *, (Display * dpy, XRRScreenResources *resources, RROutput output));\n      _FN(GetCrtcInfo, XRRCrtcInfo *, (Display * dpy, XRRScreenResources *resources, RRCrtc crtc));\n      _FN(FreeScreenResources, void, (XRRScreenResources * resources));\n      _FN(FreeOutputInfo, void, (XRROutputInfo * outputInfo));\n      _FN(FreeCrtcInfo, void, (XRRCrtcInfo * crtcInfo));\n\n      static int\n      init() {\n        static void *handle { nullptr };\n        static bool funcs_loaded = false;\n\n        if (funcs_loaded) return 0;\n\n        if (!handle) {\n          handle = dyn::handle({ \"libXrandr.so.2\", \"libXrandr.so\" });\n          if (!handle) {\n            return -1;\n          }\n        }\n\n        std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {\n          { (dyn::apiproc *) &GetScreenResources, \"XRRGetScreenResources\" },\n          { (dyn::apiproc *) &GetOutputInfo, \"XRRGetOutputInfo\" },\n          { (dyn::apiproc *) &GetCrtcInfo, \"XRRGetCrtcInfo\" },\n          { (dyn::apiproc *) &FreeScreenResources, \"XRRFreeScreenResources\" },\n          { (dyn::apiproc *) &FreeOutputInfo, \"XRRFreeOutputInfo\" },\n          { (dyn::apiproc *) &FreeCrtcInfo, \"XRRFreeCrtcInfo\" },\n        };\n\n        if (dyn::load(handle, funcs)) {\n          return -1;\n        }\n\n        funcs_loaded = true;\n        return 0;\n      }\n\n    }  // namespace rr\n    namespace fix {\n      _FN(GetCursorImage, XFixesCursorImage *, (Display * dpy));\n\n      static int\n      init() {\n        static void *handle { nullptr };\n        static bool funcs_loaded = false;\n\n        if (funcs_loaded) return 0;\n\n        if (!handle) {\n          handle = dyn::handle({ \"libXfixes.so.3\", \"libXfixes.so\" });\n          if (!handle) {\n            return -1;\n          }\n        }\n\n        std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {\n          { (dyn::apiproc *) &GetCursorImage, \"XFixesGetCursorImage\" },\n        };\n\n        if (dyn::load(handle, funcs)) {\n          return -1;\n        }\n\n        funcs_loaded = true;\n        return 0;\n      }\n    }  // namespace fix\n\n    static int\n    init() {\n      static void *handle { nullptr };\n      static bool funcs_loaded = false;\n\n      if (funcs_loaded) return 0;\n\n      if (!handle) {\n        handle = dyn::handle({ \"libX11.so.6\", \"libX11.so\" });\n        if (!handle) {\n          return -1;\n        }\n      }\n\n      std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {\n        { (dyn::apiproc *) &GetImage, \"XGetImage\" },\n        { (dyn::apiproc *) &OpenDisplay, \"XOpenDisplay\" },\n        { (dyn::apiproc *) &GetWindowAttributes, \"XGetWindowAttributes\" },\n        { (dyn::apiproc *) &Free, \"XFree\" },\n        { (dyn::apiproc *) &CloseDisplay, \"XCloseDisplay\" },\n        { (dyn::apiproc *) &InitThreads, \"XInitThreads\" },\n      };\n\n      if (dyn::load(handle, funcs)) {\n        return -1;\n      }\n\n      funcs_loaded = true;\n      return 0;\n    }\n  }  // namespace x11\n\n  namespace xcb {\n    static xcb_extension_t *shm_id;\n\n    _FN(shm_get_image_reply, xcb_shm_get_image_reply_t *,\n      (\n        xcb_connection_t * c,\n        xcb_shm_get_image_cookie_t cookie,\n        xcb_generic_error_t **e));\n\n    _FN(shm_get_image_unchecked, xcb_shm_get_image_cookie_t,\n      (\n        xcb_connection_t * c,\n        xcb_drawable_t drawable,\n        int16_t x, int16_t y,\n        uint16_t width, uint16_t height,\n        uint32_t plane_mask,\n        uint8_t format,\n        xcb_shm_seg_t shmseg,\n        uint32_t offset));\n\n    _FN(shm_attach, xcb_void_cookie_t,\n      (xcb_connection_t * c,\n        xcb_shm_seg_t shmseg,\n        uint32_t shmid,\n        uint8_t read_only));\n\n    _FN(get_extension_data, xcb_query_extension_reply_t *,\n      (xcb_connection_t * c, xcb_extension_t *ext));\n\n    _FN(get_setup, xcb_setup_t *, (xcb_connection_t * c));\n    _FN(disconnect, void, (xcb_connection_t * c));\n    _FN(connection_has_error, int, (xcb_connection_t * c));\n    _FN(connect, xcb_connection_t *, (const char *displayname, int *screenp));\n    _FN(setup_roots_iterator, xcb_screen_iterator_t, (const xcb_setup_t *R));\n    _FN(generate_id, std::uint32_t, (xcb_connection_t * c));\n\n    int\n    init_shm() {\n      static void *handle { nullptr };\n      static bool funcs_loaded = false;\n\n      if (funcs_loaded) return 0;\n\n      if (!handle) {\n        handle = dyn::handle({ \"libxcb-shm.so.0\", \"libxcb-shm.so\" });\n        if (!handle) {\n          return -1;\n        }\n      }\n\n      std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {\n        { (dyn::apiproc *) &shm_id, \"xcb_shm_id\" },\n        { (dyn::apiproc *) &shm_get_image_reply, \"xcb_shm_get_image_reply\" },\n        { (dyn::apiproc *) &shm_get_image_unchecked, \"xcb_shm_get_image_unchecked\" },\n        { (dyn::apiproc *) &shm_attach, \"xcb_shm_attach\" },\n      };\n\n      if (dyn::load(handle, funcs)) {\n        return -1;\n      }\n\n      funcs_loaded = true;\n      return 0;\n    }\n\n    int\n    init() {\n      static void *handle { nullptr };\n      static bool funcs_loaded = false;\n\n      if (funcs_loaded) return 0;\n\n      if (!handle) {\n        handle = dyn::handle({ \"libxcb.so.1\", \"libxcb.so\" });\n        if (!handle) {\n          return -1;\n        }\n      }\n\n      std::vector<std::tuple<dyn::apiproc *, const char *>> funcs {\n        { (dyn::apiproc *) &get_extension_data, \"xcb_get_extension_data\" },\n        { (dyn::apiproc *) &get_setup, \"xcb_get_setup\" },\n        { (dyn::apiproc *) &disconnect, \"xcb_disconnect\" },\n        { (dyn::apiproc *) &connection_has_error, \"xcb_connection_has_error\" },\n        { (dyn::apiproc *) &connect, \"xcb_connect\" },\n        { (dyn::apiproc *) &setup_roots_iterator, \"xcb_setup_roots_iterator\" },\n        { (dyn::apiproc *) &generate_id, \"xcb_generate_id\" },\n      };\n\n      if (dyn::load(handle, funcs)) {\n        return -1;\n      }\n\n      funcs_loaded = true;\n      return 0;\n    }\n\n#undef _FN\n  }  // namespace xcb\n\n  void\n  freeImage(XImage *);\n  void\n  freeX(XFixesCursorImage *);\n\n  using xcb_connect_t = util::dyn_safe_ptr<xcb_connection_t, &xcb::disconnect>;\n  using xcb_img_t = util::c_ptr<xcb_shm_get_image_reply_t>;\n\n  using ximg_t = util::safe_ptr<XImage, freeImage>;\n  using xcursor_t = util::safe_ptr<XFixesCursorImage, freeX>;\n\n  using crtc_info_t = util::dyn_safe_ptr<_XRRCrtcInfo, &x11::rr::FreeCrtcInfo>;\n  using output_info_t = util::dyn_safe_ptr<_XRROutputInfo, &x11::rr::FreeOutputInfo>;\n  using screen_res_t = util::dyn_safe_ptr<_XRRScreenResources, &x11::rr::FreeScreenResources>;\n\n  class shm_id_t {\n  public:\n    shm_id_t():\n        id { -1 } {}\n    shm_id_t(int id):\n        id { id } {}\n    shm_id_t(shm_id_t &&other) noexcept:\n        id(other.id) {\n      other.id = -1;\n    }\n\n    ~shm_id_t() {\n      if (id != -1) {\n        shmctl(id, IPC_RMID, nullptr);\n        id = -1;\n      }\n    }\n    int id;\n  };\n\n  class shm_data_t {\n  public:\n    shm_data_t():\n        data { (void *) -1 } {}\n    shm_data_t(void *data):\n        data { data } {}\n\n    shm_data_t(shm_data_t &&other) noexcept:\n        data(other.data) {\n      other.data = (void *) -1;\n    }\n\n    ~shm_data_t() {\n      if ((std::uintptr_t) data != -1) {\n        shmdt(data);\n      }\n    }\n\n    void *data;\n  };\n\n  struct x11_img_t: public img_t {\n    ximg_t img;\n  };\n\n  struct shm_img_t: public img_t {\n    ~shm_img_t() override {\n      delete[] data;\n      data = nullptr;\n    }\n  };\n\n  static void\n  blend_cursor(Display *display, img_t &img, int offsetX, int offsetY) {\n    xcursor_t overlay { x11::fix::GetCursorImage(display) };\n\n    if (!overlay) {\n      BOOST_LOG(error) << \"Couldn't get cursor from XFixesGetCursorImage\"sv;\n      return;\n    }\n\n    overlay->x -= overlay->xhot;\n    overlay->y -= overlay->yhot;\n\n    overlay->x -= offsetX;\n    overlay->y -= offsetY;\n\n    overlay->x = std::max((short) 0, overlay->x);\n    overlay->y = std::max((short) 0, overlay->y);\n\n    auto pixels = (int *) img.data;\n\n    auto screen_height = img.height;\n    auto screen_width = img.width;\n\n    auto delta_height = std::min<uint16_t>(overlay->height, std::max(0, screen_height - overlay->y));\n    auto delta_width = std::min<uint16_t>(overlay->width, std::max(0, screen_width - overlay->x));\n    for (auto y = 0; y < delta_height; ++y) {\n      auto overlay_begin = &overlay->pixels[y * overlay->width];\n      auto overlay_end = &overlay->pixels[y * overlay->width + delta_width];\n\n      auto pixels_begin = &pixels[(y + overlay->y) * (img.row_pitch / img.pixel_pitch) + overlay->x];\n\n      std::for_each(overlay_begin, overlay_end, [&](long pixel) {\n        int *pixel_p = (int *) &pixel;\n\n        auto colors_in = (uint8_t *) pixels_begin;\n\n        auto alpha = (*(uint *) pixel_p) >> 24u;\n        if (alpha == 255) {\n          *pixels_begin = *pixel_p;\n        }\n        else {\n          auto colors_out = (uint8_t *) pixel_p;\n          colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255;\n          colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255;\n          colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255;\n        }\n        ++pixels_begin;\n      });\n    }\n  }\n\n  struct x11_attr_t: public display_t {\n    std::chrono::nanoseconds delay;\n\n    x11::xdisplay_t xdisplay;\n    Window xwindow;\n    XWindowAttributes xattr;\n\n    mem_type_e mem_type;\n\n    /**\n     * Last X (NOT the streamed monitor!) size.\n     * This way we can trigger reinitialization if the dimensions changed while streaming\n     */\n    // int env_width, env_height;\n\n    x11_attr_t(mem_type_e mem_type):\n        xdisplay { x11::OpenDisplay(nullptr) }, xwindow {}, xattr {}, mem_type { mem_type } {\n      x11::InitThreads();\n    }\n\n    int\n    init(const std::string &display_name, const ::video::config_t &config) {\n      if (!xdisplay) {\n        BOOST_LOG(error) << \"Could not open X11 display\"sv;\n        return -1;\n      }\n\n      delay = std::chrono::nanoseconds { 1s } / config.framerate;\n\n      xwindow = DefaultRootWindow(xdisplay.get());\n\n      refresh();\n\n      int streamedMonitor = -1;\n      if (!display_name.empty()) {\n        streamedMonitor = (int) util::from_view(display_name);\n      }\n\n      if (streamedMonitor != -1) {\n        BOOST_LOG(info) << \"Configuring selected display (\"sv << streamedMonitor << \") to stream\"sv;\n        screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) };\n        int output = screenr->noutput;\n\n        output_info_t result;\n        int monitor = 0;\n        for (int x = 0; x < output; ++x) {\n          output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };\n          if (out_info) {\n            if (monitor++ == streamedMonitor) {\n              result = std::move(out_info);\n              break;\n            }\n          }\n        }\n\n        if (!result) {\n          BOOST_LOG(error) << \"Could not stream display number [\"sv << streamedMonitor << \"], there are only [\"sv << monitor << \"] displays.\"sv;\n          return -1;\n        }\n\n        if (result->crtc) {\n          crtc_info_t crt_info { x11::rr::GetCrtcInfo(xdisplay.get(), screenr.get(), result->crtc) };\n          BOOST_LOG(info)\n            << \"Streaming display: \"sv << result->name << \" with res \"sv << crt_info->width << 'x' << crt_info->height << \" offset by \"sv << crt_info->x << 'x' << crt_info->y;\n\n          width = crt_info->width;\n          height = crt_info->height;\n          offset_x = crt_info->x;\n          offset_y = crt_info->y;\n        }\n        else {\n          BOOST_LOG(warning) << \"Couldn't get requested display info, defaulting to recording entire virtual desktop\"sv;\n          width = xattr.width;\n          height = xattr.height;\n        }\n      }\n      else {\n        width = xattr.width;\n        height = xattr.height;\n      }\n\n      env_width = xattr.width;\n      env_height = xattr.height;\n\n      return 0;\n    }\n\n    /**\n     * Called when the display attributes should change.\n     */\n    void\n    refresh() {\n      x11::GetWindowAttributes(xdisplay.get(), xwindow, &xattr);  // Update xattr's\n    }\n\n    capture_e\n    capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {\n      auto next_frame = std::chrono::steady_clock::now();\n\n      sleep_overshoot_logger.reset();\n\n      while (true) {\n        auto now = std::chrono::steady_clock::now();\n\n        if (next_frame > now) {\n          std::this_thread::sleep_for(next_frame - now);\n          sleep_overshoot_logger.first_point(next_frame);\n          sleep_overshoot_logger.second_point_now_and_log();\n        }\n\n        next_frame += delay;\n        if (next_frame < now) {  // some major slowdown happened; we couldn't keep up\n          next_frame = now + delay;\n        }\n\n        std::shared_ptr<platf::img_t> img_out;\n        auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);\n        switch (status) {\n          case platf::capture_e::reinit:\n          case platf::capture_e::error:\n          case platf::capture_e::interrupted:\n            return status;\n          case platf::capture_e::timeout:\n            if (!push_captured_image_cb(std::move(img_out), false)) {\n              return platf::capture_e::ok;\n            }\n            break;\n          case platf::capture_e::ok:\n            if (!push_captured_image_cb(std::move(img_out), true)) {\n              return platf::capture_e::ok;\n            }\n            break;\n          default:\n            BOOST_LOG(error) << \"Unrecognized capture status [\"sv << (int) status << ']';\n            return status;\n        }\n      }\n\n      return capture_e::ok;\n    }\n\n    capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {\n      refresh();\n\n      // The whole X server changed, so we must reinit everything\n      if (xattr.width != env_width || xattr.height != env_height) {\n        BOOST_LOG(warning) << \"X dimensions changed in non-SHM mode, request reinit\"sv;\n        return capture_e::reinit;\n      }\n\n      if (!pull_free_image_cb(img_out)) {\n        return platf::capture_e::interrupted;\n      }\n      auto img = (x11_img_t *) img_out.get();\n\n      XImage *x_img { x11::GetImage(xdisplay.get(), xwindow, offset_x, offset_y, width, height, AllPlanes, ZPixmap) };\n      img->frame_timestamp = std::chrono::steady_clock::now();\n\n      img->width = x_img->width;\n      img->height = x_img->height;\n      img->data = (uint8_t *) x_img->data;\n      img->row_pitch = x_img->bytes_per_line;\n      img->pixel_pitch = x_img->bits_per_pixel / 8;\n      img->img.reset(x_img);\n\n      if (cursor) {\n        blend_cursor(xdisplay.get(), *img, offset_x, offset_y);\n      }\n\n      return capture_e::ok;\n    }\n\n    std::shared_ptr<img_t>\n    alloc_img() override {\n      return std::make_shared<x11_img_t>();\n    }\n\n    std::unique_ptr<avcodec_encode_device_t>\n    make_avcodec_encode_device(pix_fmt_e pix_fmt) override {\n#ifdef SUNSHINE_BUILD_VAAPI\n      if (mem_type == mem_type_e::vaapi) {\n        return va::make_avcodec_encode_device(width, height, false);\n      }\n#endif\n\n#ifdef SUNSHINE_BUILD_CUDA\n      if (mem_type == mem_type_e::cuda) {\n        return cuda::make_avcodec_encode_device(width, height, false);\n      }\n#endif\n\n      return std::make_unique<avcodec_encode_device_t>();\n    }\n\n    int\n    dummy_img(img_t *img) override {\n      // TODO: stop cheating and give black image\n      if (!img) {\n        return -1;\n      };\n      auto pull_dummy_img_callback = [&img](std::shared_ptr<platf::img_t> &img_out) -> bool {\n        img_out = img->shared_from_this();\n        return true;\n      };\n      std::shared_ptr<platf::img_t> img_out;\n      snapshot(pull_dummy_img_callback, img_out, 0s, true);\n      return 0;\n    }\n  };\n\n  struct shm_attr_t: public x11_attr_t {\n    x11::xdisplay_t shm_xdisplay;  // Prevent race condition with x11_attr_t::xdisplay\n    xcb_connect_t xcb;\n    xcb_screen_t *display;\n    std::uint32_t seg;\n\n    shm_id_t shm_id;\n\n    shm_data_t data;\n\n    task_pool_util::TaskPool::task_id_t refresh_task_id;\n\n    void\n    delayed_refresh() {\n      refresh();\n\n      refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;\n    }\n\n    shm_attr_t(mem_type_e mem_type):\n        x11_attr_t(mem_type), shm_xdisplay { x11::OpenDisplay(nullptr) } {\n      refresh_task_id = task_pool.pushDelayed(&shm_attr_t::delayed_refresh, 2s, this).task_id;\n    }\n\n    ~shm_attr_t() override {\n      while (!task_pool.cancel(refresh_task_id))\n        ;\n    }\n\n    capture_e\n    capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {\n      auto next_frame = std::chrono::steady_clock::now();\n\n      sleep_overshoot_logger.reset();\n\n      while (true) {\n        auto now = std::chrono::steady_clock::now();\n\n        if (next_frame > now) {\n          std::this_thread::sleep_for(next_frame - now);\n          sleep_overshoot_logger.first_point(next_frame);\n          sleep_overshoot_logger.second_point_now_and_log();\n        }\n\n        next_frame += delay;\n        if (next_frame < now) {  // some major slowdown happened; we couldn't keep up\n          next_frame = now + delay;\n        }\n\n        std::shared_ptr<platf::img_t> img_out;\n        auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor);\n        switch (status) {\n          case platf::capture_e::reinit:\n          case platf::capture_e::error:\n          case platf::capture_e::interrupted:\n            return status;\n          case platf::capture_e::timeout:\n            if (!push_captured_image_cb(std::move(img_out), false)) {\n              return platf::capture_e::ok;\n            }\n            break;\n          case platf::capture_e::ok:\n            if (!push_captured_image_cb(std::move(img_out), true)) {\n              return platf::capture_e::ok;\n            }\n            break;\n          default:\n            BOOST_LOG(error) << \"Unrecognized capture status [\"sv << (int) status << ']';\n            return status;\n        }\n      }\n\n      return capture_e::ok;\n    }\n\n    capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor) {\n      // The whole X server changed, so we must reinit everything\n      if (xattr.width != env_width || xattr.height != env_height) {\n        BOOST_LOG(warning) << \"X dimensions changed in SHM mode, request reinit\"sv;\n        return capture_e::reinit;\n      }\n      else {\n        auto img_cookie = xcb::shm_get_image_unchecked(xcb.get(), display->root, offset_x, offset_y, width, height, ~0, XCB_IMAGE_FORMAT_Z_PIXMAP, seg, 0);\n        auto frame_timestamp = std::chrono::steady_clock::now();\n\n        xcb_img_t img_reply { xcb::shm_get_image_reply(xcb.get(), img_cookie, nullptr) };\n        if (!img_reply) {\n          BOOST_LOG(error) << \"Could not get image reply\"sv;\n          return capture_e::reinit;\n        }\n\n        if (!pull_free_image_cb(img_out)) {\n          return platf::capture_e::interrupted;\n        }\n\n        std::copy_n((std::uint8_t *) data.data, frame_size(), img_out->data);\n        img_out->frame_timestamp = frame_timestamp;\n\n        if (cursor) {\n          blend_cursor(shm_xdisplay.get(), *img_out, offset_x, offset_y);\n        }\n\n        return capture_e::ok;\n      }\n    }\n\n    std::shared_ptr<img_t>\n    alloc_img() override {\n      auto img = std::make_shared<shm_img_t>();\n      img->width = width;\n      img->height = height;\n      img->pixel_pitch = 4;\n      img->row_pitch = img->pixel_pitch * width;\n      img->data = new std::uint8_t[height * img->row_pitch];\n\n      return img;\n    }\n\n    int\n    dummy_img(platf::img_t *img) override {\n      return 0;\n    }\n\n    int\n    init(const std::string &display_name, const ::video::config_t &config) {\n      if (x11_attr_t::init(display_name, config)) {\n        return 1;\n      }\n\n      shm_xdisplay.reset(x11::OpenDisplay(nullptr));\n      xcb.reset(xcb::connect(nullptr, nullptr));\n      if (xcb::connection_has_error(xcb.get())) {\n        return -1;\n      }\n\n      if (!xcb::get_extension_data(xcb.get(), xcb::shm_id)->present) {\n        BOOST_LOG(error) << \"Missing SHM extension\"sv;\n\n        return -1;\n      }\n\n      auto iter = xcb::setup_roots_iterator(xcb::get_setup(xcb.get()));\n      display = iter.data;\n      seg = xcb::generate_id(xcb.get());\n\n      shm_id.id = shmget(IPC_PRIVATE, frame_size(), IPC_CREAT | 0777);\n      if (shm_id.id == -1) {\n        BOOST_LOG(error) << \"shmget failed\"sv;\n        return -1;\n      }\n\n      xcb::shm_attach(xcb.get(), seg, shm_id.id, false);\n      data.data = shmat(shm_id.id, nullptr, 0);\n\n      if ((uintptr_t) data.data == -1) {\n        BOOST_LOG(error) << \"shmat failed\"sv;\n\n        return -1;\n      }\n\n      return 0;\n    }\n\n    std::uint32_t\n    frame_size() {\n      return width * height * 4;\n    }\n  };\n\n  std::shared_ptr<display_t>\n  x11_display(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {\n    if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::vaapi && hwdevice_type != platf::mem_type_e::cuda) {\n      BOOST_LOG(error) << \"Could not initialize x11 display with the given hw device type\"sv;\n      return nullptr;\n    }\n\n    if (xcb::init_shm() || xcb::init() || x11::init() || x11::rr::init() || x11::fix::init()) {\n      BOOST_LOG(error) << \"Couldn't init x11 libraries\"sv;\n\n      return nullptr;\n    }\n\n    // Attempt to use shared memory X11 to avoid copying the frame\n    auto shm_disp = std::make_shared<shm_attr_t>(hwdevice_type);\n\n    auto status = shm_disp->init(display_name, config);\n    if (status > 0) {\n      // x11_attr_t::init() failed, don't bother trying again.\n      return nullptr;\n    }\n\n    if (status == 0) {\n      return shm_disp;\n    }\n\n    // Fallback\n    auto x11_disp = std::make_shared<x11_attr_t>(hwdevice_type);\n    if (x11_disp->init(display_name, config)) {\n      return nullptr;\n    }\n\n    return x11_disp;\n  }\n\n  std::vector<std::string>\n  x11_display_names() {\n    if (load_x11() || load_xcb()) {\n      BOOST_LOG(error) << \"Couldn't init x11 libraries\"sv;\n\n      return {};\n    }\n\n    BOOST_LOG(info) << \"Detecting displays\"sv;\n\n    x11::xdisplay_t xdisplay { x11::OpenDisplay(nullptr) };\n    if (!xdisplay) {\n      return {};\n    }\n\n    auto xwindow = DefaultRootWindow(xdisplay.get());\n    screen_res_t screenr { x11::rr::GetScreenResources(xdisplay.get(), xwindow) };\n    int output = screenr->noutput;\n\n    int monitor = 0;\n    for (int x = 0; x < output; ++x) {\n      output_info_t out_info { x11::rr::GetOutputInfo(xdisplay.get(), screenr.get(), screenr->outputs[x]) };\n      if (out_info) {\n        BOOST_LOG(info) << \"Detected display: \"sv << out_info->name << \" (id: \"sv << monitor << \")\"sv << out_info->name << \" connected: \"sv << (out_info->connection == RR_Connected);\n        ++monitor;\n      }\n    }\n\n    std::vector<std::string> names;\n    names.reserve(monitor);\n\n    for (auto x = 0; x < monitor; ++x) {\n      names.emplace_back(std::to_string(x));\n    }\n\n    return names;\n  }\n\n  void\n  freeImage(XImage *p) {\n    XDestroyImage(p);\n  }\n  void\n  freeX(XFixesCursorImage *p) {\n    x11::Free(p);\n  }\n\n  int\n  load_xcb() {\n    // This will be called once only\n    static int xcb_status = xcb::init_shm() || xcb::init();\n\n    return xcb_status;\n  }\n\n  int\n  load_x11() {\n    // This will be called once only\n    static int x11_status =\n      window_system == window_system_e::NONE ||\n      x11::init() || x11::rr::init() || x11::fix::init();\n\n    return x11_status;\n  }\n\n  namespace x11 {\n    std::optional<cursor_t>\n    cursor_t::make() {\n      if (load_x11()) {\n        return std::nullopt;\n      }\n\n      cursor_t cursor;\n\n      cursor.ctx.reset((cursor_ctx_t::pointer) x11::OpenDisplay(nullptr));\n\n      return cursor;\n    }\n\n    void\n    cursor_t::capture(egl::cursor_t &img) {\n      auto display = (xdisplay_t::pointer) ctx.get();\n\n      xcursor_t xcursor = fix::GetCursorImage(display);\n\n      if (img.serial != xcursor->cursor_serial) {\n        auto buf_size = xcursor->width * xcursor->height * sizeof(int);\n\n        if (img.buffer.size() < buf_size) {\n          img.buffer.resize(buf_size);\n        }\n\n        std::transform(xcursor->pixels, xcursor->pixels + buf_size / 4, (int *) img.buffer.data(), [](long pixel) -> int {\n          return pixel;\n        });\n      }\n\n      img.data = img.buffer.data();\n      img.width = img.src_w = xcursor->width;\n      img.height = img.src_h = xcursor->height;\n      img.x = xcursor->x - xcursor->xhot;\n      img.y = xcursor->y - xcursor->yhot;\n      img.pixel_pitch = 4;\n      img.row_pitch = img.pixel_pitch * img.width;\n      img.serial = xcursor->cursor_serial;\n    }\n\n    void\n    cursor_t::blend(img_t &img, int offsetX, int offsetY) {\n      blend_cursor((xdisplay_t::pointer) ctx.get(), img, offsetX, offsetY);\n    }\n\n    xdisplay_t\n    make_display() {\n      return OpenDisplay(nullptr);\n    }\n\n    void\n    freeDisplay(_XDisplay *xdisplay) {\n      CloseDisplay(xdisplay);\n    }\n\n    void\n    freeCursorCtx(cursor_ctx_t::pointer ctx) {\n      CloseDisplay((xdisplay_t::pointer) ctx);\n    }\n  }  // namespace x11\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/linux/x11grab.h",
    "content": "/**\n * @file src/platform/linux/x11grab.h\n * @brief Declarations for x11 capture.\n */\n#pragma once\n\n#include <optional>\n\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\n// X11 Display\nextern \"C\" struct _XDisplay;\n\nnamespace egl {\n  class cursor_t;\n}\n\nnamespace platf::x11 {\n  struct cursor_ctx_raw_t;\n  void\n  freeCursorCtx(cursor_ctx_raw_t *ctx);\n  void\n  freeDisplay(_XDisplay *xdisplay);\n\n  using cursor_ctx_t = util::safe_ptr<cursor_ctx_raw_t, freeCursorCtx>;\n  using xdisplay_t = util::safe_ptr<_XDisplay, freeDisplay>;\n\n  class cursor_t {\n  public:\n    static std::optional<cursor_t>\n    make();\n\n    void\n    capture(egl::cursor_t &img);\n\n    /**\n     * Capture and blend the cursor into the image\n     *\n     * img <-- destination image\n     * offsetX, offsetY <--- Top left corner of the virtual screen\n     */\n    void\n    blend(img_t &img, int offsetX, int offsetY);\n\n    cursor_ctx_t ctx;\n  };\n\n  xdisplay_t\n  make_display();\n}  // namespace platf::x11\n"
  },
  {
    "path": "src/platform/macos/av_audio.h",
    "content": "/**\n * @file src/platform/macos/av_audio.h\n * @brief Declarations for audio capture on macOS.\n */\n#pragma once\n\n#import <AVFoundation/AVFoundation.h>\n\n#include \"third-party/TPCircularBuffer/TPCircularBuffer.h\"\n\n#define kBufferLength 4096\n\n@interface AVAudio: NSObject <AVCaptureAudioDataOutputSampleBufferDelegate> {\n@public\n  TPCircularBuffer audioSampleBuffer;\n}\n\n@property (nonatomic, assign) AVCaptureSession *audioCaptureSession;\n@property (nonatomic, assign) AVCaptureConnection *audioConnection;\n@property (nonatomic, assign) NSCondition *samplesArrivedSignal;\n\n+ (NSArray *)microphoneNames;\n+ (AVCaptureDevice *)findMicrophone:(NSString *)name;\n\n- (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels;\n\n@end\n"
  },
  {
    "path": "src/platform/macos/av_audio.m",
    "content": "/**\n * @file src/platform/macos/av_audio.m\n * @brief Definitions for audio capture on macOS.\n */\n#import \"av_audio.h\"\n\n@implementation AVAudio\n\n+ (NSArray<AVCaptureDevice *> *)microphones {\n  if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) { 10, 15, 0 })]) {\n    // This will generate a warning about AVCaptureDeviceDiscoverySession being\n    // unavailable before macOS 10.15, but we have a guard to prevent it from\n    // being called on those earlier systems.\n    // Unfortunately the supported way to silence this warning, using @available,\n    // produces linker errors for __isPlatformVersionAtLeast, so we have to use\n    // a different method.\n#pragma clang diagnostic push\n#pragma clang diagnostic ignored \"-Wunguarded-availability-new\"\n    AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInMicrophone,\n      AVCaptureDeviceTypeExternalUnknown]\n                                                                                                               mediaType:AVMediaTypeAudio\n                                                                                                                position:AVCaptureDevicePositionUnspecified];\n    return discoverySession.devices;\n#pragma clang diagnostic pop\n  }\n  else {\n    // We're intentionally using a deprecated API here specifically for versions\n    // of macOS where it's not deprecated, so we can ignore any deprecation\n    // warnings:\n#pragma clang diagnostic push\n#pragma clang diagnostic ignored \"-Wdeprecated-declarations\"\n    return [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];\n#pragma clang diagnostic pop\n  }\n}\n\n+ (NSArray<NSString *> *)microphoneNames {\n  NSMutableArray *result = [[NSMutableArray alloc] init];\n\n  for (AVCaptureDevice *device in [AVAudio microphones]) {\n    [result addObject:[device localizedName]];\n  }\n\n  return result;\n}\n\n+ (AVCaptureDevice *)findMicrophone:(NSString *)name {\n  for (AVCaptureDevice *device in [AVAudio microphones]) {\n    if ([[device localizedName] isEqualToString:name]) {\n      return device;\n    }\n  }\n\n  return nil;\n}\n\n- (void)dealloc {\n  // make sure we don't process any further samples\n  self.audioConnection = nil;\n  // make sure nothing gets stuck on this signal\n  [self.samplesArrivedSignal signal];\n  [self.samplesArrivedSignal release];\n  TPCircularBufferCleanup(&audioSampleBuffer);\n  [super dealloc];\n}\n\n- (int)setupMicrophone:(AVCaptureDevice *)device sampleRate:(UInt32)sampleRate frameSize:(UInt32)frameSize channels:(UInt8)channels {\n  self.audioCaptureSession = [[AVCaptureSession alloc] init];\n\n  NSError *error;\n  AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];\n  if (audioInput == nil) {\n    return -1;\n  }\n\n  if ([self.audioCaptureSession canAddInput:audioInput]) {\n    [self.audioCaptureSession addInput:audioInput];\n  }\n  else {\n    [audioInput dealloc];\n    return -1;\n  }\n\n  AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];\n\n  [audioOutput setAudioSettings:@{\n    (NSString *) AVFormatIDKey: [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM],\n    (NSString *) AVSampleRateKey: [NSNumber numberWithUnsignedInt:sampleRate],\n    (NSString *) AVNumberOfChannelsKey: [NSNumber numberWithUnsignedInt:channels],\n    (NSString *) AVLinearPCMBitDepthKey: [NSNumber numberWithUnsignedInt:32],\n    (NSString *) AVLinearPCMIsFloatKey: @YES,\n    (NSString *) AVLinearPCMIsNonInterleaved: @NO\n  }];\n\n  dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT,\n    QOS_CLASS_USER_INITIATED,\n    DISPATCH_QUEUE_PRIORITY_HIGH);\n  dispatch_queue_t recordingQueue = dispatch_queue_create(\"audioSamplingQueue\", qos);\n\n  [audioOutput setSampleBufferDelegate:self queue:recordingQueue];\n\n  if ([self.audioCaptureSession canAddOutput:audioOutput]) {\n    [self.audioCaptureSession addOutput:audioOutput];\n  }\n  else {\n    [audioInput release];\n    [audioOutput release];\n    return -1;\n  }\n\n  self.audioConnection = [audioOutput connectionWithMediaType:AVMediaTypeAudio];\n\n  [self.audioCaptureSession startRunning];\n\n  [audioInput release];\n  [audioOutput release];\n\n  self.samplesArrivedSignal = [[NSCondition alloc] init];\n  TPCircularBufferInit(&self->audioSampleBuffer, kBufferLength * channels);\n\n  return 0;\n}\n\n- (void)captureOutput:(AVCaptureOutput *)output\n  didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer\n         fromConnection:(AVCaptureConnection *)connection {\n  if (connection == self.audioConnection) {\n    AudioBufferList audioBufferList;\n    CMBlockBufferRef blockBuffer;\n\n    CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, 0, &blockBuffer);\n\n    // NSAssert(audioBufferList.mNumberBuffers == 1, @\"Expected interleaved PCM format but buffer contained %u streams\", audioBufferList.mNumberBuffers);\n\n    // this is safe, because an interleaved PCM stream has exactly one buffer,\n    // and we don't want to do sanity checks in a performance critical exec path\n    AudioBuffer audioBuffer = audioBufferList.mBuffers[0];\n\n    TPCircularBufferProduceBytes(&self->audioSampleBuffer, audioBuffer.mData, audioBuffer.mDataByteSize);\n    [self.samplesArrivedSignal signal];\n  }\n}\n\n@end\n"
  },
  {
    "path": "src/platform/macos/av_img_t.h",
    "content": "/**\n * @file src/platform/macos/av_img_t.h\n * @brief Declarations for AV image types on macOS.\n */\n#pragma once\n\n#include \"src/platform/common.h\"\n\n#include <CoreMedia/CoreMedia.h>\n#include <CoreVideo/CoreVideo.h>\n\nnamespace platf {\n  struct av_sample_buf_t {\n    CMSampleBufferRef buf;\n\n    explicit av_sample_buf_t(CMSampleBufferRef buf):\n        buf((CMSampleBufferRef) CFRetain(buf)) {}\n\n    ~av_sample_buf_t() {\n      if (buf != nullptr) {\n        CFRelease(buf);\n      }\n    }\n  };\n\n  struct av_pixel_buf_t {\n    CVPixelBufferRef buf;\n\n    // Constructor\n    explicit av_pixel_buf_t(CMSampleBufferRef sb):\n        buf(\n          CMSampleBufferGetImageBuffer(sb)) {\n      CVPixelBufferLockBaseAddress(buf, kCVPixelBufferLock_ReadOnly);\n    }\n\n    [[nodiscard]] uint8_t *\n    data() const {\n      return static_cast<uint8_t *>(CVPixelBufferGetBaseAddress(buf));\n    }\n\n    // Destructor\n    ~av_pixel_buf_t() {\n      if (buf != nullptr) {\n        CVPixelBufferUnlockBaseAddress(buf, kCVPixelBufferLock_ReadOnly);\n      }\n    }\n  };\n\n  struct av_img_t: img_t {\n    std::shared_ptr<av_sample_buf_t> sample_buffer;\n    std::shared_ptr<av_pixel_buf_t> pixel_buffer;\n  };\n\n  struct temp_retain_av_img_t {\n    std::shared_ptr<av_sample_buf_t> sample_buffer;\n    std::shared_ptr<av_pixel_buf_t> pixel_buffer;\n    uint8_t *data;\n\n    temp_retain_av_img_t(\n      std::shared_ptr<av_sample_buf_t> sb,\n      std::shared_ptr<av_pixel_buf_t> pb,\n      uint8_t *dt):\n        sample_buffer(std::move(sb)),\n        pixel_buffer(std::move(pb)), data(dt) {}\n  };\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/macos/av_video.h",
    "content": "/**\n * @file src/platform/macos/av_video.h\n * @brief Declarations for video capture on macOS.\n */\n#pragma once\n\n#import <AVFoundation/AVFoundation.h>\n#import <AppKit/AppKit.h>\n\nstruct CaptureSession {\n  AVCaptureVideoDataOutput *output;\n  NSCondition *captureStopped;\n};\n\n@interface AVVideo: NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>\n\n#define kMaxDisplays 32\n\n@property (nonatomic, assign) CGDirectDisplayID displayID;\n@property (nonatomic, assign) CMTime minFrameDuration;\n@property (nonatomic, assign) OSType pixelFormat;\n@property (nonatomic, assign) int frameWidth;\n@property (nonatomic, assign) int frameHeight;\n\ntypedef bool (^FrameCallbackBlock)(CMSampleBufferRef);\n\n@property (nonatomic, assign) AVCaptureSession *session;\n@property (nonatomic, assign) NSMapTable<AVCaptureConnection *, AVCaptureVideoDataOutput *> *videoOutputs;\n@property (nonatomic, assign) NSMapTable<AVCaptureConnection *, FrameCallbackBlock> *captureCallbacks;\n@property (nonatomic, assign) NSMapTable<AVCaptureConnection *, dispatch_semaphore_t> *captureSignals;\n\n+ (NSArray<NSDictionary *> *)displayNames;\n+ (NSString *)getDisplayName:(CGDirectDisplayID)displayID;\n\n- (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate;\n\n- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight;\n- (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback;\n\n@end\n"
  },
  {
    "path": "src/platform/macos/av_video.m",
    "content": "/**\n * @file src/platform/macos/av_video.m\n * @brief Definitions for video capture on macOS.\n */\n#import \"av_video.h\"\n\n@implementation AVVideo\n\n// XXX: Currently, this function only returns the screen IDs as names,\n// which is not very helpful to the user. The API to retrieve names\n// was deprecated with 10.9+.\n// However, there is a solution with little external code that can be used:\n// https://stackoverflow.com/questions/20025868/cgdisplayioserviceport-is-deprecated-in-os-x-10-9-how-to-replace\n+ (NSArray<NSDictionary *> *)displayNames {\n  CGDirectDisplayID displays[kMaxDisplays];\n  uint32_t count;\n  if (CGGetActiveDisplayList(kMaxDisplays, displays, &count) != kCGErrorSuccess) {\n    return [NSArray array];\n  }\n\n  NSMutableArray *result = [NSMutableArray array];\n\n  for (uint32_t i = 0; i < count; i++) {\n    [result addObject:@{\n      @\"id\": [NSNumber numberWithUnsignedInt:displays[i]],\n      @\"name\": [NSString stringWithFormat:@\"%d\", displays[i]],\n      @\"displayName\": [self getDisplayName:displays[i]],\n    }];\n  }\n\n  return [NSArray arrayWithArray:result];\n}\n\n+ (NSString *)getDisplayName:(CGDirectDisplayID)displayID {\n  NSScreen *screens = [NSScreen screens];\n  for (NSScreen *screen in screens) {\n    if (screen.deviceDescription[@\"NSScreenNumber\"] == [NSNumber numberWithUnsignedInt:displayID]) {\n      return screen.localizedName;\n    }\n  }\n  return nil;\n}\n\n- (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate {\n  self = [super init];\n\n  CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayID);\n\n  self.displayID = displayID;\n  self.pixelFormat = kCVPixelFormatType_32BGRA;\n  self.frameWidth = (int) CGDisplayModeGetPixelWidth(mode);\n  self.frameHeight = (int) CGDisplayModeGetPixelHeight(mode);\n  self.minFrameDuration = CMTimeMake(1, frameRate);\n  self.session = [[AVCaptureSession alloc] init];\n  self.videoOutputs = [[NSMapTable alloc] init];\n  self.captureCallbacks = [[NSMapTable alloc] init];\n  self.captureSignals = [[NSMapTable alloc] init];\n\n  CFRelease(mode);\n\n  AVCaptureScreenInput *screenInput = [[AVCaptureScreenInput alloc] initWithDisplayID:self.displayID];\n  [screenInput setMinFrameDuration:self.minFrameDuration];\n\n  if ([self.session canAddInput:screenInput]) {\n    [self.session addInput:screenInput];\n  }\n  else {\n    [screenInput release];\n    return nil;\n  }\n\n  [self.session startRunning];\n\n  return self;\n}\n\n- (void)dealloc {\n  [self.videoOutputs release];\n  [self.captureCallbacks release];\n  [self.captureSignals release];\n  [self.session stopRunning];\n  [super dealloc];\n}\n\n- (void)setFrameWidth:(int)frameWidth frameHeight:(int)frameHeight {\n  self.frameWidth = frameWidth;\n  self.frameHeight = frameHeight;\n}\n\n- (dispatch_semaphore_t)capture:(FrameCallbackBlock)frameCallback {\n  @synchronized(self) {\n    AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];\n\n    [videoOutput setVideoSettings:@{\n      (NSString *) kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithUnsignedInt:self.pixelFormat],\n      (NSString *) kCVPixelBufferWidthKey: [NSNumber numberWithInt:self.frameWidth],\n      (NSString *) kCVPixelBufferHeightKey: [NSNumber numberWithInt:self.frameHeight],\n      (NSString *) AVVideoScalingModeKey: AVVideoScalingModeResizeAspect,\n    }];\n\n    dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL,\n      QOS_CLASS_USER_INITIATED,\n      DISPATCH_QUEUE_PRIORITY_HIGH);\n    dispatch_queue_t recordingQueue = dispatch_queue_create(\"videoCaptureQueue\", qos);\n    [videoOutput setSampleBufferDelegate:self queue:recordingQueue];\n\n    [self.session stopRunning];\n\n    if ([self.session canAddOutput:videoOutput]) {\n      [self.session addOutput:videoOutput];\n    }\n    else {\n      [videoOutput release];\n      return nil;\n    }\n\n    AVCaptureConnection *videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];\n    dispatch_semaphore_t signal = dispatch_semaphore_create(0);\n\n    [self.videoOutputs setObject:videoOutput forKey:videoConnection];\n    [self.captureCallbacks setObject:frameCallback forKey:videoConnection];\n    [self.captureSignals setObject:signal forKey:videoConnection];\n\n    [self.session startRunning];\n\n    return signal;\n  }\n}\n\n- (void)captureOutput:(AVCaptureOutput *)captureOutput\n  didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer\n         fromConnection:(AVCaptureConnection *)connection {\n  FrameCallbackBlock callback = [self.captureCallbacks objectForKey:connection];\n\n  if (callback != nil) {\n    if (!callback(sampleBuffer)) {\n      @synchronized(self) {\n        [self.session stopRunning];\n        [self.captureCallbacks removeObjectForKey:connection];\n        [self.session removeOutput:[self.videoOutputs objectForKey:connection]];\n        [self.videoOutputs removeObjectForKey:connection];\n        dispatch_semaphore_signal([self.captureSignals objectForKey:connection]);\n        [self.captureSignals removeObjectForKey:connection];\n        [self.session startRunning];\n      }\n    }\n  }\n}\n\n@end\n"
  },
  {
    "path": "src/platform/macos/display.mm",
    "content": "/**\n * @file src/platform/macos/display.mm\n * @brief Definitions for display capture on macOS.\n */\n#include \"src/platform/common.h\"\n#include \"src/platform/macos/av_img_t.h\"\n#include \"src/platform/macos/av_video.h\"\n#include \"src/platform/macos/nv12_zero_device.h\"\n\n#include \"src/config.h\"\n#include \"src/logging.h\"\n\n// Avoid conflict between AVFoundation and libavutil both defining AVMediaType\n#define AVMediaType AVMediaType_FFmpeg\n#include \"src/video.h\"\n#undef AVMediaType\n\nnamespace fs = std::filesystem;\n\nnamespace platf {\n  using namespace std::literals;\n\n  struct av_display_t: public display_t {\n    AVVideo *av_capture {};\n    CGDirectDisplayID display_id {};\n\n    ~av_display_t() override {\n      [av_capture release];\n    }\n\n    capture_e\n    capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {\n      auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) {\n        auto new_sample_buffer = std::make_shared<av_sample_buf_t>(sampleBuffer);\n        auto new_pixel_buffer = std::make_shared<av_pixel_buf_t>(new_sample_buffer->buf);\n\n        std::shared_ptr<img_t> img_out;\n        if (!pull_free_image_cb(img_out)) {\n          // got interrupt signal\n          // returning false here stops capture backend\n          return false;\n        }\n        auto av_img = std::static_pointer_cast<av_img_t>(img_out);\n\n        auto old_data_retainer = std::make_shared<temp_retain_av_img_t>(\n          av_img->sample_buffer,\n          av_img->pixel_buffer,\n          img_out->data);\n\n        av_img->sample_buffer = new_sample_buffer;\n        av_img->pixel_buffer = new_pixel_buffer;\n        img_out->data = new_pixel_buffer->data();\n\n        img_out->width = (int) CVPixelBufferGetWidth(new_pixel_buffer->buf);\n        img_out->height = (int) CVPixelBufferGetHeight(new_pixel_buffer->buf);\n        img_out->row_pitch = (int) CVPixelBufferGetBytesPerRow(new_pixel_buffer->buf);\n        img_out->pixel_pitch = img_out->row_pitch / img_out->width;\n\n        old_data_retainer = nullptr;\n\n        if (!push_captured_image_cb(std::move(img_out), true)) {\n          // got interrupt signal\n          // returning false here stops capture backend\n          return false;\n        }\n\n        return true;\n      }];\n\n      // FIXME: We should time out if an image isn't returned for a while\n      dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);\n\n      return capture_e::ok;\n    }\n\n    std::shared_ptr<img_t>\n    alloc_img() override {\n      return std::make_shared<av_img_t>();\n    }\n\n    std::unique_ptr<avcodec_encode_device_t>\n    make_avcodec_encode_device(pix_fmt_e pix_fmt) override {\n      if (pix_fmt == pix_fmt_e::yuv420p) {\n        av_capture.pixelFormat = kCVPixelFormatType_32BGRA;\n\n        return std::make_unique<avcodec_encode_device_t>();\n      }\n      else if (pix_fmt == pix_fmt_e::nv12 || pix_fmt == pix_fmt_e::p010) {\n        auto device = std::make_unique<nv12_zero_device>();\n\n        device->init(static_cast<void *>(av_capture), pix_fmt, setResolution, setPixelFormat);\n\n        return device;\n      }\n      else {\n        BOOST_LOG(error) << \"Unsupported Pixel Format.\"sv;\n        return nullptr;\n      }\n    }\n\n    int\n    dummy_img(img_t *img) override {\n      auto signal = [av_capture capture:^(CMSampleBufferRef sampleBuffer) {\n        auto new_sample_buffer = std::make_shared<av_sample_buf_t>(sampleBuffer);\n        auto new_pixel_buffer = std::make_shared<av_pixel_buf_t>(new_sample_buffer->buf);\n\n        auto av_img = (av_img_t *) img;\n\n        auto old_data_retainer = std::make_shared<temp_retain_av_img_t>(\n          av_img->sample_buffer,\n          av_img->pixel_buffer,\n          img->data);\n\n        av_img->sample_buffer = new_sample_buffer;\n        av_img->pixel_buffer = new_pixel_buffer;\n        img->data = new_pixel_buffer->data();\n\n        img->width = (int) CVPixelBufferGetWidth(new_pixel_buffer->buf);\n        img->height = (int) CVPixelBufferGetHeight(new_pixel_buffer->buf);\n        img->row_pitch = (int) CVPixelBufferGetBytesPerRow(new_pixel_buffer->buf);\n        img->pixel_pitch = img->row_pitch / img->width;\n\n        old_data_retainer = nullptr;\n\n        // returning false here stops capture backend\n        return false;\n      }];\n\n      dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);\n\n      return 0;\n    }\n\n    /**\n     * A bridge from the pure C++ code of the hwdevice_t class to the pure Objective C code.\n     *\n     * display --> an opaque pointer to an object of this class\n     * width --> the intended capture width\n     * height --> the intended capture height\n     */\n    static void\n    setResolution(void *display, int width, int height) {\n      [static_cast<AVVideo *>(display) setFrameWidth:width frameHeight:height];\n    }\n\n    static void\n    setPixelFormat(void *display, OSType pixelFormat) {\n      static_cast<AVVideo *>(display).pixelFormat = pixelFormat;\n    }\n  };\n\n  std::shared_ptr<display_t>\n  display(platf::mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {\n    if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::videotoolbox) {\n      BOOST_LOG(error) << \"Could not initialize display with the given hw device type.\"sv;\n      return nullptr;\n    }\n\n    auto display = std::make_shared<av_display_t>();\n\n    // Default to main display\n    display->display_id = CGMainDisplayID();\n\n    // Print all displays available with it's name and id\n    auto display_array = [AVVideo displayNames];\n    BOOST_LOG(info) << \"Detecting displays\"sv;\n    for (NSDictionary *item in display_array) {\n      NSNumber *display_id = item[@\"id\"];\n      // We need show display's product name and corresponding display number given by user\n      NSString *name = item[@\"displayName\"];\n      // We are using CGGetActiveDisplayList that only returns active displays so hardcoded connected value in log to true\n      BOOST_LOG(info) << \"Detected display: \"sv << name.UTF8String << \" (id: \"sv << [NSString stringWithFormat:@\"%@\", display_id].UTF8String << \") connected: true\"sv;\n      if (!display_name.empty() && std::atoi(display_name.c_str()) == [display_id unsignedIntValue]) {\n        display->display_id = [display_id unsignedIntValue];\n      }\n    }\n    BOOST_LOG(info) << \"Configuring selected display (\"sv << display->display_id << \") to stream\"sv;\n\n    display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate];\n\n    if (!display->av_capture) {\n      BOOST_LOG(error) << \"Video setup failed.\"sv;\n      return nullptr;\n    }\n\n    display->width = display->av_capture.frameWidth;\n    display->height = display->av_capture.frameHeight;\n    // We also need set env_width and env_height for absolute mouse coordinates\n    display->env_width = display->width;\n    display->env_height = display->height;\n\n    return display;\n  }\n\n  std::vector<std::string>\n  display_names(mem_type_e hwdevice_type) {\n    __block std::vector<std::string> display_names;\n\n    auto display_array = [AVVideo displayNames];\n\n    display_names.reserve([display_array count]);\n    [display_array enumerateObjectsUsingBlock:^(NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {\n      NSString *name = obj[@\"name\"];\n      display_names.emplace_back(name.UTF8String);\n    }];\n\n    return display_names;\n  }\n\n  /**\n   * @brief Returns if GPUs/drivers have changed since the last call to this function.\n   * @return `true` if a change has occurred or if it is unknown whether a change occurred.\n   */\n  bool\n  needs_encoder_reenumeration() {\n    // We don't track GPU state, so we will always reenumerate. Fortunately, it is fast on macOS.\n    return true;\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/macos/display_device.cpp",
    "content": "// local includes\n#include \"src/display_device/settings.h\"\n\nnamespace display_device {\n\n  device_info_map_t\n  enum_available_devices() {\n    // Not implemented\n    return {};\n  }\n\n  std::string\n  get_display_name(const std::string &value) {\n    // Not implemented, but just passthrough the value\n    return value;\n  }\n\n  device_display_mode_map_t\n  get_current_display_modes(const std::unordered_set<std::string> &) {\n    // Not implemented\n    return {};\n  }\n\n  bool\n  set_display_modes(const device_display_mode_map_t &) {\n    // Not implemented\n    return false;\n  }\n\n  bool\n  is_primary_device(const std::string &) {\n    // Not implemented\n    return false;\n  }\n\n  bool\n  set_as_primary_device(const std::string &) {\n    // Not implemented\n    return false;\n  }\n\n  hdr_state_map_t\n  get_current_hdr_states(const std::unordered_set<std::string> &) {\n    // Not implemented\n    return {};\n  }\n\n  bool\n  set_hdr_states(const hdr_state_map_t &) {\n    // Not implemented\n    return false;\n  }\n\n  active_topology_t\n  get_current_topology() {\n    // Not implemented\n    return {};\n  }\n\n  bool\n  is_topology_valid(const active_topology_t &topology) {\n    // Not implemented\n    return false;\n  }\n\n  bool\n  is_topology_the_same(const active_topology_t &a, const active_topology_t &b) {\n    // Not implemented\n    return false;\n  }\n\n  bool\n  set_topology(const active_topology_t &) {\n    // Not implemented\n    return false;\n  }\n\n  struct settings_t::audio_data_t {\n    // Not implemented\n  };\n\n  struct settings_t::persistent_data_t {\n    // Not implemented\n  };\n\n  settings_t::settings_t() {\n    // Not implemented\n  }\n\n  settings_t::~settings_t() {\n    // Not implemented\n  }\n\n  bool\n  settings_t::is_changing_settings_going_to_fail() const {\n    // Not implemented\n    return false;\n  }\n\n  settings_t::apply_result_t\n  settings_t::apply_config(const parsed_config_t &) {\n    // Not implemented\n    return { apply_result_t::result_e::success };\n  }\n\n  bool\n  settings_t::revert_settings(revert_reason_e reason, bool skip_vdd_destroy) {\n    // Not implemented\n    (void)reason;  // Unused parameter\n    (void)skip_vdd_destroy;  // Unused parameter\n    return true;\n  }\n\n  void\n  settings_t::reset_persistence() {\n    // Not implemented\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/macos/input.cpp",
    "content": "/**\n * @file src/platform/macos/input.cpp\n * @brief Definitions for macOS input handling.\n */\n#include \"src/input.h\"\n\n#import <Carbon/Carbon.h>\n#include <chrono>\n#include <mach/mach.h>\n\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n\n#include <ApplicationServices/ApplicationServices.h>\n#include <CoreFoundation/CoreFoundation.h>\n#include <iostream>\n#include <thread>\n\n/**\n * @brief Delay for a double click, in milliseconds.\n * @todo Make this configurable.\n */\nconstexpr std::chrono::milliseconds MULTICLICK_DELAY_MS(500);\n\nnamespace platf {\n  using namespace std::literals;\n\n  struct macos_input_t {\n  public:\n    CGDirectDisplayID display {};\n    CGFloat displayScaling {};\n    CGEventSourceRef source {};\n\n    // keyboard related stuff\n    CGEventRef kb_event {};\n    CGEventFlags kb_flags {};\n\n    // mouse related stuff\n    CGEventRef mouse_event {};  // mouse event source\n    bool mouse_down[3] {};  // mouse button status\n    std::chrono::steady_clock::steady_clock::time_point last_mouse_event[3][2];  // timestamp of last mouse events\n  };\n\n  // A struct to hold a Windows keycode to Mac virtual keycode mapping.\n  struct KeyCodeMap {\n    int win_keycode;\n    int mac_keycode;\n  };\n\n  // Customized less operator for using std::lower_bound() on a KeyCodeMap array.\n  bool\n  operator<(const KeyCodeMap &a, const KeyCodeMap &b) {\n    return a.win_keycode < b.win_keycode;\n  }\n\n  // clang-format off\nconst KeyCodeMap kKeyCodesMap[] = {\n  { 0x08 /* VKEY_BACK */,                      kVK_Delete              },\n  { 0x09 /* VKEY_TAB */,                       kVK_Tab                 },\n  { 0x0A /* VKEY_BACKTAB */,                   0x21E4                  },\n  { 0x0C /* VKEY_CLEAR */,                     kVK_ANSI_KeypadClear    },\n  { 0x0D /* VKEY_RETURN */,                    kVK_Return              },\n  { 0x10 /* VKEY_SHIFT */,                     kVK_Shift               },\n  { 0x11 /* VKEY_CONTROL */,                   kVK_Control             },\n  { 0x12 /* VKEY_MENU */,                      kVK_Option              },\n  { 0x13 /* VKEY_PAUSE */,                     -1                      },\n  { 0x14 /* VKEY_CAPITAL */,                   kVK_CapsLock            },\n  { 0x15 /* VKEY_KANA */,                      kVK_JIS_Kana            },\n  { 0x15 /* VKEY_HANGUL */,                    -1                      },\n  { 0x17 /* VKEY_JUNJA */,                     -1                      },\n  { 0x18 /* VKEY_FINAL */,                     -1                      },\n  { 0x19 /* VKEY_HANJA */,                     -1                      },\n  { 0x19 /* VKEY_KANJI */,                     -1                      },\n  { 0x1B /* VKEY_ESCAPE */,                    kVK_Escape              },\n  { 0x1C /* VKEY_CONVERT */,                   -1                      },\n  { 0x1D /* VKEY_NONCONVERT */,                -1                      },\n  { 0x1E /* VKEY_ACCEPT */,                    -1                      },\n  { 0x1F /* VKEY_MODECHANGE */,                -1                      },\n  { 0x20 /* VKEY_SPACE */,                     kVK_Space               },\n  { 0x21 /* VKEY_PRIOR */,                     kVK_PageUp              },\n  { 0x22 /* VKEY_NEXT */,                      kVK_PageDown            },\n  { 0x23 /* VKEY_END */,                       kVK_End                 },\n  { 0x24 /* VKEY_HOME */,                      kVK_Home                },\n  { 0x25 /* VKEY_LEFT */,                      kVK_LeftArrow           },\n  { 0x26 /* VKEY_UP */,                        kVK_UpArrow             },\n  { 0x27 /* VKEY_RIGHT */,                     kVK_RightArrow          },\n  { 0x28 /* VKEY_DOWN */,                      kVK_DownArrow           },\n  { 0x29 /* VKEY_SELECT */,                    -1                      },\n  { 0x2A /* VKEY_PRINT */,                     -1                      },\n  { 0x2B /* VKEY_EXECUTE */,                   -1                      },\n  { 0x2C /* VKEY_SNAPSHOT */,                  -1                      },\n  { 0x2D /* VKEY_INSERT */,                    kVK_Help                },\n  { 0x2E /* VKEY_DELETE */,                    kVK_ForwardDelete       },\n  { 0x2F /* VKEY_HELP */,                      kVK_Help                },\n  { 0x30 /* VKEY_0 */,                         kVK_ANSI_0              },\n  { 0x31 /* VKEY_1 */,                         kVK_ANSI_1              },\n  { 0x32 /* VKEY_2 */,                         kVK_ANSI_2              },\n  { 0x33 /* VKEY_3 */,                         kVK_ANSI_3              },\n  { 0x34 /* VKEY_4 */,                         kVK_ANSI_4              },\n  { 0x35 /* VKEY_5 */,                         kVK_ANSI_5              },\n  { 0x36 /* VKEY_6 */,                         kVK_ANSI_6              },\n  { 0x37 /* VKEY_7 */,                         kVK_ANSI_7              },\n  { 0x38 /* VKEY_8 */,                         kVK_ANSI_8              },\n  { 0x39 /* VKEY_9 */,                         kVK_ANSI_9              },\n  { 0x41 /* VKEY_A */,                         kVK_ANSI_A              },\n  { 0x42 /* VKEY_B */,                         kVK_ANSI_B              },\n  { 0x43 /* VKEY_C */,                         kVK_ANSI_C              },\n  { 0x44 /* VKEY_D */,                         kVK_ANSI_D              },\n  { 0x45 /* VKEY_E */,                         kVK_ANSI_E              },\n  { 0x46 /* VKEY_F */,                         kVK_ANSI_F              },\n  { 0x47 /* VKEY_G */,                         kVK_ANSI_G              },\n  { 0x48 /* VKEY_H */,                         kVK_ANSI_H              },\n  { 0x49 /* VKEY_I */,                         kVK_ANSI_I              },\n  { 0x4A /* VKEY_J */,                         kVK_ANSI_J              },\n  { 0x4B /* VKEY_K */,                         kVK_ANSI_K              },\n  { 0x4C /* VKEY_L */,                         kVK_ANSI_L              },\n  { 0x4D /* VKEY_M */,                         kVK_ANSI_M              },\n  { 0x4E /* VKEY_N */,                         kVK_ANSI_N              },\n  { 0x4F /* VKEY_O */,                         kVK_ANSI_O              },\n  { 0x50 /* VKEY_P */,                         kVK_ANSI_P              },\n  { 0x51 /* VKEY_Q */,                         kVK_ANSI_Q              },\n  { 0x52 /* VKEY_R */,                         kVK_ANSI_R              },\n  { 0x53 /* VKEY_S */,                         kVK_ANSI_S              },\n  { 0x54 /* VKEY_T */,                         kVK_ANSI_T              },\n  { 0x55 /* VKEY_U */,                         kVK_ANSI_U              },\n  { 0x56 /* VKEY_V */,                         kVK_ANSI_V              },\n  { 0x57 /* VKEY_W */,                         kVK_ANSI_W              },\n  { 0x58 /* VKEY_X */,                         kVK_ANSI_X              },\n  { 0x59 /* VKEY_Y */,                         kVK_ANSI_Y              },\n  { 0x5A /* VKEY_Z */,                         kVK_ANSI_Z              },\n  { 0x5B /* VKEY_LWIN */,                      kVK_Command             },\n  { 0x5C /* VKEY_RWIN */,                      kVK_RightCommand        },\n  { 0x5D /* VKEY_APPS */,                      kVK_RightCommand        },\n  { 0x5F /* VKEY_SLEEP */,                     -1                      },\n  { 0x60 /* VKEY_NUMPAD0 */,                   kVK_ANSI_Keypad0        },\n  { 0x61 /* VKEY_NUMPAD1 */,                   kVK_ANSI_Keypad1        },\n  { 0x62 /* VKEY_NUMPAD2 */,                   kVK_ANSI_Keypad2        },\n  { 0x63 /* VKEY_NUMPAD3 */,                   kVK_ANSI_Keypad3        },\n  { 0x64 /* VKEY_NUMPAD4 */,                   kVK_ANSI_Keypad4        },\n  { 0x65 /* VKEY_NUMPAD5 */,                   kVK_ANSI_Keypad5        },\n  { 0x66 /* VKEY_NUMPAD6 */,                   kVK_ANSI_Keypad6        },\n  { 0x67 /* VKEY_NUMPAD7 */,                   kVK_ANSI_Keypad7        },\n  { 0x68 /* VKEY_NUMPAD8 */,                   kVK_ANSI_Keypad8        },\n  { 0x69 /* VKEY_NUMPAD9 */,                   kVK_ANSI_Keypad9        },\n  { 0x6A /* VKEY_MULTIPLY */,                  kVK_ANSI_KeypadMultiply },\n  { 0x6B /* VKEY_ADD */,                       kVK_ANSI_KeypadPlus     },\n  { 0x6C /* VKEY_SEPARATOR */,                 -1                      },\n  { 0x6D /* VKEY_SUBTRACT */,                  kVK_ANSI_KeypadMinus    },\n  { 0x6E /* VKEY_DECIMAL */,                   kVK_ANSI_KeypadDecimal  },\n  { 0x6F /* VKEY_DIVIDE */,                    kVK_ANSI_KeypadDivide   },\n  { 0x70 /* VKEY_F1 */,                        kVK_F1                  },\n  { 0x71 /* VKEY_F2 */,                        kVK_F2                  },\n  { 0x72 /* VKEY_F3 */,                        kVK_F3                  },\n  { 0x73 /* VKEY_F4 */,                        kVK_F4                  },\n  { 0x74 /* VKEY_F5 */,                        kVK_F5                  },\n  { 0x75 /* VKEY_F6 */,                        kVK_F6                  },\n  { 0x76 /* VKEY_F7 */,                        kVK_F7                  },\n  { 0x77 /* VKEY_F8 */,                        kVK_F8                  },\n  { 0x78 /* VKEY_F9 */,                        kVK_F9                  },\n  { 0x79 /* VKEY_F10 */,                       kVK_F10                 },\n  { 0x7A /* VKEY_F11 */,                       kVK_F11                 },\n  { 0x7B /* VKEY_F12 */,                       kVK_F12                 },\n  { 0x7C /* VKEY_F13 */,                       kVK_F13                 },\n  { 0x7D /* VKEY_F14 */,                       kVK_F14                 },\n  { 0x7E /* VKEY_F15 */,                       kVK_F15                 },\n  { 0x7F /* VKEY_F16 */,                       kVK_F16                 },\n  { 0x80 /* VKEY_F17 */,                       kVK_F17                 },\n  { 0x81 /* VKEY_F18 */,                       kVK_F18                 },\n  { 0x82 /* VKEY_F19 */,                       kVK_F19                 },\n  { 0x83 /* VKEY_F20 */,                       kVK_F20                 },\n  { 0x84 /* VKEY_F21 */,                       -1                      },\n  { 0x85 /* VKEY_F22 */,                       -1                      },\n  { 0x86 /* VKEY_F23 */,                       -1                      },\n  { 0x87 /* VKEY_F24 */,                       -1                      },\n  { 0x90 /* VKEY_NUMLOCK */,                   -1                      },\n  { 0x91 /* VKEY_SCROLL */,                    -1                      },\n  { 0xA0 /* VKEY_LSHIFT */,                    kVK_Shift               },\n  { 0xA1 /* VKEY_RSHIFT */,                    kVK_RightShift          },\n  { 0xA2 /* VKEY_LCONTROL */,                  kVK_Control             },\n  { 0xA3 /* VKEY_RCONTROL */,                  kVK_RightControl        },\n  { 0xA4 /* VKEY_LMENU */,                     kVK_Option              },\n  { 0xA5 /* VKEY_RMENU */,                     kVK_RightOption         },\n  { 0xA6 /* VKEY_BROWSER_BACK */,              -1                      },\n  { 0xA7 /* VKEY_BROWSER_FORWARD */,           -1                      },\n  { 0xA8 /* VKEY_BROWSER_REFRESH */,           -1                      },\n  { 0xA9 /* VKEY_BROWSER_STOP */,              -1                      },\n  { 0xAA /* VKEY_BROWSER_SEARCH */,            -1                      },\n  { 0xAB /* VKEY_BROWSER_FAVORITES */,         -1                      },\n  { 0xAC /* VKEY_BROWSER_HOME */,              -1                      },\n  { 0xAD /* VKEY_VOLUME_MUTE */,               -1                      },\n  { 0xAE /* VKEY_VOLUME_DOWN */,               -1                      },\n  { 0xAF /* VKEY_VOLUME_UP */,                 -1                      },\n  { 0xB0 /* VKEY_MEDIA_NEXT_TRACK */,          -1                      },\n  { 0xB1 /* VKEY_MEDIA_PREV_TRACK */,          -1                      },\n  { 0xB2 /* VKEY_MEDIA_STOP */,                -1                      },\n  { 0xB3 /* VKEY_MEDIA_PLAY_PAUSE */,          -1                      },\n  { 0xB4 /* VKEY_MEDIA_LAUNCH_MAIL */,         -1                      },\n  { 0xB5 /* VKEY_MEDIA_LAUNCH_MEDIA_SELECT */, -1                      },\n  { 0xB6 /* VKEY_MEDIA_LAUNCH_APP1 */,         -1                      },\n  { 0xB7 /* VKEY_MEDIA_LAUNCH_APP2 */,         -1                      },\n  { 0xBA /* VKEY_OEM_1 */,                     kVK_ANSI_Semicolon      },\n  { 0xBB /* VKEY_OEM_PLUS */,                  kVK_ANSI_Equal          },\n  { 0xBC /* VKEY_OEM_COMMA */,                 kVK_ANSI_Comma          },\n  { 0xBD /* VKEY_OEM_MINUS */,                 kVK_ANSI_Minus          },\n  { 0xBE /* VKEY_OEM_PERIOD */,                kVK_ANSI_Period         },\n  { 0xBF /* VKEY_OEM_2 */,                     kVK_ANSI_Slash          },\n  { 0xC0 /* VKEY_OEM_3 */,                     kVK_ANSI_Grave          },\n  { 0xDB /* VKEY_OEM_4 */,                     kVK_ANSI_LeftBracket    },\n  { 0xDC /* VKEY_OEM_5 */,                     kVK_ANSI_Backslash      },\n  { 0xDD /* VKEY_OEM_6 */,                     kVK_ANSI_RightBracket   },\n  { 0xDE /* VKEY_OEM_7 */,                     kVK_ANSI_Quote          },\n  { 0xDF /* VKEY_OEM_8 */,                     -1                      },\n  { 0xE2 /* VKEY_OEM_102 */,                   -1                      },\n  { 0xE5 /* VKEY_PROCESSKEY */,                -1                      },\n  { 0xE7 /* VKEY_PACKET */,                    -1                      },\n  { 0xF6 /* VKEY_ATTN */,                      -1                      },\n  { 0xF7 /* VKEY_CRSEL */,                     -1                      },\n  { 0xF8 /* VKEY_EXSEL */,                     -1                      },\n  { 0xF9 /* VKEY_EREOF */,                     -1                      },\n  { 0xFA /* VKEY_PLAY */,                      -1                      },\n  { 0xFB /* VKEY_ZOOM */,                      -1                      },\n  { 0xFC /* VKEY_NONAME */,                    -1                      },\n  { 0xFD /* VKEY_PA1 */,                       -1                      },\n  { 0xFE /* VKEY_OEM_CLEAR */,                 kVK_ANSI_KeypadClear    }\n};\n  // clang-format on\n\n  int\n  keysym(int keycode) {\n    KeyCodeMap key_map {};\n\n    key_map.win_keycode = keycode;\n    const KeyCodeMap *temp_map = std::lower_bound(\n      kKeyCodesMap, kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]), key_map);\n\n    if (temp_map >= kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]) ||\n        temp_map->win_keycode != keycode || temp_map->mac_keycode == -1) {\n      return -1;\n    }\n\n    return temp_map->mac_keycode;\n  }\n\n  void\n  keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) {\n    auto key = keysym(modcode);\n\n    BOOST_LOG(debug) << \"got keycode: 0x\"sv << std::hex << modcode << \", translated to: 0x\" << std::hex << key << \", release:\" << release;\n\n    if (key < 0) {\n      return;\n    }\n\n    auto macos_input = ((macos_input_t *) input.get());\n    auto event = macos_input->kb_event;\n\n    if (key == kVK_Shift || key == kVK_RightShift ||\n        key == kVK_Command || key == kVK_RightCommand ||\n        key == kVK_Option || key == kVK_RightOption ||\n        key == kVK_Control || key == kVK_RightControl) {\n      CGEventFlags mask;\n\n      switch (key) {\n        case kVK_Shift:\n        case kVK_RightShift:\n          mask = kCGEventFlagMaskShift;\n          break;\n        case kVK_Command:\n        case kVK_RightCommand:\n          mask = kCGEventFlagMaskCommand;\n          break;\n        case kVK_Option:\n        case kVK_RightOption:\n          mask = kCGEventFlagMaskAlternate;\n          break;\n        case kVK_Control:\n        case kVK_RightControl:\n          mask = kCGEventFlagMaskControl;\n          break;\n      }\n\n      macos_input->kb_flags = release ? macos_input->kb_flags & ~mask : macos_input->kb_flags | mask;\n      CGEventSetType(event, kCGEventFlagsChanged);\n      CGEventSetFlags(event, macos_input->kb_flags);\n    }\n    else {\n      CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, key);\n      CGEventSetType(event, release ? kCGEventKeyUp : kCGEventKeyDown);\n    }\n\n    CGEventPost(kCGHIDEventTap, event);\n  }\n\n  void\n  unicode(input_t &input, char *utf8, int size) {\n    BOOST_LOG(info) << \"unicode: Unicode input not yet implemented for MacOS.\"sv;\n  }\n\n  int\n  alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {\n    BOOST_LOG(info) << \"alloc_gamepad: Gamepad not yet implemented for MacOS.\"sv;\n    return -1;\n  }\n\n  void\n  free_gamepad(input_t &input, int nr) {\n    BOOST_LOG(info) << \"free_gamepad: Gamepad not yet implemented for MacOS.\"sv;\n  }\n\n  void\n  gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) {\n    BOOST_LOG(info) << \"gamepad: Gamepad not yet implemented for MacOS.\"sv;\n  }\n\n  // returns current mouse location:\n  util::point_t\n  get_mouse_loc(input_t &input) {\n    // Creating a new event every time to avoid any reuse risk\n    const auto macos_input = static_cast<macos_input_t *>(input.get());\n    const auto snapshot_event = CGEventCreate(macos_input->source);\n    const auto current = CGEventGetLocation(snapshot_event);\n    CFRelease(snapshot_event);\n    return util::point_t {\n      current.x,\n      current.y\n    };\n  }\n\n  void\n  post_mouse(\n    input_t &input,\n    const CGMouseButton button,\n    const CGEventType type,\n    const util::point_t raw_location,\n    const util::point_t previous_location,\n    const int click_count) {\n    BOOST_LOG(debug) << \"mouse_event: \"sv << button << \", type: \"sv << type << \", location:\"sv << raw_location.x << \":\"sv << raw_location.y << \" click_count: \"sv << click_count;\n\n    const auto macos_input = static_cast<macos_input_t *>(input.get());\n    const auto display = macos_input->display;\n    const auto event = macos_input->mouse_event;\n\n    // get display bounds for current display\n    const CGRect display_bounds = CGDisplayBounds(display);\n\n    // limit mouse to current display bounds\n    const auto location = CGPoint {\n      std::clamp(raw_location.x, display_bounds.origin.x, display_bounds.origin.x + display_bounds.size.width - 1),\n      std::clamp(raw_location.y, display_bounds.origin.y, display_bounds.origin.y + display_bounds.size.height - 1)\n    };\n\n    CGEventSetType(event, type);\n    CGEventSetLocation(event, location);\n    CGEventSetIntegerValueField(event, kCGMouseEventButtonNumber, button);\n    CGEventSetIntegerValueField(event, kCGMouseEventClickState, click_count);\n\n    // Include deltas so some 3D applications can consume changes (game cameras, etc)\n    const double deltaX = raw_location.x - previous_location.x;\n    const double deltaY = raw_location.y - previous_location.y;\n    CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);\n    CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);\n\n    CGEventPost(kCGHIDEventTap, event);\n  }\n\n  inline CGEventType\n  event_type_mouse(input_t &input) {\n    const auto macos_input = static_cast<macos_input_t *>(input.get());\n\n    if (macos_input->mouse_down[0]) {\n      return kCGEventLeftMouseDragged;\n    }\n    if (macos_input->mouse_down[1]) {\n      return kCGEventOtherMouseDragged;\n    }\n    if (macos_input->mouse_down[2]) {\n      return kCGEventRightMouseDragged;\n    }\n    return kCGEventMouseMoved;\n  }\n\n  void\n  set_mouse_mode(int mode) {\n    // Virtual mouse driver is Windows-only; no-op on macOS\n  }\n\n  void\n  move_mouse(\n    input_t &input,\n    const int deltaX,\n    const int deltaY) {\n    const auto current = get_mouse_loc(input);\n\n    const auto location = util::point_t { current.x + deltaX, current.y + deltaY };\n    post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, current, 0);\n  }\n\n  void\n  abs_mouse(\n    input_t &input,\n    const touch_port_t &touch_port,\n    const float x,\n    const float y) {\n    const auto macos_input = static_cast<macos_input_t *>(input.get());\n    const auto scaling = macos_input->displayScaling;\n    const auto display = macos_input->display;\n\n    auto location = util::point_t { x * scaling, y * scaling };\n    CGRect display_bounds = CGDisplayBounds(display);\n    // in order to get the correct mouse location for capturing display , we need to add the display bounds to the location\n    location.x += display_bounds.origin.x;\n    location.y += display_bounds.origin.y;\n\n    post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, get_mouse_loc(input), 0);\n  }\n\n  void\n  button_mouse(input_t &input, const int button, const bool release) {\n    CGMouseButton mac_button;\n    CGEventType event;\n\n    const auto macos_input = static_cast<macos_input_t *>(input.get());\n\n    switch (button) {\n      case 1:\n        mac_button = kCGMouseButtonLeft;\n        event = release ? kCGEventLeftMouseUp : kCGEventLeftMouseDown;\n        break;\n      case 2:\n        mac_button = kCGMouseButtonCenter;\n        event = release ? kCGEventOtherMouseUp : kCGEventOtherMouseDown;\n        break;\n      case 3:\n        mac_button = kCGMouseButtonRight;\n        event = release ? kCGEventRightMouseUp : kCGEventRightMouseDown;\n        break;\n      default:\n        BOOST_LOG(warning) << \"Unsupported mouse button for MacOS: \"sv << button;\n        return;\n    }\n\n    macos_input->mouse_down[mac_button] = !release;\n\n    // if the last mouse down was less than MULTICLICK_DELAY_MS, we send a double click event\n    const auto now = std::chrono::steady_clock::now();\n    const auto mouse_position = get_mouse_loc(input);\n\n    if (now < macos_input->last_mouse_event[mac_button][release] + MULTICLICK_DELAY_MS) {\n      post_mouse(input, mac_button, event, mouse_position, mouse_position, 2);\n    }\n    else {\n      post_mouse(input, mac_button, event, mouse_position, mouse_position, 1);\n    }\n\n    macos_input->last_mouse_event[mac_button][release] = now;\n  }\n\n  void\n  scroll(input_t &input, const int high_res_distance) {\n    CGEventRef upEvent = CGEventCreateScrollWheelEvent(\n      nullptr,\n      kCGScrollEventUnitLine,\n      2, high_res_distance > 0 ? 1 : -1, high_res_distance);\n    CGEventPost(kCGHIDEventTap, upEvent);\n    CFRelease(upEvent);\n  }\n\n  void\n  hscroll(input_t &input, int high_res_distance) {\n    // Unimplemented\n  }\n\n  /**\n   * @brief Allocates a context to store per-client input data.\n   * @param input The global input context.\n   * @return A unique pointer to a per-client input data context.\n   */\n  std::unique_ptr<client_input_t>\n  allocate_client_input_context(input_t &input) {\n    // Unused\n    return nullptr;\n  }\n\n  /**\n   * @brief Sends a touch event to the OS.\n   * @param input The client-specific input context.\n   * @param touch_port The current viewport for translating to screen coordinates.\n   * @param touch The touch event.\n   */\n  void\n  touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) {\n    // Unimplemented feature - platform_caps::pen_touch\n  }\n\n  /**\n   * @brief Sends a pen event to the OS.\n   * @param input The client-specific input context.\n   * @param touch_port The current viewport for translating to screen coordinates.\n   * @param pen The pen event.\n   */\n  void\n  pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) {\n    // Unimplemented feature - platform_caps::pen_touch\n  }\n\n  /**\n   * @brief Sends a gamepad touch event to the OS.\n   * @param input The global input context.\n   * @param touch The touch event.\n   */\n  void\n  gamepad_touch(input_t &input, const gamepad_touch_t &touch) {\n    // Unimplemented feature - platform_caps::controller_touch\n  }\n\n  /**\n   * @brief Sends a gamepad motion event to the OS.\n   * @param input The global input context.\n   * @param motion The motion event.\n   */\n  void\n  gamepad_motion(input_t &input, const gamepad_motion_t &motion) {\n    // Unimplemented\n  }\n\n  /**\n   * @brief Sends a gamepad battery event to the OS.\n   * @param input The global input context.\n   * @param battery The battery event.\n   */\n  void\n  gamepad_battery(input_t &input, const gamepad_battery_t &battery) {\n    // Unimplemented\n  }\n\n  input_t\n  input() {\n    input_t result { new macos_input_t() };\n\n    const auto macos_input = static_cast<macos_input_t *>(result.get());\n\n    // Default to main display\n    macos_input->display = CGMainDisplayID();\n\n    auto output_name = config::video.output_name;\n    // If output_name is set, try to find the display with that display id\n    if (!output_name.empty()) {\n      uint32_t max_display = 32;\n      uint32_t display_count;\n      CGDirectDisplayID displays[max_display];\n      if (CGGetActiveDisplayList(max_display, displays, &display_count) != kCGErrorSuccess) {\n        BOOST_LOG(error) << \"Unable to get active display list , error: \"sv << std::endl;\n      }\n      else {\n        for (int i = 0; i < display_count; i++) {\n          CGDirectDisplayID display_id = displays[i];\n          if (display_id == std::atoi(output_name.c_str())) {\n            macos_input->display = display_id;\n          }\n        }\n      }\n    }\n\n    // Input coordinates are based on the virtual resolution not the physical, so we need the scaling factor\n    const CGDisplayModeRef mode = CGDisplayCopyDisplayMode(macos_input->display);\n    macos_input->displayScaling = ((CGFloat) CGDisplayPixelsWide(macos_input->display)) / ((CGFloat) CGDisplayModeGetPixelWidth(mode));\n    CFRelease(mode);\n\n    macos_input->source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);\n\n    macos_input->kb_event = CGEventCreate(macos_input->source);\n    macos_input->kb_flags = 0;\n\n    macos_input->mouse_event = CGEventCreate(macos_input->source);\n    macos_input->mouse_down[0] = false;\n    macos_input->mouse_down[1] = false;\n    macos_input->mouse_down[2] = false;\n\n    BOOST_LOG(debug) << \"Display \"sv << macos_input->display << \", pixel dimension: \" << CGDisplayPixelsWide(macos_input->display) << \"x\"sv << CGDisplayPixelsHigh(macos_input->display);\n\n    return result;\n  }\n\n  void\n  freeInput(void *p) {\n    const auto *input = static_cast<macos_input_t *>(p);\n\n    CFRelease(input->source);\n    CFRelease(input->kb_event);\n    CFRelease(input->mouse_event);\n\n    delete input;\n  }\n\n  std::vector<supported_gamepad_t> &\n  supported_gamepads(input_t *input) {\n    static std::vector gamepads {\n      supported_gamepad_t { \"\", false, \"gamepads.macos_not_implemented\" }\n    };\n\n    return gamepads;\n  }\n\n  /**\n   * @brief Returns the supported platform capabilities to advertise to the client.\n   * @return Capability flags.\n   */\n  platform_caps::caps_t\n  get_capabilities() {\n    return 0;\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/macos/microphone.mm",
    "content": "/**\n * @file src/platform/macos/microphone.mm\n * @brief Definitions for microphone capture on macOS.\n */\n#include \"src/platform/common.h\"\n#include \"src/platform/macos/av_audio.h\"\n\n#include \"src/config.h\"\n#include \"src/logging.h\"\n\nnamespace platf {\n  using namespace std::literals;\n\n  struct av_mic_t: public mic_t {\n    AVAudio *av_audio_capture {};\n\n    ~av_mic_t() override {\n      [av_audio_capture release];\n    }\n\n    capture_e\n    sample(std::vector<float> &sample_in) override {\n      auto sample_size = sample_in.size();\n\n      uint32_t length = 0;\n      void *byteSampleBuffer = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &length);\n\n      while (length < sample_size * sizeof(float)) {\n        [av_audio_capture.samplesArrivedSignal wait];\n        byteSampleBuffer = TPCircularBufferTail(&av_audio_capture->audioSampleBuffer, &length);\n      }\n\n      const float *sampleBuffer = (float *) byteSampleBuffer;\n      std::vector<float> vectorBuffer(sampleBuffer, sampleBuffer + sample_size);\n\n      std::copy_n(std::begin(vectorBuffer), sample_size, std::begin(sample_in));\n\n      TPCircularBufferConsume(&av_audio_capture->audioSampleBuffer, sample_size * sizeof(float));\n\n      return capture_e::ok;\n    }\n  };\n\n  struct macos_audio_control_t: public audio_control_t {\n    AVCaptureDevice *audio_capture_device {};\n\n  public:\n    int\n    set_sink(const std::string &sink) override {\n      BOOST_LOG(warning) << \"audio_control_t::set_sink() unimplemented: \"sv << sink;\n      return 0;\n    }\n\n    std::unique_ptr<mic_t>\n    microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {\n      auto mic = std::make_unique<av_mic_t>();\n      const char *audio_sink = \"\";\n\n      if (!config::audio.sink.empty()) {\n        audio_sink = config::audio.sink.c_str();\n      }\n\n      if ((audio_capture_device = [AVAudio findMicrophone:[NSString stringWithUTF8String:audio_sink]]) == nullptr) {\n        BOOST_LOG(error) << \"opening microphone '\"sv << audio_sink << \"' failed. Please set a valid input source in the Sunshine config.\"sv;\n        BOOST_LOG(error) << \"Available inputs:\"sv;\n\n        for (NSString *name in [AVAudio microphoneNames]) {\n          BOOST_LOG(error) << \"\\t\"sv << [name UTF8String];\n        }\n\n        return nullptr;\n      }\n\n      mic->av_audio_capture = [[AVAudio alloc] init];\n\n      if ([mic->av_audio_capture setupMicrophone:audio_capture_device sampleRate:sample_rate frameSize:frame_size channels:channels]) {\n        BOOST_LOG(error) << \"Failed to setup microphone.\"sv;\n        return nullptr;\n      }\n\n      return mic;\n    }\n\n    std::optional<sink_t>\n    sink_info() override {\n      sink_t sink;\n\n      return sink;\n    }\n  };\n\n  std::unique_ptr<audio_control_t>\n  audio_control() {\n    return std::make_unique<macos_audio_control_t>();\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/macos/misc.h",
    "content": "/**\n * @file src/platform/macos/misc.h\n * @brief Miscellaneous declarations for macOS platform.\n */\n#pragma once\n\n#include <vector>\n\n#include <CoreGraphics/CoreGraphics.h>\n\nnamespace dyn {\n  typedef void (*apiproc)();\n\n  int\n  load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict = true);\n  void *\n  handle(const std::vector<const char *> &libs);\n\n}  // namespace dyn\n"
  },
  {
    "path": "src/platform/macos/misc.mm",
    "content": "/**\n * @file src/platform/macos/misc.mm\n * @brief Miscellaneous definitions for macOS platform.\n */\n\n// Required for IPV6_PKTINFO with Darwin headers\n#ifndef __APPLE_USE_RFC_3542  // NOLINT(bugprone-reserved-identifier)\n  #define __APPLE_USE_RFC_3542 1\n#endif\n\n#include <Foundation/Foundation.h>\n#include <arpa/inet.h>\n#include <dlfcn.h>\n#include <fcntl.h>\n#include <ifaddrs.h>\n#include <mach-o/dyld.h>\n#include <net/if_dl.h>\n#include <pwd.h>\n\n#include \"misc.h\"\n#include \"src/entry_handler.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/platform/run_command.h\"\n\n#include <boost/asio/ip/address.hpp>\n#include <boost/asio/ip/host_name.hpp>\n#include <boost/process/v1.hpp>\n\nusing namespace std::literals;\nnamespace fs = std::filesystem;\nnamespace bp = boost::process::v1;\n\nnamespace platf {\n\n// Even though the following two functions are available starting in macOS 10.15, they weren't\n// actually in the Mac SDK until Xcode 12.2, the first to include the SDK for macOS 11\n#if __MAC_OS_X_VERSION_MAX_ALLOWED < 110000  // __MAC_11_0\n  // If they're not in the SDK then we can use our own function definitions.\n  // Need to use weak import so that this will link in macOS 10.14 and earlier\n  extern \"C\" bool\n  CGPreflightScreenCaptureAccess(void) __attribute__((weak_import));\n  extern \"C\" bool\n  CGRequestScreenCaptureAccess(void) __attribute__((weak_import));\n#endif\n\n  std::unique_ptr<deinit_t>\n  init() {\n    // This will generate a warning about CGPreflightScreenCaptureAccess and\n    // CGRequestScreenCaptureAccess being unavailable before macOS 10.15, but\n    // we have a guard to prevent it from being called on those earlier systems.\n    // Unfortunately the supported way to silence this warning, using @available,\n    // produces linker errors for __isPlatformVersionAtLeast, so we have to use\n    // a different method.\n    // We also ignore \"tautological-pointer-compare\" because when compiling with\n    // Xcode 12.2 and later, these functions are not weakly linked and will never\n    // be null, and therefore generate this warning. Since we are weakly linking\n    // when compiling with earlier Xcode versions, the check for null is\n    // necessary, and so we ignore the warning.\n#pragma clang diagnostic push\n#pragma clang diagnostic ignored \"-Wunguarded-availability-new\"\n#pragma clang diagnostic ignored \"-Wtautological-pointer-compare\"\n    if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) { 10, 15, 0 })] &&\n        // Double check that these weakly-linked symbols have been loaded:\n        CGPreflightScreenCaptureAccess != nullptr && CGRequestScreenCaptureAccess != nullptr &&\n        !CGPreflightScreenCaptureAccess()) {\n      BOOST_LOG(error) << \"No screen capture permission!\"sv;\n      BOOST_LOG(error) << \"Please activate it in 'System Preferences' -> 'Privacy' -> 'Screen Recording'\"sv;\n      CGRequestScreenCaptureAccess();\n      return nullptr;\n    }\n#pragma clang diagnostic pop\n    return std::make_unique<deinit_t>();\n  }\n\n  fs::path\n  appdata() {\n    const char *homedir;\n    if ((homedir = getenv(\"HOME\")) == nullptr) {\n      homedir = getpwuid(geteuid())->pw_dir;\n    }\n\n    return fs::path { homedir } / \".config/sunshine\"sv;\n  }\n\n  using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;\n\n  ifaddr_t\n  get_ifaddrs() {\n    ifaddrs *p { nullptr };\n\n    getifaddrs(&p);\n\n    return ifaddr_t { p };\n  }\n\n  std::string\n  from_sockaddr(const sockaddr *const ip_addr) {\n    char data[INET6_ADDRSTRLEN] = {};\n\n    auto family = ip_addr->sa_family;\n    if (family == AF_INET6) {\n      inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data,\n        INET6_ADDRSTRLEN);\n    }\n    else if (family == AF_INET) {\n      inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data,\n        INET_ADDRSTRLEN);\n    }\n\n    return std::string { data };\n  }\n\n  std::pair<std::uint16_t, std::string>\n  from_sockaddr_ex(const sockaddr *const ip_addr) {\n    char data[INET6_ADDRSTRLEN] = {};\n\n    auto family = ip_addr->sa_family;\n    std::uint16_t port = 0;\n    if (family == AF_INET6) {\n      inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data,\n        INET6_ADDRSTRLEN);\n      port = ((sockaddr_in6 *) ip_addr)->sin6_port;\n    }\n    else if (family == AF_INET) {\n      inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data,\n        INET_ADDRSTRLEN);\n      port = ((sockaddr_in *) ip_addr)->sin_port;\n    }\n\n    return { port, std::string { data } };\n  }\n\n  std::string\n  get_mac_address(const std::string_view &address) {\n    auto ifaddrs = get_ifaddrs();\n\n    for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {\n      if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {\n        BOOST_LOG(verbose) << \"Looking for MAC of \"sv << pos->ifa_name;\n\n        struct ifaddrs *ifap, *ifaptr;\n        unsigned char *ptr;\n        std::string mac_address;\n\n        if (getifaddrs(&ifap) == 0) {\n          for (ifaptr = ifap; ifaptr != nullptr; ifaptr = (ifaptr)->ifa_next) {\n            if (!strcmp((ifaptr)->ifa_name, pos->ifa_name) && (((ifaptr)->ifa_addr)->sa_family == AF_LINK)) {\n              ptr = (unsigned char *) LLADDR((struct sockaddr_dl *) (ifaptr)->ifa_addr);\n              char buff[100];\n\n              snprintf(buff, sizeof(buff), \"%02x:%02x:%02x:%02x:%02x:%02x\",\n                *ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3), *(ptr + 4), *(ptr + 5));\n              mac_address = buff;\n              break;\n            }\n          }\n\n          freeifaddrs(ifap);\n\n          if (ifaptr != nullptr) {\n            BOOST_LOG(verbose) << \"Found MAC of \"sv << pos->ifa_name << \": \"sv << mac_address;\n            return mac_address;\n          }\n        }\n      }\n    }\n\n    BOOST_LOG(warning) << \"Unable to find MAC address for \"sv << address;\n    return \"00:00:00:00:00:00\"s;\n  }\n\n  bp::child\n  run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {\n    // clang-format off\n    if (!group) {\n      if (!file) {\n        return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_in < bp::null, bp::std_out > bp::null, bp::std_err > bp::null, bp::limit_handles, ec);\n      }\n      else {\n        return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_in < bp::null, bp::std_out > file, bp::std_err > file, bp::limit_handles, ec);\n      }\n    }\n    else {\n      if (!file) {\n        return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_in < bp::null, bp::std_out > bp::null, bp::std_err > bp::null, bp::limit_handles, ec, *group);\n      }\n      else {\n        return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_in < bp::null, bp::std_out > file, bp::std_err > file, bp::limit_handles, ec, *group);\n      }\n    }\n    // clang-format on\n  }\n\n  /**\n   * @brief Open a url in the default web browser.\n   * @param url The url to open.\n   */\n  void\n  open_url(const std::string &url) {\n    boost::filesystem::path working_dir;\n    std::string cmd = R\"(open \")\" + url + R\"(\")\";\n\n    boost::process::v1::environment _env = boost::this_process::environment();\n    std::error_code ec;\n    auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr);\n    if (ec) {\n      BOOST_LOG(warning) << \"Couldn't open url [\"sv << url << \"]: System: \"sv << ec.message();\n    }\n    else {\n      BOOST_LOG(info) << \"Opened url [\"sv << url << \"]\"sv;\n      child.detach();\n    }\n  }\n\n  /**\n   * @brief Open a url directly in the system default browser.\n   * @param url The url to open.\n   */\n  void\n  open_url_in_browser(const std::string &url) {\n    // On macOS, the open command handles this correctly\n    open_url(url);\n  }\n\n  void\n  adjust_thread_priority(thread_priority_e priority) {\n    // Unimplemented\n  }\n\n  void\n  streaming_will_start() {\n    // Nothing to do\n  }\n\n  void\n  streaming_will_stop() {\n    // Nothing to do\n  }\n\n  void\n  enter_away_mode() {\n    BOOST_LOG(info) << \"Away Mode is not yet implemented on macOS\"sv;\n  }\n\n  void\n  exit_away_mode() {\n    // No-op on macOS for now\n  }\n\n  bool\n  is_away_mode_active() {\n    return false;\n  }\n\n  bool\n  system_sleep() {\n    auto ret = std::system(\"pmset sleepnow\");\n    if (ret != 0) {\n      BOOST_LOG(error) << \"pmset sleepnow failed with code: \"sv << ret;\n      return false;\n    }\n    return true;\n  }\n\n  bool\n  system_hibernate() {\n    // macOS doesn't have a separate hibernate command, use sleep\n    return system_sleep();\n  }\n\n  void\n  restart_on_exit() {\n    char executable[2048];\n    uint32_t size = sizeof(executable);\n    if (_NSGetExecutablePath(executable, &size) < 0) {\n      BOOST_LOG(fatal) << \"NSGetExecutablePath() failed: \"sv << errno;\n      return;\n    }\n\n    // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves\n    int openmax = (int) sysconf(_SC_OPEN_MAX);\n    for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) {\n      close(fd);\n    }\n\n    // Re-exec ourselves with the same arguments\n    if (execv(executable, lifetime::get_argv()) < 0) {\n      BOOST_LOG(fatal) << \"execv() failed: \"sv << errno;\n      return;\n    }\n  }\n\n  void\n  restart() {\n    // Gracefully clean up and restart ourselves instead of exiting\n    atexit(restart_on_exit);\n    lifetime::exit_sunshine(0, true);\n  }\n\n  int\n  set_env(const std::string &name, const std::string &value) {\n    return setenv(name.c_str(), value.c_str(), 1);\n  }\n\n  int\n  unset_env(const std::string &name) {\n    return unsetenv(name.c_str());\n  }\n\n  bool\n  request_process_group_exit(std::uintptr_t native_handle) {\n    if (killpg((pid_t) native_handle, SIGTERM) == 0 || errno == ESRCH) {\n      BOOST_LOG(debug) << \"Successfully sent SIGTERM to process group: \"sv << native_handle;\n      return true;\n    }\n    else {\n      BOOST_LOG(warning) << \"Unable to send SIGTERM to process group [\"sv << native_handle << \"]: \"sv << errno;\n      return false;\n    }\n  }\n\n  bool\n  process_group_running(std::uintptr_t native_handle) {\n    return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0;\n  }\n\n  struct sockaddr_in\n  to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) {\n    struct sockaddr_in saddr_v4 = {};\n\n    saddr_v4.sin_family = AF_INET;\n    saddr_v4.sin_port = htons(port);\n\n    auto addr_bytes = address.to_bytes();\n    memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr));\n\n    return saddr_v4;\n  }\n\n  struct sockaddr_in6\n  to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) {\n    struct sockaddr_in6 saddr_v6 = {};\n\n    saddr_v6.sin6_family = AF_INET6;\n    saddr_v6.sin6_port = htons(port);\n    saddr_v6.sin6_scope_id = address.scope_id();\n\n    auto addr_bytes = address.to_bytes();\n    memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr));\n\n    return saddr_v6;\n  }\n\n  bool\n  send_batch(batched_send_info_t &send_info) {\n    // Fall back to unbatched send calls\n    return false;\n  }\n\n  bool\n  send(send_info_t &send_info) {\n    auto sockfd = (int) send_info.native_socket;\n    struct msghdr msg = {};\n\n    // Convert the target address into a sockaddr\n    struct sockaddr_in taddr_v4 = {};\n    struct sockaddr_in6 taddr_v6 = {};\n    if (send_info.target_address.is_v6()) {\n      taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port);\n\n      msg.msg_name = (struct sockaddr *) &taddr_v6;\n      msg.msg_namelen = sizeof(taddr_v6);\n    }\n    else {\n      taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port);\n\n      msg.msg_name = (struct sockaddr *) &taddr_v4;\n      msg.msg_namelen = sizeof(taddr_v4);\n    }\n\n    union {\n      char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))];\n      struct cmsghdr alignment;\n    } cmbuf {};\n    socklen_t cmbuflen = 0;\n\n    msg.msg_control = cmbuf.buf;\n    msg.msg_controllen = sizeof(cmbuf.buf);\n\n    auto pktinfo_cm = CMSG_FIRSTHDR(&msg);\n    if (send_info.source_address.is_v6()) {\n      struct in6_pktinfo pktInfo {};\n\n      struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0);\n      pktInfo.ipi6_addr = saddr_v6.sin6_addr;\n      pktInfo.ipi6_ifindex = 0;\n\n      cmbuflen += CMSG_SPACE(sizeof(pktInfo));\n\n      pktinfo_cm->cmsg_level = IPPROTO_IPV6;\n      pktinfo_cm->cmsg_type = IPV6_PKTINFO;\n      pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo));\n      memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo));\n    }\n    else {\n      struct in_pktinfo pktInfo {};\n\n      struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0);\n      pktInfo.ipi_spec_dst = saddr_v4.sin_addr;\n      pktInfo.ipi_ifindex = 0;\n\n      cmbuflen += CMSG_SPACE(sizeof(pktInfo));\n\n      pktinfo_cm->cmsg_level = IPPROTO_IP;\n      pktinfo_cm->cmsg_type = IP_PKTINFO;\n      pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo));\n      memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo));\n    }\n\n    struct iovec iovs[2] = {};\n    int iovlen = 0;\n    if (send_info.header) {\n      iovs[iovlen].iov_base = (void *) send_info.header;\n      iovs[iovlen].iov_len = send_info.header_size;\n      iovlen++;\n    }\n    iovs[iovlen].iov_base = (void *) send_info.payload;\n    iovs[iovlen].iov_len = send_info.payload_size;\n    iovlen++;\n\n    msg.msg_iov = iovs;\n    msg.msg_iovlen = iovlen;\n\n    msg.msg_controllen = cmbuflen;\n\n    auto bytes_sent = sendmsg(sockfd, &msg, 0);\n\n    // If there's no send buffer space, wait for some to be available\n    while (bytes_sent < 0 && errno == EAGAIN) {\n      struct pollfd pfd;\n\n      pfd.fd = sockfd;\n      pfd.events = POLLOUT;\n\n      if (poll(&pfd, 1, -1) != 1) {\n        BOOST_LOG(warning) << \"poll() failed: \"sv << errno;\n        break;\n      }\n\n      // Try to send again\n      bytes_sent = sendmsg(sockfd, &msg, 0);\n    }\n\n    if (bytes_sent < 0) {\n      BOOST_LOG(warning) << \"sendmsg() failed: \"sv << errno;\n      return false;\n    }\n\n    return true;\n  }\n\n  // We can't track QoS state separately for each destination on this OS,\n  // so we keep a ref count to only disable QoS options when all clients\n  // are disconnected.\n  static std::atomic<int> qos_ref_count = 0;\n\n  class qos_t: public deinit_t {\n  public:\n    qos_t(int sockfd, std::vector<std::tuple<int, int, int>> options):\n        sockfd(sockfd), options(options) {\n      qos_ref_count++;\n    }\n\n    virtual ~qos_t() {\n      if (--qos_ref_count == 0) {\n        for (const auto &tuple : options) {\n          auto reset_val = std::get<2>(tuple);\n          if (setsockopt(sockfd, std::get<0>(tuple), std::get<1>(tuple), &reset_val, sizeof(reset_val)) < 0) {\n            BOOST_LOG(warning) << \"Failed to reset option: \"sv << errno;\n          }\n        }\n      }\n    }\n\n  private:\n    int sockfd;\n    std::vector<std::tuple<int, int, int>> options;\n  };\n\n  /**\n   * @brief Enables QoS on the given socket for traffic to the specified destination.\n   * @param native_socket The native socket handle.\n   * @param address The destination address for traffic sent on this socket.\n   * @param port The destination port for traffic sent on this socket.\n   * @param data_type The type of traffic sent on this socket.\n   * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic.\n   */\n  std::unique_ptr<deinit_t>\n  enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) {\n    int sockfd = (int) native_socket;\n    std::vector<std::tuple<int, int, int>> reset_options;\n\n    // We can use SO_NET_SERVICE_TYPE to set link-layer prioritization without DSCP tagging\n    int service_type = 0;\n    switch (data_type) {\n      case qos_data_type_e::video:\n        service_type = NET_SERVICE_TYPE_VI;\n        break;\n      case qos_data_type_e::audio:\n        service_type = NET_SERVICE_TYPE_VO;\n        break;\n      default:\n        BOOST_LOG(error) << \"Unknown traffic type: \"sv << (int) data_type;\n        break;\n    }\n\n    if (service_type) {\n      if (setsockopt(sockfd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &service_type, sizeof(service_type)) == 0) {\n        // Reset SO_NET_SERVICE_TYPE to best-effort when QoS is disabled\n        reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_NET_SERVICE_TYPE, NET_SERVICE_TYPE_BE));\n      }\n      else {\n        BOOST_LOG(error) << \"Failed to set SO_NET_SERVICE_TYPE: \"sv << errno;\n      }\n    }\n\n    if (dscp_tagging) {\n      int level;\n      int option;\n      if (address.is_v6()) {\n        level = IPPROTO_IPV6;\n        option = IPV6_TCLASS;\n      }\n      else {\n        level = IPPROTO_IP;\n        option = IP_TOS;\n      }\n\n      // The specific DSCP values here are chosen to be consistent with Windows,\n      // except that we use CS6 instead of CS7 for audio traffic.\n      int dscp = 0;\n      switch (data_type) {\n        case qos_data_type_e::video:\n          dscp = 40;\n          break;\n        case qos_data_type_e::audio:\n          dscp = 48;\n          break;\n        default:\n          BOOST_LOG(error) << \"Unknown traffic type: \"sv << (int) data_type;\n          break;\n      }\n\n      if (dscp) {\n        // Shift to put the DSCP value in the correct position in the TOS field\n        dscp <<= 2;\n\n        if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) {\n          // Reset TOS to -1 when QoS is disabled\n          reset_options.emplace_back(std::make_tuple(level, option, -1));\n        }\n        else {\n          BOOST_LOG(error) << \"Failed to set TOS/TCLASS: \"sv << errno;\n        }\n      }\n    }\n\n    return std::make_unique<qos_t>(sockfd, reset_options);\n  }\n\n  std::string\n  get_host_name() {\n    try {\n      return boost::asio::ip::host_name();\n    }\n    catch (boost::system::system_error &err) {\n      BOOST_LOG(error) << \"Failed to get hostname: \"sv << err.what();\n      return \"Sunshine\"s;\n    }\n  }\n\n  class macos_high_precision_timer: public high_precision_timer {\n  public:\n    void\n    sleep_for(const std::chrono::nanoseconds &duration) override {\n      std::this_thread::sleep_for(duration);\n    }\n\n    operator bool() override {\n      return true;\n    }\n  };\n\n  std::unique_ptr<high_precision_timer>\n  create_high_precision_timer() {\n    return std::make_unique<macos_high_precision_timer>();\n  }\n}  // namespace platf\n\nnamespace dyn {\n  void *\n  handle(const std::vector<const char *> &libs) {\n    void *handle;\n\n    for (auto lib : libs) {\n      handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL);\n      if (handle) {\n        return handle;\n      }\n    }\n\n    std::stringstream ss;\n    ss << \"Couldn't find any of the following libraries: [\"sv << libs.front();\n    std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) {\n      ss << \", \"sv << lib;\n    });\n\n    ss << ']';\n\n    BOOST_LOG(error) << ss.str();\n\n    return nullptr;\n  }\n\n  int\n  load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict) {\n    int err = 0;\n    for (auto &func : funcs) {\n      TUPLE_2D_REF(fn, name, func);\n\n      *fn = (void (*)()) dlsym(handle, name);\n\n      if (!*fn && strict) {\n        BOOST_LOG(error) << \"Couldn't find function: \"sv << name;\n\n        err = -1;\n      }\n    }\n\n    return err;\n  }\n}  // namespace dyn\n"
  },
  {
    "path": "src/platform/macos/nv12_zero_device.cpp",
    "content": "/**\n * @file src/platform/macos/nv12_zero_device.cpp\n * @brief Definitions for NV12 zero copy device on macOS.\n */\n#include <utility>\n\n#include \"src/platform/macos/av_img_t.h\"\n#include \"src/platform/macos/nv12_zero_device.h\"\n\n#include \"src/video.h\"\n\nextern \"C\" {\n#include \"libavutil/imgutils.h\"\n}\n\nnamespace platf {\n\n  void\n  free_frame(AVFrame *frame) {\n    av_frame_free(&frame);\n  }\n\n  void\n  free_buffer(void *opaque, uint8_t *data) {\n    CVPixelBufferRelease((CVPixelBufferRef) data);\n  }\n\n  util::safe_ptr<AVFrame, free_frame> av_frame;\n\n  int\n  nv12_zero_device::convert(platf::img_t &img) {\n    auto *av_img = (av_img_t *) &img;\n\n    // Release any existing CVPixelBuffer previously retained for encoding\n    av_buffer_unref(&av_frame->buf[0]);\n\n    // Attach an AVBufferRef to this frame which will retain ownership of the CVPixelBuffer\n    // until av_buffer_unref() is called (above) or the frame is freed with av_frame_free().\n    //\n    // The presence of the AVBufferRef allows FFmpeg to simply add a reference to the buffer\n    // rather than having to perform a deep copy of the data buffers in avcodec_send_frame().\n    av_frame->buf[0] = av_buffer_create((uint8_t *) CFRetain(av_img->pixel_buffer->buf), 0, free_buffer, nullptr, 0);\n\n    // Place a CVPixelBufferRef at data[3] as required by AV_PIX_FMT_VIDEOTOOLBOX\n    av_frame->data[3] = (uint8_t *) av_img->pixel_buffer->buf;\n\n    return 0;\n  }\n\n  int\n  nv12_zero_device::set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) {\n    this->frame = frame;\n\n    av_frame.reset(frame);\n\n    resolution_fn(this->display, frame->width, frame->height);\n\n    return 0;\n  }\n\n  int\n  nv12_zero_device::init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn) {\n    pixel_format_fn(display, pix_fmt == pix_fmt_e::nv12 ?\n                               kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange :\n                               kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange);\n\n    this->display = display;\n    this->resolution_fn = std::move(resolution_fn);\n\n    // we never use this pointer, but its existence is checked/used\n    // by the platform independent code\n    data = this;\n\n    return 0;\n  }\n\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/macos/nv12_zero_device.h",
    "content": "/**\n * @file src/platform/macos/nv12_zero_device.h\n * @brief Declarations for NV12 zero copy device on macOS.\n */\n#pragma once\n\n#include \"src/platform/common.h\"\n\nstruct AVFrame;\n\nnamespace platf {\n  void\n  free_frame(AVFrame *frame);\n\n  class nv12_zero_device: public avcodec_encode_device_t {\n    // display holds a pointer to an av_video object. Since the namespaces of AVFoundation\n    // and FFMPEG collide, we need this opaque pointer and cannot use the definition\n    void *display;\n\n  public:\n    // this function is used to set the resolution on an av_video object that we cannot\n    // call directly because of namespace collisions between AVFoundation and FFMPEG\n    using resolution_fn_t = std::function<void(void *display, int width, int height)>;\n    resolution_fn_t resolution_fn;\n    using pixel_format_fn_t = std::function<void(void *display, int pixelFormat)>;\n\n    int\n    init(void *display, pix_fmt_e pix_fmt, resolution_fn_t resolution_fn, const pixel_format_fn_t &pixel_format_fn);\n\n    int\n    convert(img_t &img) override;\n    int\n    set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override;\n\n  private:\n    util::safe_ptr<AVFrame, free_frame> av_frame;\n  };\n\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/macos/publish.cpp",
    "content": "/**\n * @file src/platform/macos/publish.cpp\n * @brief Definitions for publishing services on macOS.\n */\n#include <dns_sd.h>\n#include <thread>\n\n#include \"src/logging.h\"\n#include \"src/network.h\"\n#include \"src/nvhttp.h\"\n#include \"src/platform/common.h\"\n\nusing namespace std::literals;\n\nnamespace platf::publish {\n  namespace {\n    /** @brief Custom deleter intended to be used for `std::unique_ptr<DNSServiceRef>`. */\n    struct ServiceRefDeleter {\n      typedef DNSServiceRef pointer;  ///< Type of object to be deleted.\n      void\n      operator()(pointer serviceRef) {\n        DNSServiceRefDeallocate(serviceRef);\n        BOOST_LOG(info) << \"Deregistered DNS service.\"sv;\n      }\n    };\n    /** @brief This class encapsulates the polling and deinitialization of our connection with\n     *         the mDNS service. Implements the `::platf::deinit_t` interface.\n     */\n    class deinit_t: public ::platf::deinit_t, std::unique_ptr<DNSServiceRef, ServiceRefDeleter> {\n    public:\n      /** @brief Construct deinit_t object.\n       *\n       * Create a thread that will use `select(2)` to wait for a response from the mDNS service.\n       * The thread will give up if an error is received or if `_stopRequested` becomes true.\n       *\n       * @param serviceRef An initialized reference to the mDNS service.\n       */\n      deinit_t(DNSServiceRef serviceRef):\n          unique_ptr(serviceRef) {\n        _thread = std::thread { [serviceRef, &_stopRequested = std::as_const(_stopRequested)]() {\n          const auto socket = DNSServiceRefSockFD(serviceRef);\n          while (!_stopRequested) {\n            auto fdset = fd_set {};\n            FD_ZERO(&fdset);\n            FD_SET(socket, &fdset);\n            auto timeout = timeval { .tv_sec = 3, .tv_usec = 0 };  // 3 second timeout\n            const auto ready = select(socket + 1, &fdset, nullptr, nullptr, &timeout);\n            if (ready == -1) {\n              BOOST_LOG(error) << \"Failed to obtain response from DNS service.\"sv;\n              break;\n            }\n            else if (ready != 0) {\n              DNSServiceProcessResult(serviceRef);\n              break;\n            }\n          }\n        } };\n      }\n      /** @brief Ensure that we gracefully finish polling the mDNS service before freeing our\n       *         connection to it.\n       */\n      ~deinit_t() override {\n        _stopRequested = true;\n        _thread.join();\n      }\n      deinit_t(const deinit_t &) = delete;\n      deinit_t &\n      operator=(const deinit_t &) = delete;\n\n    private:\n      std::thread _thread;  ///< Thread for polling the mDNS service for a response.\n      std::atomic<bool> _stopRequested = false;  ///< Whether to stop polling the mDNS service.\n    };\n\n    /** @brief Callback that will be invoked when the mDNS service finishes registering our service.\n     *  @param errorCode Describes whether the registration was successful.\n     */\n    void\n    registrationCallback(DNSServiceRef /*serviceRef*/, DNSServiceFlags /*flags*/,\n      DNSServiceErrorType errorCode, const char * /*name*/,\n      const char * /*regtype*/, const char * /*domain*/, void * /*context*/) {\n      if (errorCode != kDNSServiceErr_NoError) {\n        BOOST_LOG(error) << \"Failed to register DNS service: Error \"sv << errorCode;\n        return;\n      }\n      BOOST_LOG(info) << \"Successfully registered DNS service.\"sv;\n    }\n  }  // anonymous namespace\n\n  /**\n   * @brief Main entry point for publication of our service on macOS.\n   *\n   * This function initiates a connection to the macOS mDNS service and requests to register\n   * our Sunshine service. Registration will occur asynchronously (unless it fails immediately,\n   * which is probably only possible if the host machine is misconfigured).\n   *\n   * @return Either `nullptr` (if the registration fails immediately) or a `uniqur_ptr<deinit_t>`,\n   *         which will manage polling for a response from the mDNS service, and then, when\n   *         deconstructed, will deregister the service.\n   */\n  [[nodiscard]] std::unique_ptr<::platf::deinit_t>\n  start() {\n    auto serviceRef = DNSServiceRef {};\n    const auto status = DNSServiceRegister(\n      &serviceRef,\n      0,  // flags\n      0,  // interfaceIndex\n      nullptr,  // name\n      SERVICE_TYPE,\n      nullptr,  // domain\n      nullptr,  // host\n      htons(net::map_port(nvhttp::PORT_HTTP)),\n      0,  // txtLen\n      nullptr,  // txtRecord\n      registrationCallback,\n      nullptr  // context\n    );\n    if (status != kDNSServiceErr_NoError) {\n      BOOST_LOG(error) << \"Failed immediately to register DNS service: Error \"sv << status;\n      return nullptr;\n    }\n    return std::make_unique<deinit_t>(serviceRef);\n  }\n}  // namespace platf::publish\n"
  },
  {
    "path": "src/platform/run_command.h",
    "content": "/**\n * @file src/platform/run_command.h\n * @brief Declaration for platf::run_command().\n *\n * Kept out of `platform/common.h` to avoid Windows.h / WinSock include-order issues\n * and Boost.Process v1 inline-namespace forward-declaration conflicts.\n */\n#pragma once\n\n#include <system_error>\n\n#include <boost/filesystem/path.hpp>\n#include <boost/process/v1.hpp>\n\nnamespace platf {\n  boost::process::v1::child\n  run_command(bool elevated,\n              bool interactive,\n              const std::string &cmd,\n              boost::filesystem::path &working_dir,\n              const boost::process::v1::environment &env,\n              FILE *file,\n              std::error_code &ec,\n              boost::process::v1::group *group);\n}  // namespace platf\n\n"
  },
  {
    "path": "src/platform/windows/PolicyConfig.h",
    "content": "/**\n * @file src/platform/windows/PolicyConfig.h\n * @brief Undocumented COM-interface IPolicyConfig.\n * @details Use for setting default audio render endpoint.\n * @author EreTIk\n * @see https://kitere.github.io/\n */\n\n#pragma once\n\n#include <mmdeviceapi.h>\n\n#ifdef __MINGW32__\n  #undef DEFINE_GUID\n  #ifdef __cplusplus\n    #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) EXTERN_C const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } }\n  #else\n    #define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) const GUID DECLSPEC_SELECTANY name = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } }\n  #endif\n\nDEFINE_GUID(IID_IPolicyConfig, 0xf8679f50, 0x850a, 0x41cf, 0x9c, 0x72, 0x43, 0x0f, 0x29, 0x02, 0x90, 0xc8);\nDEFINE_GUID(CLSID_CPolicyConfigClient, 0x870af99c, 0x171d, 0x4f9e, 0xaf, 0x0d, 0xe6, 0x3d, 0xf4, 0x0c, 0x2b, 0xc9);\n\n#endif\n\ninterface DECLSPEC_UUID(\"f8679f50-850a-41cf-9c72-430f290290c8\") IPolicyConfig;\nclass DECLSPEC_UUID(\"870af99c-171d-4f9e-af0d-e63df40c2bc9\") CPolicyConfigClient;\n// ----------------------------------------------------------------------------\n// class CPolicyConfigClient\n// {870af99c-171d-4f9e-af0d-e63df40c2bc9}\n//\n// interface IPolicyConfig\n// {f8679f50-850a-41cf-9c72-430f290290c8}\n//\n// Query interface:\n// CComPtr<IPolicyConfig> PolicyConfig;\n// PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigClient));\n//\n// @compatible: Windows 7 and Later\n// ----------------------------------------------------------------------------\ninterface IPolicyConfig: public IUnknown {\npublic:\n  virtual HRESULT\n  GetMixFormat(\n    PCWSTR,\n    WAVEFORMATEX **);\n\n  virtual HRESULT STDMETHODCALLTYPE\n  GetDeviceFormat(\n    PCWSTR,\n    INT,\n    WAVEFORMATEX **);\n\n  virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat(\n    PCWSTR);\n\n  virtual HRESULT STDMETHODCALLTYPE\n  SetDeviceFormat(\n    PCWSTR,\n    WAVEFORMATEX *,\n    WAVEFORMATEX *);\n\n  virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod(\n    PCWSTR,\n    INT,\n    PINT64,\n    PINT64);\n\n  virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod(\n    PCWSTR,\n    PINT64);\n\n  virtual HRESULT STDMETHODCALLTYPE\n  GetShareMode(\n    PCWSTR,\n    struct DeviceShareMode *);\n\n  virtual HRESULT STDMETHODCALLTYPE\n  SetShareMode(\n    PCWSTR,\n    struct DeviceShareMode *);\n\n  virtual HRESULT STDMETHODCALLTYPE\n  GetPropertyValue(\n    PCWSTR,\n    const PROPERTYKEY &,\n    PROPVARIANT *);\n\n  virtual HRESULT STDMETHODCALLTYPE\n  SetPropertyValue(\n    PCWSTR,\n    const PROPERTYKEY &,\n    PROPVARIANT *);\n\n  virtual HRESULT STDMETHODCALLTYPE\n  SetDefaultEndpoint(\n    PCWSTR wszDeviceId,\n    ERole eRole);\n\n  virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility(\n    PCWSTR,\n    INT);\n};\n"
  },
  {
    "path": "src/platform/windows/audio.cpp",
    "content": "/**\n * @file src/platform/windows/audio.cpp\n * @brief Definitions for Windows audio capture.\n */\n#define INITGUID\n\n// platform includes\n#include <audioclient.h>\n#include <avrt.h>\n#include <mmdeviceapi.h>\n#include <mutex>\n#include <newdev.h>\n#include <roapi.h>\n#include <synchapi.h>\n\n// local includes\n#include \"mic_write.h\"\n#include \"misc.h\"\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n\n// Must be the last included file\n// clang-format off\n#include \"PolicyConfig.h\"\n// clang-format on\n\nDEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2);  // DEVPROP_TYPE_STRING\nDEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);  // DEVPROP_TYPE_STRING\nDEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2);\n\n#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(__amd64__) || defined(_M_AMD64)\n  #define STEAM_DRIVER_SUBDIR L\"x64\"\n#else\n  #warning No known Steam audio driver for this architecture\n#endif\n\nnamespace {\n\n  constexpr auto SAMPLE_RATE = 48000;\n  constexpr auto STEAM_AUDIO_DRIVER_PATH = L\"%CommonProgramFiles(x86)%\\\\Steam\\\\drivers\\\\Windows10\\\\\" STEAM_DRIVER_SUBDIR L\"\\\\SteamStreamingSpeakers.inf\";\n\n  constexpr auto waveformat_mask_stereo = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;\n\n  constexpr auto waveformat_mask_surround51_with_backspeakers = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT |\n                                                                SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY |\n                                                                SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT;\n\n  constexpr auto waveformat_mask_surround51_with_sidespeakers = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT |\n                                                                SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY |\n                                                                SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT;\n\n  constexpr auto waveformat_mask_surround71 = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT |\n                                              SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY |\n                                              SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT |\n                                              SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT;\n\n  constexpr auto waveformat_mask_surround714 = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT |\n                                               SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY |\n                                               SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT |\n                                               SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT |\n                                               SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_FRONT_RIGHT |\n                                               SPEAKER_TOP_BACK_LEFT | SPEAKER_TOP_BACK_RIGHT;\n\n  enum class sample_format_e {\n    f32,\n    s32,\n    s24in32,\n    s24,\n    s16,\n    _size,\n  };\n\n  constexpr WAVEFORMATEXTENSIBLE\n  create_waveformat(sample_format_e sample_format, WORD channel_count, DWORD channel_mask) {\n    WAVEFORMATEXTENSIBLE waveformat = {};\n\n    switch (sample_format) {\n      default:\n      case sample_format_e::f32:\n        waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;\n        waveformat.Format.wBitsPerSample = 32;\n        waveformat.Samples.wValidBitsPerSample = 32;\n        break;\n\n      case sample_format_e::s32:\n        waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;\n        waveformat.Format.wBitsPerSample = 32;\n        waveformat.Samples.wValidBitsPerSample = 32;\n        break;\n\n      case sample_format_e::s24in32:\n        waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;\n        waveformat.Format.wBitsPerSample = 32;\n        waveformat.Samples.wValidBitsPerSample = 24;\n        break;\n\n      case sample_format_e::s24:\n        waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;\n        waveformat.Format.wBitsPerSample = 24;\n        waveformat.Samples.wValidBitsPerSample = 24;\n        break;\n\n      case sample_format_e::s16:\n        waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;\n        waveformat.Format.wBitsPerSample = 16;\n        waveformat.Samples.wValidBitsPerSample = 16;\n        break;\n    }\n\n    static_assert((int) sample_format_e::_size == 5, \"Unrecognized sample_format_e\");\n\n    waveformat.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;\n    waveformat.Format.nChannels = channel_count;\n    waveformat.Format.nSamplesPerSec = SAMPLE_RATE;\n\n    waveformat.Format.nBlockAlign = waveformat.Format.nChannels * waveformat.Format.wBitsPerSample / 8;\n    waveformat.Format.nAvgBytesPerSec = waveformat.Format.nSamplesPerSec * waveformat.Format.nBlockAlign;\n    waveformat.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);\n\n    waveformat.dwChannelMask = channel_mask;\n\n    return waveformat;\n  }\n\n  using virtual_sink_waveformats_t = std::vector<WAVEFORMATEXTENSIBLE>;\n\n  /**\n   * @brief List of supported waveformats for an N-channel virtual audio device\n   * @tparam channel_count Number of virtual audio channels\n   * @returns std::vector<WAVEFORMATEXTENSIBLE>\n   * @note The list of virtual formats returned are sorted in preference order and the first valid\n   *       format will be used. All bits-per-sample options are listed because we try to match\n   *       this to the default audio device. See also: set_format() below.\n   */\n  template <WORD channel_count>\n  virtual_sink_waveformats_t\n  create_virtual_sink_waveformats() {\n    if constexpr (channel_count == 2) {\n      auto channel_mask = waveformat_mask_stereo;\n      // The 32-bit formats are a lower priority for stereo because using one will disable Dolby/DTS\n      // spatial audio mode if the user enabled it on the Steam speaker.\n      return {\n        create_waveformat(sample_format_e::s24in32, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s24, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s16, channel_count, channel_mask),\n        create_waveformat(sample_format_e::f32, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s32, channel_count, channel_mask),\n      };\n    }\n    else if (channel_count == 6) {\n      auto channel_mask1 = waveformat_mask_surround51_with_backspeakers;\n      auto channel_mask2 = waveformat_mask_surround51_with_sidespeakers;\n      return {\n        create_waveformat(sample_format_e::f32, channel_count, channel_mask1),\n        create_waveformat(sample_format_e::f32, channel_count, channel_mask2),\n        create_waveformat(sample_format_e::s32, channel_count, channel_mask1),\n        create_waveformat(sample_format_e::s32, channel_count, channel_mask2),\n        create_waveformat(sample_format_e::s24in32, channel_count, channel_mask1),\n        create_waveformat(sample_format_e::s24in32, channel_count, channel_mask2),\n        create_waveformat(sample_format_e::s24, channel_count, channel_mask1),\n        create_waveformat(sample_format_e::s24, channel_count, channel_mask2),\n        create_waveformat(sample_format_e::s16, channel_count, channel_mask1),\n        create_waveformat(sample_format_e::s16, channel_count, channel_mask2),\n      };\n    }\n    else if (channel_count == 8) {\n      auto channel_mask = waveformat_mask_surround71;\n      return {\n        create_waveformat(sample_format_e::f32, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s32, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s24in32, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s24, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s16, channel_count, channel_mask),\n      };\n    }\n    else if (channel_count == 12) {\n      auto channel_mask = waveformat_mask_surround714;\n      return {\n        create_waveformat(sample_format_e::f32, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s32, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s24in32, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s24, channel_count, channel_mask),\n        create_waveformat(sample_format_e::s16, channel_count, channel_mask),\n      };\n    }\n  }\n\n  std::string\n  waveformat_to_pretty_string(const WAVEFORMATEXTENSIBLE &waveformat) {\n    std::string result = waveformat.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT ? \"F\" :\n                         waveformat.SubFormat == KSDATAFORMAT_SUBTYPE_PCM        ? \"S\" :\n                                                                                   \"UNKNOWN\";\n\n    result += std::to_string(waveformat.Samples.wValidBitsPerSample) + \" \" +\n              std::to_string(waveformat.Format.nSamplesPerSec) + \" \";\n\n    switch (waveformat.dwChannelMask) {\n      case (waveformat_mask_stereo):\n        result += \"2.0\";\n        break;\n\n      case (waveformat_mask_surround51_with_backspeakers):\n        result += \"5.1\";\n        break;\n\n      case (waveformat_mask_surround51_with_sidespeakers):\n        result += \"5.1 (sidespeakers)\";\n        break;\n\n      case (waveformat_mask_surround71):\n        result += \"7.1\";\n        break;\n\n      case (waveformat_mask_surround714):\n        result += \"7.1.4\";\n        break;\n\n      default:\n        result += std::to_string(waveformat.Format.nChannels) + \" channels (unrecognized)\";\n        break;\n    }\n\n    return result;\n  }\n\n}  // namespace\n\nusing namespace std::literals;\n\nnamespace platf::audio {\n\n  template <class T>\n  void\n  co_task_free(T *p) {\n    CoTaskMemFree((LPVOID) p);\n  }\n\n  using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;\n  using device_t = util::safe_ptr<IMMDevice, Release<IMMDevice>>;\n  using collection_t = util::safe_ptr<IMMDeviceCollection, Release<IMMDeviceCollection>>;\n  using audio_client_t = util::safe_ptr<IAudioClient, Release<IAudioClient>>;\n  using audio_capture_t = util::safe_ptr<IAudioCaptureClient, Release<IAudioCaptureClient>>;\n  using wave_format_t = util::safe_ptr<WAVEFORMATEX, co_task_free<WAVEFORMATEX>>;\n  using wstring_t = util::safe_ptr<WCHAR, co_task_free<WCHAR>>;\n  using handle_t = util::safe_ptr_v2<void, BOOL, CloseHandle>;\n  using policy_t = util::safe_ptr<IPolicyConfig, Release<IPolicyConfig>>;\n  using prop_t = util::safe_ptr<IPropertyStore, Release<IPropertyStore>>;\n\n  class co_init_t: public deinit_t {\n  public:\n    co_init_t() {\n      CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY);\n    }\n\n    ~co_init_t() override {\n      CoUninitialize();\n    }\n  };\n\n  class prop_var_t {\n  public:\n    prop_var_t() {\n      PropVariantInit(&prop);\n    }\n\n    ~prop_var_t() {\n      PropVariantClear(&prop);\n    }\n\n    PROPVARIANT prop;\n  };\n\n  struct format_t {\n    WORD channel_count;\n    std::string name;\n    int capture_waveformat_channel_mask;\n    virtual_sink_waveformats_t virtual_sink_waveformats;\n  };\n\n  const std::array<const format_t, 4> formats = {\n    format_t {\n      2,\n      \"Stereo\",\n      waveformat_mask_stereo,\n      create_virtual_sink_waveformats<2>(),\n    },\n    format_t {\n      6,\n      \"Surround 5.1\",\n      waveformat_mask_surround51_with_backspeakers,\n      create_virtual_sink_waveformats<6>(),\n    },\n    format_t {\n      8,\n      \"Surround 7.1\",\n      waveformat_mask_surround71,\n      create_virtual_sink_waveformats<8>(),\n    },\n    format_t {\n      12,\n      \"Surround 7.1.4\",\n      waveformat_mask_surround714,\n      create_virtual_sink_waveformats<12>(),\n    },\n  };\n\n  audio_client_t\n  make_audio_client(device_t &device, const format_t &format) {\n    audio_client_t audio_client;\n    auto status = device->Activate(\n      IID_IAudioClient,\n      CLSCTX_ALL,\n      nullptr,\n      (void **) &audio_client);\n\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Couldn't activate Device: [0x\"sv << util::hex(status).to_string_view() << ']';\n\n      return nullptr;\n    }\n\n    WAVEFORMATEXTENSIBLE capture_waveformat =\n      create_waveformat(sample_format_e::f32, format.channel_count, format.capture_waveformat_channel_mask);\n\n    {\n      wave_format_t mixer_waveformat;\n      status = audio_client->GetMixFormat(&mixer_waveformat);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't get mix format for audio device: [0x\"sv << util::hex(status).to_string_view() << ']';\n        return nullptr;\n      }\n\n      // Prefer the native channel layout of captured audio device when channel counts match\n      if (mixer_waveformat->nChannels == format.channel_count &&\n          mixer_waveformat->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&\n          mixer_waveformat->cbSize >= 22) {\n        auto waveformatext_pointer = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(mixer_waveformat.get());\n        capture_waveformat.dwChannelMask = waveformatext_pointer->dwChannelMask;\n      }\n\n      BOOST_LOG(info) << \"Audio mixer format is \"sv << mixer_waveformat->wBitsPerSample << \"-bit, \"sv\n                      << mixer_waveformat->nSamplesPerSec << \" Hz, \"sv\n                      << ((mixer_waveformat->nSamplesPerSec != 48000) ? \"will be resampled to 48000 by Windows\"sv : \"no resampling needed\"sv);\n    }\n\n    status = audio_client->Initialize(\n      AUDCLNT_SHAREMODE_SHARED,\n      AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK |\n        AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY,  // Enable automatic resampling to 48 KHz\n      0,\n      0,\n      (LPWAVEFORMATEX) &capture_waveformat,\n      nullptr);\n\n    if (status) {\n      BOOST_LOG(error) << \"Couldn't initialize audio client for [\"sv << format.name << \"]: [0x\"sv << util::hex(status).to_string_view() << ']';\n      return nullptr;\n    }\n\n    BOOST_LOG(info) << \"Audio capture format is \"sv << logging::bracket(waveformat_to_pretty_string(capture_waveformat));\n\n    return audio_client;\n  }\n\n  device_t\n  default_device(device_enum_t &device_enum) {\n    device_t device;\n    HRESULT status;\n    status = device_enum->GetDefaultAudioEndpoint(\n      eRender,\n      eConsole,\n      &device);\n\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Couldn't get default audio endpoint [0x\"sv << util::hex(status).to_string_view() << ']';\n\n      return nullptr;\n    }\n\n    return device;\n  }\n\n  class audio_notification_t: public ::IMMNotificationClient {\n  public:\n    audio_notification_t() {\n    }\n\n    // IUnknown implementation (unused by IMMDeviceEnumerator)\n    ULONG STDMETHODCALLTYPE\n    AddRef() {\n      return 1;\n    }\n\n    ULONG STDMETHODCALLTYPE\n    Release() {\n      return 1;\n    }\n\n    HRESULT STDMETHODCALLTYPE\n    QueryInterface(REFIID riid, VOID **ppvInterface) {\n      if (IID_IUnknown == riid) {\n        AddRef();\n        *ppvInterface = (IUnknown *) this;\n        return S_OK;\n      }\n      else if (__uuidof(IMMNotificationClient) == riid) {\n        AddRef();\n        *ppvInterface = (IMMNotificationClient *) this;\n        return S_OK;\n      }\n      else {\n        *ppvInterface = NULL;\n        return E_NOINTERFACE;\n      }\n    }\n\n    // IMMNotificationClient\n    HRESULT STDMETHODCALLTYPE\n    OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) {\n      if (flow == eRender) {\n        default_render_device_changed_flag.store(true);\n      }\n      return S_OK;\n    }\n\n    HRESULT STDMETHODCALLTYPE\n    OnDeviceAdded(LPCWSTR pwstrDeviceId) {\n      return S_OK;\n    }\n\n    HRESULT STDMETHODCALLTYPE\n    OnDeviceRemoved(LPCWSTR pwstrDeviceId) {\n      return S_OK;\n    }\n\n    HRESULT STDMETHODCALLTYPE\n    OnDeviceStateChanged(\n      LPCWSTR pwstrDeviceId,\n      DWORD dwNewState) {\n      return S_OK;\n    }\n\n    HRESULT STDMETHODCALLTYPE\n    OnPropertyValueChanged(\n      LPCWSTR pwstrDeviceId,\n      const PROPERTYKEY key) {\n      return S_OK;\n    }\n\n    /**\n     * @brief Checks if the default rendering device changed and resets the change flag\n     * @return `true` if the device changed since last call\n     */\n    bool\n    check_default_render_device_changed() {\n      return default_render_device_changed_flag.exchange(false);\n    }\n\n  private:\n    std::atomic_bool default_render_device_changed_flag;\n  };\n\n  class mic_wasapi_t: public mic_t {\n  public:\n    capture_e\n    sample(std::vector<float> &sample_out) override {\n      auto sample_size = sample_out.size();\n\n      // Refill the sample buffer if needed\n      while (sample_buf_pos - std::begin(sample_buf) < sample_size) {\n        auto capture_result = _fill_buffer();\n        if (capture_result != capture_e::ok) {\n          return capture_result;\n        }\n      }\n\n      // Fill the output buffer with samples\n      std::copy_n(std::begin(sample_buf), sample_size, std::begin(sample_out));\n\n      // Move any excess samples to the front of the buffer\n      std::move(&sample_buf[sample_size], sample_buf_pos, std::begin(sample_buf));\n      sample_buf_pos -= sample_size;\n\n      return capture_e::ok;\n    }\n\n    int\n    init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out) {\n      audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr));\n      if (!audio_event) {\n        BOOST_LOG(error) << \"Couldn't create Event handle\"sv;\n\n        return -1;\n      }\n\n      HRESULT status;\n\n      status = CoCreateInstance(\n        CLSID_MMDeviceEnumerator,\n        nullptr,\n        CLSCTX_ALL,\n        IID_IMMDeviceEnumerator,\n        (void **) &device_enum);\n\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't create Device Enumerator [0x\"sv << util::hex(status).to_string_view() << ']';\n\n        return -1;\n      }\n\n      status = device_enum->RegisterEndpointNotificationCallback(&endpt_notification);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't register endpoint notification [0x\"sv << util::hex(status).to_string_view() << ']';\n\n        return -1;\n      }\n\n      auto device = default_device(device_enum);\n      if (!device) {\n        return -1;\n      }\n\n      for (const auto &format : formats) {\n        if (format.channel_count != channels_out) {\n          BOOST_LOG(debug) << \"Skipping audio format [\"sv << format.name << \"] with channel count [\"sv\n                           << format.channel_count << \" != \"sv << channels_out << ']';\n          continue;\n        }\n\n        BOOST_LOG(debug) << \"Trying audio format [\"sv << format.name << ']';\n        audio_client = make_audio_client(device, format);\n\n        if (audio_client) {\n          BOOST_LOG(debug) << \"Found audio format [\"sv << format.name << ']';\n          channels = channels_out;\n          break;\n        }\n      }\n\n      if (!audio_client) {\n        BOOST_LOG(error) << \"Couldn't find supported format for audio\"sv;\n        return -1;\n      }\n\n      REFERENCE_TIME default_latency;\n      audio_client->GetDevicePeriod(&default_latency, nullptr);\n      default_latency_ms = default_latency / 1000;\n\n      std::uint32_t frames;\n      status = audio_client->GetBufferSize(&frames);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't acquire the number of audio frames [0x\"sv << util::hex(status).to_string_view() << ']';\n\n        return -1;\n      }\n\n      // *2 --> needs to fit double\n      sample_buf = util::buffer_t<float> { std::max(frames, frame_size) * 2 * channels_out };\n      sample_buf_pos = std::begin(sample_buf);\n\n      status = audio_client->GetService(IID_IAudioCaptureClient, (void **) &audio_capture);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't initialize audio capture client [0x\"sv << util::hex(status).to_string_view() << ']';\n\n        return -1;\n      }\n\n      status = audio_client->SetEventHandle(audio_event.get());\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't set event handle [0x\"sv << util::hex(status).to_string_view() << ']';\n\n        return -1;\n      }\n\n      {\n        DWORD task_index = 0;\n        mmcss_task_handle = AvSetMmThreadCharacteristics(\"Pro Audio\", &task_index);\n        if (!mmcss_task_handle) {\n          BOOST_LOG(error) << \"Couldn't associate audio capture thread with Pro Audio MMCSS task [0x\" << util::hex(GetLastError()).to_string_view() << ']';\n        }\n      }\n\n      status = audio_client->Start();\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't start recording [0x\"sv << util::hex(status).to_string_view() << ']';\n\n        return -1;\n      }\n\n      return 0;\n    }\n\n    ~mic_wasapi_t() override {\n      if (device_enum) {\n        device_enum->UnregisterEndpointNotificationCallback(&endpt_notification);\n      }\n\n      if (audio_client) {\n        audio_client->Stop();\n      }\n\n      if (mmcss_task_handle) {\n        AvRevertMmThreadCharacteristics(mmcss_task_handle);\n      }\n    }\n\n  private:\n    capture_e\n    _fill_buffer() {\n      HRESULT status;\n\n      // Total number of samples\n      struct sample_aligned_t {\n        std::uint32_t uninitialized;\n        float *samples;\n      } sample_aligned;\n\n      // number of samples / number of channels\n      struct block_aligned_t {\n        std::uint32_t audio_sample_size;\n      } block_aligned;\n\n      // Check if the default audio device has changed\n      if (endpt_notification.check_default_render_device_changed()) {\n        // Invoke the audio_control_t's callback if it wants one\n        if (default_endpt_changed_cb) {\n          (*default_endpt_changed_cb)();\n        }\n\n        // Reinitialize to pick up the new default device\n        return capture_e::reinit;\n      }\n\n      status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE);\n      switch (status) {\n        case WAIT_OBJECT_0:\n          break;\n        case WAIT_TIMEOUT:\n          return capture_e::timeout;\n        default:\n          BOOST_LOG(error) << \"Couldn't wait for audio event: [0x\"sv << util::hex(status).to_string_view() << ']';\n          return capture_e::error;\n      }\n\n      std::uint32_t packet_size {};\n      for (\n        status = audio_capture->GetNextPacketSize(&packet_size);\n        SUCCEEDED(status) && packet_size > 0;\n        status = audio_capture->GetNextPacketSize(&packet_size)) {\n        DWORD buffer_flags;\n        status = audio_capture->GetBuffer(\n          (BYTE **) &sample_aligned.samples,\n          &block_aligned.audio_sample_size,\n          &buffer_flags,\n          nullptr,\n          nullptr);\n\n        switch (status) {\n          case S_OK:\n            break;\n          case AUDCLNT_E_DEVICE_INVALIDATED:\n            return capture_e::reinit;\n          default:\n            BOOST_LOG(error) << \"Couldn't capture audio [0x\"sv << util::hex(status).to_string_view() << ']';\n            return capture_e::error;\n        }\n\n        if (buffer_flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) {\n          BOOST_LOG(debug) << \"Audio capture signaled buffer discontinuity\";\n        }\n\n        sample_aligned.uninitialized = std::end(sample_buf) - sample_buf_pos;\n        auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * channels);\n\n        if (n < block_aligned.audio_sample_size * channels) {\n          BOOST_LOG(warning) << \"Audio capture buffer overflow\";\n        }\n\n        if (buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) {\n          std::fill_n(sample_buf_pos, n, 0);\n        }\n        else {\n          std::copy_n(sample_aligned.samples, n, sample_buf_pos);\n        }\n\n        sample_buf_pos += n;\n\n        audio_capture->ReleaseBuffer(block_aligned.audio_sample_size);\n      }\n\n      if (status == AUDCLNT_E_DEVICE_INVALIDATED) {\n        return capture_e::reinit;\n      }\n\n      if (FAILED(status)) {\n        return capture_e::error;\n      }\n\n      return capture_e::ok;\n    }\n\n  public:\n    handle_t audio_event;\n\n    device_enum_t device_enum;\n    device_t device;\n    audio_client_t audio_client;\n    audio_capture_t audio_capture;\n\n    audio_notification_t endpt_notification;\n    std::optional<std::function<void()>> default_endpt_changed_cb;\n\n    REFERENCE_TIME default_latency_ms;\n\n    util::buffer_t<float> sample_buf;\n    float *sample_buf_pos;\n    int channels;\n\n    HANDLE mmcss_task_handle = NULL;\n  };\n\n  std::unique_ptr<mic_write_wasapi_t> mic_redirect_device;\n\n  class audio_control_t: public ::platf::audio_control_t {\n  public:\n    std::optional<sink_t>\n    sink_info() override {\n      sink_t sink;\n\n      // Fill host sink name with the device_id of the current default audio device.\n      {\n        auto device = default_device(device_enum);\n        if (!device) {\n          return std::nullopt;\n        }\n\n        audio::wstring_t id;\n        device->GetId(&id);\n\n        sink.host = to_utf8(id.get());\n      }\n\n      // Prepare to search for the device_id of the virtual audio sink device,\n      // this device can be either user-configured or\n      // the Steam Streaming Speakers we use by default.\n      match_fields_list_t match_list;\n      if (config::audio.virtual_sink.empty()) {\n        match_list = match_steam_speakers();\n      }\n      else {\n        match_list = match_all_fields(from_utf8(config::audio.virtual_sink));\n      }\n\n      // Search for the virtual audio sink device currently present in the system.\n      auto matched = find_device_id(match_list);\n      if (matched) {\n        // Prepare to fill virtual audio sink names with device_id.\n        auto device_id = to_utf8(matched->second);\n        // Also prepend format name (basically channel layout at the moment)\n        // because we don't want to extend the platform interface.\n        sink.null = std::make_optional(sink_t::null_t {\n          \"virtual-\"s + formats[0].name + device_id,\n          \"virtual-\"s + formats[1].name + device_id,\n          \"virtual-\"s + formats[2].name + device_id,\n          \"virtual-\"s + formats[3].name + device_id,\n        });\n      }\n      else if (!config::audio.virtual_sink.empty()) {\n        BOOST_LOG(warning) << \"Couldn't find the specified virtual audio sink \" << config::audio.virtual_sink;\n      }\n\n      return sink;\n    }\n\n    bool\n    is_sink_available(const std::string &sink) override {\n      const auto match_list = match_all_fields(from_utf8(sink));\n      const auto matched = find_device_id(match_list);\n      return static_cast<bool>(matched);\n    }\n\n    /**\n     * @brief Extract virtual audio sink information possibly encoded in the sink name.\n     * @param sink The sink name\n     * @return A pair of device_id and format reference if the sink name matches\n     *         our naming scheme for virtual audio sinks, `std::nullopt` otherwise.\n     */\n    std::optional<std::pair<std::wstring, std::reference_wrapper<const format_t>>>\n    extract_virtual_sink_info(const std::string &sink) {\n      // Encoding format:\n      // [virtual-(format name)]device_id\n      std::string current = sink;\n      auto prefix = \"virtual-\"sv;\n      if (current.find(prefix) == 0) {\n        current = current.substr(prefix.size(), current.size() - prefix.size());\n\n        for (const auto &format : formats) {\n          auto &name = format.name;\n          if (current.find(name) == 0) {\n            auto device_id = from_utf8(current.substr(name.size(), current.size() - name.size()));\n            return std::make_pair(device_id, std::reference_wrapper(format));\n          }\n        }\n      }\n\n      return std::nullopt;\n    }\n\n    std::unique_ptr<mic_t>\n    microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) override {\n      auto mic = std::make_unique<mic_wasapi_t>();\n\n      if (mic->init(sample_rate, frame_size, channels)) {\n        return nullptr;\n      }\n\n      // If this is a virtual sink, set a callback that will change the sink back if it's changed\n      auto virtual_sink_info = extract_virtual_sink_info(assigned_sink);\n      if (virtual_sink_info) {\n        mic->default_endpt_changed_cb = [this] {\n          BOOST_LOG(info) << \"Resetting sink to [\"sv << assigned_sink << \"] after default changed\";\n          set_sink(assigned_sink);\n        };\n      }\n\n      return mic;\n    }\n\n    /**\n     * If the requested sink is a virtual sink, meaning no speakers attached to\n     * the host, then we can seamlessly set the format to stereo and surround sound.\n     *\n     * Any virtual sink detected will be prefixed by:\n     *    virtual-(format name)\n     * If it doesn't contain that prefix, then the format will not be changed\n     */\n    std::optional<std::wstring>\n    set_format(const std::string &sink) {\n      if (sink.empty()) {\n        return std::nullopt;\n      }\n\n      auto virtual_sink_info = extract_virtual_sink_info(sink);\n\n      if (!virtual_sink_info) {\n        // Sink name does not begin with virtual-(format name), hence it's not a virtual sink\n        // and we don't want to change playback format of the corresponding device.\n        // Also need to perform matching, sink name is not necessarily device_id in this case.\n        auto matched = find_device_id(match_all_fields(from_utf8(sink)));\n        if (matched) {\n          return matched->second;\n        }\n        else {\n          BOOST_LOG(error) << \"Couldn't find audio sink \" << sink;\n          return std::nullopt;\n        }\n      }\n\n      // When switching to a Steam virtual speaker device, try to retain the bit depth of the\n      // default audio device. Switching from a 16-bit device to a 24-bit one has been known to\n      // cause glitches for some users.\n      int wanted_bits_per_sample = 32;\n      auto current_default_dev = default_device(device_enum);\n      if (current_default_dev) {\n        audio::prop_t prop;\n        prop_var_t current_device_format;\n\n        if (SUCCEEDED(current_default_dev->OpenPropertyStore(STGM_READ, &prop)) && SUCCEEDED(prop->GetValue(PKEY_AudioEngine_DeviceFormat, &current_device_format.prop))) {\n          auto *format = (WAVEFORMATEXTENSIBLE *) current_device_format.prop.blob.pBlobData;\n          wanted_bits_per_sample = format->Samples.wValidBitsPerSample;\n          BOOST_LOG(info) << \"Virtual audio device will use \"sv << wanted_bits_per_sample << \"-bit to match default device\"sv;\n        }\n      }\n\n      auto &device_id = virtual_sink_info->first;\n      auto &waveformats = virtual_sink_info->second.get().virtual_sink_waveformats;\n      for (const auto &waveformat : waveformats) {\n        // We're using completely undocumented and unlisted API,\n        // better not pass objects without copying them first.\n        auto device_id_copy = device_id;\n        auto waveformat_copy = waveformat;\n        auto waveformat_copy_pointer = reinterpret_cast<WAVEFORMATEX *>(&waveformat_copy);\n\n        if (wanted_bits_per_sample != waveformat.Samples.wValidBitsPerSample) {\n          continue;\n        }\n\n        WAVEFORMATEXTENSIBLE p {};\n        if (SUCCEEDED(policy->SetDeviceFormat(device_id_copy.c_str(), waveformat_copy_pointer, (WAVEFORMATEX *) &p))) {\n          BOOST_LOG(info) << \"Changed virtual audio sink format to \" << logging::bracket(waveformat_to_pretty_string(waveformat));\n          return device_id;\n        }\n      }\n\n      BOOST_LOG(error) << \"Couldn't set virtual audio sink waveformat\";\n      return std::nullopt;\n    }\n\n    int\n    set_sink(const std::string &sink) override {\n      auto device_id = set_format(sink);\n      if (!device_id) {\n        return -1;\n      }\n\n      int failure {};\n      for (int x = 0; x < (int) ERole_enum_count; ++x) {\n        auto status = policy->SetDefaultEndpoint(device_id->c_str(), (ERole) x);\n        if (status) {\n          // Depending on the format of the string, we could get either of these errors\n          if (status == HRESULT_FROM_WIN32(ERROR_NOT_FOUND) || status == E_INVALIDARG) {\n            BOOST_LOG(warning) << \"Audio sink not found: \"sv << sink;\n          }\n          else {\n            BOOST_LOG(warning) << \"Couldn't set [\"sv << sink << \"] to role [\"sv << x << \"]: 0x\"sv << util::hex(status).to_string_view();\n          }\n\n          ++failure;\n        }\n      }\n\n      // Remember the assigned sink name, so we have it for later if we need to set it\n      // back after another application changes it\n      if (!failure) {\n        assigned_sink = sink;\n      }\n\n      return failure;\n    }\n\n    enum class match_field_e {\n      device_id,  ///< Match device_id\n      device_friendly_name,  ///< Match endpoint friendly name\n      adapter_friendly_name,  ///< Match adapter friendly name\n      device_description,  ///< Match endpoint description\n    };\n\n    using match_fields_list_t = std::vector<std::pair<match_field_e, std::wstring>>;\n    using matched_field_t = std::pair<match_field_e, std::wstring>;\n\n    audio_control_t::match_fields_list_t\n    match_steam_speakers() {\n      return {\n        { match_field_e::adapter_friendly_name, L\"Steam Streaming Speakers\" }\n      };\n    }\n\n    audio_control_t::match_fields_list_t\n    match_all_fields(const std::wstring &name) {\n      return {\n        { match_field_e::device_id, name },  // {0.0.0.00000000}.{29dd7668-45b2-4846-882d-950f55bf7eb8}\n        { match_field_e::device_friendly_name, name },  // Digital Audio (S/PDIF) (High Definition Audio Device)\n        { match_field_e::device_description, name },  // Digital Audio (S/PDIF)\n        { match_field_e::adapter_friendly_name, name },  // High Definition Audio Device\n      };\n    }\n\n    /**\n     * @brief Search for currently present audio device_id using multiple match fields.\n     * @param match_list Pairs of match fields and values\n     * @return Optional pair of matched field and device_id\n     */\n    std::optional<matched_field_t>\n    find_device_id(const match_fields_list_t &match_list) {\n      if (match_list.empty()) {\n        return std::nullopt;\n      }\n\n      collection_t collection;\n      auto status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't enumerate: [0x\"sv << util::hex(status).to_string_view() << ']';\n        return std::nullopt;\n      }\n\n      UINT count = 0;\n      collection->GetCount(&count);\n\n      std::vector<std::wstring> matched(match_list.size());\n      for (auto x = 0; x < count; ++x) {\n        audio::device_t device;\n        collection->Item(x, &device);\n\n        audio::wstring_t wstring_id;\n        device->GetId(&wstring_id);\n        std::wstring device_id = wstring_id.get();\n\n        audio::prop_t prop;\n        device->OpenPropertyStore(STGM_READ, &prop);\n\n        prop_var_t adapter_friendly_name;\n        prop_var_t device_friendly_name;\n        prop_var_t device_desc;\n\n        prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop);\n        prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop);\n        prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop);\n\n        for (size_t i = 0; i < match_list.size(); i++) {\n          if (matched[i].empty()) {\n            const wchar_t *match_value = nullptr;\n            switch (match_list[i].first) {\n              case match_field_e::device_id:\n                match_value = device_id.c_str();\n                break;\n\n              case match_field_e::device_friendly_name:\n                match_value = device_friendly_name.prop.pwszVal;\n                break;\n\n              case match_field_e::adapter_friendly_name:\n                match_value = adapter_friendly_name.prop.pwszVal;\n                break;\n\n              case match_field_e::device_description:\n                match_value = device_desc.prop.pwszVal;\n                break;\n            }\n            if (match_value && std::wcscmp(match_value, match_list[i].second.c_str()) == 0) {\n              matched[i] = device_id;\n            }\n          }\n        }\n      }\n\n      for (size_t i = 0; i < match_list.size(); i++) {\n        if (!matched[i].empty()) {\n          return matched_field_t(match_list[i].first, matched[i]);\n        }\n      }\n\n      return std::nullopt;\n    }\n\n    /**\n     * @brief Resets the default audio device from Steam Streaming Speakers.\n     */\n    void\n    reset_default_device() {\n      auto matched_steam = find_device_id(match_steam_speakers());\n      if (!matched_steam) {\n        return;\n      }\n      auto steam_device_id = matched_steam->second;\n\n      {\n        // Get the current default audio device (if present)\n        auto current_default_dev = default_device(device_enum);\n        if (!current_default_dev) {\n          return;\n        }\n\n        audio::wstring_t current_default_id;\n        current_default_dev->GetId(&current_default_id);\n\n        // If Steam Streaming Speakers are already not default, we're done.\n        if (steam_device_id != current_default_id.get()) {\n          return;\n        }\n      }\n\n      // Disable the Steam Streaming Speakers temporarily to allow the OS to pick a new default.\n      auto hr = policy->SetEndpointVisibility(steam_device_id.c_str(), FALSE);\n      if (FAILED(hr)) {\n        BOOST_LOG(warning) << \"Failed to disable Steam audio device: \"sv << util::hex(hr).to_string_view();\n        return;\n      }\n\n      // Get the newly selected default audio device\n      auto new_default_dev = default_device(device_enum);\n\n      // Enable the Steam Streaming Speakers again\n      hr = policy->SetEndpointVisibility(steam_device_id.c_str(), TRUE);\n      if (FAILED(hr)) {\n        BOOST_LOG(warning) << \"Failed to enable Steam audio device: \"sv << util::hex(hr).to_string_view();\n        return;\n      }\n\n      // If there's now no audio device, the Steam Streaming Speakers were the only device available.\n      // There's no other device to set as the default, so just return.\n      if (!new_default_dev) {\n        return;\n      }\n\n      audio::wstring_t new_default_id;\n      new_default_dev->GetId(&new_default_id);\n\n      // Set the new default audio device\n      for (int x = 0; x < (int) ERole_enum_count; ++x) {\n        policy->SetDefaultEndpoint(new_default_id.get(), (ERole) x);\n      }\n\n      BOOST_LOG(info) << \"Successfully reset default audio device\"sv;\n    }\n\n    /**\n     * @brief Installs the Steam Streaming Speakers driver, if present.\n     * @return `true` if installation was successful.\n     */\n    bool\n    install_steam_audio_drivers() {\n#ifdef STEAM_DRIVER_SUBDIR\n      // MinGW's libnewdev.a is missing DiInstallDriverW() even though the headers have it,\n      // so we have to load it at runtime. It's Vista or later, so it will always be available.\n      auto newdev = LoadLibraryExW(L\"newdev.dll\", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);\n      if (!newdev) {\n        BOOST_LOG(error) << \"newdev.dll failed to load\"sv;\n        return false;\n      }\n      auto fg = util::fail_guard([newdev]() {\n        FreeLibrary(newdev);\n      });\n\n      auto fn_DiInstallDriverW = (decltype(DiInstallDriverW) *) GetProcAddress(newdev, \"DiInstallDriverW\");\n      if (!fn_DiInstallDriverW) {\n        BOOST_LOG(error) << \"DiInstallDriverW() is missing\"sv;\n        return false;\n      }\n\n      // Get the current default audio device (if present)\n      auto old_default_dev = default_device(device_enum);\n\n      // Install the Steam Streaming Speakers driver\n      WCHAR driver_path[MAX_PATH] = {};\n      ExpandEnvironmentStringsW(STEAM_AUDIO_DRIVER_PATH, driver_path, ARRAYSIZE(driver_path));\n      if (fn_DiInstallDriverW(nullptr, driver_path, 0, nullptr)) {\n        BOOST_LOG(info) << \"Successfully installed Steam Streaming Speakers\"sv;\n\n        // Wait for 5 seconds to allow the audio subsystem to reconfigure things before\n        // modifying the default audio device or enumerating devices again.\n        Sleep(5000);\n\n        // If there was a previous default device, restore that original device as the\n        // default output device just in case installing the new one changed it.\n        if (old_default_dev) {\n          audio::wstring_t old_default_id;\n          old_default_dev->GetId(&old_default_id);\n\n          for (int x = 0; x < (int) ERole_enum_count; ++x) {\n            policy->SetDefaultEndpoint(old_default_id.get(), (ERole) x);\n          }\n        }\n\n        return true;\n      }\n      else {\n        auto err = GetLastError();\n        switch (err) {\n          case ERROR_ACCESS_DENIED:\n            BOOST_LOG(warning) << \"Administrator privileges are required to install Steam Streaming Speakers\"sv;\n            break;\n          case ERROR_FILE_NOT_FOUND:\n          case ERROR_PATH_NOT_FOUND:\n            BOOST_LOG(info) << \"Steam audio drivers not found. This is expected if you don't have Steam installed.\"sv;\n            break;\n          default:\n            BOOST_LOG(warning) << \"Failed to install Steam audio drivers: \"sv << err;\n            break;\n        }\n\n        return false;\n      }\n#else\n      BOOST_LOG(warning) << \"Unable to install Steam Streaming Speakers on unknown architecture\"sv;\n      return false;\n#endif\n    }\n\n    int\n    init() {\n      auto status = CoCreateInstance(\n        CLSID_CPolicyConfigClient,\n        nullptr,\n        CLSCTX_ALL,\n        IID_IPolicyConfig,\n        (void **) &policy);\n\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't create audio policy config: [0x\"sv << util::hex(status).to_string_view() << ']';\n\n        return -1;\n      }\n\n      status = CoCreateInstance(\n        CLSID_MMDeviceEnumerator,\n        nullptr,\n        CLSCTX_ALL,\n        IID_IMMDeviceEnumerator,\n        (void **) &device_enum);\n\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't create Device Enumerator: [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      return 0;\n    }\n\n    ~audio_control_t() override {\n    }\n\n    policy_t policy;\n    audio::device_enum_t device_enum;\n    std::string assigned_sink;\n\n    int\n    init_mic_redirect_device() {\n      static std::mutex mic_device_mutex;\n      std::lock_guard<std::mutex> lock(mic_device_mutex);\n\n      if (!mic_redirect_device) {\n        mic_redirect_device = std::make_unique<mic_write_wasapi_t>();\n      }\n\n      if (mic_redirect_device->init() != 0) {\n        BOOST_LOG(warning) << \"Failed to initialize client mic redirection device\";\n        mic_redirect_device.reset();\n        return -1;\n      }\n\n      BOOST_LOG(info) << \"Successfully initialized client mic redirection device\";\n\n      return 0;\n    }\n\n    void\n    release_mic_redirect_device() {\n      static std::mutex mic_device_mutex;\n      std::lock_guard<std::mutex> lock(mic_device_mutex);\n\n      if (mic_redirect_device) {\n        mic_redirect_device->restore_audio_devices();\n      }\n    }\n\n    int\n    write_mic_data(const char *data, size_t len, uint16_t seq = 0) {\n      static std::mutex mic_device_mutex;\n      std::lock_guard<std::mutex> lock(mic_device_mutex);\n\n      if (!mic_redirect_device || mic_redirect_device->is_cleaning_up.load()) {\n        BOOST_LOG(warning) << \"Mic redirect device not available or cleaning up\";\n        return -1;\n      }\n\n      return mic_redirect_device->write_data(data, len, seq);\n    }\n  };\n}  // namespace platf::audio\n\nnamespace platf {\n\n  // It's not big enough to justify it's own source file :/\n  namespace dxgi {\n    int\n    init();\n  }\n\n  std::unique_ptr<audio_control_t>\n  audio_control() {\n    auto control = std::make_unique<audio::audio_control_t>();\n\n    if (control->init()) {\n      return nullptr;\n    }\n\n    // Install Steam Streaming Speakers if needed. We do this during audio_control() to ensure\n    // the sink information returned includes the new Steam Streaming Speakers device.\n    if (config::audio.install_steam_drivers && !control->find_device_id(control->match_steam_speakers())) {\n      // This is best effort. Don't fail if it doesn't work.\n      control->install_steam_audio_drivers();\n    }\n\n    return control;\n  }\n\n  std::unique_ptr<deinit_t>\n  init() {\n    if (dxgi::init()) {\n      return nullptr;\n    }\n\n    // Initialize COM\n    auto co_init = std::make_unique<platf::audio::co_init_t>();\n\n    // If Steam Streaming Speakers are currently the default audio device,\n    // change the default to something else (if another device is available).\n    audio::audio_control_t audio_ctrl;\n    if (audio_ctrl.init() == 0) {\n      audio_ctrl.reset_default_device();\n    }\n\n    return co_init;\n  }\n}  // namespace platf"
  },
  {
    "path": "src/platform/windows/display.h",
    "content": "/**\n * @file src/platform/windows/display.h\n * @brief Declarations for the Windows display backend.\n */\n#pragma once\n\n#include <chrono>\n#include <optional>\n\n#include <d3d11.h>\n#include <d3d11_4.h>\n#include <d3dcommon.h>\n#include <dwmapi.h>\n#include <dxgi.h>\n#include <dxgi1_6.h>\n\n#include <Unknwn.h>\n#include <winrt/Windows.Graphics.Capture.h>\n#include <AMF/core/Factory.h>\n#include <AMF/core/CurrentTime.h>\n\n#include \"src/platform/common.h\"\n#include \"src/utility.h\"\n#include \"src/video.h\"\n\nnamespace platf::dxgi {\n  extern const char *format_str[];\n\n  // Add D3D11_CREATE_DEVICE_DEBUG here to enable the D3D11 debug runtime.\n  // You should have a debugger like WinDbg attached to receive debug messages.\n  auto constexpr D3D11_CREATE_DEVICE_FLAGS = 0;\n\n  template <class T>\n  void\n  Release(T *dxgi) {\n    dxgi->Release();\n  }\n\n  inline\n  void\n  FreeLibraryHelper(void *item) {\n    FreeLibrary((HMODULE) item);\n  }\n\n  using hmodule_t = util::safe_ptr<void, FreeLibraryHelper>;\n  using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;\n  using dxgi_t = util::safe_ptr<IDXGIDevice, Release<IDXGIDevice>>;\n  using dxgi1_t = util::safe_ptr<IDXGIDevice1, Release<IDXGIDevice1>>;\n  using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;\n  using device1_t = util::safe_ptr<ID3D11Device1, Release<ID3D11Device1>>;\n  using device_ctx_t = util::safe_ptr<ID3D11DeviceContext, Release<ID3D11DeviceContext>>;\n  using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;\n  using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;\n  using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;\n  using output5_t = util::safe_ptr<IDXGIOutput5, Release<IDXGIOutput5>>;\n  using output6_t = util::safe_ptr<IDXGIOutput6, Release<IDXGIOutput6>>;\n  using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;\n  using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;\n  using texture1d_t = util::safe_ptr<ID3D11Texture1D, Release<ID3D11Texture1D>>;\n  using resource_t = util::safe_ptr<IDXGIResource, Release<IDXGIResource>>;\n  using resource1_t = util::safe_ptr<IDXGIResource1, Release<IDXGIResource1>>;\n  using multithread_t = util::safe_ptr<ID3D11Multithread, Release<ID3D11Multithread>>;\n  using vs_t = util::safe_ptr<ID3D11VertexShader, Release<ID3D11VertexShader>>;\n  using ps_t = util::safe_ptr<ID3D11PixelShader, Release<ID3D11PixelShader>>;\n  using cs_t = util::safe_ptr<ID3D11ComputeShader, Release<ID3D11ComputeShader>>;\n  using blend_t = util::safe_ptr<ID3D11BlendState, Release<ID3D11BlendState>>;\n  using input_layout_t = util::safe_ptr<ID3D11InputLayout, Release<ID3D11InputLayout>>;\n  using render_target_t = util::safe_ptr<ID3D11RenderTargetView, Release<ID3D11RenderTargetView>>;\n  using shader_res_t = util::safe_ptr<ID3D11ShaderResourceView, Release<ID3D11ShaderResourceView>>;\n  using uav_t = util::safe_ptr<ID3D11UnorderedAccessView, Release<ID3D11UnorderedAccessView>>;\n  using buf_t = util::safe_ptr<ID3D11Buffer, Release<ID3D11Buffer>>;\n  using raster_state_t = util::safe_ptr<ID3D11RasterizerState, Release<ID3D11RasterizerState>>;\n  using sampler_state_t = util::safe_ptr<ID3D11SamplerState, Release<ID3D11SamplerState>>;\n  using blob_t = util::safe_ptr<ID3DBlob, Release<ID3DBlob>>;\n  using depth_stencil_state_t = util::safe_ptr<ID3D11DepthStencilState, Release<ID3D11DepthStencilState>>;\n  using depth_stencil_view_t = util::safe_ptr<ID3D11DepthStencilView, Release<ID3D11DepthStencilView>>;\n  using keyed_mutex_t = util::safe_ptr<IDXGIKeyedMutex, Release<IDXGIKeyedMutex>>;\n\n  namespace video {\n    using device_t = util::safe_ptr<ID3D11VideoDevice, Release<ID3D11VideoDevice>>;\n    using ctx_t = util::safe_ptr<ID3D11VideoContext, Release<ID3D11VideoContext>>;\n    using processor_t = util::safe_ptr<ID3D11VideoProcessor, Release<ID3D11VideoProcessor>>;\n    using processor_out_t = util::safe_ptr<ID3D11VideoProcessorOutputView, Release<ID3D11VideoProcessorOutputView>>;\n    using processor_in_t = util::safe_ptr<ID3D11VideoProcessorInputView, Release<ID3D11VideoProcessorInputView>>;\n    using processor_enum_t = util::safe_ptr<ID3D11VideoProcessorEnumerator, Release<ID3D11VideoProcessorEnumerator>>;\n  }  // namespace video\n\n  class hwdevice_t;\n  struct cursor_t {\n    std::vector<std::uint8_t> img_data;\n\n    DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info;\n    int x, y;\n    bool visible;\n  };\n\n  class gpu_cursor_t {\n  public:\n    gpu_cursor_t():\n        cursor_view { 0, 0, 0, 0, 0.0f, 1.0f } {};\n\n    void\n    set_pos(LONG topleft_x, LONG topleft_y, LONG display_width, LONG display_height, DXGI_MODE_ROTATION display_rotation, bool visible) {\n      this->topleft_x = topleft_x;\n      this->topleft_y = topleft_y;\n      this->display_width = display_width;\n      this->display_height = display_height;\n      this->display_rotation = display_rotation;\n      this->visible = visible;\n      update_viewport();\n    }\n\n    void\n    set_texture(LONG texture_width, LONG texture_height, texture2d_t &&texture) {\n      this->texture = std::move(texture);\n      this->texture_width = texture_width;\n      this->texture_height = texture_height;\n      update_viewport();\n    }\n\n    void\n    update_viewport() {\n      switch (display_rotation) {\n        case DXGI_MODE_ROTATION_UNSPECIFIED:\n        case DXGI_MODE_ROTATION_IDENTITY:\n          cursor_view.TopLeftX = topleft_x;\n          cursor_view.TopLeftY = topleft_y;\n          cursor_view.Width = texture_width;\n          cursor_view.Height = texture_height;\n          break;\n\n        case DXGI_MODE_ROTATION_ROTATE90:\n          cursor_view.TopLeftX = topleft_y;\n          cursor_view.TopLeftY = display_width - texture_width - topleft_x;\n          cursor_view.Width = texture_height;\n          cursor_view.Height = texture_width;\n          break;\n\n        case DXGI_MODE_ROTATION_ROTATE180:\n          cursor_view.TopLeftX = display_width - texture_width - topleft_x;\n          cursor_view.TopLeftY = display_height - texture_height - topleft_y;\n          cursor_view.Width = texture_width;\n          cursor_view.Height = texture_height;\n          break;\n\n        case DXGI_MODE_ROTATION_ROTATE270:\n          cursor_view.TopLeftX = display_height - texture_height - topleft_y;\n          cursor_view.TopLeftY = topleft_x;\n          cursor_view.Width = texture_height;\n          cursor_view.Height = texture_width;\n          break;\n      }\n    }\n\n    texture2d_t texture;\n    LONG texture_width;\n    LONG texture_height;\n\n    LONG topleft_x;\n    LONG topleft_y;\n\n    LONG display_width;\n    LONG display_height;\n    DXGI_MODE_ROTATION display_rotation;\n\n    shader_res_t input_res;\n\n    D3D11_VIEWPORT cursor_view;\n\n    bool visible;\n  };\n\n  class display_base_t: public display_t {\n  public:\n    int\n    init(const ::video::config_t &config, const std::string &display_name);\n\n    capture_e\n    capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override;\n\n    factory1_t factory;\n    adapter_t adapter;\n    output_t output;\n    device_t device;\n    device_ctx_t device_ctx;\n    DXGI_RATIONAL display_refresh_rate;\n    int display_refresh_rate_rounded;\n\n    DXGI_MODE_ROTATION display_rotation = DXGI_MODE_ROTATION_UNSPECIFIED;\n    int width_before_rotation;\n    int height_before_rotation;\n\n    int client_frame_rate;  // Integer framerate for backward compatibility\n    DXGI_RATIONAL client_frame_rate_rational;  // Fractional framerate for NTSC support (e.g., 60000/1001 = 59.94fps)\n    int adapter_index;\n    int output_index;\n\n    DXGI_FORMAT capture_format;\n\n    /**\n     * @brief Indicates whether the display's output colorspace uses linear gamma.\n     *\n     * This is determined from DXGI_OUTPUT_DESC1.ColorSpace:\n     *   - G10 (gamma 1.0, linear):  capture_linear_gamma = true   (ACM / scRGB)\n     *   - G22 (gamma ~2.2, sRGB):   capture_linear_gamma = false  (normal SDR)\n     *   - G2084 (PQ / HDR):         capture_linear_gamma = true   (linear light)\n     *\n     * Shader selection requires BOTH linear_gamma AND FP16 pixel format to use the\n     * linear-input shader (ApplySRGBCurve). This is because:\n     *   - FP16 + G10/G2084: data is truly in linear light → must apply transfer function\n     *   - B8G8R8A8 + G10:   data was converted to sRGB by the capture API (e.g. WGC\n     *     requesting 8-bit while display is in ACM mode) → already has sRGB gamma\n     *   - FP16 + G22:       driver returned FP16 data with sRGB gamma → identity shader\n     */\n    bool capture_linear_gamma = false;\n\n    D3D_FEATURE_LEVEL feature_level;\n\n    std::unique_ptr<high_precision_timer> timer = create_high_precision_timer();\n\n    typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS {\n      D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE,  ///< Idle priority class\n      D3DKMT_SCHEDULINGPRIORITYCLASS_BELOW_NORMAL,  ///< Below normal priority class\n      D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL,  ///< Normal priority class\n      D3DKMT_SCHEDULINGPRIORITYCLASS_ABOVE_NORMAL,  ///< Above normal priority class\n      D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH,  ///< High priority class\n      D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME  ///< Realtime priority class\n    } D3DKMT_SCHEDULINGPRIORITYCLASS;\n\n    typedef UINT D3DKMT_HANDLE;\n\n    typedef struct _D3DKMT_OPENADAPTERFROMLUID {\n      LUID AdapterLuid;\n      D3DKMT_HANDLE hAdapter;\n    } D3DKMT_OPENADAPTERFROMLUID;\n\n    typedef struct _D3DKMT_WDDM_2_7_CAPS {\n      union {\n        struct\n        {\n          UINT HwSchSupported : 1;\n          UINT HwSchEnabled : 1;\n          UINT HwSchEnabledByDefault : 1;\n          UINT IndependentVidPnVSyncControl : 1;\n          UINT Reserved : 28;\n        };\n        UINT Value;\n      };\n    } D3DKMT_WDDM_2_7_CAPS;\n\n    typedef struct _D3DKMT_QUERYADAPTERINFO {\n      D3DKMT_HANDLE hAdapter;\n      UINT Type;\n      VOID *pPrivateDriverData;\n      UINT PrivateDriverDataSize;\n    } D3DKMT_QUERYADAPTERINFO;\n\n    const UINT KMTQAITYPE_WDDM_2_7_CAPS = 70;\n\n    typedef struct _D3DKMT_CLOSEADAPTER {\n      D3DKMT_HANDLE hAdapter;\n    } D3DKMT_CLOSEADAPTER;\n\n    typedef NTSTATUS(WINAPI *PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS);\n    typedef NTSTATUS(WINAPI *PD3DKMTOpenAdapterFromLuid)(D3DKMT_OPENADAPTERFROMLUID *);\n    typedef NTSTATUS(WINAPI *PD3DKMTQueryAdapterInfo)(D3DKMT_QUERYADAPTERINFO *);\n    typedef NTSTATUS(WINAPI *PD3DKMTCloseAdapter)(D3DKMT_CLOSEADAPTER *);\n\n    virtual bool\n    is_hdr() override;\n    virtual bool\n    get_hdr_metadata(SS_HDR_METADATA &metadata) override;\n\n    const char *\n    dxgi_format_to_string(DXGI_FORMAT format);\n    const char *\n    colorspace_to_string(DXGI_COLOR_SPACE_TYPE type);\n    virtual std::vector<DXGI_FORMAT>\n    get_supported_capture_formats() = 0;\n\n  private:\n    // Cached HDR metadata for change detection\n    std::optional<SS_HDR_METADATA> cached_hdr_metadata;\n    std::chrono::steady_clock::time_point last_hdr_check_time;\n    static constexpr std::chrono::milliseconds hdr_check_interval { 1000 };  // Check every 1 second\n\n  protected:\n    int\n    get_pixel_pitch() {\n      return (capture_format == DXGI_FORMAT_R16G16B16A16_FLOAT) ? 8 : 4;\n    }\n\n    virtual capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) = 0;\n    virtual capture_e\n    release_snapshot() = 0;\n    virtual int\n    complete_img(img_t *img, bool dummy) = 0;\n  };\n\n  /**\n   * Display component for devices that use software encoders.\n   */\n  class display_ram_t: public display_base_t {\n  public:\n    std::shared_ptr<img_t>\n    alloc_img() override;\n    int\n    dummy_img(img_t *img) override;\n    int\n    complete_img(img_t *img, bool dummy) override;\n    std::vector<DXGI_FORMAT>\n    get_supported_capture_formats() override;\n\n    std::unique_ptr<avcodec_encode_device_t>\n    make_avcodec_encode_device(pix_fmt_e pix_fmt) override;\n\n    D3D11_MAPPED_SUBRESOURCE img_info;\n    texture2d_t texture;\n  };\n\n  /**\n   * Display component for devices that use hardware encoders.\n   */\n  class display_vram_t: public display_base_t, public std::enable_shared_from_this<display_vram_t> {\n  public:\n    std::shared_ptr<img_t>\n    alloc_img() override;\n    int\n    dummy_img(img_t *img_base) override;\n    int\n    complete_img(img_t *img_base, bool dummy) override;\n    std::vector<DXGI_FORMAT>\n    get_supported_capture_formats() override;\n\n    bool\n    is_codec_supported(std::string_view name, const ::video::config_t &config) override;\n\n    std::unique_ptr<avcodec_encode_device_t>\n    make_avcodec_encode_device(pix_fmt_e pix_fmt) override;\n\n    std::unique_ptr<nvenc_encode_device_t>\n    make_nvenc_encode_device(pix_fmt_e pix_fmt) override;\n\n    std::unique_ptr<amf_encode_device_t>\n    make_amf_encode_device(pix_fmt_e pix_fmt) override;\n\n    std::atomic<uint32_t> next_image_id;\n  };\n\n  /**\n   * Display duplicator that uses the DirectX Desktop Duplication API.\n   */\n  class duplication_t {\n  public:\n    dup_t dup;\n    bool has_frame {};\n    std::chrono::steady_clock::time_point last_protected_content_warning_time {};\n\n    int\n    init(display_base_t *display, const ::video::config_t &config);\n    capture_e\n    next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p);\n    capture_e\n    reset(dup_t::pointer dup_p = dup_t::pointer());\n    capture_e\n    release_frame();\n\n    ~duplication_t();\n  };\n\n  /**\n   * Display backend that uses DDAPI with a software encoder.\n   */\n  class display_ddup_ram_t: public display_ram_t {\n  public:\n    int\n    init(const ::video::config_t &config, const std::string &display_name);\n    capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;\n    capture_e\n    release_snapshot() override;\n\n    duplication_t dup;\n    cursor_t cursor;\n  };\n\n  /**\n   * Display backend that uses DDAPI with a hardware encoder.\n   */\n  class display_ddup_vram_t: public display_vram_t {\n  public:\n    int\n    init(const ::video::config_t &config, const std::string &display_name);\n    capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;\n    capture_e\n    release_snapshot() override;\n\n    duplication_t dup;\n    sampler_state_t sampler_linear;\n    // Point sampler for high-quality resampling shaders (avoid double-filtering).\n    sampler_state_t sampler_point;\n\n    blend_t blend_alpha;\n    blend_t blend_invert;\n    blend_t blend_disable;\n\n    ps_t cursor_ps;\n    vs_t cursor_vs;\n\n    gpu_cursor_t cursor_alpha;\n    gpu_cursor_t cursor_xor;\n\n    texture2d_t old_surface_delayed_destruction;\n    std::chrono::steady_clock::time_point old_surface_timestamp;\n    std::variant<std::monostate, texture2d_t, std::shared_ptr<platf::img_t>> last_frame_variant;\n  };\n\n  /**\n   * @brief Recover ConsentPromptBehaviorAdmin registry value if a previous WGC session crashed.\n   * Safe to call from any capture mode — checks for backup file and restores if found.\n   */\n  void\n  recover_secure_desktop();\n\n  /**\n   * Display duplicator that uses the Windows.Graphics.Capture API.\n   */\n  class wgc_capture_t {\n    winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice uwp_device { nullptr };\n    winrt::Windows::Graphics::Capture::GraphicsCaptureItem item { nullptr };\n    winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool frame_pool { nullptr };\n    winrt::Windows::Graphics::Capture::GraphicsCaptureSession capture_session { nullptr };\n    winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame produced_frame { nullptr }, consumed_frame { nullptr };\n    SRWLOCK frame_lock = SRWLOCK_INIT;\n    CONDITION_VARIABLE frame_present_cv;\n    void\n    on_frame_arrived(winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const &sender, winrt::Windows::Foundation::IInspectable const &);\n\n  public:\n    HWND captured_window_hwnd { nullptr };  // Store window handle if capturing a window\n    std::string desired_window_title;  // Store desired window title (for logging/debugging)\n    int window_capture_width { 0 };  // Actual window capture width (may differ from display width)\n    int window_capture_height { 0 };  // Actual window capture height (may differ from display height)\n    wgc_capture_t();\n    ~wgc_capture_t();\n\n    int\n    init(display_base_t *display, const ::video::config_t &config);\n    capture_e\n    next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time);\n    capture_e\n    release_frame();\n    int\n    set_cursor_visible(bool);\n    \n    /**\n     * @brief Check if the captured window is still valid.\n     * @return true if window is valid or not capturing a window, false if window is invalid.\n     */\n    bool\n    is_window_valid() const;\n  };\n\n  /**\n   * Display backend that uses Windows.Graphics.Capture with a software encoder.\n   */\n  class display_wgc_ram_t: public display_ram_t {\n    wgc_capture_t dup;\n\n  public:\n    int\n    init(const ::video::config_t &config, const std::string &display_name);\n    std::shared_ptr<img_t>\n    alloc_img() override;\n    capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;\n    capture_e\n    release_snapshot() override;\n  };\n\n  /**\n   * Display backend that uses Windows.Graphics.Capture with a hardware encoder.\n   */\n  class display_wgc_vram_t: public display_vram_t {\n    wgc_capture_t dup;\n\n  public:\n    int\n    init(const ::video::config_t &config, const std::string &display_name);\n    std::shared_ptr<img_t>\n    alloc_img() override;\n    capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;\n    capture_e\n    release_snapshot() override;\n  };\n\n  class amd_capture_t {\n\n  public:\n    amd_capture_t();\n    ~amd_capture_t();\n\n    int\n    init(display_base_t *display, const ::video::config_t &config, int output_index);\n    capture_e\n    next_frame(std::chrono::milliseconds timeout, amf::AMFData** out);\n    capture_e\n    release_frame();\n\n    hmodule_t amfrt_lib;\n    amf_uint64 amf_version;\n    amf::AMFFactory *amf_factory;\n\n    amf::AMFContextPtr context;\n    amf::AMFComponentPtr captureComp;\n    amf::AMFSurfacePtr capturedSurface;\n    amf_int64 capture_format;\n    AMFSize resolution;\n  };\n\n\n  /**\n   * Display backend that uses AMD Display Capture with a hardware encoder.\n   * Main purpose is to capture AMD Fluid Motion Frames (AFMF)\n   */\n  class display_amd_vram_t: public display_vram_t {\n    amd_capture_t dup;\n\n    blend_t blend_invert;\n    blend_t blend_disable;\n\n    ps_t cursor_ps;\n    vs_t cursor_vs;\n\n    buf_t cursor_info;\n\n  public:\n    int\n    init(const ::video::config_t &config, const std::string &display_name);\n    capture_e\n    snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;\n    capture_e\n    release_snapshot() override;\n  };\n\n}  // namespace platf::dxgi\n"
  },
  {
    "path": "src/platform/windows/display_amd.cpp",
    "content": "/**\n * @file src/platform/windows/display_amd.cpp\n * @brief Display capture implementation using AMD Direct Capture\n */\n\nextern \"C\" {\n#include <libavcodec/avcodec.h>\n#include <libavutil/hwcontext_d3d11va.h>\n}\n\n#include \"display.h\"\n#include \"misc.h\"\n#include \"src/config.h\"\n#include \"src/main.h\"\n#include \"src/video.h\"\n\n#include <AMF/components/DisplayCapture.h>\n#include <AMF/components/VideoConverter.h>\n#include <AMF/core/Trace.h>\n\nnamespace platf {\n  using namespace std::literals;\n}\n\nnamespace platf::dxgi {\n  amd_capture_t::amd_capture_t() {\n  }\n\n  amd_capture_t::~amd_capture_t() {\n    AMF_RESULT result;\n\n    // Before terminating the Display Capture component, we need to drain the remaining frames\n    result = captureComp->Drain();\n    if (result == AMF_OK) {\n      do {\n        result = captureComp->QueryOutput((amf::AMFData **) &capturedSurface);\n        Sleep(1);\n      } while (result != AMF_EOF);\n    }\n    captureComp->Terminate();\n\n    context->Terminate();\n    captureComp = nullptr;\n    context = nullptr;\n    capturedSurface = nullptr;\n  }\n\n  capture_e\n  amd_capture_t::release_frame() {\n    if (capturedSurface != nullptr) {\n      capturedSurface = nullptr;\n    }\n\n    return capture_e::ok;\n  }\n\n  /**\n   * @brief Get the next frame from the producer thread.\n   * If not available, the capture thread blocks until one is, or the wait times out.\n   * @param timeout how long to wait for the next frame\n   * @param out pointer to AMFSurfacePtr\n   */\n  capture_e\n  amd_capture_t::next_frame(std::chrono::milliseconds timeout, amf::AMFData **out) {\n    release_frame();\n\n    AMF_RESULT result;\n    auto capture_start = std::chrono::steady_clock::now();\n    do {\n      result = captureComp->QueryOutput(out);\n      if (result == AMF_REPEAT) {\n        if (std::chrono::steady_clock::now() - capture_start >= timeout) {\n          return platf::capture_e::timeout;\n        }\n        Sleep(1);\n      }\n    } while (result == AMF_REPEAT);\n\n    if (result != AMF_OK) {\n      // Check if the underlying D3D11 device is lost (TDR, driver crash, etc.)\n      if (context) {\n        auto *d3d_device = static_cast<ID3D11Device *>(context->GetDX11Device(amf::AMF_DX11_1));\n        if (d3d_device) {\n          auto removed_reason = d3d_device->GetDeviceRemovedReason();\n          if (removed_reason != S_OK) {\n            BOOST_LOG(error) << \"AMD DirectCapture: D3D11 device lost, reason: 0x\"sv << util::hex(removed_reason).to_string_view() << \", requesting reinit\"sv;\n            return capture_e::reinit;\n          }\n        }\n      }\n      BOOST_LOG(warning) << \"AMD DirectCapture: QueryOutput failed with result: \"sv << result;\n      return capture_e::timeout;\n    }\n    return capture_e::ok;\n  }\n\n  int\n  amd_capture_t::init(display_base_t *display, const ::video::config_t &config, int output_index) {\n    // We have to load AMF before calling the base init() because we will need it loaded\n    // when our test_capture() function is called.\n    amfrt_lib.reset(LoadLibraryW(AMF_DLL_NAME));\n    if (!amfrt_lib) {\n      // Probably not an AMD GPU system\n      return -1;\n    }\n\n    auto fn_AMFQueryVersion = (AMFQueryVersion_Fn) GetProcAddress((HMODULE) amfrt_lib.get(), AMF_QUERY_VERSION_FUNCTION_NAME);\n    auto fn_AMFInit = (AMFInit_Fn) GetProcAddress((HMODULE) amfrt_lib.get(), AMF_INIT_FUNCTION_NAME);\n\n    if (!fn_AMFQueryVersion || !fn_AMFInit) {\n      BOOST_LOG(error) << \"Missing required AMF function!\"sv;\n      return -1;\n    }\n\n    auto result = fn_AMFQueryVersion(&amf_version);\n    if (result != AMF_OK) {\n      BOOST_LOG(error) << \"AMFQueryVersion() failed: \"sv << result;\n      return -1;\n    }\n\n    // We don't support anything older than AMF 1.4.30. We'll gracefully fall back to DDAPI.\n    if (amf_version < AMF_MAKE_FULL_VERSION(1, 4, 30, 0)) {\n      BOOST_LOG(warning) << \"AMD Direct Capture is not supported on AMF version\"sv\n                         << AMF_GET_MAJOR_VERSION(amf_version) << '.'\n                         << AMF_GET_MINOR_VERSION(amf_version) << '.'\n                         << AMF_GET_SUBMINOR_VERSION(amf_version) << '.'\n                         << AMF_GET_BUILD_VERSION(amf_version);\n      BOOST_LOG(warning) << \"Consider updating your AMD graphics driver for better capture performance!\"sv;\n      return -1;\n    }\n\n    // Initialize AMF library\n    result = fn_AMFInit(AMF_FULL_VERSION, &amf_factory);\n    if (result != AMF_OK) {\n      BOOST_LOG(error) << \"AMFInit() failed: \"sv << result;\n      return -1;\n    }\n\n    DXGI_ADAPTER_DESC adapter_desc;\n    display->adapter->GetDesc(&adapter_desc);\n\n    // Bail if this is not an AMD GPU\n    if (adapter_desc.VendorId != 0x1002) {\n      return -1;\n    }\n\n    // Create the capture context\n    result = amf_factory->CreateContext(&context);\n\n    if (result != AMF_OK) {\n      BOOST_LOG(error) << \"CreateContext() failed: \"sv << result;\n      return -1;\n    }\n\n    // Associate the context with our ID3D11Device. This will enable multithread protection on the device.\n    result = context->InitDX11(display->device.get());\n    if (result != AMF_OK) {\n      BOOST_LOG(error) << \"InitDX11() failed: \"sv << result;\n      return -1;\n    }\n\n    display->capture_format = DXGI_FORMAT_UNKNOWN;\n\n    // Create the DisplayCapture component\n    result = amf_factory->CreateComponent(context, AMFDisplayCapture, &(captureComp));\n    if (result != AMF_OK) {\n      BOOST_LOG(error) << \"CreateComponent(AMFDisplayCapture) failed: \"sv << result;\n      return -1;\n    }\n\n    // Set parameters for non-blocking capture\n    captureComp->SetProperty(AMF_DISPLAYCAPTURE_MONITOR_INDEX, output_index);\n    captureComp->SetProperty(AMF_DISPLAYCAPTURE_FRAMERATE, AMFConstructRate(config.framerate, 1));\n    captureComp->SetProperty(AMF_DISPLAYCAPTURE_MODE, AMF_DISPLAYCAPTURE_MODE_WAIT_FOR_PRESENT);\n    captureComp->SetProperty(AMF_DISPLAYCAPTURE_DUPLICATEOUTPUT, true);\n\n    // Initialize capture\n    result = captureComp->Init(amf::AMF_SURFACE_UNKNOWN, 0, 0);\n    if (result != AMF_OK) {\n      BOOST_LOG(error) << \"DisplayCapture::Init() failed: \"sv << result;\n      return -1;\n    }\n\n    captureComp->GetProperty(AMF_DISPLAYCAPTURE_FORMAT, &(capture_format));\n    captureComp->GetProperty(AMF_DISPLAYCAPTURE_RESOLUTION, &(resolution));\n    BOOST_LOG(info) << \"Desktop resolution [\"sv << resolution.width << 'x' << resolution.height << ']';\n\n    BOOST_LOG(info) << \"Using AMD Direct Capture API for display capture\"sv;\n\n    return 0;\n  }\n\n}  // namespace platf::dxgi"
  },
  {
    "path": "src/platform/windows/display_base.cpp",
    "content": "/**\n * @file src/platform/windows/display_base.cpp\n * @brief Definitions for the Windows display base code.\n */\n#include <algorithm>\n#include <cmath>\n#include <initguid.h>\n#include <thread>\n\n#include <boost/algorithm/string/join.hpp>\n#include <boost/process/v1.hpp>\n\n#include <MinHook.h>\n\n// We have to include boost/process/v1.hpp before display.h due to WinSock.h,\n// but that prevents the definition of NTSTATUS so we must define it ourself.\ntypedef long NTSTATUS;\n\n// Definition from the WDK's d3dkmthk.h\ntypedef enum _D3DKMT_GPU_PREFERENCE_QUERY_STATE: DWORD {\n  D3DKMT_GPU_PREFERENCE_STATE_UNINITIALIZED,  ///< The GPU preference isn't initialized.\n  D3DKMT_GPU_PREFERENCE_STATE_HIGH_PERFORMANCE,  ///< The highest performing GPU is preferred.\n  D3DKMT_GPU_PREFERENCE_STATE_MINIMUM_POWER,  ///< The minimum-powered GPU is preferred.\n  D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED,  ///< A GPU preference isn't specified.\n  D3DKMT_GPU_PREFERENCE_STATE_NOT_FOUND,  ///< A GPU preference isn't found.\n  D3DKMT_GPU_PREFERENCE_STATE_USER_SPECIFIED_GPU  ///< A specific GPU is preferred.\n} D3DKMT_GPU_PREFERENCE_QUERY_STATE;\n\n#include \"display.h\"\n#include \"display_device/windows_utils.h\"\n#include \"misc.h\"\n#include \"src/config.h\"\n#include \"src/display_device/display_device.h\"\n#include \"src/globals.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/video.h\"\n\nnamespace platf {\n  using namespace std::literals;\n}\nnamespace platf::dxgi {\n  namespace bp = boost::process::v1;\n\n  /**\n   * DDAPI-specific initialization goes here.\n   */\n  int\n  duplication_t::init(display_base_t *display, const ::video::config_t &config) {\n    HRESULT status;\n\n    // Capture format will be determined from the first call to AcquireNextFrame()\n    display->capture_format = DXGI_FORMAT_UNKNOWN;\n\n    // FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD\n    {\n      // IDXGIOutput5 is optional, but can provide improved performance and wide color support\n      dxgi::output5_t output5 {};\n      status = display->output->QueryInterface(IID_IDXGIOutput5, (void **) &output5);\n      if (SUCCEEDED(status)) {\n        // Ask the display implementation which formats it supports\n        auto supported_formats = display->get_supported_capture_formats();\n        if (supported_formats.empty()) {\n          BOOST_LOG(warning) << \"No compatible capture formats for this encoder\"sv;\n          return -1;\n        }\n\n        // We try this twice, in case we still get an error on reinitialization\n        for (int x = 0; x < 2; ++x) {\n          // Ensure we can duplicate the current display\n          syncThreadDesktop();\n\n          status = output5->DuplicateOutput1((IUnknown *) display->device.get(), 0, supported_formats.size(), supported_formats.data(), &dup);\n          if (SUCCEEDED(status)) {\n            break;\n          }\n          std::this_thread::sleep_for(200ms);\n        }\n\n        // We don't retry with DuplicateOutput() because we can hit this codepath when we're racing\n        // with mode changes and we don't want to accidentally fall back to suboptimal capture if\n        // we get unlucky and succeed below.\n        if (FAILED(status)) {\n          BOOST_LOG(warning) << \"DuplicateOutput1 Failed [0x\"sv << util::hex(status).to_string_view() << ']';\n          return -1;\n        }\n      }\n      else {\n        BOOST_LOG(warning) << \"IDXGIOutput5 is not supported by your OS. Capture performance may be reduced.\"sv;\n\n        dxgi::output1_t output1 {};\n        status = display->output->QueryInterface(IID_IDXGIOutput1, (void **) &output1);\n        if (FAILED(status)) {\n          BOOST_LOG(error) << \"Failed to query IDXGIOutput1 from the output\"sv;\n          return -1;\n        }\n\n        for (int x = 0; x < 2; ++x) {\n          // Ensure we can duplicate the current display\n          syncThreadDesktop();\n\n          status = output1->DuplicateOutput((IUnknown *) display->device.get(), &dup);\n          if (SUCCEEDED(status)) {\n            break;\n          }\n          std::this_thread::sleep_for(200ms);\n        }\n\n        if (FAILED(status)) {\n          BOOST_LOG(error) << \"DuplicateOutput Failed [0x\"sv << util::hex(status).to_string_view() << ']';\n          return -1;\n        }\n      }\n    }\n\n    DXGI_OUTDUPL_DESC dup_desc;\n    dup->GetDesc(&dup_desc);\n\n    display->display_refresh_rate = dup_desc.ModeDesc.RefreshRate;\n    double display_refresh_rate_decimal = (double) display->display_refresh_rate.Numerator / display->display_refresh_rate.Denominator;\n\n    BOOST_LOG(info) << \"Desktop resolution [\"sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']'\n                    << \", Desktop format [\"sv << display->dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']'\n                    << \", Display refresh rate [\" << display_refresh_rate_decimal << \"Hz]\"\n                    << \", Requested frame rate [\" << display->client_frame_rate << \"fps]\";\n    display->display_refresh_rate_rounded = lround(display_refresh_rate_decimal);\n    return 0;\n  }\n\n  capture_e\n  duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) {\n    auto capture_status = release_frame();\n    if (capture_status != capture_e::ok) {\n      return capture_status;\n    }\n\n    auto status = dup->AcquireNextFrame(timeout.count(), &frame_info, res_p);\n\n    switch (status) {\n      case S_OK:\n        // ProtectedContentMaskedOut seems to semi-randomly be TRUE or FALSE even when protected content\n        // is on screen the whole time, so we can't just print when it changes. Instead we'll keep track\n        // of the last time we printed the warning and print another if we haven't printed one recently.\n        if (frame_info.ProtectedContentMaskedOut && std::chrono::steady_clock::now() > last_protected_content_warning_time + 10s) {\n          BOOST_LOG(warning) << \"Windows is currently blocking DRM-protected content from capture. You may see black regions where this content would be.\"sv;\n          last_protected_content_warning_time = std::chrono::steady_clock::now();\n        }\n\n        has_frame = true;\n        return capture_e::ok;\n      case DXGI_ERROR_WAIT_TIMEOUT:\n        return capture_e::timeout;\n      case WAIT_ABANDONED:\n      case DXGI_ERROR_ACCESS_LOST:\n      case DXGI_ERROR_ACCESS_DENIED:\n        return capture_e::reinit;\n      case DXGI_ERROR_DEVICE_REMOVED:\n      case DXGI_ERROR_DEVICE_RESET:\n        BOOST_LOG(error) << \"D3D11 device lost during AcquireNextFrame [0x\"sv << util::hex(status).to_string_view() << \"], requesting reinit\"sv;\n        return capture_e::reinit;\n      default:\n        BOOST_LOG(error) << \"Couldn't acquire next frame [0x\"sv << util::hex(status).to_string_view();\n        return capture_e::error;\n    }\n  }\n\n  capture_e\n  duplication_t::reset(dup_t::pointer dup_p) {\n    auto capture_status = release_frame();\n\n    dup.reset(dup_p);\n\n    return capture_status;\n  }\n\n  capture_e\n  duplication_t::release_frame() {\n    if (!has_frame) {\n      return capture_e::ok;\n    }\n\n    auto status = dup->ReleaseFrame();\n    has_frame = false;\n    switch (status) {\n      case S_OK:\n        return capture_e::ok;\n\n      case DXGI_ERROR_INVALID_CALL:\n        BOOST_LOG(warning) << \"Duplication frame already released\";\n        return capture_e::ok;\n\n      case DXGI_ERROR_ACCESS_LOST:\n        return capture_e::reinit;\n\n      case DXGI_ERROR_DEVICE_REMOVED:\n      case DXGI_ERROR_DEVICE_RESET:\n        BOOST_LOG(error) << \"D3D11 device lost during ReleaseFrame [0x\"sv << util::hex(status).to_string_view() << \"], requesting reinit\"sv;\n        return capture_e::reinit;\n\n      default:\n        BOOST_LOG(error) << \"Error while releasing duplication frame [0x\"sv << util::hex(status).to_string_view();\n        return capture_e::error;\n    }\n  }\n\n  duplication_t::~duplication_t() {\n    release_frame();\n  }\n\n  capture_e\n  display_base_t::capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) {\n    auto adjust_client_frame_rate = [&]() -> DXGI_RATIONAL {\n      double requested_fps = static_cast<double>(client_frame_rate_rational.Numerator) / client_frame_rate_rational.Denominator;\n\n      // Check if client requested an NTSC framerate (denominator is 1001)\n      bool is_ntsc_request = (client_frame_rate_rational.Denominator == 1001);\n\n      // Adjust capture frame interval when display refresh rate is not integral but very close to requested fps.\n      if (display_refresh_rate.Denominator > 1) {\n        double display_fps = static_cast<double>(display_refresh_rate.Numerator) / display_refresh_rate.Denominator;\n\n        // Check if display refresh rate matches the requested NTSC framerate pattern\n        // For example, client requests 60000/1001 (59.94fps), display is 59940/1000 (59.94fps)\n        if (is_ntsc_request) {\n          // Check if display refresh rate is close to the requested NTSC rate\n          double ratio = display_fps / requested_fps;\n          if (ratio > 0.999 && ratio < 1.001) {\n            // Display matches requested NTSC rate, use display refresh rate for perfect sync\n            BOOST_LOG(info) << \"Display refresh rate (\" << display_fps << \"fps) matches NTSC request (\"\n                            << requested_fps << \"fps), using display timing for capture\";\n            return display_refresh_rate;\n          }\n          // Check if display is a multiple of the requested rate (e.g., 120Hz display, 60fps request)\n          int multiplier = static_cast<int>(std::round(display_fps / requested_fps));\n          if (multiplier > 1) {\n            double expected = requested_fps * multiplier;\n            if (std::abs(display_fps - expected) / expected < 0.001) {\n              // Display is a clean multiple, adjust the NTSC rate\n              DXGI_RATIONAL adjusted = { display_refresh_rate.Numerator, display_refresh_rate.Denominator * static_cast<UINT>(multiplier) };\n              double adjusted_fps = static_cast<double>(adjusted.Numerator) / adjusted.Denominator;\n              BOOST_LOG(info) << \"Adjusted NTSC capture rate from \" << requested_fps << \"fps to \"\n                              << adjusted_fps << \"fps to match display (\" << display_fps << \"fps / \" << multiplier << \")\";\n              return adjusted;\n            }\n          }\n        }\n\n        // Original logic for non-NTSC rates\n        DXGI_RATIONAL candidate = display_refresh_rate;\n        if (client_frame_rate % display_refresh_rate_rounded == 0) {\n          candidate.Numerator *= client_frame_rate / display_refresh_rate_rounded;\n        }\n        else if (display_refresh_rate_rounded % client_frame_rate == 0) {\n          candidate.Denominator *= display_refresh_rate_rounded / client_frame_rate;\n        }\n        double candidate_rate = static_cast<double>(candidate.Numerator) / candidate.Denominator;\n        // Can only decrease requested fps, otherwise client may start accumulating frames and suffer increased latency.\n        if (requested_fps > candidate_rate && candidate_rate / requested_fps > 0.99) {\n          BOOST_LOG(info) << \"Adjusted capture rate to \" << candidate_rate << \"fps to better match display\";\n          return candidate;\n        }\n      }\n\n      // Use the client's requested fractional framerate directly\n      return client_frame_rate_rational;\n    };\n\n    DXGI_RATIONAL client_frame_rate_adjusted = adjust_client_frame_rate();\n    std::optional<std::chrono::steady_clock::time_point> frame_pacing_group_start;\n    uint32_t frame_pacing_group_frames = 0;\n\n    // Keep the display awake during capture. If the display goes to sleep during\n    // capture, best case is that capture stops until it powers back on. However,\n    // worst case it will trigger us to reinit DD, waking the display back up in\n    // a neverending cycle of waking and sleeping the display of an idle machine.\n    SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);\n    auto clear_display_required = util::fail_guard([]() {\n      SetThreadExecutionState(ES_CONTINUOUS);\n    });\n\n    sleep_overshoot_logger.reset();\n\n    while (true) {\n      // This will return false if the HDR state changes or for any number of other\n      // display or GPU changes. We should reinit to examine the updated state of\n      // the display subsystem. It is recommended to call this once per frame.\n      if (!factory->IsCurrent()) {\n        return platf::capture_e::reinit;\n      }\n\n      // Check for HDR metadata changes periodically\n      if (const auto now = std::chrono::steady_clock::now(); now - last_hdr_check_time >= hdr_check_interval) {\n        last_hdr_check_time = now;\n\n        if (is_hdr()) {\n          SS_HDR_METADATA current_metadata;\n          if (get_hdr_metadata(current_metadata)) {\n            if (!cached_hdr_metadata) {\n              // First check, cache the metadata\n              cached_hdr_metadata = current_metadata;\n            }\n            else if (cached_hdr_metadata->maxDisplayLuminance != current_metadata.maxDisplayLuminance ||\n                     cached_hdr_metadata->minDisplayLuminance != current_metadata.minDisplayLuminance ||\n                     cached_hdr_metadata->maxFullFrameLuminance != current_metadata.maxFullFrameLuminance) {\n              // HDR metadata changed\n              BOOST_LOG(info) << \"HDR metadata changed, reinitializing capture\";\n              cached_hdr_metadata = current_metadata;\n              return capture_e::reinit;\n            }\n          }\n        }\n        else if (cached_hdr_metadata) {\n          // Not in HDR mode, clear cache\n          cached_hdr_metadata.reset();\n        }\n      }\n\n      platf::capture_e status = capture_e::ok;\n      std::shared_ptr<img_t> img_out;\n\n      // Try to continue frame pacing group, snapshot() is called with zero timeout after waiting for client frame interval\n      if (frame_pacing_group_start) {\n        const uint32_t seconds = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator / client_frame_rate_adjusted.Numerator;\n        const uint32_t remainder = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator % client_frame_rate_adjusted.Numerator;\n        const auto sleep_target = *frame_pacing_group_start +\n                                  std::chrono::nanoseconds(1s) * seconds +\n                                  std::chrono::nanoseconds(1s) * remainder / client_frame_rate_adjusted.Numerator;\n        const auto sleep_period = sleep_target - std::chrono::steady_clock::now();\n\n        if (sleep_period <= 0ns) {\n          // We missed next frame time, invalidating current frame pacing group\n          frame_pacing_group_start = std::nullopt;\n          frame_pacing_group_frames = 0;\n          status = capture_e::timeout;\n        }\n        else {\n          timer->sleep_for(sleep_period);\n          sleep_overshoot_logger.first_point(sleep_target);\n          sleep_overshoot_logger.second_point_now_and_log();\n\n          // Try with 0ms timeout first (non-blocking check)\n          status = snapshot(pull_free_image_cb, img_out, 0ms, *cursor);\n\n          // If 0ms timeout failed but we're very close to the target time, try once more with a small timeout\n          // This helps catch frames that arrive slightly early or late due to timing variations\n          if (status == capture_e::timeout) {\n            const auto time_since_target = std::chrono::steady_clock::now() - sleep_target;\n            // If we're within 2ms of the target time, try one more time with a small timeout\n            if (time_since_target < 2ms && time_since_target > -2ms) {\n              status = snapshot(pull_free_image_cb, img_out, 2ms, *cursor);\n            }\n          }\n\n          if (status == capture_e::ok && img_out) {\n            frame_pacing_group_frames += 1;\n          }\n          else {\n            frame_pacing_group_start = std::nullopt;\n            frame_pacing_group_frames = 0;\n          }\n        }\n      }\n\n      // Start new frame pacing group if necessary, snapshot() is called with non-zero timeout\n      if (status == capture_e::timeout || (status == capture_e::ok && !frame_pacing_group_start)) {\n        // Optimization: Use short timeout polling instead of long timeout to reduce lock contention.\n        // The D3D11 device is protected by an unfair lock that is held the entire time that\n        // IDXGIOutputDuplication::AcquireNextFrame() is running. Using short timeouts based on\n        // display refresh rate allows us to release the lock more frequently, giving the encoding\n        // thread opportunities to acquire it for operations like creating dummy images or initializing shared state.\n        // This prevents encoder reinitialization from taking several seconds due to lock starvation.\n        //\n        // Calculate optimal short timeout based on display refresh rate (aim for ~half a frame interval)\n        // This ensures we poll frequently enough to catch frames quickly while still releasing the lock regularly.\n        auto short_timeout = std::chrono::milliseconds(16);  // Default to ~60fps frame interval\n        if (display_refresh_rate_rounded > 0) {\n          // Calculate half a frame interval in milliseconds, with minimum of 4ms and maximum of 16ms\n          auto frame_interval_ms = 1000.0 / display_refresh_rate_rounded;\n          short_timeout = std::chrono::milliseconds(std::max(4, std::min(16, static_cast<int>(frame_interval_ms / 2))));\n        }\n        constexpr auto max_total_timeout = 200ms;\n        const auto max_attempts = static_cast<int>((max_total_timeout.count() + short_timeout.count() - 1) / short_timeout.count());\n\n        status = capture_e::timeout;\n        for (int attempt = 0; attempt < max_attempts && status == capture_e::timeout; ++attempt) {\n          status = snapshot(pull_free_image_cb, img_out, short_timeout, *cursor);\n\n          // If we got a frame or error, break immediately\n          if (status != capture_e::timeout) {\n            break;\n          }\n\n          // Release the snapshot to free the lock before next attempt\n          // This gives encoding thread a chance to acquire the device lock\n          release_snapshot();\n\n          // Small sleep to yield CPU and allow encoding thread to run\n          if (attempt < max_attempts - 1) {\n            std::this_thread::sleep_for(1ms);\n          }\n        }\n\n        if (status == capture_e::ok && img_out) {\n          frame_pacing_group_start = img_out->frame_timestamp;\n\n          if (!frame_pacing_group_start) {\n            BOOST_LOG(warning) << \"snapshot() provided image without timestamp\";\n            frame_pacing_group_start = std::chrono::steady_clock::now();\n          }\n\n          frame_pacing_group_frames = 1;\n        }\n      }\n\n      switch (status) {\n        case platf::capture_e::reinit:\n        case platf::capture_e::error:\n        case platf::capture_e::interrupted:\n          return status;\n        case platf::capture_e::timeout:\n          if (!push_captured_image_cb(std::move(img_out), false)) {\n            return capture_e::ok;\n          }\n          break;\n        case platf::capture_e::ok:\n          if (!push_captured_image_cb(std::move(img_out), true)) {\n            return capture_e::ok;\n          }\n          break;\n        default:\n          BOOST_LOG(error) << \"Unrecognized capture status [\"sv << (int) status << ']';\n          return status;\n      }\n\n      status = release_snapshot();\n      if (status != platf::capture_e::ok) {\n        return status;\n      }\n    }\n\n    return capture_e::ok;\n  }\n\n  /**\n   * @brief Tests to determine if the Desktop Duplication API can capture the given output.\n   * @details When testing for enumeration only, we avoid resyncing the thread desktop.\n   * @param adapter The DXGI adapter to use for capture.\n   * @param output The DXGI output to capture.\n   * @param enumeration_only Specifies whether this test is occurring for display enumeration.\n   */\n  bool\n  test_dxgi_duplication(adapter_t &adapter, output_t &output, bool enumeration_only) {\n    D3D_FEATURE_LEVEL featureLevels[] {\n      D3D_FEATURE_LEVEL_11_1,\n      D3D_FEATURE_LEVEL_11_0,\n      D3D_FEATURE_LEVEL_10_1,\n      D3D_FEATURE_LEVEL_10_0,\n      D3D_FEATURE_LEVEL_9_3,\n      D3D_FEATURE_LEVEL_9_2,\n      D3D_FEATURE_LEVEL_9_1\n    };\n\n    device_t device;\n    auto status = D3D11CreateDevice(\n      adapter.get(),\n      D3D_DRIVER_TYPE_UNKNOWN,\n      nullptr,\n      D3D11_CREATE_DEVICE_FLAGS,\n      featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL),\n      D3D11_SDK_VERSION,\n      &device,\n      nullptr,\n      nullptr);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create D3D11 device for DD test [0x\"sv << util::hex(status).to_string_view() << ']';\n      return false;\n    }\n\n    output1_t output1;\n    status = output->QueryInterface(IID_IDXGIOutput1, (void **) &output1);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to query IDXGIOutput1 from the output\"sv;\n      return false;\n    }\n\n    // Check if we can use the Desktop Duplication API on this output\n    for (int x = 0; x < 2; ++x) {\n      dup_t dup;\n\n      // Only resynchronize the thread desktop when not enumerating displays.\n      // During enumeration, the caller will do this only once to ensure\n      // a consistent view of available outputs.\n      if (!enumeration_only) {\n        syncThreadDesktop();\n      }\n\n      status = output1->DuplicateOutput((IUnknown *) device.get(), &dup);\n      if (SUCCEEDED(status)) {\n        return true;\n      }\n\n      // If we're not resyncing the thread desktop and we don't have permission to\n      // capture the current desktop, just bail immediately. Retrying won't help.\n      if (enumeration_only && status == E_ACCESSDENIED) {\n        break;\n      }\n      else {\n        std::this_thread::sleep_for(200ms);\n      }\n    }\n\n    BOOST_LOG(error) << \"DuplicateOutput() test failed [0x\"sv << util::hex(status).to_string_view() << ']';\n    return false;\n  }\n\n  /**\n   * @brief Hook for NtGdiDdDDIGetCachedHybridQueryValue() from win32u.dll.\n   * @param gpuPreference A pointer to the location where the preference will be written.\n   * @return Always STATUS_SUCCESS if valid arguments are provided.\n   */\n  NTSTATUS\n  __stdcall NtGdiDdDDIGetCachedHybridQueryValueHook(D3DKMT_GPU_PREFERENCE_QUERY_STATE *gpuPreference) {\n    // By faking a cached GPU preference state of D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, this will\n    // prevent DXGI from performing the normal GPU preference resolution that looks at the registry,\n    // power settings, and the hybrid adapter DDI interface to pick a GPU. Instead, we will not be\n    // bound to any specific GPU. This will prevent DXGI from performing output reparenting (moving\n    // outputs from their true location to the render GPU), which breaks DDA.\n    if (gpuPreference) {\n      *gpuPreference = D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED;\n      return 0;  // STATUS_SUCCESS\n    }\n    else {\n      return STATUS_INVALID_PARAMETER;\n    }\n  }\n\n  int\n  display_base_t::init(const ::video::config_t &config, const std::string &display_name) {\n    static std::once_flag windows_cpp_once_flag;\n\n    std::call_once(windows_cpp_once_flag, []() {\n      if (auto user32 = LoadLibraryA(\"user32.dll\")) {\n        if (auto f = (BOOL(*)(HANDLE)) GetProcAddress(user32, \"SetProcessDpiAwarenessContext\")) {\n          f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);\n        }\n        FreeLibrary(user32);\n      }\n\n      // We aren't calling MH_Uninitialize(), but that's okay because this hook lasts for the life of the process\n      MH_Initialize();\n      MH_CreateHookApi(L\"win32u.dll\", \"NtGdiDdDDIGetCachedHybridQueryValue\", (void *) NtGdiDdDDIGetCachedHybridQueryValueHook, nullptr);\n      MH_EnableHook(MH_ALL_HOOKS);\n    });\n\n    // Get rectangle of full desktop for absolute mouse coordinates\n    env_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);\n    env_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);\n\n    HRESULT status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create DXGIFactory1 [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n\n    const auto adapter_name = from_utf8(config::video.adapter_name);\n    const bool is_rdp_session = !is_running_as_system_user && display_device::w_utils::is_any_rdp_session_active();\n    auto output_name = is_rdp_session ? std::wstring {} : from_utf8(display_name);\n\n    if (is_rdp_session) {\n      BOOST_LOG(info) << \"[Display Init] RDP session detected - using first available RDP virtual display\";\n    }\n    else {\n      BOOST_LOG(debug) << \"[Display Init] Initializing display: \" << display_name;\n    }\n\n    adapter_t::pointer adapter_p;\n    for (int tries = 0; tries < 2 && !output; ++tries) {\n      if (tries == 1) {\n        SetThreadExecutionState(ES_DISPLAY_REQUIRED);\n        Sleep(500);\n      }\n\n      for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {\n        dxgi::adapter_t adapter_tmp { adapter_p };\n\n        DXGI_ADAPTER_DESC1 adapter_desc;\n        adapter_tmp->GetDesc1(&adapter_desc);\n\n        if (!adapter_name.empty() && adapter_desc.Description != adapter_name) {\n          continue;\n        }\n\n        dxgi::output_t::pointer output_p;\n        for (int y = 0; adapter_tmp->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {\n          dxgi::output_t output_tmp { output_p };\n\n          DXGI_OUTPUT_DESC desc;\n          output_tmp->GetDesc(&desc);\n\n          if (!is_rdp_session && !output_name.empty() && desc.DeviceName != output_name) {\n            continue;\n          }\n\n          const bool output_accepted = is_rdp_session ||\n                                       (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp, false));\n\n          if (output_accepted) {\n            BOOST_LOG(is_rdp_session ? info : debug) << \"[Display Init] Selected display: \" << to_utf8(desc.DeviceName);\n\n            output = std::move(output_tmp);\n            offset_x = desc.DesktopCoordinates.left;\n            offset_y = desc.DesktopCoordinates.top;\n            width = desc.DesktopCoordinates.right - offset_x;\n            height = desc.DesktopCoordinates.bottom - offset_y;\n            display_rotation = desc.Rotation;\n\n            if (display_rotation == DXGI_MODE_ROTATION_ROTATE90 ||\n                display_rotation == DXGI_MODE_ROTATION_ROTATE270) {\n              width_before_rotation = height;\n              height_before_rotation = width;\n            }\n            else {\n              width_before_rotation = width;\n              height_before_rotation = height;\n            }\n\n            offset_x -= GetSystemMetrics(SM_XVIRTUALSCREEN);\n            offset_y -= GetSystemMetrics(SM_YVIRTUALSCREEN);\n\n            adapter = std::move(adapter_tmp);\n            break;\n          }\n        }\n\n        if (output) break;\n      }\n    }\n\n    if (!output) {\n      BOOST_LOG(error) << \"Failed to locate an output device\"sv;\n      return -1;\n    }\n\n    constexpr D3D_FEATURE_LEVEL featureLevels[] {\n      D3D_FEATURE_LEVEL_11_1,\n      D3D_FEATURE_LEVEL_11_0,\n      D3D_FEATURE_LEVEL_10_1,\n      D3D_FEATURE_LEVEL_10_0,\n      D3D_FEATURE_LEVEL_9_3,\n      D3D_FEATURE_LEVEL_9_2,\n      D3D_FEATURE_LEVEL_9_1\n    };\n\n    status = adapter->QueryInterface(IID_IDXGIAdapter, (void **) &adapter_p);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to query IDXGIAdapter interface\"sv;\n      return -1;\n    }\n\n    status = D3D11CreateDevice(\n      adapter_p,\n      D3D_DRIVER_TYPE_UNKNOWN,\n      nullptr,\n      D3D11_CREATE_DEVICE_FLAGS,\n      featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL),\n      D3D11_SDK_VERSION,\n      &device,\n      &feature_level,\n      &device_ctx);\n\n    adapter_p->Release();\n\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create D3D11 device [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n\n    DXGI_ADAPTER_DESC adapter_desc;\n    adapter->GetDesc(&adapter_desc);\n\n    BOOST_LOG(info)\n      << \"Device Description : \" << to_utf8(adapter_desc.Description)\n      << \", Vendor ID: 0x\"sv << util::hex(adapter_desc.VendorId).to_string_view()\n      << \", Device ID: 0x\"sv << util::hex(adapter_desc.DeviceId).to_string_view()\n      << \", Video Mem: \"sv << adapter_desc.DedicatedVideoMemory / 1048576 << \" MiB\"sv\n      << \", Feature Level: 0x\"sv << util::hex(feature_level).to_string_view()\n      << \", Capture: \"sv << width << 'x' << height\n      << \", Offset: \"sv << offset_x << 'x' << offset_y;\n\n    // Bump up thread priority\n    {\n      TOKEN_PRIVILEGES tp;\n      HANDLE token;\n      LUID val;\n\n      if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token)) {\n        if (LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) {\n          tp.PrivilegeCount = 1;\n          tp.Privileges[0].Luid = val;\n          tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;\n          AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL);\n        }\n        CloseHandle(token);\n      }\n\n      if (HMODULE gdi32 = GetModuleHandleA(\"GDI32\")) {\n        auto check_hags = [gdi32, this](const LUID &adapter_luid) -> bool {\n          auto d3dkmt_open_adapter = (PD3DKMTOpenAdapterFromLuid) GetProcAddress(gdi32, \"D3DKMTOpenAdapterFromLuid\");\n          auto d3dkmt_query_adapter_info = (PD3DKMTQueryAdapterInfo) GetProcAddress(gdi32, \"D3DKMTQueryAdapterInfo\");\n          auto d3dkmt_close_adapter = (PD3DKMTCloseAdapter) GetProcAddress(gdi32, \"D3DKMTCloseAdapter\");\n          if (!d3dkmt_open_adapter || !d3dkmt_query_adapter_info || !d3dkmt_close_adapter) {\n            return false;\n          }\n\n          D3DKMT_OPENADAPTERFROMLUID d3dkmt_adapter = { adapter_luid };\n          if (FAILED(d3dkmt_open_adapter(&d3dkmt_adapter))) {\n            return false;\n          }\n\n          D3DKMT_WDDM_2_7_CAPS d3dkmt_adapter_caps = {};\n          D3DKMT_QUERYADAPTERINFO d3dkmt_adapter_info = {};\n          d3dkmt_adapter_info.hAdapter = d3dkmt_adapter.hAdapter;\n          d3dkmt_adapter_info.Type = KMTQAITYPE_WDDM_2_7_CAPS;\n          d3dkmt_adapter_info.pPrivateDriverData = &d3dkmt_adapter_caps;\n          d3dkmt_adapter_info.PrivateDriverDataSize = sizeof(d3dkmt_adapter_caps);\n\n          bool result = SUCCEEDED(d3dkmt_query_adapter_info(&d3dkmt_adapter_info)) && d3dkmt_adapter_caps.HwSchEnabled;\n\n          D3DKMT_CLOSEADAPTER d3dkmt_close_adapter_wrap = { d3dkmt_adapter.hAdapter };\n          d3dkmt_close_adapter(&d3dkmt_close_adapter_wrap);\n\n          return result;\n        };\n\n        if (auto d3dkmt_set_process_priority = (PD3DKMTSetProcessSchedulingPriorityClass) GetProcAddress(gdi32, \"D3DKMTSetProcessSchedulingPriorityClass\")) {\n          const bool hags_enabled = check_hags(adapter_desc.AdapterLuid);\n          auto priority = D3DKMT_SCHEDULINGPRIORITYCLASS_REALTIME;\n\n          // As of 2023.07, NVIDIA driver has unfixed bug(s) where \"realtime\" can cause unrecoverable encoding freeze or outright driver crash\n          // This issue happens more frequently with HAGS, in DX12 games or when VRAM is filled close to max capacity\n          // Track OBS to see if they find better workaround or NVIDIA fixes it on their end, they seem to be in communication\n          if (adapter_desc.VendorId == 0x10DE && hags_enabled && !config::video.nv_realtime_hags) {\n            priority = D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH;\n          }\n\n          BOOST_LOG(info) << \"HAGS: \" << (hags_enabled ? \"enabled\" : \"disabled\")\n                          << \", GPU priority: \" << (priority == D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH ? \"high\" : \"realtime\");\n\n          if (FAILED(d3dkmt_set_process_priority(GetCurrentProcess(), priority))) {\n            BOOST_LOG(warning) << \"Failed to adjust GPU priority. Run as administrator for optimal performance.\";\n          }\n        }\n      }\n\n      dxgi::dxgi_t dxgi;\n      status = device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to query DXGI interface [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      if (FAILED(dxgi->SetGPUThreadPriority(7))) {\n        BOOST_LOG(warning) << \"Failed to increase GPU thread priority.\";\n      }\n    }\n\n    // Try to reduce latency\n    {\n      dxgi::dxgi1_t dxgi {};\n      if (SUCCEEDED(device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi))) {\n        dxgi->SetMaximumFrameLatency(1);\n      }\n    }\n\n    client_frame_rate = config.framerate;\n\n    if (config.frameRateNum > 0 && config.frameRateDen > 0) {\n      client_frame_rate_rational = { static_cast<UINT>(config.frameRateNum), static_cast<UINT>(config.frameRateDen) };\n      BOOST_LOG(info) << \"Fractional framerate: \" << config.frameRateNum << \"/\" << config.frameRateDen\n                      << \" (\" << static_cast<double>(config.frameRateNum) / config.frameRateDen << \" fps)\";\n    }\n    else {\n      client_frame_rate_rational = { static_cast<UINT>(config.framerate), 1 };\n    }\n\n    dxgi::output6_t output6 {};\n    status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6);\n    if (SUCCEEDED(status)) {\n      DXGI_OUTPUT_DESC1 desc1;\n      output6->GetDesc1(&desc1);\n\n      auto is_hdr_metadata_valid = [](const DXGI_OUTPUT_DESC1 &desc) {\n        return desc.MinLuminance >= 0 &&\n               desc.MinLuminance < desc.MaxLuminance &&\n               desc.MaxLuminance > 0 &&\n               desc.MaxFullFrameLuminance <= desc.MaxLuminance &&\n               desc.MaxFullFrameLuminance <= 4000;\n      };\n\n      if (!is_hdr_metadata_valid(desc1) && !is_rdp_session) {\n        for (int retry = 0; retry < 3 && !is_hdr_metadata_valid(desc1); ++retry) {\n          std::this_thread::sleep_for(std::chrono::milliseconds(100 * (1 << retry)));\n          output6->GetDesc1(&desc1);\n        }\n      }\n\n      BOOST_LOG(info)\n        << \"HDR: \"sv << colorspace_to_string(desc1.ColorSpace)\n        << \", Bits: \"sv << desc1.BitsPerColor\n        << \", Luminance: \"sv << desc1.MinLuminance << '/' << desc1.MaxLuminance << '/' << desc1.MaxFullFrameLuminance << \" nits\"sv;\n\n      // Determine if the captured frames are in linear gamma (need shader conversion).\n      //\n      // The DXGI_COLOR_SPACE_TYPE from the output descriptor tells us the gamma:\n      //   - G10 (gamma 1.0, linear):  scRGB / Windows ACM → data is linear light\n      //   - G2084 (PQ):               HDR mode → DWM outputs scRGB linear, shader applies PQ curve\n      //   - G22 (gamma ~2.2, sRGB):   normal SDR → data already has sRGB gamma\n      //\n      // When capture_linear_gamma is true, the pixel shader must apply a transfer function\n      // (sRGB, PQ, or HLG depending on the encoding colorspace) to convert from linear light.\n      // When false, the captured frames already carry sRGB gamma and should be used as-is.\n      capture_linear_gamma = (desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 ||\n                              desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020);\n      BOOST_LOG(info) << \"Capture gamma: \"sv\n                      << (desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 ? \"linear (HDR/PQ)\" :\n                          capture_linear_gamma ? \"linear (G10, scRGB/ACM)\" :\n                                                 \"sRGB (G22)\");\n    }\n\n    if (!timer || !*timer) {\n      BOOST_LOG(error) << \"Uninitialized high precision timer\";\n      return -1;\n    }\n\n    // Initialize HDR metadata cache for change detection\n    cached_hdr_metadata.reset();\n    last_hdr_check_time = std::chrono::steady_clock::now();\n\n    return 0;\n  }\n\n  bool\n  display_base_t::is_hdr() {\n    dxgi::output6_t output6 {};\n\n    auto status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6);\n    if (FAILED(status)) {\n      BOOST_LOG(warning) << \"Failed to query IDXGIOutput6 from the output\"sv;\n      return false;\n    }\n\n    DXGI_OUTPUT_DESC1 desc1;\n    output6->GetDesc1(&desc1);\n\n    return desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020;\n  }\n\n  bool\n  display_base_t::get_hdr_metadata(SS_HDR_METADATA &metadata) {\n    dxgi::output6_t output6 {};\n\n    std::memset(&metadata, 0, sizeof(metadata));\n\n    auto status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6);\n    if (FAILED(status)) {\n      BOOST_LOG(warning) << \"Failed to query IDXGIOutput6 from the output\"sv;\n      return false;\n    }\n\n    DXGI_OUTPUT_DESC1 desc1;\n    output6->GetDesc1(&desc1);\n\n    // The primaries reported here seem to correspond to scRGB (Rec. 709)\n    // which we then convert to Rec 2020 in our scRGB FP16 -> PQ shader\n    // prior to encoding. It's not clear to me if we're supposed to report\n    // the primaries of the original colorspace or the one we've converted\n    // it to, but let's just report Rec 2020 primaries and D65 white level\n    // to avoid confusing clients by reporting Rec 709 primaries with a\n    // Rec 2020 colorspace. It seems like most clients ignore the primaries\n    // in the metadata anyway (luminance range is most important).\n    desc1.RedPrimary[0] = 0.708f;\n    desc1.RedPrimary[1] = 0.292f;\n    desc1.GreenPrimary[0] = 0.170f;\n    desc1.GreenPrimary[1] = 0.797f;\n    desc1.BluePrimary[0] = 0.131f;\n    desc1.BluePrimary[1] = 0.046f;\n    desc1.WhitePoint[0] = 0.3127f;\n    desc1.WhitePoint[1] = 0.3290f;\n\n    metadata.displayPrimaries[0].x = desc1.RedPrimary[0] * 50000;\n    metadata.displayPrimaries[0].y = desc1.RedPrimary[1] * 50000;\n    metadata.displayPrimaries[1].x = desc1.GreenPrimary[0] * 50000;\n    metadata.displayPrimaries[1].y = desc1.GreenPrimary[1] * 50000;\n    metadata.displayPrimaries[2].x = desc1.BluePrimary[0] * 50000;\n    metadata.displayPrimaries[2].y = desc1.BluePrimary[1] * 50000;\n\n    metadata.whitePoint.x = desc1.WhitePoint[0] * 50000;\n    metadata.whitePoint.y = desc1.WhitePoint[1] * 50000;\n\n    metadata.maxDisplayLuminance = desc1.MaxLuminance;\n    metadata.minDisplayLuminance = desc1.MinLuminance * 10000;\n\n    // These are content-specific metadata parameters that this interface doesn't give us\n    metadata.maxContentLightLevel = 0;\n    metadata.maxFrameAverageLightLevel = 0;\n\n    metadata.maxFullFrameLuminance = desc1.MaxFullFrameLuminance;\n\n    return true;\n  }\n\n  const char *format_str[] = {\n    \"DXGI_FORMAT_UNKNOWN\",\n    \"DXGI_FORMAT_R32G32B32A32_TYPELESS\",\n    \"DXGI_FORMAT_R32G32B32A32_FLOAT\",\n    \"DXGI_FORMAT_R32G32B32A32_UINT\",\n    \"DXGI_FORMAT_R32G32B32A32_SINT\",\n    \"DXGI_FORMAT_R32G32B32_TYPELESS\",\n    \"DXGI_FORMAT_R32G32B32_FLOAT\",\n    \"DXGI_FORMAT_R32G32B32_UINT\",\n    \"DXGI_FORMAT_R32G32B32_SINT\",\n    \"DXGI_FORMAT_R16G16B16A16_TYPELESS\",\n    \"DXGI_FORMAT_R16G16B16A16_FLOAT\",\n    \"DXGI_FORMAT_R16G16B16A16_UNORM\",\n    \"DXGI_FORMAT_R16G16B16A16_UINT\",\n    \"DXGI_FORMAT_R16G16B16A16_SNORM\",\n    \"DXGI_FORMAT_R16G16B16A16_SINT\",\n    \"DXGI_FORMAT_R32G32_TYPELESS\",\n    \"DXGI_FORMAT_R32G32_FLOAT\",\n    \"DXGI_FORMAT_R32G32_UINT\",\n    \"DXGI_FORMAT_R32G32_SINT\",\n    \"DXGI_FORMAT_R32G8X24_TYPELESS\",\n    \"DXGI_FORMAT_D32_FLOAT_S8X24_UINT\",\n    \"DXGI_FORMAT_R32_FLOAT_X8X24_TYPELESS\",\n    \"DXGI_FORMAT_X32_TYPELESS_G8X24_UINT\",\n    \"DXGI_FORMAT_R10G10B10A2_TYPELESS\",\n    \"DXGI_FORMAT_R10G10B10A2_UNORM\",\n    \"DXGI_FORMAT_R10G10B10A2_UINT\",\n    \"DXGI_FORMAT_R11G11B10_FLOAT\",\n    \"DXGI_FORMAT_R8G8B8A8_TYPELESS\",\n    \"DXGI_FORMAT_R8G8B8A8_UNORM\",\n    \"DXGI_FORMAT_R8G8B8A8_UNORM_SRGB\",\n    \"DXGI_FORMAT_R8G8B8A8_UINT\",\n    \"DXGI_FORMAT_R8G8B8A8_SNORM\",\n    \"DXGI_FORMAT_R8G8B8A8_SINT\",\n    \"DXGI_FORMAT_R16G16_TYPELESS\",\n    \"DXGI_FORMAT_R16G16_FLOAT\",\n    \"DXGI_FORMAT_R16G16_UNORM\",\n    \"DXGI_FORMAT_R16G16_UINT\",\n    \"DXGI_FORMAT_R16G16_SNORM\",\n    \"DXGI_FORMAT_R16G16_SINT\",\n    \"DXGI_FORMAT_R32_TYPELESS\",\n    \"DXGI_FORMAT_D32_FLOAT\",\n    \"DXGI_FORMAT_R32_FLOAT\",\n    \"DXGI_FORMAT_R32_UINT\",\n    \"DXGI_FORMAT_R32_SINT\",\n    \"DXGI_FORMAT_R24G8_TYPELESS\",\n    \"DXGI_FORMAT_D24_UNORM_S8_UINT\",\n    \"DXGI_FORMAT_R24_UNORM_X8_TYPELESS\",\n    \"DXGI_FORMAT_X24_TYPELESS_G8_UINT\",\n    \"DXGI_FORMAT_R8G8_TYPELESS\",\n    \"DXGI_FORMAT_R8G8_UNORM\",\n    \"DXGI_FORMAT_R8G8_UINT\",\n    \"DXGI_FORMAT_R8G8_SNORM\",\n    \"DXGI_FORMAT_R8G8_SINT\",\n    \"DXGI_FORMAT_R16_TYPELESS\",\n    \"DXGI_FORMAT_R16_FLOAT\",\n    \"DXGI_FORMAT_D16_UNORM\",\n    \"DXGI_FORMAT_R16_UNORM\",\n    \"DXGI_FORMAT_R16_UINT\",\n    \"DXGI_FORMAT_R16_SNORM\",\n    \"DXGI_FORMAT_R16_SINT\",\n    \"DXGI_FORMAT_R8_TYPELESS\",\n    \"DXGI_FORMAT_R8_UNORM\",\n    \"DXGI_FORMAT_R8_UINT\",\n    \"DXGI_FORMAT_R8_SNORM\",\n    \"DXGI_FORMAT_R8_SINT\",\n    \"DXGI_FORMAT_A8_UNORM\",\n    \"DXGI_FORMAT_R1_UNORM\",\n    \"DXGI_FORMAT_R9G9B9E5_SHAREDEXP\",\n    \"DXGI_FORMAT_R8G8_B8G8_UNORM\",\n    \"DXGI_FORMAT_G8R8_G8B8_UNORM\",\n    \"DXGI_FORMAT_BC1_TYPELESS\",\n    \"DXGI_FORMAT_BC1_UNORM\",\n    \"DXGI_FORMAT_BC1_UNORM_SRGB\",\n    \"DXGI_FORMAT_BC2_TYPELESS\",\n    \"DXGI_FORMAT_BC2_UNORM\",\n    \"DXGI_FORMAT_BC2_UNORM_SRGB\",\n    \"DXGI_FORMAT_BC3_TYPELESS\",\n    \"DXGI_FORMAT_BC3_UNORM\",\n    \"DXGI_FORMAT_BC3_UNORM_SRGB\",\n    \"DXGI_FORMAT_BC4_TYPELESS\",\n    \"DXGI_FORMAT_BC4_UNORM\",\n    \"DXGI_FORMAT_BC4_SNORM\",\n    \"DXGI_FORMAT_BC5_TYPELESS\",\n    \"DXGI_FORMAT_BC5_UNORM\",\n    \"DXGI_FORMAT_BC5_SNORM\",\n    \"DXGI_FORMAT_B5G6R5_UNORM\",\n    \"DXGI_FORMAT_B5G5R5A1_UNORM\",\n    \"DXGI_FORMAT_B8G8R8A8_UNORM\",\n    \"DXGI_FORMAT_B8G8R8X8_UNORM\",\n    \"DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM\",\n    \"DXGI_FORMAT_B8G8R8A8_TYPELESS\",\n    \"DXGI_FORMAT_B8G8R8A8_UNORM_SRGB\",\n    \"DXGI_FORMAT_B8G8R8X8_TYPELESS\",\n    \"DXGI_FORMAT_B8G8R8X8_UNORM_SRGB\",\n    \"DXGI_FORMAT_BC6H_TYPELESS\",\n    \"DXGI_FORMAT_BC6H_UF16\",\n    \"DXGI_FORMAT_BC6H_SF16\",\n    \"DXGI_FORMAT_BC7_TYPELESS\",\n    \"DXGI_FORMAT_BC7_UNORM\",\n    \"DXGI_FORMAT_BC7_UNORM_SRGB\",\n    \"DXGI_FORMAT_AYUV\",\n    \"DXGI_FORMAT_Y410\",\n    \"DXGI_FORMAT_Y416\",\n    \"DXGI_FORMAT_NV12\",\n    \"DXGI_FORMAT_P010\",\n    \"DXGI_FORMAT_P016\",\n    \"DXGI_FORMAT_420_OPAQUE\",\n    \"DXGI_FORMAT_YUY2\",\n    \"DXGI_FORMAT_Y210\",\n    \"DXGI_FORMAT_Y216\",\n    \"DXGI_FORMAT_NV11\",\n    \"DXGI_FORMAT_AI44\",\n    \"DXGI_FORMAT_IA44\",\n    \"DXGI_FORMAT_P8\",\n    \"DXGI_FORMAT_A8P8\",\n    \"DXGI_FORMAT_B4G4R4A4_UNORM\",\n\n    NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,\n\n    \"DXGI_FORMAT_P208\",\n    \"DXGI_FORMAT_V208\",\n    \"DXGI_FORMAT_V408\"\n  };\n\n  const char *\n  display_base_t::dxgi_format_to_string(DXGI_FORMAT format) {\n    return format_str[format];\n  }\n\n  const char *\n  display_base_t::colorspace_to_string(DXGI_COLOR_SPACE_TYPE type) {\n    const char *type_str[] = {\n      \"DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709\",\n      \"DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709\",\n      \"DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709\",\n      \"DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P2020\",\n      \"DXGI_COLOR_SPACE_RESERVED\",\n      \"DXGI_COLOR_SPACE_YCBCR_FULL_G22_NONE_P709_X601\",\n      \"DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601\",\n      \"DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601\",\n      \"DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709\",\n      \"DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709\",\n      \"DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020\",\n      \"DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P2020\",\n      \"DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020\",\n      \"DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020\",\n      \"DXGI_COLOR_SPACE_RGB_STUDIO_G2084_NONE_P2020\",\n      \"DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_TOPLEFT_P2020\",\n      \"DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_TOPLEFT_P2020\",\n      \"DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020\",\n      \"DXGI_COLOR_SPACE_YCBCR_STUDIO_GHLG_TOPLEFT_P2020\",\n      \"DXGI_COLOR_SPACE_YCBCR_FULL_GHLG_TOPLEFT_P2020\",\n      \"DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P709\",\n      \"DXGI_COLOR_SPACE_RGB_STUDIO_G24_NONE_P2020\",\n      \"DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P709\",\n      \"DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_LEFT_P2020\",\n      \"DXGI_COLOR_SPACE_YCBCR_STUDIO_G24_TOPLEFT_P2020\",\n    };\n\n    if (type < ARRAYSIZE(type_str)) {\n      return type_str[type];\n    }\n    else {\n      return \"UNKNOWN\";\n    }\n  }\n\n}  // namespace platf::dxgi\n\nnamespace platf {\n  /**\n   * Pick a display adapter and capture method.\n   * @param hwdevice_type enables possible use of hardware encoder\n   */\n  std::shared_ptr<display_t>\n  display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {\n    // Recover ConsentPromptBehaviorAdmin if Sunshine previously crashed during WGC capture (runs only once)\n    dxgi::recover_secure_desktop();\n\n    auto try_init = [&](auto disp) -> std::shared_ptr<display_t> {\n      if (!disp->init(config, display_name)) {\n        return disp;\n      }\n      return nullptr;\n    };\n\n    // Build list of capture methods to try\n    std::vector<std::string> try_types;\n\n    if (config::video.capture.empty()) {\n      if (is_running_as_system_user) {\n        // WGC is not available in service mode\n        try_types = { \"ddx\" };\n        BOOST_LOG(info) << \"Running in service mode, using DDX capture (WGC not available in services)\"sv;\n      }\n      else {\n        try_types = { \"ddx\", \"wgc\" };\n      }\n    }\n    else if (config::video.capture == \"wgc\" && is_running_as_system_user) {\n      // WGC explicitly requested but unavailable in service mode\n      BOOST_LOG(warning) << \"WGC capture is not available in service mode. Automatically switching to DDX capture.\"sv;\n      try_types = { \"ddx\" };\n    }\n    else {\n      try_types = { config::video.capture };\n    }\n\n    for (const auto &type : try_types) {\n      std::shared_ptr<display_t> ret;\n\n      if (type == \"amd\" && hwdevice_type == mem_type_e::dxgi) {\n        ret = try_init(std::make_shared<dxgi::display_amd_vram_t>());\n      }\n      else if (type == \"ddx\") {\n        if (hwdevice_type == mem_type_e::dxgi) {\n          ret = try_init(std::make_shared<dxgi::display_ddup_vram_t>());\n        }\n        else if (hwdevice_type == mem_type_e::system) {\n          ret = try_init(std::make_shared<dxgi::display_ddup_ram_t>());\n        }\n      }\n      else if (type == \"wgc\" && !is_running_as_system_user) {\n        if (hwdevice_type == mem_type_e::dxgi) {\n          ret = try_init(std::make_shared<dxgi::display_wgc_vram_t>());\n        }\n        else if (hwdevice_type == mem_type_e::system) {\n          ret = try_init(std::make_shared<dxgi::display_wgc_ram_t>());\n        }\n      }\n\n      if (ret) {\n        return ret;\n      }\n    }\n\n    BOOST_LOG(error) << \"Failed to create display for: \" << display_name;\n    return nullptr;\n  }\n\n  std::vector<std::string>\n  display_names(mem_type_e) {\n    std::vector<std::string> display_names;\n\n    HRESULT status;\n\n    BOOST_LOG(debug) << \"Detecting monitors...\"sv;\n\n    // We sync the thread desktop once before we start the enumeration process\n    // to ensure test_dxgi_duplication() returns consistent results for all GPUs\n    // even if the current desktop changes during our enumeration process.\n    // It is critical that we either fully succeed in enumeration or fully fail,\n    // otherwise it can lead to the capture code switching monitors unexpectedly.\n    syncThreadDesktop();\n\n    dxgi::factory1_t factory;\n    status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create DXGIFactory1 [0x\"sv << util::hex(status).to_string_view() << ']';\n      return {};\n    }\n\n    dxgi::adapter_t adapter;\n    for (int x = 0; factory->EnumAdapters1(x, &adapter) != DXGI_ERROR_NOT_FOUND; ++x) {\n      DXGI_ADAPTER_DESC1 adapter_desc;\n      adapter->GetDesc1(&adapter_desc);\n\n      BOOST_LOG(debug)\n        << std::endl\n        << \"ADAPTER:\"sv\n        << \",Device Name: \"sv << to_utf8(adapter_desc.Description)\n        << \",Device Vendor ID : 0x\"sv << util::hex(adapter_desc.VendorId).to_string_view()\n        << \",Device Device ID : 0x\"sv << util::hex(adapter_desc.DeviceId).to_string_view()\n        << \",Device Video Mem : \"sv << adapter_desc.DedicatedVideoMemory / 1048576 << \" MiB\"sv\n        << \",Device Sys Mem: \"sv << adapter_desc.DedicatedSystemMemory / 1048576 << \" MiB\"sv\n        << \",Share Sys Mem: \"sv << adapter_desc.SharedSystemMemory / 1048576 << \" MiB\"sv;\n\n      dxgi::output_t::pointer output_p {};\n      for (int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {\n        dxgi::output_t output { output_p };\n\n        DXGI_OUTPUT_DESC desc;\n        output->GetDesc(&desc);\n\n        auto device_name = to_utf8(desc.DeviceName);\n\n        auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left;\n        auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top;\n\n        BOOST_LOG(debug)\n          << \"Output Name: \"sv << device_name\n          << \",AttachedToDesktop : \"sv << (desc.AttachedToDesktop ? \"yes\"sv : \"no\"sv)\n          << \",Resolution: \"sv << width << 'x' << height;\n\n        // Don't include the display in the list if we can't actually capture it\n        // In RDP sessions, accept any enumerated output since virtual displays may not report attachment status correctly\n        bool is_rdp = !is_running_as_system_user && display_device::w_utils::is_any_rdp_session_active();\n\n        bool can_capture = false;\n\n        if (is_rdp) {\n          // In RDP, accept any enumerated output regardless of AttachedToDesktop status\n          // Virtual displays may not report this correctly\n          can_capture = true;\n          BOOST_LOG(debug) << \"[Display] RDP session: accepting enumerated display (AttachedToDesktop=\"\n                           << (desc.AttachedToDesktop ? \"yes\" : \"no\") << \"): \" << device_name;\n        }\n        else if (desc.AttachedToDesktop) {\n          // Normal session: test DXGI Desktop Duplication only for desktop-attached outputs\n          can_capture = dxgi::test_dxgi_duplication(adapter, output, true);\n          if (can_capture) {\n            BOOST_LOG(debug) << \"[Display] Desktop Duplication test passed: \" << device_name;\n          }\n          else {\n            BOOST_LOG(debug) << \"[Display] 跳过 DXGI测试失败 不可用显示器: \" << device_name;\n          }\n        }\n        else {\n          BOOST_LOG(debug) << \"[Display] 跳过 None AttachedToDesktop 不可用显示器: \" << device_name;\n        }\n\n        if (can_capture) {\n          display_names.emplace_back(std::move(device_name));\n          BOOST_LOG(debug) << \"[Display] 添加可用显示器: \" << device_name;\n        }\n      }\n    }\n    BOOST_LOG(debug) << \"[Display] 显示器枚举完成，找到 \" << display_names.size() << \" 个可用显示器\";\n    return display_names;\n  }\n\n  std::vector<std::string>\n  adapter_names() {\n    std::vector<std::string> adapter_names;\n\n    HRESULT status;\n\n    dxgi::factory1_t factory;\n    status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create DXGIFactory1 [0x\"sv << util::hex(status).to_string_view() << ']';\n      return {};\n    }\n\n    dxgi::adapter_t adapter;\n    for (int x = 0; factory->EnumAdapters1(x, &adapter) != DXGI_ERROR_NOT_FOUND; ++x) {\n      DXGI_ADAPTER_DESC1 adapter_desc;\n      adapter->GetDesc1(&adapter_desc);\n\n      auto adapter_name = to_utf8(adapter_desc.Description);\n      adapter_names.emplace_back(std::move(adapter_name));\n    }\n\n    return adapter_names;\n  }\n\n  /**\n   * @brief Returns if GPUs/drivers have changed since the last call to this function.\n   * @return `true` if a change has occurred or if it is unknown whether a change occurred.\n   */\n  bool\n  needs_encoder_reenumeration() {\n    // Serialize access to the static DXGI factory\n    static std::mutex reenumeration_state_lock;\n    auto lg = std::lock_guard(reenumeration_state_lock);\n\n    // Keep a reference to the DXGI factory, which will keep track of changes internally.\n    static dxgi::factory1_t factory;\n    if (!factory || !factory->IsCurrent()) {\n      factory.reset();\n\n      auto status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to create DXGIFactory1 [0x\"sv << util::hex(status).to_string_view() << ']';\n        factory.release();\n      }\n\n      // Always request reenumeration on the first streaming session just to ensure we\n      // can deal with any initialization races that may occur when the system is booting.\n      BOOST_LOG(info) << \"Encoder reenumeration is required\"sv;\n      return true;\n    }\n    else {\n      // The DXGI factory from last time is still current, so no encoder changes have occurred.\n      return false;\n    }\n  }\n}  // namespace platf"
  },
  {
    "path": "src/platform/windows/display_device/device_hdr_states.cpp",
    "content": "// local includes\n#include \"src/display_device/to_string.h\"\n#include \"src/logging.h\"\n#include \"windows_utils.h\"\n\nnamespace display_device {\n\n  namespace {\n\n    /**\n     * @see set_hdr_states for a description as this was split off to reduce cognitive complexity.\n     */\n    bool\n    do_set_states(const hdr_state_map_t &states) {\n      const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n      if (!display_data) {\n        return false;\n      }\n\n      for (const auto &[device_id, state] : states) {\n        if (state == hdr_state_e::unknown) {\n          continue;\n        }\n\n        const auto path { w_utils::get_active_path(device_id, display_data->paths) };\n        if (!path) {\n          BOOST_LOG(error) << \"Failed to find device for \" << device_id << \"!\";\n          return false;\n        }\n\n        const auto current_state { w_utils::get_hdr_state(*path) };\n        if (current_state == hdr_state_e::unknown) {\n          BOOST_LOG(error) << \"HDR state cannot be changed for \" << device_id << \"!\";\n          return false;\n        }\n\n        // 仅当「请求关闭且当前已关闭」时跳过。请求启用 HDR 时始终执行 set，避免因 get_hdr_state\n        // 滞后/错误（如 VDD 或拓扑刚变更）误判为已 enabled 而跳过，导致主机端实际仍为 SDR。\n        if (state == hdr_state_e::disabled && current_state == hdr_state_e::disabled) {\n          BOOST_LOG(debug) << \"HDR state for \" << device_id << \" is already disabled, skipping\";\n          continue;\n        }\n\n        if (!w_utils::set_hdr_state(*path, state == hdr_state_e::enabled)) {\n          return false;\n        }\n      }\n\n      return true;\n    }\n\n  }  // namespace\n\n  hdr_state_map_t\n  get_current_hdr_states(const std::unordered_set<std::string> &device_ids) {\n    if (device_ids.empty()) {\n      BOOST_LOG(error) << \"Device id set is empty!\";\n      return {};\n    }\n\n    const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n    if (!display_data) {\n      // Error already logged\n      return {};\n    }\n\n    hdr_state_map_t states;\n    for (const auto &device_id : device_ids) {\n      const auto path { w_utils::get_active_path(device_id, display_data->paths) };\n      if (!path) {\n        BOOST_LOG(error) << \"Failed to find device for \" << device_id << \"!\";\n        return {};\n      }\n\n      states[device_id] = w_utils::get_hdr_state(*path);\n    }\n\n    return states;\n  }\n\n  bool\n  set_hdr_states(const hdr_state_map_t &states) {\n    if (states.empty()) {\n      BOOST_LOG(error) << \"States map is empty!\";\n      return false;\n    }\n\n    std::unordered_set<std::string> device_ids;\n    for (const auto &[device_id, _] : states) {\n      if (!device_ids.insert(device_id).second) {\n        // Sanity check since, it's technically not possible with unordered map to have duplicate keys\n        BOOST_LOG(error) << \"Duplicate device id provided: \" << device_id << \"!\";\n        return false;\n      }\n    }\n\n    const auto original_states { get_current_hdr_states(device_ids) };\n    if (original_states.empty()) {\n      // Error already logged\n      return false;\n    }\n\n    if (!do_set_states(states)) {\n      do_set_states(original_states);  // return value does not matter\n      return false;\n    }\n\n    return true;\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/windows/display_device/device_modes.cpp",
    "content": "// local includes\n#include \"src/logging.h\"\n#include \"windows_utils.h\"\n\nnamespace display_device {\n\n  namespace {\n\n    /**\n     * @brief Check if the refresh rates are almost equal.\n     * @param r1 First refresh rate.\n     * @param r2 Second refresh rate.\n     * @return True if refresh rates are almost equal, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const bool almost_equal = fuzzy_compare_refresh_rates(refresh_rate_t { 60, 1 }, refresh_rate_t { 5985, 100 });\n     * const bool not_equal = fuzzy_compare_refresh_rates(refresh_rate_t { 60, 1 }, refresh_rate_t { 5585, 100 });\n     * ```\n     */\n    bool\n    fuzzy_compare_refresh_rates(const refresh_rate_t &r1, const refresh_rate_t &r2) {\n      if (r1.denominator > 0 && r2.denominator > 0) {\n        const float r1_f { static_cast<float>(r1.numerator) / static_cast<float>(r1.denominator) };\n        const float r2_f { static_cast<float>(r2.numerator) / static_cast<float>(r2.denominator) };\n        return (std::abs(r1_f - r2_f) <= 1.f);\n      }\n\n      return false;\n    }\n\n    /**\n     * @brief Check if the display modes are almost equal.\n     * @param mode_a First mode.\n     * @param mode_b Second mode.\n     * @return True if display modes are almost equal, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const bool almost_equal = fuzzy_compare_refresh_rates(display_mode_t { { 1920, 1080 }, { 60, 1 } },\n     *                                                       display_mode_t { { 1920, 1080 }, { 5985, 100 } });\n     * const bool not_equal = fuzzy_compare_refresh_rates(display_mode_t { { 1920, 1080 }, { 60, 1 } },\n     *                                                    display_mode_t { { 1920, 1080 }, { 5585, 100 } });\n     * ```\n     */\n    bool\n    fuzzy_compare_modes(const display_mode_t &mode_a, const display_mode_t &mode_b) {\n      return mode_a.resolution.width == mode_b.resolution.width &&\n             mode_a.resolution.height == mode_b.resolution.height &&\n             fuzzy_compare_refresh_rates(mode_a.refresh_rate, mode_b.refresh_rate);\n    }\n\n    /**\n     * @brief Get all the missing duplicate device ids for the provided device ids.\n     * @param device_ids Device ids to find the missing duplicate ids for.\n     * @returns A list of device ids containing the provided device ids and all unspecified ids\n     *          for duplicated displays.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const auto device_ids_with_duplicates = get_all_duplicated_devices({ \"MY_ID1\" });\n     * ```\n     */\n    std::unordered_set<std::string>\n    get_all_duplicated_devices(const std::unordered_set<std::string> &device_ids) {\n      const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n      if (!display_data) {\n        // Error already logged\n        return {};\n      }\n\n      std::unordered_set<std::string> all_device_ids;\n      for (const auto &device_id : device_ids) {\n        if (device_id.empty()) {\n          BOOST_LOG(error) << \"Device it is empty!\";\n          return {};\n        }\n\n        const auto provided_path { w_utils::get_active_path(device_id, display_data->paths) };\n        if (!provided_path) {\n          BOOST_LOG(warning) << \"Failed to find device for \" << device_id << \"!\";\n          return {};\n        }\n\n        const auto provided_path_source_mode { w_utils::get_source_mode(w_utils::get_source_index(*provided_path, display_data->modes), display_data->modes) };\n        if (!provided_path_source_mode) {\n          BOOST_LOG(error) << \"Active device does not have a source mode: \" << device_id << \"!\";\n          return {};\n        }\n\n        // We will now iterate over all the active paths (provided path included) and check if\n        // any of them are duplicated.\n        for (const auto &path : display_data->paths) {\n          const auto device_info { w_utils::get_device_info_for_valid_path(path, w_utils::ACTIVE_ONLY_DEVICES) };\n          if (!device_info) {\n            continue;\n          }\n\n          if (all_device_ids.count(device_info->device_id) > 0) {\n            // Already checked\n            continue;\n          }\n\n          const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(path, display_data->modes), display_data->modes) };\n          if (!source_mode) {\n            BOOST_LOG(error) << \"Active device does not have a source mode: \" << device_info->device_id << \"!\";\n            return {};\n          }\n\n          if (!w_utils::are_modes_duplicated(*provided_path_source_mode, *source_mode)) {\n            continue;\n          }\n\n          all_device_ids.insert(device_info->device_id);\n        }\n      }\n\n      return all_device_ids;\n    }\n\n    /**\n     * @see set_display_modes for a description as this was split off to reduce cognitive complexity.\n     */\n    bool\n    do_set_modes(const device_display_mode_map_t &modes, bool allow_changes) {\n      auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n      if (!display_data) {\n        // Error already logged\n        return false;\n      }\n\n      bool changes_applied { false };\n      for (const auto &[device_id, mode] : modes) {\n        const auto path { w_utils::get_active_path(device_id, display_data->paths) };\n        if (!path) {\n          BOOST_LOG(error) << \"Failed to find device for \" << device_id << \"!\";\n          return false;\n        }\n\n        const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) };\n        if (!source_mode) {\n          BOOST_LOG(error) << \"Active device does not have a source mode: \" << device_id << \"!\";\n          return false;\n        }\n\n        bool new_changes { false };\n        const bool resolution_changed { source_mode->width != mode.resolution.width || source_mode->height != mode.resolution.height };\n\n        bool refresh_rate_changed { false };\n        if (allow_changes) {\n          refresh_rate_changed = !fuzzy_compare_refresh_rates(refresh_rate_t { path->targetInfo.refreshRate.Numerator, path->targetInfo.refreshRate.Denominator }, mode.refresh_rate);\n        }\n        else {\n          // Since we are in strict mode, do not fuzzy compare it\n          refresh_rate_changed = path->targetInfo.refreshRate.Numerator != mode.refresh_rate.numerator ||\n                                 path->targetInfo.refreshRate.Denominator != mode.refresh_rate.denominator;\n        }\n\n        if (resolution_changed) {\n          source_mode->width = mode.resolution.width;\n          source_mode->height = mode.resolution.height;\n          new_changes = true;\n        }\n\n        if (refresh_rate_changed) {\n          path->targetInfo.refreshRate = { mode.refresh_rate.numerator, mode.refresh_rate.denominator };\n          new_changes = true;\n        }\n\n        if (new_changes) {\n          // Clear the target index so that Windows has to select/modify the target to best match the requirements.\n          w_utils::set_target_index(*path, boost::none);\n          w_utils::set_desktop_index(*path, boost::none);  // Part of struct containing target index and so it needs to be cleared\n        }\n\n        changes_applied = changes_applied || new_changes;\n      }\n\n      if (!changes_applied) {\n        BOOST_LOG(debug) << \"No changes were made to display modes as they are equal.\";\n        return true;\n      }\n\n      UINT32 flags { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE };\n      if (allow_changes) {\n        // It's probably best for Windows to select the \"best\" display settings for us. However, in case we\n        // have custom resolution set in nvidia control panel for example, this flag will prevent successfully applying\n        // settings to it.\n        flags |= SDC_ALLOW_CHANGES;\n      }\n\n      const LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), display_data->modes.size(), display_data->modes.data(), flags) };\n      if (result != ERROR_SUCCESS) {\n        BOOST_LOG(error) << w_utils::get_error_string(result) << \" failed to set display mode!\";\n        return false;\n      }\n\n      return true;\n    };\n\n    /**\n     * @brief Attempt to set display modes by explicitly specifying both source and target mode parameters.\n     *\n     * This is a fallback for cases where Windows CCD cannot automatically match source modes to target modes\n     * (e.g., non-standard resolutions like 2560x1600 on newly created virtual display paths).\n     * Instead of clearing the target mode index and relying on Windows' mode matching algorithm,\n     * this function directly modifies the existing target mode entry with the desired parameters.\n     *\n     * @param modes The desired display modes to set.\n     * @return True if successfully applied, false otherwise.\n     */\n    bool\n    do_set_modes_with_explicit_target(const device_display_mode_map_t &modes) {\n      auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n      if (!display_data) {\n        return false;\n      }\n\n      bool changes_applied { false };\n      for (const auto &[device_id, mode] : modes) {\n        auto path { w_utils::get_active_path(device_id, display_data->paths) };\n        if (!path) {\n          BOOST_LOG(error) << \"Failed to find device for explicit target mode set: \" << device_id << \"!\";\n          return false;\n        }\n\n        auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) };\n        if (!source_mode) {\n          BOOST_LOG(error) << \"Active device does not have a source mode: \" << device_id << \"!\";\n          return false;\n        }\n\n        // Update source mode with desired resolution\n        const bool resolution_changed { source_mode->width != mode.resolution.width || source_mode->height != mode.resolution.height };\n        if (resolution_changed) {\n          source_mode->width = mode.resolution.width;\n          source_mode->height = mode.resolution.height;\n          changes_applied = true;\n        }\n\n        // Update path refresh rate\n        const bool refresh_rate_changed { path->targetInfo.refreshRate.Numerator != mode.refresh_rate.numerator ||\n                                          path->targetInfo.refreshRate.Denominator != mode.refresh_rate.denominator };\n        if (refresh_rate_changed) {\n          path->targetInfo.refreshRate = { mode.refresh_rate.numerator, mode.refresh_rate.denominator };\n          changes_applied = true;\n        }\n\n        // Instead of clearing the target index, try to update the target mode entry in-place.\n        // This avoids relying on Windows CCD's automatic source-to-target mode matching,\n        // which can fail for non-standard resolutions on newly created virtual display paths.\n        const UINT32 target_idx { path->targetInfo.targetModeInfoIdx };\n        if (target_idx == DISPLAYCONFIG_PATH_TARGET_MODE_IDX_INVALID || target_idx >= display_data->modes.size()) {\n          BOOST_LOG(warning) << \"Explicit target mode fallback: no valid target mode entry for \" << device_id << \", skipping.\";\n          return false;\n        }\n\n        auto &target_mode_info = display_data->modes[target_idx];\n        if (target_mode_info.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_TARGET) {\n          BOOST_LOG(warning) << \"Explicit target mode fallback: mode entry is not a TARGET type for \" << device_id << \", skipping.\";\n          return false;\n        }\n\n        auto &signal = target_mode_info.targetMode.targetVideoSignalInfo;\n        const UINT32 width = mode.resolution.width;\n        const UINT32 height = mode.resolution.height;\n        const UINT32 vsync_num = mode.refresh_rate.numerator;\n        const UINT32 vsync_den = mode.refresh_rate.denominator > 0 ? mode.refresh_rate.denominator : 1;\n\n        signal.activeSize.cx = width;\n        signal.activeSize.cy = height;\n        signal.totalSize.cx = width;\n        signal.totalSize.cy = height;\n        signal.vSyncFreq.Numerator = vsync_num;\n        signal.vSyncFreq.Denominator = vsync_den;\n        signal.hSyncFreq.Numerator = vsync_num * height;\n        signal.hSyncFreq.Denominator = vsync_den;\n        signal.pixelRate = static_cast<UINT64>(vsync_num) * width * height / vsync_den;\n        signal.scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE;\n\n        // Clear the desktop image index so Windows reselects it.\n        // A stale DISPLAYCONFIG_MODE_INFO_TYPE_DESKTOP_IMAGE entry (with old size)\n        // can cause SetDisplayConfig to fail with ERROR_GEN_FAILURE when using\n        // SDC_VIRTUAL_MODE_AWARE, even though we are keeping the target index.\n        w_utils::set_desktop_index(*path, boost::none);\n\n        changes_applied = true;\n      }\n\n      if (!changes_applied) {\n        return false;\n      }\n\n      const UINT32 flags { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE };\n      const LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), display_data->modes.size(), display_data->modes.data(), flags) };\n      if (result != ERROR_SUCCESS) {\n        BOOST_LOG(warning) << w_utils::get_error_string(result) << \" failed to set display mode with explicit target!\";\n        return false;\n      }\n\n      return true;\n    }\n\n  }  // namespace\n\n  device_display_mode_map_t\n  get_current_display_modes(const std::unordered_set<std::string> &device_ids) {\n    if (device_ids.empty()) {\n      BOOST_LOG(error) << \"Device id set is empty!\";\n      return {};\n    }\n\n    const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n    if (!display_data) {\n      // Error already logged\n      return {};\n    }\n\n    device_display_mode_map_t current_modes;\n    for (const auto &device_id : device_ids) {\n      if (device_id.empty()) {\n        BOOST_LOG(error) << \"Device id is empty!\";\n        return {};\n      }\n\n      const auto path { w_utils::get_active_path(device_id, display_data->paths) };\n      if (!path) {\n        BOOST_LOG(error) << \"Failed to find device for \" << device_id << \"!\";\n        return {};\n      }\n\n      const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) };\n      if (!source_mode) {\n        BOOST_LOG(error) << \"Active device does not have a source mode: \" << device_id << \"!\";\n        return {};\n      }\n\n      // For whatever reason they put refresh rate into path, but not the resolution.\n      const auto target_refresh_rate { path->targetInfo.refreshRate };\n      current_modes[device_id] = display_mode_t {\n        { source_mode->width, source_mode->height },\n        { target_refresh_rate.Numerator, target_refresh_rate.Denominator }\n      };\n    }\n\n    return current_modes;\n  }\n\n  bool\n  set_display_modes(const device_display_mode_map_t &modes) {\n    if (modes.empty()) {\n      BOOST_LOG(error) << \"Modes map is empty!\";\n      return false;\n    }\n\n    std::unordered_set<std::string> device_ids;\n    for (const auto &[device_id, _] : modes) {\n      if (!device_ids.insert(device_id).second) {\n        // Sanity check since, it's technically not possible with unordered map to have duplicate keys\n        BOOST_LOG(error) << \"Duplicate device id provided: \" << device_id << \"!\";\n        return false;\n      }\n    }\n\n    // Here it is important to check that we have all the necessary modes, otherwise\n    // setting modes will fail with ambiguous message.\n    //\n    // Duplicated devices can have different target modes (monitor) with different refresh rate,\n    // however this does not apply to the source mode (frame buffer?) and they must have same\n    // resolution.\n    //\n    // Without SDC_VIRTUAL_MODE_AWARE, devices would share the same source mode entry, but now\n    // they have separate entries that are more or less identical.\n    //\n    // To avoid surprising end-user with unexpected source mode change, we validate that all duplicate\n    // devices were provided instead of guessing modes automatically. This also resolve the problem of\n    // having to choose refresh rate for duplicate display - leave it to the end-user of this function...\n    const auto all_device_ids { get_all_duplicated_devices(device_ids) };\n    if (all_device_ids.empty()) {\n      BOOST_LOG(error) << \"Failed to get all duplicated devices!\";\n      return false;\n    }\n\n    if (all_device_ids.size() != device_ids.size()) {\n      BOOST_LOG(error) << \"Not all modes for duplicate displays were provided!\";\n      return false;\n    }\n\n    const auto original_modes { get_current_display_modes(device_ids) };\n    if (original_modes.empty()) {\n      // Error already logged\n      return false;\n    }\n\n    constexpr bool allow_changes { true };\n    const bool first_attempt_ok { do_set_modes(modes, allow_changes) };\n\n    const auto all_modes_match = [&modes](const device_display_mode_map_t &current_modes) {\n      for (const auto &[device_id, requested_mode] : modes) {\n        auto mode_it { current_modes.find(device_id) };\n        if (mode_it == std::end(current_modes)) {\n          // This race condition of disconnecting display device is technically possible...\n          return false;\n        }\n\n        if (!fuzzy_compare_modes(mode_it->second, requested_mode)) {\n          return false;\n        }\n      }\n\n      return true;\n    };\n\n    auto current_modes { get_current_display_modes(device_ids) };\n    if (first_attempt_ok && !current_modes.empty() && all_modes_match(current_modes)) {\n      return true;\n    }\n\n    // We have a problem when using SetDisplayConfig with SDC_ALLOW_CHANGES\n    // where it decides to use our new mode merely as a suggestion.\n    //\n    // This is good, since we don't have to be very precise with refresh rate,\n    // but also bad since it can just ignore our specified mode.\n    //\n    // However, it is possible that the user has created a custom display mode\n    // which is not exposed to the via Windows settings app. To allow this\n    // resolution to be selected, we actually need to omit SDC_ALLOW_CHANGES\n    // flag.\n    BOOST_LOG(info) << \"Failed to change display modes using Windows recommended modes, trying to set modes more strictly!\";\n    if (do_set_modes(modes, !allow_changes)) {\n      current_modes = get_current_display_modes(device_ids);\n      if (!current_modes.empty() && all_modes_match(current_modes)) {\n        return true;\n      }\n    }\n\n    // Fallback: explicitly set target mode parameters instead of relying on Windows CCD mode matching.\n    // This helps with non-standard resolutions (e.g. 16:10 like 2560x1600) on newly created virtual\n    // display paths where the CCD topology database has no matching mode entries.\n    // This also handles the case where the first attempt fails with ERROR_GEN_FAILURE.\n    BOOST_LOG(info) << \"CCD mode matching failed, trying to set modes with explicit target mode parameters.\";\n    if (do_set_modes_with_explicit_target(modes)) {\n      current_modes = get_current_display_modes(device_ids);\n      if (!current_modes.empty() && all_modes_match(current_modes)) {\n        return true;\n      }\n    }\n\n    do_set_modes(original_modes, allow_changes);  // Return value does not matter as we are trying out best to undo\n    BOOST_LOG(error) << \"Failed to set display mode(-s) completely!\";\n    return false;\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/windows/display_device/device_topology.cpp",
    "content": "// lib includes\n#include <boost/process/v1.hpp>\n#include <boost/property_tree/json_parser.hpp>\n#include <boost/property_tree/ptree.hpp>\n#include <boost/property_tree/xml_parser.hpp>\n#include <boost/variant.hpp>\n#include <codecvt>\n#include <icm.h>\n#include <iostream>\n#include <windows.h>\n\n// local includes\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/platform/run_command.h\"\n#include \"src/platform/windows/misc.h\"\n#include \"windows_utils.h\"\n\nnamespace display_device {\n\n  namespace pt = boost::property_tree;\n\n  namespace {\n\n    /**\n     * @brief Contains arbitrary data collected from queried display paths.\n     */\n    struct path_data_t {\n      std::unordered_map<UINT32, std::size_t> source_id_to_path_index; /**< Maps source ids to its index in the path list. */\n      LUID source_adapter_id {}; /**< Adapter id shared by all source ids. */\n      boost::optional<UINT32> active_source; /**< Currently active source id. */\n    };\n\n    /**\n     * @brief Ordered map of [DEVICE_ID -> path_data_t].\n     * @see path_data_t\n     */\n    using path_data_map_t = std::map<std::string, path_data_t>;\n\n    /**\n     * @brief Check if adapter ids are equal.\n     * @param id_a First id to check.\n     * @param id_b Second id to check.\n     * @return True if equal, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const bool equal = compareAdapterIds({ 12, 34 }, { 12, 34 });\n     * const bool not_equal = compareAdapterIds({ 12, 34 }, { 12, 56 });\n     * ```\n     */\n    bool\n    compareAdapterIds(const LUID &id_a, const LUID &id_b) {\n      return id_a.HighPart == id_b.HighPart && id_a.LowPart == id_b.LowPart;\n    }\n\n    /**\n     * @brief Stringify adapter id.\n     * @param id Id to stringify.\n     * @return String representation of the id.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const bool id_string = to_string({ 12, 34 });\n     * ```\n     */\n    std::string\n    to_string(const LUID &id) {\n      return std::to_string(id.HighPart) + std::to_string(id.LowPart);\n    }\n\n    /**\n     * @brief Collect arbitrary data from provided paths.\n     *\n     * This function filters paths that can be used later on and\n     * collects some arbitrary data for a quick lookup.\n     *\n     * @param paths List of paths.\n     * @returns Data for valid paths.\n     * @see query_display_config on how to get paths from the system.\n     * @see make_new_paths_for_topology for the actual data use example.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * std::vector<DISPLAYCONFIG_PATH_INFO> paths;\n     * const auto path_data = make_device_path_data(paths);\n     * ```\n     */\n    path_data_map_t\n    make_device_path_data(const std::vector<DISPLAYCONFIG_PATH_INFO> &paths) {\n      path_data_map_t path_data;\n      std::unordered_map<std::string, std::string> paths_to_ids;\n      for (std::size_t index = 0; index < paths.size(); ++index) {\n        const auto &path { paths[index] };\n\n        const auto device_info { w_utils::get_device_info_for_valid_path(path, w_utils::ALL_DEVICES) };\n        if (!device_info) {\n          // Path is not valid\n          continue;\n        }\n\n        const auto prev_device_id_for_path_it { paths_to_ids.find(device_info->device_path) };\n        if (prev_device_id_for_path_it != std::end(paths_to_ids)) {\n          if (prev_device_id_for_path_it->second != device_info->device_id) {\n            BOOST_LOG(error) << \"Duplicate display device id found: \" << device_info->device_id << \" (device path: \" << device_info->device_path << \")\";\n            return {};\n          }\n        }\n        else {\n          BOOST_LOG(verbose) << \"New valid device id entry for device \" << device_info->device_id << \" (device path: \" << device_info->device_path << \")\";\n          paths_to_ids[device_info->device_path] = device_info->device_id;\n        }\n\n        auto path_data_it { path_data.find(device_info->device_id) };\n        if (path_data_it != std::end(path_data)) {\n          if (!compareAdapterIds(path_data_it->second.source_adapter_id, path.sourceInfo.adapterId)) {\n            // Sanity check, should not be possible since adapter in embedded in the device path\n            BOOST_LOG(error) << \"Device path \" << device_info->device_path << \" has different adapters!\";\n            return {};\n          }\n\n          path_data_it->second.source_id_to_path_index[path.sourceInfo.id] = index;\n        }\n        else {\n          path_data[device_info->device_id] = path_data_t {\n            { { path.sourceInfo.id, index } },\n            path.sourceInfo.adapterId,\n            // Since active paths are always in the front, this is the only time we check (when we add new entry)\n            w_utils::is_active(path) ? boost::make_optional(path.sourceInfo.id) : boost::none\n          };\n        }\n      }\n\n      return path_data;\n    }\n\n    /**\n     * @brief Select the best possible paths to be used for the requested topology based on the data that is available to us.\n     *\n     * If the paths will be used for a completely new topology (Windows has never had it set), we need to take into\n     * account the source id availability per the adapter - duplicated displays must share the same source id\n     * (if they belong to the same adapter) and have different ids if they are not duplicated displays.\n     *\n     * There are limited amount of available ids (see comments in the code) so we will abort early if we are\n     * out of ids.\n     *\n     * The paths for a topology that already exists (Windows has set it at least once) does not have to follow\n     * the mentioned \"source id\" rule. Windows will simply ignore them (since we will ask it to later) and select\n     * paths that were previously configured (that might differ in source ids) based on the paths that we provide.\n     *\n     * @param new_topology Topology that we want to have in the end.\n     * @param path_data Collected arbitrary path data.\n     * @param paths Display paths.\n     * @return A list of path that will make up new topology, or an empty list if function fails.\n     */\n    std::vector<DISPLAYCONFIG_PATH_INFO>\n    make_new_paths_for_topology(const active_topology_t &new_topology, const path_data_map_t &path_data, const std::vector<DISPLAYCONFIG_PATH_INFO> &paths) {\n      std::vector<DISPLAYCONFIG_PATH_INFO> new_paths;\n\n      UINT32 group_id { 0 };\n      std::unordered_map<std::string, std::unordered_set<UINT32>> used_source_ids_per_adapter;\n      const auto is_source_id_already_used = [&used_source_ids_per_adapter](const LUID &adapter_id, UINT32 source_id) {\n        auto entry_it { used_source_ids_per_adapter.find(to_string(adapter_id)) };\n        if (entry_it != std::end(used_source_ids_per_adapter)) {\n          return entry_it->second.count(source_id) > 0;\n        }\n\n        return false;\n      };\n\n      for (const auto &group : new_topology) {\n        std::unordered_map<std::string, UINT32> used_source_ids_per_adapter_per_group;\n        const auto get_already_used_source_id_in_group = [&used_source_ids_per_adapter_per_group](const LUID &adapter_id) -> boost::optional<UINT32> {\n          auto entry_it { used_source_ids_per_adapter_per_group.find(to_string(adapter_id)) };\n          if (entry_it != std::end(used_source_ids_per_adapter_per_group)) {\n            return entry_it->second;\n          }\n\n          return boost::none;\n        };\n\n        for (const std::string &device_id : group) {\n          auto path_data_it { path_data.find(device_id) };\n          if (path_data_it == std::end(path_data)) {\n            BOOST_LOG(error) << \"Device \" << device_id << \" does not exist in the available topology data!\";\n            return {};\n          }\n\n          std::size_t selected_path_index {};\n          const auto &device_data { path_data_it->second };\n\n          const auto already_used_source_id { get_already_used_source_id_in_group(device_data.source_adapter_id) };\n          if (already_used_source_id) {\n            // Some device in the group is already using the source id, and we belong to the same adapter.\n            // This means we must also use the path with matching source id.\n            auto path_source_it { device_data.source_id_to_path_index.find(*already_used_source_id) };\n            if (path_source_it == std::end(device_data.source_id_to_path_index)) {\n              BOOST_LOG(error) << \"Device \" << device_id << \" does not have a path with a source id \" << *already_used_source_id << \"!\";\n              return {};\n            }\n\n            selected_path_index = path_source_it->second;\n          }\n          else {\n            // Here we want to select a path index that has the lowest index (the \"best\" of paths), but only\n            // if the source id is still free. Technically we don't need to find the lowest index, but that's\n            // what will match the Windows' behaviour the closest if we need to create new topology in the end.\n            boost::optional<std::size_t> path_index_candidate;\n            UINT32 used_source_id {};\n            for (const auto &[source_id, index] : device_data.source_id_to_path_index) {\n              if (is_source_id_already_used(device_data.source_adapter_id, source_id)) {\n                continue;\n              }\n\n              if (!path_index_candidate || index < *path_index_candidate) {\n                path_index_candidate = index;\n                used_source_id = source_id;\n              }\n            }\n\n            if (!path_index_candidate) {\n              // Apparently nvidia GPU can only render 4 different sources at a time (according to Google).\n              // However, it seems to be true only for physical connections as we also have virtual displays.\n              //\n              // Virtual displays have different adapter ids than the physical connection ones, but GPU still\n              // has to render them, so I don't know how this 4 source limitation makes sense then?\n              //\n              // In short, this arbitrary limitation should not affect virtual displays when the GPU is at its limit.\n              BOOST_LOG(error) << \"Device \" << device_id << \" cannot be enabled as the adapter has no more free source id (GPU limitation)!\";\n              return {};\n            }\n\n            selected_path_index = *path_index_candidate;\n            used_source_ids_per_adapter[to_string(device_data.source_adapter_id)].insert(used_source_id);\n            used_source_ids_per_adapter_per_group[to_string(device_data.source_adapter_id)] = used_source_id;\n          }\n\n          auto selected_path { paths.at(selected_path_index) };\n\n          // All the indexes must be cleared and only the group id specified\n          w_utils::set_source_index(selected_path, boost::none);\n          w_utils::set_target_index(selected_path, boost::none);\n          w_utils::set_desktop_index(selected_path, boost::none);\n          w_utils::set_clone_group_id(selected_path, group_id);\n          w_utils::set_active(selected_path);  // We also need to mark it as active...\n\n          new_paths.push_back(selected_path);\n        }\n\n        group_id++;\n      }\n\n      return new_paths;\n    }\n\n    /**\n     * @see set_topology for a description as this was split off to reduce cognitive complexity.\n     */\n    bool\n    do_set_topology(const active_topology_t &new_topology) {\n      auto display_data { w_utils::query_display_config(w_utils::ALL_DEVICES) };\n      if (!display_data) {\n        // Error already logged\n        return false;\n      }\n\n      const auto path_data { make_device_path_data(display_data->paths) };\n      if (path_data.empty()) {\n        // Error already logged\n        return false;\n      }\n\n      auto paths { make_new_paths_for_topology(new_topology, path_data, display_data->paths) };\n      if (paths.empty()) {\n        // Error already logged\n        return false;\n      }\n\n      UINT32 flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE };\n      LONG result { SetDisplayConfig(paths.size(), paths.data(), 0, nullptr, flags) };\n      if (result == ERROR_GEN_FAILURE) {\n        BOOST_LOG(warning) << w_utils::get_error_string(result) << \" failed to change topology using the topology from Windows DB! Asking Windows to create the topology.\";\n\n        flags = SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES /* This flag is probably not needed, but who knows really... (not MSDOCS at least) */ | SDC_VIRTUAL_MODE_AWARE | SDC_SAVE_TO_DATABASE;\n        result = SetDisplayConfig(paths.size(), paths.data(), 0, nullptr, flags);\n        if (result != ERROR_SUCCESS) {\n          BOOST_LOG(error) << w_utils::get_error_string(result) << \" failed to create new topology configuration!\";\n          return false;\n        }\n      }\n      else if (result != ERROR_SUCCESS) {\n        BOOST_LOG(error) << w_utils::get_error_string(result) << \" failed to change topology configuration!\";\n        return false;\n      }\n\n      return true;\n    }\n\n  }  // namespace\n\n  device_info_map_t\n  enum_available_devices() {\n    auto display_data { w_utils::query_display_config(w_utils::ALL_DEVICES) };\n    if (!display_data) {\n      // Error already logged\n      return {};\n    }\n\n    device_info_map_t available_devices;\n    const auto topology_data { make_device_path_data(display_data->paths) };\n    if (topology_data.empty()) {\n      // Error already logged\n      return {};\n    }\n\n    for (const auto &[device_id, data] : topology_data) {\n      const auto &path { display_data->paths.at(data.source_id_to_path_index.at(data.active_source.get_value_or(0))) };\n\n      if (w_utils::is_active(path)) {\n        const auto mode { w_utils::get_source_mode(w_utils::get_source_index(path, display_data->modes), display_data->modes) };\n\n        available_devices[device_id] = device_info_t {\n          w_utils::get_display_name(path),\n          w_utils::get_friendly_name(path),\n          mode && w_utils::is_primary(*mode) ? device_state_e::primary : device_state_e::active,\n          w_utils::get_hdr_state(path)\n        };\n      }\n      else {\n        available_devices[device_id] = device_info_t {\n          std::string {},  // Inactive devices can have multiple display names, so it's just meaningless use any\n          w_utils::get_friendly_name(path),\n          device_state_e::inactive,\n          hdr_state_e::unknown\n        };\n      }\n    }\n\n    return available_devices;\n  }\n\n  std::string\n  find_device_by_friendlyname(const std::string &friendly_name) {\n    const auto devices { enum_available_devices() };\n    if (devices.empty()) {\n      // Transient during display reinit or right after VDD create; avoid error level\n      BOOST_LOG(warning) << \"Get display device by friendly name: display device list is empty!\";\n      return {};\n    }\n\n    const auto device_it { std::find_if(std::begin(devices), std::end(devices), [&friendly_name](const auto &entry) {\n      return entry.second.friendly_name == friendly_name;\n    }) };\n    if (device_it == std::end(devices)) {\n      return {};\n    }\n\n    return device_it->first;\n  }\n\n  active_topology_t\n  get_current_topology() {\n    const auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n    if (!display_data) {\n      // Error already logged\n      return {};\n    }\n\n    // Duplicate displays can be identified by having the same x/y position. Here we have a\n    // \"position to index\" map for a simple and lazy lookup in case we have to add a device to the\n    // topology group.\n    std::unordered_map<std::string, std::size_t> position_to_topology_index;\n    active_topology_t topology;\n    for (const auto &path : display_data->paths) {\n      const auto device_info { w_utils::get_device_info_for_valid_path(path, w_utils::ACTIVE_ONLY_DEVICES) };\n      if (!device_info) {\n        continue;\n      }\n\n      const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(path, display_data->modes), display_data->modes) };\n      if (!source_mode) {\n        BOOST_LOG(error) << \"Active device does not have a source mode: \" << device_info->device_id << \"!\";\n        return {};\n      }\n\n      const std::string lazy_lookup { std::to_string(source_mode->position.x) + std::to_string(source_mode->position.y) };\n      auto index_it { position_to_topology_index.find(lazy_lookup) };\n\n      if (index_it == std::end(position_to_topology_index)) {\n        position_to_topology_index[lazy_lookup] = topology.size();\n        topology.push_back({ device_info->device_id });\n      }\n      else {\n        topology.at(index_it->second).push_back(device_info->device_id);\n      }\n    }\n\n    return topology;\n  }\n\n  bool\n  is_topology_valid(const active_topology_t &topology) {\n    if (topology.empty()) {\n      BOOST_LOG(warning) << \"Topology input is empty!\";\n      return false;\n    }\n\n    std::unordered_set<std::string> device_ids;\n    for (const auto &group : topology) {\n      // Size 2 is a Windows' limitation.\n      // You CAN set the group to be more than 2, but then\n      // Windows' settings app breaks since it was not designed for this :/\n      if (group.empty() || group.size() > 2) {\n        BOOST_LOG(warning) << \"Topology group is invalid!\";\n        return false;\n      }\n\n      for (const auto &device_id : group) {\n        if (device_ids.count(device_id) > 0) {\n          BOOST_LOG(warning) << \"Duplicate device ids found!\";\n          return false;\n        }\n\n        device_ids.insert(device_id);\n      }\n    }\n\n    return true;\n  }\n\n  bool\n  is_topology_the_same(const active_topology_t &topology_a, const active_topology_t &topology_b) {\n    const auto sort_topology = [](active_topology_t &topology) {\n      for (auto &group : topology) {\n        std::sort(std::begin(group), std::end(group));\n      }\n\n      std::sort(std::begin(topology), std::end(topology));\n    };\n\n    auto a_copy { topology_a };\n    auto b_copy { topology_b };\n\n    // On Windows order does not matter.\n    sort_topology(a_copy);\n    sort_topology(b_copy);\n\n    return a_copy == b_copy;\n  }\n\n  bool\n  set_topology(const active_topology_t &new_topology) {\n    if (!is_topology_valid(new_topology)) {\n      BOOST_LOG(error) << \"Topology input is invalid!\";\n      return false;\n    }\n\n    const auto current_topology { get_current_topology() };\n    if (current_topology.empty()) {\n      BOOST_LOG(error) << \"Failed to get current topology!\";\n      return false;\n    }\n\n    if (is_topology_the_same(current_topology, new_topology)) {\n      BOOST_LOG(debug) << \"Same topology provided.\";\n      return true;\n    }\n\n    if (do_set_topology(new_topology)) {\n      const auto updated_topology { get_current_topology() };\n      if (!updated_topology.empty()) {\n        if (is_topology_the_same(new_topology, updated_topology)) {\n          return true;\n        }\n        else {\n          // There is an interesting bug in Windows when you have nearly\n          // identical devices, drivers or something. For example, imagine you have:\n          //    AM   - Actual Monitor\n          //    IDD1 - Virtual display 1\n          //    IDD2 - Virtual display 2\n          //\n          // You can have the following topology:\n          //    [[AM, IDD1]]\n          // but not this:\n          //    [[AM, IDD2]]\n          //\n          // Windows API will just default to:\n          //    [[AM, IDD1]]\n          // even if you provide the second variant. Windows API will think\n          // it's OK and just return ERROR_SUCCESS in this case and there is\n          // nothing you can do. Even the Windows' settings app will not\n          // be able to set the desired topology.\n          //\n          // There seems to be a workaround - you need to make sure the IDD1\n          // device is used somewhere else in the topology, like:\n          //    [[AM, IDD2], [IDD1]]\n          //\n          // However, since we have this bug an additional sanity check is needed\n          // regardless of what Windows report back to us.\n          BOOST_LOG(error) << \"Failed to change topology due to Windows bug or because the display is in deep sleep!\";\n        }\n      }\n      else {\n        BOOST_LOG(error) << \"Failed to get updated topology!\";\n      }\n\n      // Revert back to the original topology\n      do_set_topology(current_topology);  // Return value does not matter\n    }\n\n    return false;\n  }\n\n  bool\n  apply_hdr_profile(const std::string &client_name) {\n    pt::ptree clientArray;\n    std::stringstream ss(config::nvhttp.clients);\n    read_json(ss, clientArray);\n\n    std::string profile_name;\n    for (const auto &client : clientArray) {\n      if (client.second.get<std::string>(\"name\") == client_name) {\n        if (auto profile = client.second.get_optional<std::string>(\"hdrProfile\")) {\n          profile_name = *profile;\n        }\n        break;\n      }\n    }\n\n    if (profile_name.empty()) return true;\n\n    auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n\n    if (!display_data) return false;\n\n    auto dev_path { w_utils::get_active_path(config::video.output_name, display_data->paths) };\n    if (!dev_path) return false;\n\n    std::string driver_path { w_utils::get_device_driver_path(*dev_path) };\n    BOOST_LOG(info) << \"Display Driver path: \" << driver_path;\n\n    if (driver_path.empty()) return false;\n\n    BOOST_LOG(info) << \"Applying hdr profile: \" << profile_name << \" for \" << client_name;\n\n    // set hdr profile to registry\n    boost::process::v1::environment _env = boost::this_process::environment();\n    auto working_dir = boost::filesystem::path();\n\n    std::error_code ec;\n    std::string cmd = \"\\\"\" + (std::filesystem::path(SUNSHINE_ASSETS_DIR).parent_path() / \"tools\" / \"setreg.exe\").string() + \"\\\" -registryPath \\\"HKCU:\\\\Software\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\ICM\\\\ProfileAssociations\\\\Display\\\\\" + driver_path + \"\\\" -valueName \\\"ICMProfileAC\\\" -valueData \\\"\" + profile_name + \"\\\"\";\n\n    auto child = platf::run_command(true, true, cmd, working_dir, _env, nullptr, ec, nullptr);\n    if (ec) {\n      BOOST_LOG(warning) << \"Couldn't run cmd [\"sv << cmd << \"]: System: \"sv << ec.message();\n    }\n    else {\n      BOOST_LOG(info) << \"Executing Set RegistryValue cmd [\"sv << cmd << \"]\"sv;\n      child.detach();\n    }\n\n    return true;\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/windows/display_device/general_functions.cpp",
    "content": "// standard includes\n#include <unordered_set>\n\n// local includes\n#include \"src/globals.h\"\n#include \"src/logging.h\"\n#include \"windows_utils.h\"\n\nnamespace display_device {\n\n  std::string\n  get_display_name(const std::string &device_id) {\n    if (device_id.empty()) {\n      return {};\n    }\n\n    const bool is_vdd = (device_id == VDD_NAME);\n    const std::string resolved_device_id = is_vdd ? display_device::find_device_by_friendlyname(ZAKO_NAME) : device_id;\n\n    if (resolved_device_id.empty()) {\n      return {};\n    }\n\n    // Helper lambda to find display name from display data\n    auto find_display_name = [&resolved_device_id](bool active_only) -> std::string {\n      auto display_data = w_utils::query_display_config(active_only);\n      if (!display_data) {\n        return {};\n      }\n\n      if (active_only) {\n        const auto path = w_utils::get_active_path(resolved_device_id, display_data->paths);\n        if (path) {\n          return w_utils::get_display_name(*path);\n        }\n      }\n      else {\n        const auto path = std::find_if(display_data->paths.begin(), display_data->paths.end(),\n          [&resolved_device_id](const auto &entry) {\n            const auto device_info = w_utils::get_device_info_for_valid_path(entry, w_utils::ALL_DEVICES);\n            return device_info && device_info->device_id == resolved_device_id;\n          });\n        if (path != display_data->paths.end()) {\n          return w_utils::get_display_name(*path);\n        }\n      }\n      return {};\n    };\n\n    // First, try active devices\n    if (auto display_name = find_display_name(w_utils::ACTIVE_ONLY_DEVICES); !display_name.empty()) {\n      return display_name;\n    }\n\n    // If not found in active devices, also try all devices (including inactive)\n    // This is useful for devices that may be in transition (e.g., VDD devices after creation)\n    // or for devices that are temporarily inactive but still have a valid display name\n    if (auto display_name = find_display_name(w_utils::ALL_DEVICES); !display_name.empty()) {\n      BOOST_LOG(debug) << \"Found device in inactive devices, display name: \" << display_name;\n      return display_name;\n    }\n\n    BOOST_LOG(debug) << \"Failed to find device for \" << resolved_device_id << \"!\";\n    return {};\n  }\n\n  std::string\n  get_display_friendly_name(const std::string &device_id) {\n    if (device_id.empty()) {\n      // Valid return, no error\n      return {};\n    }\n\n    const auto display_data { w_utils::query_display_config(w_utils::ALL_DEVICES) };\n    if (!display_data) {\n      // Error already logged\n      return {};\n    }\n\n    const auto path { std::find_if(std::begin(display_data->paths), std::end(display_data->paths), [&](const auto &entry) {\n      const auto device_info = w_utils::get_device_info_for_valid_path(entry, w_utils::ALL_DEVICES);\n      return device_info && device_info->device_id == device_id;\n    }) };\n    if (path == std::end(display_data->paths)) {\n      // Debug level, because inactive device is valid case for this function\n      BOOST_LOG(debug) << \"Failed to find device for \" << device_id << \"!\";\n      return {};\n    }\n\n    const auto display_friendly_name { w_utils::get_friendly_name(*path) };\n    if (display_friendly_name.empty()) {\n      BOOST_LOG(error) << \"Device \" << device_id << \" has no display name assigned.\";\n    }\n\n    return display_friendly_name;\n  }\n\n  bool\n  is_primary_device(const std::string &device_id) {\n    if (device_id.empty()) {\n      BOOST_LOG(error) << \"Device id is empty!\";\n      return false;\n    }\n\n    auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n    if (!display_data) {\n      // Error already logged\n      return false;\n    }\n\n    const auto path { w_utils::get_active_path(device_id, display_data->paths) };\n    if (!path) {\n      BOOST_LOG(error) << \"Failed to find device for \" << device_id << \"!\";\n      return false;\n    }\n\n    const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) };\n    if (!source_mode) {\n      BOOST_LOG(error) << \"Active device does not have a source mode: \" << device_id << \"!\";\n      return false;\n    }\n\n    return w_utils::is_primary(*source_mode);\n  }\n\n  bool\n  set_as_primary_device(const std::string &device_id) {\n    if (device_id.empty()) {\n      BOOST_LOG(error) << \"Device id is empty!\";\n      return false;\n    }\n\n    auto display_data { w_utils::query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n    if (!display_data) {\n      // Error already logged\n      return false;\n    }\n\n    // Get the current origin point of the device (the one that we want to make primary)\n    POINTL origin;\n    {\n      const auto path { w_utils::get_active_path(device_id, display_data->paths) };\n      if (!path) {\n        BOOST_LOG(error) << \"Failed to find device for \" << device_id << \"!\";\n        return false;\n      }\n\n      const auto source_mode { w_utils::get_source_mode(w_utils::get_source_index(*path, display_data->modes), display_data->modes) };\n      if (!source_mode) {\n        BOOST_LOG(error) << \"Active device does not have a source mode: \" << device_id << \"!\";\n        return false;\n      }\n\n      if (w_utils::is_primary(*source_mode)) {\n        BOOST_LOG(debug) << \"Device \" << device_id << \" is already a primary device.\";\n        return true;\n      }\n\n      origin = source_mode->position;\n    }\n\n    // Without verifying if the paths are valid or not (SetDisplayConfig will verify for us),\n    // shift their source mode origin points accordingly, so that the provided\n    // device moves to (0, 0) position and others to their new positions.\n    std::unordered_set<UINT32> modified_modes;\n    for (auto &path : display_data->paths) {\n      const auto current_id { w_utils::get_device_id(path) };\n      const auto source_index { w_utils::get_source_index(path, display_data->modes) };\n      auto source_mode { w_utils::get_source_mode(source_index, display_data->modes) };\n\n      if (!source_index || !source_mode) {\n        BOOST_LOG(error) << \"Active device does not have a source mode: \" << current_id << \"!\";\n        return false;\n      }\n\n      if (modified_modes.find(*source_index) != std::end(modified_modes)) {\n        // Happens when VIRTUAL_MODE_AWARE is not specified when querying paths, probably will never happen in our case, but just to be safe...\n        BOOST_LOG(debug) << \"Device \" << current_id << \" shares the same mode index as a previous device. Device is duplicated. Skipping.\";\n        continue;\n      }\n\n      source_mode->position.x -= origin.x;\n      source_mode->position.y -= origin.y;\n\n      modified_modes.insert(*source_index);\n    }\n\n    const UINT32 flags { SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_SAVE_TO_DATABASE | SDC_VIRTUAL_MODE_AWARE };\n    const LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), display_data->modes.size(), display_data->modes.data(), flags) };\n    if (result != ERROR_SUCCESS) {\n      BOOST_LOG(error) << w_utils::get_error_string(result) << \" failed to set primary mode for \" << device_id << \"!\";\n      return false;\n    }\n\n    return true;\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/windows/display_device/session_listener.cpp",
    "content": "// local includes\n#include \"session_listener.h\"\n#include \"windows_utils.h\"\n#include \"src/logging.h\"\n\n#include <atomic>\n\nnamespace display_device {\n\n  // Static member initialization\n  std::mutex SessionEventListener::mutex_;\n  SessionEventListener::UnlockCallback SessionEventListener::pending_task_;\n  std::thread SessionEventListener::worker_thread_;\n  std::queue<SessionEventListener::UnlockCallback> SessionEventListener::task_queue_;\n  std::condition_variable SessionEventListener::cv_;\n  bool SessionEventListener::worker_running_ = false;\n  HWND SessionEventListener::hidden_window_ = nullptr;\n  std::thread SessionEventListener::message_thread_;\n  std::atomic<bool> SessionEventListener::thread_running_ { false };\n  std::atomic<bool> SessionEventListener::initialized_ { false };\n  std::atomic<bool> SessionEventListener::event_based_ { false };\n\n  namespace {\n    const wchar_t *WINDOW_CLASS_NAME = L\"SunshineSessionListener\";\n    std::condition_variable init_cv_;\n    std::mutex init_mutex_;\n    bool init_complete_ = false;\n    bool init_success_ = false;\n  }\n\n  LRESULT CALLBACK\n  SessionEventListener::window_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {\n    if (message == WM_WTSSESSION_CHANGE) {\n      switch (wparam) {\n        case WTS_SESSION_UNLOCK:\n          {\n            BOOST_LOG(info) << \"[SessionListener] 检测到会话解锁事件\";\n            \n            // 将pending_task_移入队列执行\n            {\n              std::lock_guard<std::mutex> lock(mutex_);\n              if (pending_task_) {\n                BOOST_LOG(info) << \"[SessionListener] 执行解锁任务\";\n                task_queue_.push(std::move(pending_task_));\n                pending_task_ = nullptr;\n                cv_.notify_one();\n              }\n            }\n          }\n          break;\n        case WTS_SESSION_LOCK:\n          BOOST_LOG(info) << \"[SessionListener] 检测到会话锁定事件\";\n          break;\n        case WTS_CONSOLE_DISCONNECT:\n          BOOST_LOG(info) << \"[SessionListener] 检测到控制台断开事件\";\n          break;\n        default:\n          break;\n      }\n    }\n    else if (message == WM_DESTROY) {\n      PostQuitMessage(0);\n    }\n    return DefWindowProcW(hwnd, message, wparam, lparam);\n  }\n\n  void\n  SessionEventListener::message_loop() {\n    WNDCLASSEXW wc = {};\n    wc.cbSize = sizeof(WNDCLASSEXW);\n    wc.lpfnWndProc = window_proc;\n    wc.hInstance = GetModuleHandle(nullptr);\n    wc.lpszClassName = WINDOW_CLASS_NAME;\n\n    if (!RegisterClassExW(&wc)) {\n      DWORD last_error = GetLastError();\n      if (last_error != ERROR_CLASS_ALREADY_EXISTS) {\n        BOOST_LOG(error) << \"[SessionListener] 注册窗口类失败: \" << last_error;\n        {\n          std::lock_guard<std::mutex> lock(init_mutex_);\n          init_complete_ = true;\n          init_success_ = false;\n        }\n        init_cv_.notify_one();\n        return;\n      }\n    }\n\n    hidden_window_ = CreateWindowExW(\n      0, WINDOW_CLASS_NAME, L\"SunshineSessionListenerWindow\",\n      0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, GetModuleHandle(nullptr), nullptr\n    );\n\n    if (!hidden_window_) {\n      BOOST_LOG(error) << \"[SessionListener] 创建隐藏窗口失败: \" << GetLastError();\n      {\n        std::lock_guard<std::mutex> lock(init_mutex_);\n        init_complete_ = true;\n        init_success_ = false;\n      }\n      init_cv_.notify_one();\n      return;\n    }\n\n    if (!WTSRegisterSessionNotification(hidden_window_, NOTIFY_FOR_THIS_SESSION)) {\n      BOOST_LOG(warning) << \"[SessionListener] 注册会话通知失败: \" << GetLastError();\n      DestroyWindow(hidden_window_);\n      hidden_window_ = nullptr;\n      {\n        std::lock_guard<std::mutex> lock(init_mutex_);\n        init_complete_ = true;\n        init_success_ = false;\n      }\n      init_cv_.notify_one();\n      return;\n    }\n\n    BOOST_LOG(info) << \"[SessionListener] 会话事件监听器初始化成功\";\n    \n    {\n      std::lock_guard<std::mutex> lock(init_mutex_);\n      init_complete_ = true;\n      init_success_ = true;\n    }\n    init_cv_.notify_one();\n\n    MSG msg;\n    while (thread_running_ && GetMessage(&msg, nullptr, 0, 0)) {\n      TranslateMessage(&msg);\n      DispatchMessage(&msg);\n    }\n\n    if (hidden_window_) {\n      WTSUnRegisterSessionNotification(hidden_window_);\n      DestroyWindow(hidden_window_);\n      hidden_window_ = nullptr;\n    }\n    UnregisterClassW(WINDOW_CLASS_NAME, GetModuleHandle(nullptr));\n  }\n\n  void\n  SessionEventListener::worker_loop() {\n    BOOST_LOG(info) << \"[SessionListener] Worker线程已启动\";\n    \n    while (true) {\n      UnlockCallback task;\n      \n      {\n        std::unique_lock<std::mutex> lock(mutex_);\n        cv_.wait(lock, [] { \n          return !task_queue_.empty() || !worker_running_; \n        });\n        \n        if (!worker_running_ && task_queue_.empty()) {\n          break;\n        }\n        \n        if (task_queue_.empty()) {\n          continue;\n        }\n        \n        task = std::move(task_queue_.front());\n        task_queue_.pop();\n      }\n      \n      // 在锁外执行任务，避免死锁\n      try {\n        task();\n      }\n      catch (const std::exception& e) {\n        BOOST_LOG(error) << \"[SessionListener] 任务执行异常: \" << e.what();\n      }\n    }\n    \n    BOOST_LOG(info) << \"[SessionListener] Worker线程已退出\";\n  }\n\n  bool\n  SessionEventListener::init() {\n    if (initialized_) {\n      return event_based_;\n    }\n\n    {\n      std::lock_guard<std::mutex> lock(init_mutex_);\n      init_complete_ = false;\n      init_success_ = false;\n    }\n\n    // 启动worker线程\n    {\n      std::lock_guard<std::mutex> lock(mutex_);\n      worker_running_ = true;\n    }\n    worker_thread_ = std::thread(worker_loop);\n\n    // 启动消息线程\n    thread_running_ = true;\n    message_thread_ = std::thread(message_loop);\n\n    // 等待初始化完成\n    {\n      std::unique_lock<std::mutex> lock(init_mutex_);\n      init_cv_.wait(lock, [] { return init_complete_; });\n    }\n\n    initialized_ = true;\n    event_based_ = init_success_;\n\n    if (!event_based_) {\n      BOOST_LOG(warning) << \"[SessionListener] 事件监听器初始化失败\";\n      thread_running_ = false;\n      if (message_thread_.joinable()) {\n        message_thread_.join();\n      }\n    }\n\n    return event_based_;\n  }\n\n  void\n  SessionEventListener::deinit() {\n    if (!initialized_) {\n      return;\n    }\n\n    BOOST_LOG(info) << \"[SessionListener] 开始清理\";\n\n    thread_running_ = false;\n    if (hidden_window_) {\n      PostMessage(hidden_window_, WM_QUIT, 0, 0);\n    }\n    if (message_thread_.joinable()) {\n      message_thread_.join();\n    }\n\n    // 停止worker线程\n    {\n      std::lock_guard<std::mutex> lock(mutex_);\n      worker_running_ = false;\n      cv_.notify_one();\n    }\n\n    if (worker_thread_.joinable()) {\n      worker_thread_.join();\n    }\n\n    // 清理状态\n    {\n      std::lock_guard<std::mutex> lock(mutex_);\n      pending_task_ = nullptr;\n      while (!task_queue_.empty()) {\n        task_queue_.pop();\n      }\n    }\n\n    initialized_ = false;\n    event_based_ = false;\n    BOOST_LOG(info) << \"[SessionListener] 清理完成\";\n  }\n\n  bool\n  SessionEventListener::is_event_based() {\n    return event_based_;\n  }\n\n  void\n  SessionEventListener::add_unlock_task(UnlockCallback task) {\n    if (!task) {\n      return;\n    }\n    \n    const bool is_locked = w_utils::is_user_session_locked();\n    \n    std::lock_guard<std::mutex> lock(mutex_);\n    \n    if (!is_locked) {\n      // 未锁定：直接提交到队列执行\n      BOOST_LOG(info) << \"[SessionListener] 当前未锁定，立即执行任务\";\n      task_queue_.push(std::move(task));\n      cv_.notify_one();\n    }\n    else {\n      // 锁定中：保存任务等待解锁\n      BOOST_LOG(info) << \"[SessionListener] 任务已加入解锁队列\";\n      pending_task_ = std::move(task);\n    }\n  }\n\n  void\n  SessionEventListener::clear_unlock_task() {\n    std::lock_guard<std::mutex> lock(mutex_);\n    pending_task_ = nullptr;\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/windows/display_device/session_listener.h",
    "content": "#pragma once\n\n// Windows includes must come first\n#include <windows.h>\n\n// standard includes\n#include <atomic>\n#include <condition_variable>\n#include <functional>\n#include <mutex>\n#include <queue>\n#include <thread>\n\n// lib includes\n#include <wtsapi32.h>\n\nnamespace display_device {\n\n  /**\n   * @brief Listens for Windows session events (lock/unlock).\n   * Uses WTSRegisterSessionNotification for event-based detection.\n   */\n  class SessionEventListener {\n  public:\n    using UnlockCallback = std::function<void()>;\n\n    /**\n     * @brief Initialize the session event listener.\n     * @returns True if event-based listening was successfully registered.\n     */\n    static bool\n    init();\n\n    /**\n     * @brief Cleanup and unregister the session event listener.\n     */\n    static void\n    deinit();\n\n    /**\n     * @brief Check if event-based listening is active.\n     */\n    static bool\n    is_event_based();\n\n    /**\n     * @brief Add a task to be executed on unlock (or immediately if already unlocked).\n     * @param task Function to execute.\n     * @note New task replaces existing pending task to avoid duplicates.\n     */\n    static void\n    add_unlock_task(UnlockCallback task);\n\n    /**\n     * @brief Clear the pending unlock task.\n     */\n    static void\n    clear_unlock_task();\n\n  private:\n    // 单一mutex管理所有共享状态\n    static std::mutex mutex_;\n    \n    // 解锁等待任务（单任务模式避免重复）\n    static UnlockCallback pending_task_;\n    \n    // Worker线程执行任务\n    static std::thread worker_thread_;\n    static std::queue<UnlockCallback> task_queue_;\n    static std::condition_variable cv_;\n    static bool worker_running_;\n    \n    // 消息循环线程\n    static HWND hidden_window_;\n    static std::thread message_thread_;\n    static std::atomic<bool> thread_running_;\n    static std::atomic<bool> initialized_;\n    static std::atomic<bool> event_based_;\n\n    static LRESULT CALLBACK\n    window_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);\n    \n    static void\n    message_loop();\n    \n    static void\n    worker_loop();\n  };\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/windows/display_device/settings.cpp",
    "content": "// standard includes\n#include <algorithm>\n#include <chrono>\n#include <fstream>\n#include <thread>\n\n// local includes\n#include \"settings_topology.h\"\n#include \"src/audio.h\"\n#include \"src/display_device/session.h\"\n#include \"src/display_device/to_string.h\"\n#include \"src/globals.h\"\n#include \"src/logging.h\"\n#include \"src/config.h\"\n#include \"src/rtsp.h\"\n#include \"windows_utils.h\"\n\nnamespace display_device {\n\n  struct settings_t::persistent_data_t {\n    topology_pair_t topology; /**< Contains topology before the modification and the one we modified. */\n    std::string original_primary_display; /**< Original primary display in the topology we modified. Empty value if we didn't modify it. */\n    device_display_mode_map_t original_modes; /**< Original display modes in the topology we modified. Empty value if we didn't modify it. */\n    hdr_state_map_t original_hdr_states; /**< Original display HDR states in the topology we modified. Empty value if we didn't modify it. */\n\n    /**\n     * @brief Check if the persistent data contains any meaningful modifications that need to be reverted.\n     * @returns True if the data contains something that needs to be reverted, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * settings_t::persistent_data_t data;\n     * if (data.contains_modifications()) {\n     *   // save persistent data\n     * }\n     * ```\n     */\n    [[nodiscard]] bool\n    contains_modifications() const {\n      return !is_topology_the_same(topology.initial, topology.modified) ||\n             !original_primary_display.empty() ||\n             !original_modes.empty() ||\n             !original_hdr_states.empty();\n    }\n\n    // For JSON serialization\n    NLOHMANN_DEFINE_TYPE_INTRUSIVE(persistent_data_t, topology, original_primary_display, original_modes, original_hdr_states)\n  };\n\n  struct settings_t::audio_data_t {\n    /**\n     * @brief A reference to the audio context that will automatically extend the audio session.\n     * @note It is auto-initialized here for convenience.\n     */\n    decltype(audio::get_audio_ctx_ref()) audio_ctx_ref { audio::get_audio_ctx_ref() };\n  };\n\n  namespace {\n\n    /**\n     * @brief Get one of the primary display ids found in the topology metadata.\n     * @param metadata Topology metadata that also includes current active topology.\n     * @return Device id for the primary device, or empty string if primary device not found somehow.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * topology_metadata_t metadata;\n     * const std::string primary_device_id = get_current_primary_display(metadata);\n     * ```\n     */\n    std::string\n    get_current_primary_display(const topology_metadata_t &metadata) {\n      for (const auto &group : metadata.current_topology) {\n        for (const auto &device_id : group) {\n          if (is_primary_device(device_id)) {\n            return device_id;\n          }\n        }\n      }\n\n      return std::string {};\n    }\n\n    /**\n     * @brief Compute the new primary display id based on the information we have.\n     * @param original_primary_display Original device id (the one before our first modification or from current topology).\n     * @param metadata The current metadata that we are evaluating.\n     * @return Primary display id that matches the requirements.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * topology_metadata_t metadata;\n     * const std::string primary_device_id = determine_new_primary_display(\"MY_DEVICE_ID\", metadata);\n     * ```\n     */\n    std::string\n    determine_new_primary_display(const std::string &original_primary_display, const topology_metadata_t &metadata) {\n      if (metadata.primary_device_requested) {\n        // Primary device was requested - no device was specified by user.\n        // This means we are keeping whatever display we have.\n        return original_primary_display;\n      }\n\n      // For primary devices it is enough to set 1 as a primary display, as the whole duplicated group\n      // will become primary displays.\n      const auto new_primary_device { metadata.duplicated_devices.front() };\n      return new_primary_device;\n    }\n\n    /**\n     * @brief Change the primary display based on the configuration and previously configured primary display.\n     *\n     * The function performs the necessary steps for changing the primary display if needed.\n     * It also evaluates for possible changes in the configuration and undoes the changes\n     * we have made before.\n     *\n     * @param device_prep Device preparation value from the configuration.\n     * @param previous_primary_display Device id of the original primary display we have initially changed (can be empty).\n     * @param metadata Additional data with info about the current topology.\n     * @return Device id to be used when reverting all settings (can be empty string), or an empty optional if the function fails.\n     */\n    boost::optional<std::string>\n    handle_primary_display_configuration(const parsed_config_t::device_prep_e &device_prep, const std::string &previous_primary_display, const topology_metadata_t &metadata) {\n      if (device_prep == parsed_config_t::device_prep_e::ensure_primary) {\n        const auto original_primary_display { previous_primary_display.empty() ? get_current_primary_display(metadata) : previous_primary_display };\n        const auto new_primary_display { determine_new_primary_display(original_primary_display, metadata) };\n\n        BOOST_LOG(info) << \"Changing primary display to: \" << new_primary_display;\n        if (!set_as_primary_device(new_primary_display)) {\n          // Error already logged\n          return boost::none;\n        }\n\n        // Here we preserve the data from persistence (unless there's none) as in the end that is what we want to go back to.\n        return original_primary_display;\n      }\n\n      if (!previous_primary_display.empty()) {\n        BOOST_LOG(info) << \"Changing primary display back to: \" << previous_primary_display;\n        if (!set_as_primary_device(previous_primary_display)) {\n          // Error already logged\n          return boost::none;\n        }\n      }\n\n      return std::string {};\n    }\n\n    /**\n     * @brief Compute the new display modes based on the information we have.\n     * @param resolution Resolution value from the configuration.\n     * @param refresh_rate Refresh rate value from the configuration.\n     * @param original_display_modes Original display modes (the ones before our first modification or from current topology)\n     *                               that we use as a base we will apply changes to.\n     * @param metadata The current metadata that we are evaluating.\n     * @return New display modes for the topology.\n     */\n    device_display_mode_map_t\n    determine_new_display_modes(const boost::optional<resolution_t> &resolution, const boost::optional<refresh_rate_t> &refresh_rate, const device_display_mode_map_t &original_display_modes, const topology_metadata_t &metadata) {\n      device_display_mode_map_t new_modes { original_display_modes };\n\n      if (resolution) {\n        // For duplicate devices the resolution must match no matter what, otherwise\n        // they cannot be duplicated, which breaks Windows' rules.\n        for (const auto &device_id : metadata.duplicated_devices) {\n          new_modes[device_id].resolution = *resolution;\n        }\n      }\n\n      if (refresh_rate) {\n        if (metadata.primary_device_requested) {\n          // No device has been specified, so if they're all are primary devices\n          // we need to apply the refresh rate change to all duplicates\n          for (const auto &device_id : metadata.duplicated_devices) {\n            new_modes[device_id].refresh_rate = *refresh_rate;\n          }\n        }\n        else {\n          // Even if we have duplicate devices, their refresh rate may differ\n          // and since the device was specified, let's apply the refresh\n          // rate only to the specified device.\n          new_modes[metadata.duplicated_devices.front()].refresh_rate = *refresh_rate;\n        }\n      }\n\n      return new_modes;\n    }\n\n    /**\n     * @brief Remove entries from a device_id-keyed map whose keys are not in the valid set.\n     */\n    template<typename MapT>\n    void\n    filter_stale_devices(MapT &map, const std::unordered_set<std::string> &valid_ids, const char *label) {\n      for (auto it = map.begin(); it != map.end();) {\n        if (valid_ids.find(it->first) == valid_ids.end()) {\n          BOOST_LOG(warning) << \"Removing stale device from \" << label << \": \" << it->first;\n          it = map.erase(it);\n        }\n        else {\n          ++it;\n        }\n      }\n    }\n\n    /**\n     * @brief Remove VDD device entries from a device_id-keyed map.\n     *\n     * VDD device IDs are unstable (change on destroy/recreate), so they should not be\n     * persisted. This function removes them before saving to persistent_data.\n     */\n    template<typename MapT>\n    void\n    filter_vdd_devices(MapT &map) {\n      for (auto it = map.begin(); it != map.end();) {\n        if (get_display_friendly_name(it->first) == ZAKO_NAME) {\n          BOOST_LOG(debug) << \"Excluding VDD device from persistence: \" << it->first;\n          it = map.erase(it);\n        }\n        else {\n          ++it;\n        }\n      }\n    }\n\n    /**\n     * @brief Modify the display modes based on the configuration and previously configured display modes.\n     *\n     * The function performs the necessary steps for changing the display modes if needed.\n     * It also evaluates for possible changes in the configuration and undoes the changes\n     * we have made before.\n     *\n     * @param resolution Resolution value from the configuration.\n     * @param refresh_rate Refresh rate value from the configuration.\n     * @param previous_display_modes Original display modes that we have initially changed (can be empty).\n     * @param metadata Additional data with info about the current topology.\n     * @return Display modes to be used when reverting all settings (can be empty map), or an empty optional if the function fails.\n     */\n    boost::optional<device_display_mode_map_t>\n    handle_display_mode_configuration(const boost::optional<resolution_t> &resolution, const boost::optional<refresh_rate_t> &refresh_rate, const device_display_mode_map_t &previous_display_modes, const topology_metadata_t &metadata) {\n      // Build a set of device IDs present in current topology to filter out stale entries\n      // (e.g. old VDD device IDs lingering in persistent_data after a client switch).\n      const auto valid_device_ids { get_device_ids_from_topology(metadata.current_topology) };\n      const std::unordered_set<std::string> valid_ids_set(valid_device_ids.begin(), valid_device_ids.end());\n\n      if (resolution || refresh_rate) {\n        const auto original_display_modes { previous_display_modes.empty() ? get_current_display_modes(valid_device_ids) : previous_display_modes };\n        auto new_display_modes { determine_new_display_modes(resolution, refresh_rate, original_display_modes, metadata) };\n\n        filter_stale_devices(new_display_modes, valid_ids_set, \"display modes\");\n\n        BOOST_LOG(info) << \"Changing display modes to: \" << to_string(new_display_modes);\n        if (!set_display_modes(new_display_modes)) {\n          // Error already logged\n          return boost::none;\n        }\n\n        // Here we preserve the data from persistence (unless there's none) as in the end that is what we want to go back to.\n        return original_display_modes;\n      }\n\n      if (!previous_display_modes.empty()) {\n        device_display_mode_map_t filtered_modes { previous_display_modes };\n        filter_stale_devices(filtered_modes, valid_ids_set, \"rollback display modes\");\n\n        if (!filtered_modes.empty()) {\n          BOOST_LOG(info) << \"Changing display modes back to: \" << to_string(filtered_modes);\n          if (!set_display_modes(filtered_modes)) {\n            // Error already logged\n            return boost::none;\n          }\n        }\n      }\n\n      return device_display_mode_map_t {};\n    }\n\n    /**\n     * @brief Reverse (\"blank\") HDR states for newly enabled devices.\n     *\n     * Some newly enabled displays do not handle HDR state correctly (IDD HDR display for example).\n     * The colors can become very blown out/high contrast. A simple workaround is to toggle the HDR state\n     * once the display has \"settled down\" or something.\n     *\n     * This is what this function does, it changes the HDR state to the opposite states that we will have in the\n     * end, sleeps for a little and then allows us to continue changing HDR states to the final ones.\n     *\n     * \"blank\" comes as an inspiration from \"vblank\" as this function is meant to be used before changing the HDR\n     * states to clean up something.\n     *\n     * @param states Final states for the devices that we want to blank.\n     * @param newly_enabled_devices Devices to perform blanking for.\n     * @return False if the function has failed to set HDR states, true otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * hdr_state_map_t new_states;\n     * const bool success = blank_hdr_states(new_states, { \"DEVICE_ID\" });\n     * ```\n     */\n    bool\n    blank_hdr_states(const hdr_state_map_t &states, const std::unordered_set<std::string> &newly_enabled_devices) {\n      const std::chrono::milliseconds delay { 2333 };\n      if (delay > std::chrono::milliseconds::zero()) {\n        bool state_changed { false };\n        auto toggled_states { states };\n        for (const auto &device_id : newly_enabled_devices) {\n          auto state_it { toggled_states.find(device_id) };\n          if (state_it == std::end(toggled_states)) {\n            continue;\n          }\n\n          if (state_it->second == hdr_state_e::enabled) {\n            state_it->second = hdr_state_e::disabled;\n            state_changed = true;\n          }\n          else if (state_it->second == hdr_state_e::disabled) {\n            state_it->second = hdr_state_e::enabled;\n            state_changed = true;\n          }\n        }\n\n        if (state_changed) {\n          BOOST_LOG(debug) << \"Toggling HDR states for newly enabled devices and waiting for \" << delay.count() << \"ms before actually applying the correct states.\";\n          if (!set_hdr_states(toggled_states)) {\n            // Error already logged\n            return false;\n          }\n\n          std::this_thread::sleep_for(delay);\n        }\n      }\n\n      return true;\n    }\n\n    /**\n     * @brief Wait for display operations to stabilize before HDR changes.\n     *\n     * This function ensures that other display operations (topology changes,\n     * mode changes, primary display changes) have stabilized before applying\n     * HDR state changes. This prevents conflicts and ensures proper HDR handling.\n     *\n     * @param metadata Topology metadata containing information about current state.\n     * @return True if operations have stabilized, false if timeout occurred.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * topology_metadata_t metadata;\n     * if (wait_for_display_stability(metadata)) {\n     *   // Safe to apply HDR changes\n     * }\n     * ```\n     */\n    bool\n    wait_for_display_stability(const topology_metadata_t &metadata) {\n      constexpr int max_attempts = 10;\n      constexpr auto stability_check_interval = std::chrono::milliseconds(500);\n      constexpr auto max_wait_time = std::chrono::milliseconds(5000);\n\n      BOOST_LOG(debug) << \"等待显示器操作稳定，准备进行HDR切换...\";\n\n      auto start_time = std::chrono::steady_clock::now();\n\n      for (int attempt = 0; attempt < max_attempts; ++attempt) {\n        // 检查是否超时\n        auto elapsed = std::chrono::steady_clock::now() - start_time;\n        if (elapsed > max_wait_time) {\n          BOOST_LOG(warning) << \"等待显示器稳定超时，继续执行HDR切换\";\n          return false;\n        }\n\n        // 检查当前拓扑是否稳定\n        auto current_topology = get_current_topology();\n        if (is_topology_the_same(current_topology, metadata.current_topology)) {\n          // 检查显示模式是否稳定\n          auto current_modes = get_current_display_modes(get_device_ids_from_topology(current_topology));\n          bool modes_stable = true;\n\n          for (const auto &device_id : metadata.duplicated_devices) {\n            auto current_mode_it = current_modes.find(device_id);\n            if (current_mode_it == current_modes.end()) {\n              modes_stable = false;\n              break;\n            }\n          }\n\n          if (modes_stable) {\n            BOOST_LOG(debug) << \"显示器操作已稳定，可以进行HDR切换\";\n            return true;\n          }\n        }\n\n        std::this_thread::sleep_for(stability_check_interval);\n      }\n\n      BOOST_LOG(warning) << \"显示器稳定检查达到最大尝试次数，继续执行HDR切换\";\n      return false;\n    }\n\n    /**\n     * @brief Compute the new HDR states based on the information we have.\n     * @param change_hdr_state HDR state value from the configuration.\n     * @param original_hdr_states Original HDR states (the ones before our first modification or from current topology)\n     *                            that we use as a base we will apply changes to.\n     * @param metadata The current metadata that we are evaluating.\n     * @return New HDR states for the topology.\n     */\n    hdr_state_map_t\n    determine_new_hdr_states(const boost::optional<bool> &change_hdr_state, const hdr_state_map_t &original_hdr_states, const topology_metadata_t &metadata) {\n      hdr_state_map_t new_states { original_hdr_states };\n\n      if (change_hdr_state) {\n        const hdr_state_e final_state { *change_hdr_state ? hdr_state_e::enabled : hdr_state_e::disabled };\n        const auto try_update_new_state = [&new_states, final_state](const std::string &device_id) {\n          const auto current_state { new_states[device_id] };\n          if (current_state == hdr_state_e::unknown) {\n            return;\n          }\n\n          new_states[device_id] = final_state;\n        };\n\n        if (metadata.primary_device_requested) {\n          // No device has been specified, so if they're all are primary devices\n          // we need to apply the HDR state change to all duplicates\n          for (const auto &device_id : metadata.duplicated_devices) {\n            try_update_new_state(device_id);\n          }\n        }\n        else {\n          // Even if we have duplicate devices, their HDR states may differ\n          // and since the device was specified, let's apply the HDR state\n          // only to the specified device.\n          try_update_new_state(metadata.duplicated_devices.front());\n        }\n      }\n\n      return new_states;\n    }\n\n    /**\n     * @brief Modify the display HDR states based on the configuration and previously configured display HDR states.\n     *\n     * The function performs the necessary steps for changing the display HDR states if needed.\n     * It also evaluates for possible changes in the configuration and undoes the changes\n     * we have made before.\n     *\n     * @param change_hdr_state HDR state value from the configuration.\n     * @param previous_hdr_states Original display HDR states have initially changed (can be empty).\n     * @param metadata Additional data with info about the current topology.\n     * @return Display HDR states to be used when reverting all settings (can be empty map), or an empty optional if the function fails.\n     */\n    boost::optional<hdr_state_map_t>\n    handle_hdr_state_configuration(const boost::optional<bool> &change_hdr_state, const hdr_state_map_t &previous_hdr_states, const topology_metadata_t &metadata) {\n      // Build valid device ID set from current topology to filter stale entries\n      const auto valid_device_ids { get_device_ids_from_topology(metadata.current_topology) };\n      const std::unordered_set<std::string> valid_ids_set(valid_device_ids.begin(), valid_device_ids.end());\n\n      if (change_hdr_state) {\n        const auto original_hdr_states { previous_hdr_states.empty() ? get_current_hdr_states(valid_device_ids) : previous_hdr_states };\n        auto new_hdr_states { determine_new_hdr_states(change_hdr_state, original_hdr_states, metadata) };\n        filter_stale_devices(new_hdr_states, valid_ids_set, \"HDR states\");\n\n        BOOST_LOG(info) << \"Changing hdr states to: \" << to_string(new_hdr_states);\n        if (!blank_hdr_states(new_hdr_states, metadata.newly_enabled_devices) || !set_hdr_states(new_hdr_states)) {\n          // Error already logged\n          return boost::none;\n        }\n\n        // Here we preserve the data from persistence (unless there's none) as in the end that is what we want to go back to.\n        return original_hdr_states;\n      }\n\n      if (!previous_hdr_states.empty()) {\n        hdr_state_map_t filtered_hdr { previous_hdr_states };\n        filter_stale_devices(filtered_hdr, valid_ids_set, \"rollback HDR states\");\n\n        if (!filtered_hdr.empty()) {\n          BOOST_LOG(info) << \"Changing hdr states back to: \" << to_string(filtered_hdr);\n          if (!blank_hdr_states(filtered_hdr, metadata.newly_enabled_devices) || !set_hdr_states(filtered_hdr)) {\n            // Error already logged\n            return boost::none;\n          }\n        }\n      }\n\n      return hdr_state_map_t {};\n    }\n\n    /**\n     * @brief Revert settings to the ones found in the persistent data.\n     * @param data Reference to persistent data containing original settings.\n     * @param data_modified Reference to a boolean that is set to true if changes are made to the persistent data reference.\n     * @return True if all settings within persistent data have been reverted, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * bool data_modified { false };\n     * settings_t::persistent_data_t data;\n     *\n     * if (!try_revert_settings(data, data_modified)) {\n     *   if (data_modified) {\n     *     // Update the persistent file\n     *   }\n     * }\n     * ```\n     */\n    bool\n    try_revert_settings(settings_t::persistent_data_t &data, bool &data_modified, bool skip_vdd_destroy = false) {\n      try {\n        nlohmann::json json_data = data;\n        BOOST_LOG(debug) << \"Reverting persistent display settings from:\\n\"\n                         << json_data.dump(4);\n      }\n      catch (const std::exception &err) {\n        BOOST_LOG(error) << \"Failed to dump persistent display settings: \" << err.what();\n      }\n\n      if (!data.contains_modifications()) {\n        return true;\n      }\n\n      // 在移除VDD之前，先检查拓扑中是否有VDD\n      // 收集VDD的设备ID，分别记录：\n      // - vdd_device_ids: 所有VDD设备ID（用于从HDR/modes中清理）\n      // - vdd_in_modified_only: 只在modified拓扑中的VDD（需要销毁）\n      std::unordered_set<std::string> vdd_device_ids;\n      std::unordered_set<std::string> vdd_in_initial;\n      \n      // 收集 initial 拓扑中的 VDD\n      for (const auto &group : data.topology.initial) {\n        for (const auto &device_id : group) {\n          const auto friendly_name = get_display_friendly_name(device_id);\n          if (friendly_name == ZAKO_NAME) {\n            vdd_device_ids.insert(device_id);\n            vdd_in_initial.insert(device_id);\n          }\n        }\n      }\n      \n      // 收集 modified 拓扑中的 VDD\n      for (const auto &group : data.topology.modified) {\n        for (const auto &device_id : group) {\n          const auto friendly_name = get_display_friendly_name(device_id);\n          if (friendly_name == ZAKO_NAME) {\n            vdd_device_ids.insert(device_id);\n          }\n        }\n      }\n\n      // 如果有VDD不在initial拓扑中（由Sunshine创建），则销毁\n      // 如果VDD在initial拓扑中（用户常驻VDD），则保留\n      // 如果启用了\"保持启用\"模式，也保留VDD\n      bool should_destroy_vdd = false;\n      if (!config::video.vdd_keep_enabled) {\n        for (const auto &vdd_id : vdd_device_ids) {\n          if (vdd_in_initial.count(vdd_id) == 0) {\n            should_destroy_vdd = true;\n            break;\n          }\n        }\n      }\n      \n      if (skip_vdd_destroy) {\n        BOOST_LOG(debug) << \"VDD已由调用方销毁，跳过try_revert_settings中的VDD销毁逻辑\";\n      }\n      else if (config::video.vdd_keep_enabled) {\n        BOOST_LOG(debug) << \"VDD保持启用模式已开启，保留VDD\";\n      }\n      else if (should_destroy_vdd) {\n        BOOST_LOG(info) << \"检测到Sunshine创建的VDD（不在初始拓扑中），销毁VDD\";\n        display_device::session_t::get().destroy_vdd_monitor();\n      }\n      else if (!vdd_in_initial.empty()) {\n        BOOST_LOG(debug) << \"VDD在初始拓扑中（常驻VDD），保留不销毁\";\n      }\n\n      // Remove VDD devices from topology before reverting, as VDD may have been destroyed\n      // This function now returns the IDs of removed devices\n      const auto vdd_ids_from_initial = remove_vdd_from_topology(data.topology.initial);\n      const auto vdd_ids_from_modified = remove_vdd_from_topology(data.topology.modified);\n      \n      // Merge VDD IDs from both topologies\n      std::unordered_set<std::string> all_removed_vdd_ids = vdd_ids_from_initial;\n      all_removed_vdd_ids.insert(vdd_ids_from_modified.begin(), vdd_ids_from_modified.end());\n      \n      if (!all_removed_vdd_ids.empty()) {\n        BOOST_LOG(info) << \"Removed VDD devices from persistent topology (VDD may have been destroyed)\";\n        data_modified = true;\n        \n        // Clean up HDR states and display modes using the VDD IDs from topology removal\n        // This works even after VDD is destroyed, since we got IDs before checking friendly_name\n        for (const auto& vdd_id : all_removed_vdd_ids) {\n          if (data.original_hdr_states.erase(vdd_id) > 0) {\n            BOOST_LOG(debug) << \"Removed VDD from original_hdr_states: \" << vdd_id;\n          }\n          if (data.original_modes.erase(vdd_id) > 0) {\n            BOOST_LOG(debug) << \"Removed VDD from original_modes: \" << vdd_id;\n          }\n        }\n      }\n\n      const bool modified_topology_valid = is_topology_valid(data.topology.modified);\n      const bool initial_topology_valid = is_topology_valid(data.topology.initial);\n      const bool have_changes_for_modified_topology = !data.original_primary_display.empty() ||\n                                                      !data.original_modes.empty() ||\n                                                      !data.original_hdr_states.empty();\n\n      std::unordered_set<std::string> newly_enabled_devices;\n      bool partially_failed = false;\n      auto current_topology = get_current_topology();\n\n      // Handle modified topology changes\n      if (have_changes_for_modified_topology) {\n        if (modified_topology_valid && set_topology(data.topology.modified)) {\n          newly_enabled_devices = get_newly_enabled_devices_from_topology(current_topology, data.topology.modified);\n          current_topology = data.topology.modified;\n\n          // Revert HDR states\n          if (!data.original_hdr_states.empty()) {\n            BOOST_LOG(info) << \"Changing back the HDR states to: \" << to_string(data.original_hdr_states);\n            if (set_hdr_states(data.original_hdr_states)) {\n              data.original_hdr_states.clear();\n              data_modified = true;\n            }\n            else {\n              partially_failed = true;\n            }\n          }\n\n          // Revert display modes\n          if (!data.original_modes.empty()) {\n            BOOST_LOG(info) << \"Changing back the display modes to: \" << to_string(data.original_modes);\n            if (set_display_modes(data.original_modes)) {\n              data.original_modes.clear();\n              data_modified = true;\n            }\n            else {\n              partially_failed = true;\n            }\n          }\n\n          // Revert primary display\n          if (!data.original_primary_display.empty()) {\n            BOOST_LOG(info) << \"Changing back the primary device to: \" << data.original_primary_display;\n            if (set_as_primary_device(data.original_primary_display)) {\n              data.original_primary_display.clear();\n              data_modified = true;\n            }\n            else {\n              partially_failed = true;\n            }\n          }\n        }\n        else if (!modified_topology_valid) {\n          // Modified topology invalid, clear settings that depend on it\n          BOOST_LOG(warning) << \"Modified topology invalid, skipping restoration of HDR, modes, and primary display\";\n          data.original_hdr_states.clear();\n          data.original_modes.clear();\n          data.original_primary_display.clear();\n          data_modified = true;\n        }\n        else {\n          BOOST_LOG(error) << \"Cannot switch to the topology to undo changes!\";\n          partially_failed = true;\n        }\n      }\n\n      // Revert to initial topology\n      BOOST_LOG(info) << \"Changing display topology back to: \" << to_string(data.topology.initial);\n      if (initial_topology_valid) {\n        if (set_topology(data.topology.initial)) {\n          newly_enabled_devices.merge(get_newly_enabled_devices_from_topology(current_topology, data.topology.initial));\n          current_topology = data.topology.initial;\n          data_modified = true;\n        }\n        else {\n          BOOST_LOG(error) << \"Failed to switch back to the initial topology!\";\n          partially_failed = true;\n        }\n      }\n      else {\n        BOOST_LOG(warning) << \"Initial topology invalid (VDD may have been removed), keeping current topology\";\n      }\n\n      // Fix HDR states for newly enabled devices\n      if (!newly_enabled_devices.empty()) {\n        const auto current_hdr_states = get_current_hdr_states(get_device_ids_from_topology(current_topology));\n        BOOST_LOG(debug) << \"Trying to fix HDR states (if needed).\";\n        blank_hdr_states(current_hdr_states, newly_enabled_devices);\n        set_hdr_states(current_hdr_states);\n      }\n\n      return !partially_failed;\n    }\n\n    /**\n     * @brief Save settings to the JSON file.\n     * @param filepath Filepath for the persistent data.\n     * @param data Persistent data to save.\n     * @return True if the filepath is empty or the data was saved to the file, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * settings_t::persistent_data_t data;\n     *\n     * if (save_settings(\"/foo/bar.json\", data)) {\n     *   // Do stuff...\n     * }\n     * ```\n     */\n    bool\n    save_settings(const std::filesystem::path &filepath, const settings_t::persistent_data_t &data) {\n      if (filepath.empty()) {\n        BOOST_LOG(warning) << \"No filename was specified for persistent display device configuration.\";\n        return true;\n      }\n\n      try {\n        std::ofstream file(filepath, std::ios::out | std::ios::trunc);\n        nlohmann::json json_data = data;\n\n        // Write json with indentation\n        file << std::setw(4) << json_data << std::endl;\n        BOOST_LOG(debug) << \"Saved persistent display settings:\\n\"\n                         << json_data.dump(4);\n        return true;\n      }\n      catch (const std::exception &err) {\n        BOOST_LOG(error) << \"Failed to save display settings: \" << err.what();\n      }\n\n      return false;\n    }\n\n    /**\n     * @brief Load persistent data from the JSON file.\n     * @param filepath Filepath to load data from.\n     * @return Unique pointer to the persistent data if it was loaded successfully, nullptr otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * auto data = load_settings(\"/foo/bar.json\");\n     * ```\n     */\n    std::unique_ptr<settings_t::persistent_data_t>\n    load_settings(const std::filesystem::path &filepath) {\n      try {\n        if (!filepath.empty() && std::filesystem::exists(filepath)) {\n          std::ifstream file(filepath);\n          return std::make_unique<settings_t::persistent_data_t>(nlohmann::json::parse(file));\n        }\n      }\n      catch (const std::exception &err) {\n        BOOST_LOG(error) << \"Failed to load saved display settings: \" << err.what();\n      }\n\n      return nullptr;\n    }\n\n    /**\n     * @brief Remove the file.\n     * @param filepath Filepath to remove.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * remove_file(\"/foo/bar.json\");\n     * ```\n     */\n    void\n    remove_file(const std::filesystem::path &filepath) {\n      try {\n        if (!filepath.empty()) {\n          std::filesystem::remove(filepath);\n        }\n      }\n      catch (const std::exception &err) {\n        BOOST_LOG(error) << \"Failed to remove \" << filepath << \". Error: \" << err.what();\n      }\n    }\n\n  }  // namespace\n\n  settings_t::settings_t() = default;\n\n  settings_t::~settings_t() = default;\n\n  bool\n  settings_t::is_changing_settings_going_to_fail() const {\n    const bool session_locked = w_utils::is_user_session_locked();\n    \n    // 如果会话已锁定，直接返回true，跳过CCD API测试\n    // 这避免了在锁屏状态下频繁调用显示API导致ERROR_ACCESS_DENIED和WATCHDOG事件\n    if (session_locked) {\n      BOOST_LOG(info) << \"Changing settings will fail - session_locked: true\";\n      return true;\n    }\n    \n    const bool no_ccd_access = w_utils::test_no_access_to_ccd_api();\n    if (no_ccd_access) {\n      BOOST_LOG(info) << \"Changing settings will fail - no_ccd_access: true\";\n    }\n    \n    return no_ccd_access;\n  }\n\n  settings_t::apply_result_t\n  settings_t::apply_config(\n    const parsed_config_t &config,\n    const rtsp_stream::launch_session_t &session,\n    const boost::optional<active_topology_t> &pre_saved_initial_topology) {\n    const auto do_apply_config { [this, &pre_saved_initial_topology](const parsed_config_t &config) -> settings_t::apply_result_t {\n      // 检测是否为VDD模式\n      const bool is_vdd_mode = config.use_vdd && *config.use_vdd;\n\n      // 根据模式选择不同的拓扑处理方式\n      boost::optional<handled_topology_result_t> topology_result;\n      bool failed_while_reverting_settings { false };\n\n      if (is_vdd_mode) {\n        // VDD模式：拓扑由 vdd_prep 控制（在 prepare_vdd 中已处理），这里只获取 metadata\n        // 这里不修改拓扑，分辨率、刷新率、HDR 等设置仍然会应用\n        BOOST_LOG(info) << \"VDD mode: topology controlled by vdd_prep in prepare_vdd, only getting current topology metadata\";\n        topology_result = get_current_topology_metadata(config.device_id);\n\n        // 如果有预保存的初始拓扑（在 VDD 创建前保存的物理显示器拓扑），\n        // 用它替换 get_current_topology_metadata 返回的初始拓扑。\n        // 否则恢复时 remove_vdd_from_topology 会将初始拓扑清空，\n        // 导致物理显示器无法被重新启用。\n        if (topology_result && pre_saved_initial_topology && !pre_saved_initial_topology->empty()) {\n          BOOST_LOG(info) << \"VDD mode: using pre-saved initial topology (physical displays) instead of current VDD-only topology\";\n          topology_result->pair.initial = *pre_saved_initial_topology;\n        }\n      }\n      else {\n        // 普通模式：device_prep 控制拓扑\n        if (config.device_prep == parsed_config_t::device_prep_e::no_operation) {\n          BOOST_LOG(info) << \"Display device preparation mode is set to no_operation, topology will not be changed\";\n        }\n\n        const boost::optional<topology_pair_t> previously_configured_topology { \n          persistent_data ? boost::make_optional(persistent_data->topology) : boost::none \n        };\n\n        // On Windows the display settings are kept per an active topology list - each topology\n        // has separate configuration saved in the database. Therefore, we must always switch\n        // to the topology we want to modify before we actually start applying settings.\n        topology_result = handle_device_topology_configuration(config, previously_configured_topology, [&]() {\n          const bool audio_sink_was_captured { audio_data != nullptr };\n          if (!revert_settings(revert_reason_e::topology_switch)) {\n            failed_while_reverting_settings = true;\n            return false;\n          }\n\n          if (audio_sink_was_captured && !audio_data) {\n            audio_data = std::make_unique<audio_data_t>();\n          }\n          return true;\n        }, pre_saved_initial_topology);\n      }\n\n      if (!topology_result) {\n        // Error already logged\n        return { failed_while_reverting_settings ? apply_result_t::result_e::revert_fail : apply_result_t::result_e::topology_fail };\n      }\n\n      // Once we have switched to the correct topology, we need to select where we want to\n      // save persistent data.\n      //\n      // If we already have cached persistent data, we want to use that, however we must NOT\n      // take over the topology \"pair\" from the result as the initial topology doest not\n      // reflect the actual initial topology before we made our first changes.\n      //\n      // There is no better way to somehow always guess the initial topology we want to revert to.\n      // The user could have switched topology when the stream was paused, then technically we could\n      // try to switch back to that topology. However, the display could have also turned off and the\n      // topology was automatically changed by Windows. In this case we don't want to switch back to\n      // that topology since it was not the user's decision.\n      //\n      // Therefore, we are always sticking with the first initial topology before the first configuration\n      // was applied.\n      persistent_data_t new_settings { topology_result->pair };\n      persistent_data_t &current_settings { persistent_data ? *persistent_data : new_settings };\n\n      const auto persist_settings = [&]() -> apply_result_t {\n        if (current_settings.contains_modifications()) {\n          if (!persistent_data) {\n            persistent_data = std::make_unique<persistent_data_t>(new_settings);\n          }\n\n          if (!save_settings(filepath, *persistent_data)) {\n            return { apply_result_t::result_e::file_save_fail };\n          }\n        }\n        else if (persistent_data) {\n          if (!revert_settings(revert_reason_e::config_cleanup)) {\n            // Sanity check, as the revert_settings should always pass\n            // at this point since our settings contain no modifications.\n            return { apply_result_t::result_e::revert_fail };\n          }\n        }\n\n        return { apply_result_t::result_e::success };\n      };\n\n      // Since we will be modifying system state in multiple steps, we\n      // have no choice, but to save any changes we have made so\n      // that we can undo them if anything fails.\n      auto save_guard = util::fail_guard([&]() {\n        persist_settings();  // Ignoring the return value\n      });\n\n      // Here each of the handler returns full set of their specific settings for\n      // all the displays in the topology.\n      //\n      // We have the same train of though here as with the topology - if we are\n      // controlling some parts of the display settings, we are taking what\n      // we have before any modification by us are sticking with it until we\n      // release the control.\n      //\n      // Also, since we keep settings for all the displays (not only the ones that\n      // we modify), we can use these settings as a base that will revert whatever\n      // we did before if we are re-applying settings with different configuration.\n      //\n      // User modified the resolution manually? Well, he shouldn't have. If we\n      // are responsible for the resolution, then hands off! Initial settings\n      // will be re-applied when the paused session is resumed.\n\n      const auto original_primary_display { handle_primary_display_configuration(config.device_prep, current_settings.original_primary_display, topology_result->metadata) };\n      if (!original_primary_display) {\n        // Error already logged\n        return { apply_result_t::result_e::primary_display_fail };\n      }\n      current_settings.original_primary_display = *original_primary_display;\n\n      const auto original_modes { handle_display_mode_configuration(config.resolution, config.refresh_rate, current_settings.original_modes, topology_result->metadata) };\n      if (!original_modes) {\n        // Error already logged\n        return { apply_result_t::result_e::modes_fail };\n      }\n      current_settings.original_modes = *original_modes;\n      filter_vdd_devices(current_settings.original_modes);\n\n      // 如果有HDR切换操作，等待其他操作稳定后再进行HDR切换\n      if (config.change_hdr_state) {\n        BOOST_LOG(info) << \"检测到HDR切换操作，等待其他显示器操作稳定...\";\n        if (!wait_for_display_stability(topology_result->metadata)) {\n          BOOST_LOG(warning) << \"显示器稳定检查未完全通过，但继续执行HDR切换\";\n        }\n      }\n\n      const auto original_hdr_states { handle_hdr_state_configuration(config.change_hdr_state, current_settings.original_hdr_states, topology_result->metadata) };\n      if (!original_hdr_states) {\n        // Error already logged\n        return { apply_result_t::result_e::hdr_states_fail };\n      }\n      current_settings.original_hdr_states = *original_hdr_states;\n      filter_vdd_devices(current_settings.original_hdr_states);\n\n      save_guard.disable();\n      return persist_settings();\n    } };\n\n    BOOST_LOG(info) << \"Applying configuration to the display device.\";\n    const bool display_may_change { config.device_prep == parsed_config_t::device_prep_e::ensure_only_display };\n    if (display_may_change && !audio_data) {\n      // It is very likely that in this situation our \"current\" audio device will be gone, so we\n      // want to capture the audio sink immediately and extend the audio session until we revert our changes.\n      BOOST_LOG(debug) << \"Capturing audio sink before changing display\";\n      audio_data = std::make_unique<audio_data_t>();\n    }\n\n    const auto result { do_apply_config(config) };\n    if (result) {\n      if (!display_may_change && audio_data) {\n        // Just to be safe in the future when the video config can be reloaded\n        // without Sunshine restarting, we should clean up, because in this situation\n        // we have had to revert the changes that turned off other displays. Thus, extending\n        // the session for a display that again exist is pointless.\n        BOOST_LOG(debug) << \"Releasing captured audio sink\";\n        audio_data = nullptr;\n      }\n\n      if (config.change_hdr_state) {\n        std::thread { [&client_name = session.client_name]() {\n          if (!display_device::apply_hdr_profile(client_name)) {\n            BOOST_LOG(warning) << \"Failed to apply HDR profile for client: \" << client_name << \"retrying later...\";\n            std::this_thread::sleep_for(2s);\n            display_device::apply_hdr_profile(client_name);\n          }\n        } }\n          .detach();\n      }\n    }\n\n    if (!result) {\n      BOOST_LOG(error) << \"Failed to configure display:\\n\"\n                       << result.get_error_message();\n    }\n    else {\n      BOOST_LOG(info) << \"Display device configuration applied.\";\n    }\n    return result;\n  }\n\n  bool\n  settings_t::revert_settings(revert_reason_e reason, bool skip_vdd_destroy) {\n    static const char *reason_strs[] = { \"串流结束\", \"拓扑切换\", \"配置清理\", \"重置持久化\" };\n    const char *reason_str = reason_strs[static_cast<int>(reason)];\n    BOOST_LOG(info) << \"正在恢复显示设备设置 (原因: \" << reason_str << \")\";\n\n    // 加载持久化设置数据\n    if (!persistent_data) {\n      BOOST_LOG(info) << \"加载显示设备持久化设置\";\n      persistent_data = load_settings(filepath);\n    }\n\n    // 如果存在持久化数据，尝试恢复设置\n    if (persistent_data) {\n      // 尝试恢复设置\n      bool data_updated { false };\n      bool success = try_revert_settings(*persistent_data, data_updated, skip_vdd_destroy);\n      if (!success) {\n        if (data_updated) {\n          save_settings(filepath, *persistent_data);  // 忽略返回值\n        }\n        BOOST_LOG(error) << \"恢复显示设备设置失败！如有异常请尝试关闭基地显示器，或手动修改系统显示设置~\";\n      }\n\n      // 清理持久化数据\n      remove_file(filepath);\n      persistent_data = nullptr;\n\n      // 释放音频数据\n      if (reason != revert_reason_e::topology_switch) {\n        if (audio_data) {\n          BOOST_LOG(debug) << \"释放捕获的音频接收器\";\n          audio_data = nullptr;\n        }\n      }\n\n      if (success) {\n        BOOST_LOG(info) << \"显示设备配置已恢复\";\n      }\n    }\n    return true;\n  }\n\n  void\n  settings_t::reset_persistence() {\n    BOOST_LOG(info) << \"Purging persistent display device data (trying to reset settings one last time).\";\n    if (persistent_data && !revert_settings(revert_reason_e::persistence_reset)) {\n      BOOST_LOG(info) << \"Failed to revert settings - proceeding to reset persistence.\";\n    }\n\n    remove_file(filepath);\n    persistent_data = nullptr;\n\n    if (audio_data) {\n      BOOST_LOG(debug) << \"Releasing captured audio sink\";\n      audio_data = nullptr;\n    }\n  }\n\n  bool\n  settings_t::has_persistent_data() const {\n    return persistent_data != nullptr;\n  }\n\n  bool\n  settings_t::is_vdd_in_initial_topology() const {\n    if (!persistent_data) {\n      return false;\n    }\n    \n    for (const auto &group : persistent_data->topology.initial) {\n      for (const auto &device_id : group) {\n        const auto friendly_name = get_display_friendly_name(device_id);\n        if (friendly_name == ZAKO_NAME) {\n          return true;\n        }\n      }\n    }\n    return false;\n  }\n\n  void\n  settings_t::remove_vdd_from_initial_topology(const std::string& vdd_id) {\n    if (!persistent_data) {\n      return;\n    }\n    \n    // Remove from initial topology\n    for (auto& group : persistent_data->topology.initial) {\n      group.erase(\n        std::remove(group.begin(), group.end(), vdd_id),\n        group.end()\n      );\n    }\n    \n    // Remove from modified topology\n    for (auto& group : persistent_data->topology.modified) {\n      group.erase(\n        std::remove(group.begin(), group.end(), vdd_id),\n        group.end()\n      );\n    }\n    \n    // Remove VDD from HDR states (avoid trying to restore HDR for destroyed VDD)\n    if (persistent_data->original_hdr_states.erase(vdd_id) > 0) {\n      BOOST_LOG(debug) << \"Removed VDD from original_hdr_states: \" << vdd_id;\n    }\n    \n    // Remove VDD from display modes (avoid trying to restore mode for destroyed VDD)\n    if (persistent_data->original_modes.erase(vdd_id) > 0) {\n      BOOST_LOG(debug) << \"Removed VDD from original_modes: \" << vdd_id;\n    }\n    \n    // Save updated persistent data\n    save_settings(filepath, *persistent_data);\n  }\n\n  void\n  settings_t::replace_vdd_id(const std::string& old_id, const std::string& new_id) {\n    if (!persistent_data) {\n      return;\n    }\n    \n    // Replace in initial topology\n    for (auto& group : persistent_data->topology.initial) {\n      std::replace(group.begin(), group.end(), old_id, new_id);\n    }\n    \n    // Replace in modified topology\n    for (auto& group : persistent_data->topology.modified) {\n      std::replace(group.begin(), group.end(), old_id, new_id);\n    }\n    \n    // Replace VDD ID in HDR states map\n    if (auto it = persistent_data->original_hdr_states.find(old_id); it != persistent_data->original_hdr_states.end()) {\n      auto hdr_value = it->second;\n      persistent_data->original_hdr_states.erase(it);\n      persistent_data->original_hdr_states[new_id] = hdr_value;\n      BOOST_LOG(debug) << \"Replaced VDD ID in original_hdr_states: \" << old_id << \" -> \" << new_id;\n    }\n    \n    // Replace VDD ID in display modes map\n    if (auto it = persistent_data->original_modes.find(old_id); it != persistent_data->original_modes.end()) {\n      auto mode_value = it->second;\n      persistent_data->original_modes.erase(it);\n      persistent_data->original_modes[new_id] = mode_value;\n      BOOST_LOG(debug) << \"Replaced VDD ID in original_modes: \" << old_id << \" -> \" << new_id;\n    }\n    \n    // Save updated persistent data\n    save_settings(filepath, *persistent_data);\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/windows/display_device/settings_topology.cpp",
    "content": "// standard includes\n#include <thread>\n\n// local includes\n#include \"settings_topology.h\"\n#include \"src/display_device/to_string.h\"\n#include \"src/globals.h\"\n#include \"src/logging.h\"\n\nnamespace display_device {\n\n  namespace {\n    /**\n     * @brief 基于初始拓扑，补全那些当前inactive但应该恢复的设备\n     * @param base_topology 基础拓扑（通常是当前拓扑）\n     * @param requested_device_id 请求的设备ID\n     * @param initial_topology_devices 初始拓扑中的设备列表（只补全这些设备）\n     * @return 补全后的拓扑\n     */\n    active_topology_t\n    augment_topology_with_inactive_devices(\n      const active_topology_t &base_topology,\n      const std::string &requested_device_id,\n      const boost::optional<std::unordered_set<std::string>> &initial_topology_devices = boost::none) {\n      \n      // 先拷贝一份作为候选结果\n      active_topology_t augmented_topology { base_topology };\n\n      // 收集当前拓扑中的设备 id，避免重复添加\n      const auto existing_ids { get_device_ids_from_topology(augmented_topology) };\n\n      const auto available_devices { enum_available_devices() };\n      if (available_devices.empty()) {\n        return base_topology;\n      }\n\n      // 如果提供了初始拓扑设备列表，只补全这些设备\n      // 否则补全所有inactive设备（旧行为）\n      if (initial_topology_devices && !initial_topology_devices->empty()) {\n        BOOST_LOG(debug) << \"Augmenting topology based on initial topology devices (respecting user's original configuration)\";\n        \n        for (const auto &device_id : *initial_topology_devices) {\n          // 已经在拓扑中的设备不需要再处理\n          if (existing_ids.count(device_id) > 0) {\n            continue;\n          }\n\n          // 检查设备是否可用且是inactive状态\n          auto device_it = available_devices.find(device_id);\n          if (device_it == available_devices.end()) {\n            BOOST_LOG(debug) << \"Device from initial topology not available: \" << device_id;\n            continue;\n          }\n\n          if (device_it->second.device_state != device_state_e::inactive) {\n            // 设备已经是active或其他状态，不需要补全\n            continue;\n          }\n\n          BOOST_LOG(debug) << \"Augmenting topology with device from initial topology: \" << device_id;\n          augmented_topology.push_back({ device_id });\n        }\n      }\n      else {\n        // 没有初始拓扑约束的场景（非VDD）\n        // augment_topology的真正作用应该是：把在final_topology中但因为某些原因变成inactive的设备重新激活\n        // 但determine_final_topology已经决定了要激活哪些设备，我们不应该再自动添加新设备\n        // 所以这里直接返回base_topology，不做任何补全\n        BOOST_LOG(debug) << \"No initial topology constraint, relying on determine_final_topology result without augmentation\";\n        return base_topology;\n      }\n\n      // 如果补全后的拓扑不合法，则保守地退回原始拓扑，避免把系统弄到奇怪状态\n      if (!augmented_topology.empty() && !is_topology_valid(augmented_topology)) {\n        BOOST_LOG(warning) << \"Augmented display topology is invalid, falling back to original topology.\";\n        return base_topology;\n      }\n\n      return augmented_topology;\n    }\n\n    /**\n     * @brief Get all device ids that belong in the same group as provided ids (duplicated displays).\n     * @param device_id Device id to search for in the topology.\n     * @param topology Topology to search.\n     * @return A list of device ids, with the provided device id always at the front.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const auto duplicated_devices = get_duplicate_devices(\"MY_DEVICE_ID\", get_current_topology());\n     * ```\n     */\n    std::vector<std::string>\n    get_duplicate_devices(const std::string &device_id, const active_topology_t &topology) {\n      std::vector<std::string> duplicated_devices;\n\n      duplicated_devices.clear();\n      duplicated_devices.push_back(device_id);\n\n      for (const auto &group : topology) {\n        for (const auto &group_device_id : group) {\n          if (device_id == group_device_id) {\n            std::copy_if(std::begin(group), std::end(group), std::back_inserter(duplicated_devices), [&](const auto &id) {\n              return id != device_id;\n            });\n            break;\n          }\n        }\n      }\n\n      return duplicated_devices;\n    }\n\n    /**\n     * @brief Check if device id is found in the active topology.\n     * @param device_id Device id to search for in the topology.\n     * @param topology Topology to search.\n     * @return True if device id is in the topology, false otherwise.\n     *\n     * EXAMPLES:\n     * ```cpp\n     * const bool is_in_topology = is_device_found_in_active_topology(\"MY_DEVICE_ID\", get_current_topology());\n     * ```\n     */\n    bool\n    is_device_found_in_active_topology(const std::string &device_id, const active_topology_t &topology) {\n      for (const auto &group : topology) {\n        for (const auto &group_device_id : group) {\n          if (device_id == group_device_id) {\n            return true;\n          }\n        }\n      }\n\n      return false;\n    }\n\n    /**\n     * @brief Compute the final topology based on the information we have.\n     * @param device_prep The device preparation setting from user configuration.\n     * @param primary_device_requested  Indicates that the user did NOT specify device id to be used.\n     * @param duplicated_devices Devices that we need to handle.\n     * @param topology The current topology that we are evaluating.\n     * @return Topology that matches requirements and should be set.\n     */\n    active_topology_t\n    determine_final_topology(parsed_config_t::device_prep_e device_prep, const bool primary_device_requested, const std::vector<std::string> &duplicated_devices, const active_topology_t &topology) {\n      boost::optional<active_topology_t> final_topology;\n\n      const bool topology_change_requested { device_prep != parsed_config_t::device_prep_e::no_operation };\n      if (topology_change_requested) {\n        if (device_prep == parsed_config_t::device_prep_e::ensure_only_display) {\n          // Device needs to be the only one that's active or if it's a PRIMARY device,\n          // only the whole PRIMARY group needs to be active (in case they are duplicated)\n\n          if (primary_device_requested) {\n            if (topology.size() > 1) {\n              // There are other topology groups other than the primary devices,\n              // so we need to change that\n              final_topology = active_topology_t { { duplicated_devices } };\n            }\n            else {\n              // Primary device group is the only one active, nothing to do\n            }\n          }\n          else {\n            // Since primary_device_requested == false, it means a device was specified via config by the user\n            // and is the only device that needs to be enabled\n\n            if (is_device_found_in_active_topology(duplicated_devices.front(), topology)) {\n              // Device is currently active in the active topology group\n\n              if (duplicated_devices.size() > 1 || topology.size() > 1) {\n                // We have more than 1 device in the group, or we have more than 1 topology groups.\n                // We need to disable all other devices\n                final_topology = active_topology_t { { duplicated_devices.front() } };\n              }\n              else {\n                // Our device is the only one that's active, nothing to do\n              }\n            }\n            else {\n              // Our device is not active, we need to activate it and ONLY it\n              final_topology = active_topology_t { { duplicated_devices.front() } };\n            }\n          }\n        }\n        // device_prep_e::ensure_active || device_prep_e::ensure_primary\n        else {\n          //  The device needs to be active at least.\n\n          if (primary_device_requested || is_device_found_in_active_topology(duplicated_devices.front(), topology)) {\n            // Device is already active, nothing to do here\n          }\n          else {\n            // Create the extended topology as it's probably what makes sense the most...\n            final_topology = topology;\n            final_topology->push_back({ duplicated_devices.front() });\n          }\n        }\n      }\n\n      return final_topology ? *final_topology : topology;\n    }\n\n  }  // namespace\n\n  std::unordered_set<std::string>\n  remove_vdd_from_topology(active_topology_t &topology) {\n    std::unordered_set<std::string> removed_device_ids;\n    \n    // Get list of available devices (includes both active and inactive devices)\n    // This ensures we don't remove inactive devices that can be re-enabled\n    const auto available_devices = enum_available_devices();\n    std::unordered_set<std::string> available_device_ids;\n    for (const auto &[device_id, info] : available_devices) {\n      // Include all devices (active, inactive, primary) - they all can potentially be used\n      available_device_ids.insert(device_id);\n    }\n\n    for (auto &group : topology) {\n      auto new_end = std::remove_if(group.begin(), group.end(),\n        [&removed_device_ids, &available_device_ids](const std::string &device_id) {\n          // First check if device exists in available devices\n          // Note: available_devices includes inactive devices, so inactive devices will pass this check\n          const bool device_exists = available_device_ids.count(device_id) > 0;\n          \n          if (!device_exists) {\n            // Device doesn't exist in available devices at all - remove it\n            // This means the device was truly destroyed (e.g., VDD uninstalled, physical display disconnected)\n            // It's safe to remove as it cannot be re-enabled\n            BOOST_LOG(debug) << \"Removing non-existent device from topology: \" << device_id;\n            removed_device_ids.insert(device_id);  // Track removed ID\n            return true;\n          }\n          \n          // Device exists (could be active or inactive), check if it's VDD by friendly name\n          // Only remove if it's VDD - inactive physical displays will be preserved\n          const auto friendly_name = get_display_friendly_name(device_id);\n          if (friendly_name == ZAKO_NAME) {\n            BOOST_LOG(debug) << \"Removing VDD device from topology: \" << device_id;\n            removed_device_ids.insert(device_id);  // Track removed ID\n            return true;\n          }\n          \n          // Device exists and is not VDD - preserve it (even if inactive, it can be re-enabled)\n          return false;\n        });\n      group.erase(new_end, group.end());\n    }\n\n    // Remove empty groups\n    topology.erase(\n      std::remove_if(topology.begin(), topology.end(),\n        [](const auto &group) { return group.empty(); }),\n      topology.end());\n\n    return removed_device_ids;\n  }\n\n  /**\n   * @brief Enumerate and get one of the devices matching the id or\n   *        any of the primary devices if id is unspecified.\n   * @param device_id Id to find in enumerated devices.\n   * @return Device id, or empty string if an error has occurred.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string primary_device = find_one_of_the_available_devices(\"\");\n   * const std::string id_that_matches_provided_id = find_one_of_the_available_devices(primary_device);\n   * ```\n   */\n  std::string\n  find_one_of_the_available_devices(const std::string &device_id) {\n    const auto devices { enum_available_devices() };\n    if (devices.empty()) {\n      // Transient during display reinit or right after VDD create; avoid error level\n      BOOST_LOG(warning) << \"Find one of the available devices: display device list is empty!\";\n      return {};\n    }\n    BOOST_LOG(info) << \"Available display devices: \" << to_string(devices);\n\n    const auto device_it { std::find_if(std::begin(devices), std::end(devices), [&device_id](const auto &entry) {\n      return device_id.empty() ? entry.second.device_state == device_state_e::primary : entry.first == device_id;\n    }) };\n    if (device_it == std::end(devices)) {\n      BOOST_LOG(warning) << \"Device \" << (device_id.empty() ? \"PRIMARY\" : device_id) << \" not found in the list of available devices!\";\n      return {};\n    }\n\n    return device_it->first;\n  }\n\n  std::unordered_set<std::string>\n  get_device_ids_from_topology(const active_topology_t &topology) {\n    std::unordered_set<std::string> device_ids;\n    for (const auto &group : topology) {\n      for (const auto &device_id : group) {\n        device_ids.insert(device_id);\n      }\n    }\n\n    return device_ids;\n  }\n\n  std::unordered_set<std::string>\n  get_newly_enabled_devices_from_topology(const active_topology_t &previous_topology, const active_topology_t &new_topology) {\n    const auto prev_ids { get_device_ids_from_topology(previous_topology) };\n    auto new_ids { get_device_ids_from_topology(new_topology) };\n\n    for (auto &id : prev_ids) {\n      new_ids.erase(id);\n    }\n\n    return new_ids;\n  }\n\n  boost::optional<handled_topology_result_t>\n  handle_device_topology_configuration(\n    const parsed_config_t &config,\n    const boost::optional<topology_pair_t> &previously_configured_topology,\n    const std::function<bool()> &revert_settings,\n    const boost::optional<active_topology_t> &pre_saved_initial_topology) {\n    const bool primary_device_requested { config.device_id.empty() };\n    const std::string requested_device_id { find_one_of_the_available_devices(config.device_id) };\n    if (requested_device_id.empty()) {\n      // Error already logged\n      return boost::none;\n    }\n\n    // If we still have a previously configured topology, we could potentially skip making any changes to the topology.\n    // However, it could also mean that we need to revert any previous changes in case the final topology has changed somehow.\n    if (previously_configured_topology) {\n      // Here we are pretending to be in an initial topology and want to perform reevaluation in case the\n      // user has changed the settings while the stream was paused. For the proper \"evaluation\" order,\n      // see logic outside this conditional.\n      const auto prev_duplicated_devices { get_duplicate_devices(requested_device_id, previously_configured_topology->initial) };\n      auto prev_final_topology { determine_final_topology(config.device_prep, primary_device_requested, prev_duplicated_devices, previously_configured_topology->initial) };\n\n      // 与当前实现保持一致：在非「仅启用」模式下，也对“历史期望拓扑”做一次补全，\n      // 这样在比较是否需要回滚时，不会因为我们额外补上的 inactive 设备导致无意义的回滚与再次切换。\n      if (config.device_prep != parsed_config_t::device_prep_e::ensure_only_display) {\n        prev_final_topology = augment_topology_with_inactive_devices(prev_final_topology, requested_device_id);\n      }\n\n      // There is also an edge case where we can have a different number of primary duplicated devices, which wasn't the case\n      // during the initial topology configuration. If the user requested to use the primary device,\n      // the prev_final_topology would not reflect that change in primary duplicated devices. Therefore, we also need\n      // to evaluate current topology (which would have the new state of primary devices) and arrive at the\n      // same final topology as the prev_final_topology.\n      const auto current_topology { get_current_topology() };\n      const auto duplicated_devices { get_duplicate_devices(requested_device_id, current_topology) };\n      auto final_topology { determine_final_topology(config.device_prep, primary_device_requested, duplicated_devices, current_topology) };\n\n      if (config.device_prep != parsed_config_t::device_prep_e::ensure_only_display) {\n        final_topology = augment_topology_with_inactive_devices(final_topology, requested_device_id);\n      }\n\n      // If the topology we are switching to is the same as the final topology we had before, that means\n      // user did not change anything, and we don't need to revert changes.\n      if (!is_topology_the_same(previously_configured_topology->modified, prev_final_topology) ||\n          !is_topology_the_same(previously_configured_topology->modified, final_topology)) {\n        BOOST_LOG(warning) << \"Previous topology does not match the new one. Reverting previous changes!\";\n        if (!revert_settings()) {\n          return boost::none;\n        }\n      }\n    }\n\n    // Regardless of whether the user has made any changes to the user configuration or not, we always\n    // need to evaluate the current topology and perform the switch if needed as the user might\n    // have been playing around with active displays while the stream was paused.\n\n    const auto current_topology { get_current_topology() };\n    if (!is_topology_valid(current_topology)) {\n      BOOST_LOG(error) << \"Display topology is invalid!\";\n      return boost::none;\n    }\n\n    // When dealing with the \"requested device\" here and in other functions we need to keep\n    // in mind that it could belong to a duplicated display and thus all of them\n    // need to be taken into account, which complicates everything...\n    \n    // 在VDD场景下，使用真实初始拓扑来计算duplicated_devices和final_topology\n    // 这样可以基于用户串流前的真实状态来构建目标拓扑\n    const auto &topology_for_calculation = pre_saved_initial_topology ? *pre_saved_initial_topology : current_topology;\n    \n    auto duplicated_devices { get_duplicate_devices(requested_device_id, topology_for_calculation) };\n    auto final_topology { determine_final_topology(config.device_prep, primary_device_requested, duplicated_devices, topology_for_calculation) };\n\n    // 只在特定模式下才调用augment_topology\n    // no_operation模式：不调整任何内容，跳过\n    // ensure_only_display模式：只启用指定设备，不补全，跳过\n    if (config.device_prep != parsed_config_t::device_prep_e::ensure_only_display &&\n        config.device_prep != parsed_config_t::device_prep_e::no_operation) {\n      // 如果有预保存的初始拓扑（VDD场景），只补全在初始拓扑中的设备\n      // 这样可以尊重用户的原始配置（不会打开用户手动关闭的显示器）\n      if (pre_saved_initial_topology) {\n        const auto initial_devices = get_device_ids_from_topology(*pre_saved_initial_topology);\n        BOOST_LOG(debug) << \"Augmenting topology with constraints from initial topology (VDD scenario)\";\n        final_topology = augment_topology_with_inactive_devices(final_topology, requested_device_id, initial_devices);\n      }\n    }\n\n    BOOST_LOG(debug) << \"Current display topology: \" << to_string(current_topology);\n    if (!is_topology_the_same(current_topology, final_topology)) {\n      BOOST_LOG(info) << \"Changing display topology to: \" << to_string(final_topology);\n      if (!set_topology(final_topology)) {\n        // Error already logged.\n        return boost::none;\n      }\n\n      // It is possible that we no longer have duplicate displays, so we need to update the list\n      duplicated_devices = get_duplicate_devices(requested_device_id, final_topology);\n    }\n\n    // This check is mainly to cover the case for \"config.device_prep == no_operation\" as we at least\n    // have to validate that the device exists, but it doesn't hurt to double-check it in all cases.\n    if (!is_device_found_in_active_topology(requested_device_id, final_topology)) {\n      BOOST_LOG(error) << \"Device \" << requested_device_id << \" is not active!\";\n      return boost::none;\n    }\n\n    // 如果有预保存的初始拓扑（在VDD创建前保存的），使用它作为真实初始拓扑\n    // 否则使用当前拓扑（可能已被VDD破坏）\n    const auto real_initial_topology = pre_saved_initial_topology ? *pre_saved_initial_topology : current_topology;\n    \n    return handled_topology_result_t {\n      topology_pair_t {\n        real_initial_topology,  // 使用真实的初始拓扑\n        final_topology },\n      topology_metadata_t {\n        final_topology,\n        get_newly_enabled_devices_from_topology(current_topology, final_topology),\n        primary_device_requested,\n        duplicated_devices }\n    };\n  }\n\n  boost::optional<handled_topology_result_t>\n  get_current_topology_metadata(const std::string &device_id) {\n    const std::string requested_device_id { find_one_of_the_available_devices(device_id) };\n    if (requested_device_id.empty()) {\n      BOOST_LOG(error) << \"Device not found: \" << device_id;\n      return boost::none;\n    }\n\n    // 获取活跃拓扑并检查设备是否可用，带重试以应对 HDR/拓扑变更后的短暂不稳定\n    active_topology_t current_topology;\n    bool device_active = false;\n    constexpr int max_retries = 3;\n    constexpr auto retry_delay = std::chrono::milliseconds(500);\n\n    for (int attempt = 0; attempt < max_retries; ++attempt) {\n      current_topology = get_current_topology();\n      if (!is_topology_valid(current_topology)) {\n        BOOST_LOG(warning) << \"Display topology is invalid (attempt \" << (attempt + 1) << \"/\" << max_retries << \")\";\n        if (attempt + 1 < max_retries) {\n          std::this_thread::sleep_for(retry_delay);\n          continue;\n        }\n        BOOST_LOG(error) << \"Display topology is invalid after all retries!\";\n        return boost::none;\n      }\n\n      if (is_device_found_in_active_topology(requested_device_id, current_topology)) {\n        device_active = true;\n        break;\n      }\n\n      BOOST_LOG(warning) << \"Device \" << requested_device_id << \" is not active (attempt \" << (attempt + 1) << \"/\" << max_retries << \"), waiting for display to stabilize...\";\n      if (attempt + 1 < max_retries) {\n        std::this_thread::sleep_for(retry_delay);\n      }\n    }\n\n    if (!device_active) {\n      BOOST_LOG(error) << \"Device \" << requested_device_id << \" is not active after \" << max_retries << \" retries!\";\n      return boost::none;\n    }\n\n    const bool primary_device_requested { device_id.empty() };\n    const auto duplicated_devices { get_duplicate_devices(requested_device_id, current_topology) };\n\n    // VDD模式：不修改拓扑，使用当前拓扑作为initial和modified\n    return handled_topology_result_t {\n      topology_pair_t {\n        current_topology,\n        current_topology },\n      topology_metadata_t {\n        current_topology,\n        {},  // 没有新启用的设备\n        primary_device_requested,\n        duplicated_devices }\n    };\n  }\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/windows/display_device/settings_topology.h",
    "content": "#pragma once\n\n// local includes\n#include \"src/display_device/settings.h\"\n\nnamespace display_device {\n\n  /**\n   * @brief Contains metadata about the current topology.\n   */\n  struct topology_metadata_t {\n    active_topology_t current_topology; /**< The currently active topology. */\n    std::unordered_set<std::string> newly_enabled_devices; /**< A list of device ids that were newly enabled after changing topology. */\n    bool primary_device_requested; /**< Indicates that the user did NOT specify device id to be used. */\n    std::vector<std::string> duplicated_devices; /**< A list of devices id that we need to handle. If user specified device id, it will always be the first entry. */\n  };\n\n  /**\n   * @brief Container for active topologies.\n   * @note Both topologies can be the same.\n   */\n  struct topology_pair_t {\n    active_topology_t initial; /**< The initial topology that we had before we switched. */\n    active_topology_t modified; /**< The topology that we have modified. */\n\n    // For JSON serialization\n    NLOHMANN_DEFINE_TYPE_INTRUSIVE(topology_pair_t, initial, modified)\n  };\n\n  /**\n   * @brief Contains the result after handling the configuration.\n   * @see handle_device_topology_configuration\n   */\n  struct handled_topology_result_t {\n    topology_pair_t pair;\n    topology_metadata_t metadata;\n  };\n\n  /**\n   * @brief Get all ids from the active topology structure.\n   * @param topology Topology to get ids from.\n   * @returns A list of device ids.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const auto device_ids = get_device_ids_from_topology(get_current_topology());\n   * ```\n   */\n  std::unordered_set<std::string>\n  get_device_ids_from_topology(const active_topology_t &topology);\n\n  /**\n   * @brief Get new device ids that were not present in previous topology.\n   * @param previous_topology The previous topology.\n   * @param new_topology A new topology.\n   * @return A list of devices ids.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * active_topology_t old_topology { { \"ID_1\" } };\n   * active_topology_t new_topology { { \"ID_1\" }, { \"ID_2\" } };\n   * const auto device_ids = get_newly_enabled_devices_from_topology(old_topology, new_topology);\n   * // device_ids contains \"ID_2\"\n   * ```\n   */\n  std::unordered_set<std::string>\n  get_newly_enabled_devices_from_topology(const active_topology_t &previous_topology, const active_topology_t &new_topology);\n\n  /**\n   * @brief Modify the topology based on the configuration and previously configured topology.\n   *\n   * The function performs the necessary steps for changing topology if needed.\n   * It evaluates the previous configuration in case we are just updating\n   * some of the settings (like resolution) where topology change might not be necessary.\n   *\n   * In case the function determines that we need to revert all of the previous settings\n   * since the new topology is not compatible with the previously configured one, the revert_settings\n   * parameter will be called to completely revert all changes.\n   *\n   * @param config Configuration to be evaluated.\n   * @param previously_configured_topology A result from a earlier call of this function.\n   * @param revert_settings A function-proxy that can be used to revert all of the changes made to the device displays.\n   * @return A result object, or an empty optional if the function fails.\n   */\n  boost::optional<handled_topology_result_t>\n  handle_device_topology_configuration(\n    const parsed_config_t &config,\n    const boost::optional<topology_pair_t> &previously_configured_topology,\n    const std::function<bool()> &revert_settings,\n    const boost::optional<active_topology_t> &pre_saved_initial_topology = boost::none);\n\n  /**\n   * @brief Remove VDD devices and non-existent devices from topology.\n   * @param topology Topology to clean.\n   * @return True if any device was removed, false otherwise.\n   * @note This function will NOT remove inactive devices that can be re-enabled.\n   *       enum_available_devices() includes both active and inactive devices,\n   *       so inactive devices will be preserved in the topology.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * active_topology_t topology { { \"DEVICE_1\" }, { \"VDD_DEVICE\" } };\n   * if (remove_vdd_from_topology(topology)) {\n   *   // VDD device was removed\n   * }\n   * ```\n   */\n  std::unordered_set<std::string>\n  remove_vdd_from_topology(active_topology_t &topology);\n\n  /**\n   * @brief Get current topology metadata without modifying anything.\n   * @param device_id The device ID to use for metadata.\n   * @return A topology metadata object with current state.\n   *\n   * This is a simplified version of handle_device_topology_configuration\n   * for VDD mode where we don't want to modify topology but still need\n   * the metadata for resolution/refresh rate settings.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const auto metadata = get_current_topology_metadata(\"DEVICE_ID\");\n   * ```\n   */\n  boost::optional<handled_topology_result_t>\n  get_current_topology_metadata(const std::string &device_id);\n\n}  // namespace display_device\n"
  },
  {
    "path": "src/platform/windows/display_device/windows_utils.cpp",
    "content": "// lib includes\n#include <boost/algorithm/string.hpp>\n#include <boost/uuid/name_generator_sha1.hpp>\n#include <boost/uuid/uuid.hpp>\n#include <boost/uuid/uuid_io.hpp>\n\n// standard includes\n#include <iostream>\n#include <system_error>\n\n// local includes\n#include \"src/logging.h\"\n#include \"src/platform/windows/misc.h\"\n#include \"src/utility.h\"\n#include \"windows_utils.h\"\n\n// Windows includes after \"windows.h\"\n#include <SetupApi.h>\n#include <wtsapi32.h>\n\nnamespace display_device::w_utils {\n\n  namespace {\n\n    /**\n     * @see get_monitor_device_path description for more information as this\n     *      function is identical except that it returns wide-string instead\n     *      of a normal one.\n     */\n    std::wstring\n    get_monitor_device_path_wstr(const DISPLAYCONFIG_PATH_INFO &path) {\n      DISPLAYCONFIG_TARGET_DEVICE_NAME target_name = {};\n      target_name.header.adapterId = path.targetInfo.adapterId;\n      target_name.header.id = path.targetInfo.id;\n      target_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;\n      target_name.header.size = sizeof(target_name);\n\n      LONG result { DisplayConfigGetDeviceInfo(&target_name.header) };\n      if (result != ERROR_SUCCESS) {\n        BOOST_LOG(error) << get_error_string(result) << \" failed to get target device name!\";\n        return {};\n      }\n\n      return std::wstring { target_name.monitorDevicePath };\n    }\n\n    /**\n     * @brief Helper method for dealing with SetupAPI.\n     * @returns True if device interface path was retrieved and is non-empty, false otherwise.\n     * @see get_device_id implementation for more context regarding this madness.\n     */\n    bool\n    get_device_interface_detail(HDEVINFO dev_info_handle, SP_DEVICE_INTERFACE_DATA &dev_interface_data, std::wstring &dev_interface_path, SP_DEVINFO_DATA &dev_info_data) {\n      DWORD required_size_in_bytes { 0 };\n      if (SetupDiGetDeviceInterfaceDetailW(dev_info_handle, &dev_interface_data, nullptr, 0, &required_size_in_bytes, nullptr)) {\n        BOOST_LOG(error) << \"\\\"SetupDiGetDeviceInterfaceDetailW\\\" did not fail, what?!\";\n        return false;\n      }\n      else if (required_size_in_bytes <= 0) {\n        BOOST_LOG(error) << get_error_string(static_cast<LONG>(GetLastError())) << \" \\\"SetupDiGetDeviceInterfaceDetailW\\\" failed while getting size.\";\n        return false;\n      }\n\n      std::vector<std::uint8_t> buffer;\n      buffer.resize(required_size_in_bytes);\n\n      // This part is just EVIL!\n      auto detail_data { reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_W *>(buffer.data()) };\n      detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);\n\n      if (!SetupDiGetDeviceInterfaceDetailW(dev_info_handle, &dev_interface_data, detail_data, required_size_in_bytes, nullptr, &dev_info_data)) {\n        BOOST_LOG(error) << get_error_string(static_cast<LONG>(GetLastError())) << \" \\\"SetupDiGetDeviceInterfaceDetailW\\\" failed.\";\n        return false;\n      }\n\n      dev_interface_path = std::wstring { detail_data->DevicePath };\n      return !dev_interface_path.empty();\n    }\n\n    /**\n     * @brief Helper method for dealing with SetupAPI.\n     * @returns True if instance id was retrieved and is non-empty, false otherwise.\n     * @see get_device_id implementation for more context regarding this madness.\n     */\n    bool\n    get_device_instance_id(HDEVINFO dev_info_handle, SP_DEVINFO_DATA &dev_info_data, std::wstring &instance_id) {\n      DWORD required_size_in_characters { 0 };\n      if (SetupDiGetDeviceInstanceIdW(dev_info_handle, &dev_info_data, nullptr, 0, &required_size_in_characters)) {\n        BOOST_LOG(error) << \"\\\"SetupDiGetDeviceInstanceIdW\\\" did not fail, what?!\";\n        return false;\n      }\n      else if (required_size_in_characters <= 0) {\n        BOOST_LOG(error) << get_error_string(static_cast<LONG>(GetLastError())) << \" \\\"SetupDiGetDeviceInstanceIdW\\\" failed while getting size.\";\n        return false;\n      }\n\n      instance_id.resize(required_size_in_characters);\n      if (!SetupDiGetDeviceInstanceIdW(dev_info_handle, &dev_info_data, instance_id.data(), instance_id.size(), nullptr)) {\n        BOOST_LOG(error) << get_error_string(static_cast<LONG>(GetLastError())) << \" \\\"SetupDiGetDeviceInstanceIdW\\\" failed.\";\n        return false;\n      }\n\n      return !instance_id.empty();\n    }\n\n    /**\n     * @brief Helper method for dealing with SetupAPI.\n     * @returns True if EDID was retrieved and is non-empty, false otherwise.\n     * @see get_device_id implementation for more context regarding this madness.\n     */\n    bool\n    get_device_edid(HDEVINFO dev_info_handle, SP_DEVINFO_DATA &dev_info_data, std::vector<BYTE> &edid) {\n      // We could just directly open the registry key as the path is known, but we can also use the this\n      HKEY reg_key { SetupDiOpenDevRegKey(dev_info_handle, &dev_info_data, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ) };\n      if (reg_key == INVALID_HANDLE_VALUE) {\n        BOOST_LOG(error) << get_error_string(static_cast<LONG>(GetLastError())) << \" \\\"SetupDiOpenDevRegKey\\\" failed.\";\n        return false;\n      }\n\n      const auto reg_key_cleanup {\n        util::fail_guard([&reg_key]() {\n          const auto status { RegCloseKey(reg_key) };\n          if (status != ERROR_SUCCESS) {\n            BOOST_LOG(error) << get_error_string(status) << \" \\\"RegCloseKey\\\" failed.\";\n          }\n        })\n      };\n\n      DWORD required_size_in_bytes { 0 };\n      auto status { RegQueryValueExW(reg_key, L\"EDID\", nullptr, nullptr, nullptr, &required_size_in_bytes) };\n      if (status != ERROR_SUCCESS) {\n        // ERROR_FILE_NOT_FOUND (2) is expected for some displays (virtual, RDP, etc.)\n        // Code has fallback mechanism, so this is not a critical error\n        if (status == ERROR_FILE_NOT_FOUND) {\n          BOOST_LOG(debug) << get_error_string(status) << \" \\\"RegQueryValueExW\\\" failed when getting size (EDID not found, using fallback).\";\n        }\n        else {\n          BOOST_LOG(warning) << get_error_string(status) << \" \\\"RegQueryValueExW\\\" failed when getting size.\";\n        }\n        return false;\n      }\n\n      edid.resize(required_size_in_bytes);\n\n      status = RegQueryValueExW(reg_key, L\"EDID\", nullptr, nullptr, edid.data(), &required_size_in_bytes);\n      if (status != ERROR_SUCCESS) {\n        // ERROR_FILE_NOT_FOUND (2) is expected for some displays (virtual, RDP, etc.)\n        // Code has fallback mechanism, so this is not a critical error\n        if (status == ERROR_FILE_NOT_FOUND) {\n          BOOST_LOG(debug) << get_error_string(status) << \" \\\"RegQueryValueExW\\\" failed when getting size (EDID not found, using fallback).\";\n        }\n        else {\n          BOOST_LOG(warning) << get_error_string(status) << \" \\\"RegQueryValueExW\\\" failed when getting size.\";\n        }\n        return false;\n      }\n\n      return !edid.empty();\n    }\n\n    std::string\n    get_driver_key(HDEVINFO dev_info_handle, SP_DEVINFO_DATA &dev_info_data) {\n      DWORD dataType;\n      BYTE buffer[256];\n      DWORD bufferSize = sizeof(buffer);\n      if (SetupDiGetDeviceRegistryPropertyW(dev_info_handle, &dev_info_data, SPDRP_DRIVER, &dataType, buffer, bufferSize, NULL)) {\n        if (dataType == REG_SZ) {\n          // Get the driver key from the device registry. Known that the driver key is 40 chars.\n          std::string driver_key = platf::to_utf8(reinterpret_cast<wchar_t*>(buffer));\n          BOOST_LOG(info) << \"get driver_key: \" << driver_key;\n          return driver_key;\n        }\n      }\n      return \"\";\n    }\n\n  }  // namespace\n\n  std::string\n  get_error_string(LONG error_code) {\n    std::error_code ec(error_code, std::system_category());\n    std::stringstream error;\n    error << \"[code: \";\n    switch (error_code) {\n      case ERROR_INVALID_PARAMETER:\n        error << \"ERROR_INVALID_PARAMETER\";\n        break;\n      case ERROR_NOT_SUPPORTED:\n        error << \"ERROR_NOT_SUPPORTED\";\n        break;\n      case ERROR_ACCESS_DENIED:\n        error << \"ERROR_ACCESS_DENIED\";\n        break;\n      case ERROR_INSUFFICIENT_BUFFER:\n        error << \"ERROR_INSUFFICIENT_BUFFER\";\n        break;\n      case ERROR_GEN_FAILURE:\n        error << \"ERROR_GEN_FAILURE\";\n        break;\n      case ERROR_SUCCESS:\n        error << \"ERROR_SUCCESS\";\n        break;\n      default:\n        error << error_code;\n        break;\n    }\n    \n    error << \", message: \" << ec.message() << \"]\";\n    return error.str();\n  }\n\n  bool\n  is_primary(const DISPLAYCONFIG_SOURCE_MODE &mode) {\n    return mode.position.x == 0 && mode.position.y == 0;\n  }\n\n  bool\n  are_modes_duplicated(const DISPLAYCONFIG_SOURCE_MODE &mode_a, const DISPLAYCONFIG_SOURCE_MODE &mode_b) {\n    return mode_a.position.x == mode_b.position.x && mode_a.position.y == mode_b.position.y;\n  }\n\n  bool\n  is_available(const DISPLAYCONFIG_PATH_INFO &path) {\n    return path.targetInfo.targetAvailable == TRUE;\n  }\n\n  bool\n  is_active(const DISPLAYCONFIG_PATH_INFO &path) {\n    return static_cast<bool>(path.flags & DISPLAYCONFIG_PATH_ACTIVE);\n  }\n\n  void\n  set_active(DISPLAYCONFIG_PATH_INFO &path) {\n    path.flags |= DISPLAYCONFIG_PATH_ACTIVE;\n  }\n\n  std::string\n  get_device_id(const DISPLAYCONFIG_PATH_INFO &path) {\n    const auto device_path { get_monitor_device_path_wstr(path) };\n    if (device_path.empty()) {\n      // Error already logged\n      return {};\n    }\n\n    static const GUID monitor_guid { 0xe6f07b5f, 0xee97, 0x4a90, { 0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7 } };\n    std::vector<BYTE> device_id_data;\n\n    HDEVINFO dev_info_handle { SetupDiGetClassDevsW(&monitor_guid, nullptr, nullptr, DIGCF_DEVICEINTERFACE) };\n    if (dev_info_handle) {\n      const auto dev_info_handle_cleanup {\n        util::fail_guard([&dev_info_handle]() {\n          if (!SetupDiDestroyDeviceInfoList(dev_info_handle)) {\n            BOOST_LOG(error) << get_error_string(static_cast<LONG>(GetLastError())) << \" \\\"SetupDiDestroyDeviceInfoList\\\" failed.\";\n          }\n        })\n      };\n\n      SP_DEVICE_INTERFACE_DATA dev_interface_data {};\n      dev_interface_data.cbSize = sizeof(dev_interface_data);\n      for (DWORD monitor_index = 0;; ++monitor_index) {\n        if (!SetupDiEnumDeviceInterfaces(dev_info_handle, nullptr, &monitor_guid, monitor_index, &dev_interface_data)) {\n          const DWORD error_code { GetLastError() };\n          if (error_code == ERROR_NO_MORE_ITEMS) {\n            break;\n          }\n\n          BOOST_LOG(warning) << get_error_string(static_cast<LONG>(error_code)) << \" \\\"SetupDiEnumDeviceInterfaces\\\" failed.\";\n          continue;\n        }\n\n        std::wstring dev_interface_path;\n        SP_DEVINFO_DATA dev_info_data {};\n        dev_info_data.cbSize = sizeof(dev_info_data);\n        if (!get_device_interface_detail(dev_info_handle, dev_interface_data, dev_interface_path, dev_info_data)) {\n          // Error already logged\n          continue;\n        }\n\n        if (!boost::iequals(dev_interface_path, device_path)) {\n          continue;\n        }\n\n        // Instance ID is unique in the system and persists restarts, but not driver re-installs.\n        // It looks like this:\n        //     DISPLAY\\ACI27EC\\5&4FD2DE4&5&UID4352 (also used in the device path it seems)\n        //                a    b    c    d    e\n        //\n        //  a) Hardware ID - stable\n        //  b) Either a bus number or has something to do with device capabilities - stable\n        //  c) Another ID, somehow tied to adapter (not an adapter ID from path object) - stable\n        //  d) Some sort of rotating counter thing, changes after driver reinstall - unstable\n        //  e) Seems to be the same as a target ID from path, it changes based on GPU port - semi-stable\n        //\n        // The instance ID also seems to be a part of the registry key (in case some other info is needed in the future):\n        //     HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Enum\\DISPLAY\\ACI27EC\\5&4fd2de4&5&UID4352\n\n        std::wstring instance_id;\n        if (!get_device_instance_id(dev_info_handle, dev_info_data, instance_id)) {\n          // Error already logged\n          break;\n        }\n\n        if (!get_device_edid(dev_info_handle, dev_info_data, device_id_data)) {\n          // Error already logged\n          break;\n        }\n\n        // We are going to discard the unstable parts of the instance ID and merge the stable parts with the edid buffer (if available)\n        auto unstable_part_index = instance_id.find_first_of(L'&', 0);\n        if (unstable_part_index != std::wstring::npos) {\n          unstable_part_index = instance_id.find_first_of(L'&', unstable_part_index + 1);\n        }\n\n        if (unstable_part_index == std::wstring::npos) {\n          BOOST_LOG(error) << \"Failed to split off the stable part from instance id string \" << platf::to_utf8(instance_id);\n          break;\n        }\n\n        auto semi_stable_part_index = instance_id.find_first_of(L'&', unstable_part_index + 1);\n        if (semi_stable_part_index == std::wstring::npos) {\n          BOOST_LOG(error) << \"Failed to split off the semi-stable part from instance id string \" << platf::to_utf8(instance_id);\n          break;\n        }\n\n        BOOST_LOG(verbose) << \"Creating device id for path \" << platf::to_utf8(device_path) << \" from EDID and instance ID: \" << platf::to_utf8({ std::begin(instance_id), std::begin(instance_id) + unstable_part_index }) << platf::to_utf8({ std::begin(instance_id) + semi_stable_part_index, std::end(instance_id) });\n        device_id_data.insert(std::end(device_id_data),\n          reinterpret_cast<const BYTE *>(instance_id.data()),\n          reinterpret_cast<const BYTE *>(instance_id.data() + unstable_part_index));\n        device_id_data.insert(std::end(device_id_data),\n          reinterpret_cast<const BYTE *>(instance_id.data() + semi_stable_part_index),\n          reinterpret_cast<const BYTE *>(instance_id.data() + instance_id.size()));\n        break;\n      }\n    }\n\n    if (device_id_data.empty()) {\n      // Using the device path as a fallback, which is always unique, but not as stable as the preferred one\n      BOOST_LOG(verbose) << \"Creating device id from path \" << platf::to_utf8(device_path);\n      device_id_data.insert(std::end(device_id_data),\n        reinterpret_cast<const BYTE *>(device_path.data()),\n        reinterpret_cast<const BYTE *>(device_path.data() + device_path.size()));\n    }\n\n    static constexpr boost::uuids::uuid ns_id {};  // null namespace = no salt\n    const auto boost_uuid { boost::uuids::name_generator_sha1 { ns_id }(device_id_data.data(), device_id_data.size()) };\n    return \"{\" + boost::uuids::to_string(boost_uuid) + \"}\";\n  }\n\n  std::string\n  get_device_driver_path(const DISPLAYCONFIG_PATH_INFO &path) {\n    const auto device_path { get_monitor_device_path_wstr(path) };\n    if (device_path.empty()) {\n      // Error already logged\n      return {};\n    }\n\n    static const GUID monitor_guid { 0xe6f07b5f, 0xee97, 0x4a90, { 0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7 } };\n    std::string driver_path;\n\n    HDEVINFO dev_info_handle { SetupDiGetClassDevsW(&monitor_guid, nullptr, nullptr, DIGCF_DEVICEINTERFACE) };\n    if (dev_info_handle) {\n      const auto dev_info_handle_cleanup {\n        util::fail_guard([&dev_info_handle]() {\n          if (!SetupDiDestroyDeviceInfoList(dev_info_handle)) {\n            BOOST_LOG(error) << get_error_string(static_cast<LONG>(GetLastError())) << \" \\\"SetupDiDestroyDeviceInfoList\\\" failed.\";\n          }\n        })\n      };\n\n      SP_DEVICE_INTERFACE_DATA dev_interface_data {};\n      dev_interface_data.cbSize = sizeof(dev_interface_data);\n      for (DWORD monitor_index = 0;; ++monitor_index) {\n        if (!SetupDiEnumDeviceInterfaces(dev_info_handle, nullptr, &monitor_guid, monitor_index, &dev_interface_data)) {\n          const DWORD error_code { GetLastError() };\n          if (error_code == ERROR_NO_MORE_ITEMS) {\n            break;\n          }\n\n          BOOST_LOG(warning) << get_error_string(static_cast<LONG>(error_code)) << \" \\\"SetupDiEnumDeviceInterfaces\\\" failed.\";\n          continue;\n        }\n\n        std::wstring dev_interface_path;\n        SP_DEVINFO_DATA dev_info_data {};\n        dev_info_data.cbSize = sizeof(dev_info_data);\n        if (!get_device_interface_detail(dev_info_handle, dev_interface_data, dev_interface_path, dev_info_data)) {\n          // Error already logged\n          continue;\n        }\n\n        if (!boost::iequals(dev_interface_path, device_path)) {\n          continue;\n        }\n\n        driver_path = get_driver_key(dev_info_handle, dev_info_data);\n      }\n    }\n\n    return driver_path;\n  }\n\n  std::string\n  get_monitor_device_path(const DISPLAYCONFIG_PATH_INFO &path) {\n    return platf::to_utf8(get_monitor_device_path_wstr(path));\n  }\n\n  std::string\n  get_friendly_name(const DISPLAYCONFIG_PATH_INFO &path) {\n    DISPLAYCONFIG_TARGET_DEVICE_NAME target_name = {};\n    target_name.header.adapterId = path.targetInfo.adapterId;\n    target_name.header.id = path.targetInfo.id;\n    target_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;\n    target_name.header.size = sizeof(target_name);\n\n    if (LONG result = DisplayConfigGetDeviceInfo(&target_name.header); result != ERROR_SUCCESS) {\n      BOOST_LOG(error) << get_error_string(result) << \" failed to get target device name!\";\n      return {};\n    }\n\n    // For standard EDID-based displays, use the friendly name from EDID\n    if (target_name.flags.friendlyNameFromEdid) {\n      return platf::to_utf8(target_name.monitorFriendlyDeviceName);\n    }\n\n    // For virtual displays (RDP, VMs, etc.) without EDID, use the GDI device name\n    if (auto gdi_name = get_display_name(path); !gdi_name.empty()) {\n      return gdi_name;\n    }\n\n    // Fallback: Create a synthetic friendly name based on path info\n    return \"Virtual_Display_\" + std::to_string(path.targetInfo.id);\n  }\n\n  std::string\n  get_display_name(const DISPLAYCONFIG_PATH_INFO &path) {\n    DISPLAYCONFIG_SOURCE_DEVICE_NAME source_name = {};\n    source_name.header.id = path.sourceInfo.id;\n    source_name.header.adapterId = path.sourceInfo.adapterId;\n    source_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;\n    source_name.header.size = sizeof(source_name);\n\n    LONG result { DisplayConfigGetDeviceInfo(&source_name.header) };\n    if (result != ERROR_SUCCESS) {\n      BOOST_LOG(error) << get_error_string(result) << \" failed to get display name! \";\n      return {};\n    }\n\n    return platf::to_utf8(source_name.viewGdiDeviceName);\n  }\n\n  hdr_state_e\n  get_hdr_state(const DISPLAYCONFIG_PATH_INFO &path) {\n    if (!is_active(path)) {\n      // Checking if active to suppress the error message below.\n      return hdr_state_e::unknown;\n    }\n\n    DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO color_info = {};\n    color_info.header.adapterId = path.targetInfo.adapterId;\n    color_info.header.id = path.targetInfo.id;\n    color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;\n    color_info.header.size = sizeof(color_info);\n\n    LONG result { DisplayConfigGetDeviceInfo(&color_info.header) };\n    if (result != ERROR_SUCCESS) {\n      BOOST_LOG(error) << get_error_string(result) << \" failed to get advanced color info! \";\n      return hdr_state_e::unknown;\n    }\n\n    return color_info.advancedColorSupported ? color_info.advancedColorEnabled ? hdr_state_e::enabled : hdr_state_e::disabled : hdr_state_e::unknown;\n  }\n\n  bool\n  set_hdr_state(const DISPLAYCONFIG_PATH_INFO &path, bool enable) {\n    DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE color_state = {};\n    color_state.header.adapterId = path.targetInfo.adapterId;\n    color_state.header.id = path.targetInfo.id;\n    color_state.header.type = DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE;\n    color_state.header.size = sizeof(color_state);\n\n    color_state.enableAdvancedColor = enable ? 1 : 0;\n\n    LONG result { DisplayConfigSetDeviceInfo(&color_state.header) };\n    if (result != ERROR_SUCCESS) {\n      BOOST_LOG(error) << get_error_string(result) << \" failed to set advanced color info!\";\n      return false;\n    }\n\n    return true;\n  }\n\n  boost::optional<UINT32>\n  get_source_index(const DISPLAYCONFIG_PATH_INFO &path, const std::vector<DISPLAYCONFIG_MODE_INFO> &modes) {\n    // The MS docs is not clear when to access union struct or not. It appears that union struct is available,\n    // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying.\n    //\n    // The docs state, however, that it is only available when\n    // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases\n    // were found where the flag is not set and the union is still being used.\n\n    const UINT32 index { path.sourceInfo.sourceModeInfoIdx };\n    if (index == DISPLAYCONFIG_PATH_SOURCE_MODE_IDX_INVALID) {\n      return boost::none;\n    }\n\n    if (index >= modes.size()) {\n      BOOST_LOG(error) << \"Source index \" << index << \" is out of range \" << modes.size();\n      return boost::none;\n    }\n\n    return index;\n  }\n\n  void\n  set_source_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional<UINT32> &index) {\n    // The MS docs is not clear when to access union struct or not. It appears that union struct is available,\n    // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying.\n    //\n    // The docs state, however, that it is only available when\n    // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases\n    // were found where the flag is not set and the union is still being used.\n\n    if (index) {\n      path.sourceInfo.sourceModeInfoIdx = *index;\n    }\n    else {\n      path.sourceInfo.sourceModeInfoIdx = DISPLAYCONFIG_PATH_SOURCE_MODE_IDX_INVALID;\n    }\n  }\n\n  void\n  set_target_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional<UINT32> &index) {\n    // The MS docs is not clear when to access union struct or not. It appears that union struct is available,\n    // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying.\n    //\n    // The docs state, however, that it is only available when\n    // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases\n    // were found where the flag is not set and the union is still being used.\n\n    if (index) {\n      path.targetInfo.targetModeInfoIdx = *index;\n    }\n    else {\n      path.targetInfo.targetModeInfoIdx = DISPLAYCONFIG_PATH_TARGET_MODE_IDX_INVALID;\n    }\n  }\n\n  void\n  set_desktop_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional<UINT32> &index) {\n    // The MS docs is not clear when to access union struct or not. It appears that union struct is available,\n    // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying.\n    //\n    // The docs state, however, that it is only available when\n    // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases\n    // were found where the flag is not set and the union is still being used.\n\n    if (index) {\n      path.targetInfo.desktopModeInfoIdx = *index;\n    }\n    else {\n      path.targetInfo.desktopModeInfoIdx = DISPLAYCONFIG_PATH_DESKTOP_IMAGE_IDX_INVALID;\n    }\n  }\n\n  void\n  set_clone_group_id(DISPLAYCONFIG_PATH_INFO &path, const boost::optional<UINT32> &id) {\n    // The MS docs is not clear when to access union struct or not. It appears that union struct is available,\n    // whenever QDC_VIRTUAL_MODE_AWARE is specified when querying.\n    //\n    // The docs state, however, that it is only available when\n    // DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE flag is set, but that is just BS (maybe copy-pasta mistake), because some cases\n    // were found where the flag is not set and the union is still being used.\n\n    if (id) {\n      path.sourceInfo.cloneGroupId = *id;\n    }\n    else {\n      path.sourceInfo.cloneGroupId = DISPLAYCONFIG_PATH_CLONE_GROUP_INVALID;\n    }\n  }\n\n  const DISPLAYCONFIG_SOURCE_MODE *\n  get_source_mode(const boost::optional<UINT32> &index, const std::vector<DISPLAYCONFIG_MODE_INFO> &modes) {\n    if (!index) {\n      return nullptr;\n    }\n\n    if (*index >= modes.size()) {\n      BOOST_LOG(error) << \"Source index \" << *index << \" is out of range \" << modes.size();\n      return nullptr;\n    }\n\n    const auto &mode { modes[*index] };\n    if (mode.infoType != DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE) {\n      BOOST_LOG(error) << \"Mode at index \" << *index << \" is not source mode!\";\n      return nullptr;\n    }\n\n    return &mode.sourceMode;\n  }\n\n  DISPLAYCONFIG_SOURCE_MODE *\n  get_source_mode(const boost::optional<UINT32> &index, std::vector<DISPLAYCONFIG_MODE_INFO> &modes) {\n    return const_cast<DISPLAYCONFIG_SOURCE_MODE *>(get_source_mode(index, const_cast<const std::vector<DISPLAYCONFIG_MODE_INFO> &>(modes)));\n  }\n\n  boost::optional<device_info_t>\n  get_device_info_for_valid_path(const DISPLAYCONFIG_PATH_INFO &path, bool must_be_active) {\n    if (!is_available(path)) {\n      // Could be transient issue according to MSDOCS (no longer available, but still \"active\")\n      return boost::none;\n    }\n\n    if (must_be_active) {\n      if (!is_active(path)) {\n        return boost::none;\n      }\n    }\n\n    const auto device_path { get_monitor_device_path(path) };\n    if (device_path.empty()) {\n      return boost::none;\n    }\n\n    const auto device_id { get_device_id(path) };\n    if (device_id.empty()) {\n      return boost::none;\n    }\n\n    const auto display_name { get_display_name(path) };\n    if (display_name.empty()) {\n      return boost::none;\n    }\n\n    return device_info_t { device_path, device_id };\n  }\n\n  boost::optional<path_and_mode_data_t>\n  query_display_config(bool active_only) {\n    // When we want to enable/disable displays, we need to get all paths as they will not be active.\n    // This will require some additional filtering of duplicate and otherwise useless paths.\n    UINT32 base_flags = active_only ? QDC_ONLY_ACTIVE_PATHS : QDC_ALL_PATHS;\n\n    // Try with QDC_VIRTUAL_MODE_AWARE first (supported from W10), then fallback without it.\n    for (bool virtual_mode_aware : { true, false }) {\n      UINT32 flags = base_flags | (virtual_mode_aware ? QDC_VIRTUAL_MODE_AWARE : 0);\n\n      std::vector<DISPLAYCONFIG_PATH_INFO> paths;\n      std::vector<DISPLAYCONFIG_MODE_INFO> modes;\n      LONG result = ERROR_SUCCESS;\n\n      do {\n        UINT32 path_count { 0 };\n        UINT32 mode_count { 0 };\n\n        result = GetDisplayConfigBufferSizes(flags, &path_count, &mode_count);\n        if (result != ERROR_SUCCESS) {\n          BOOST_LOG(virtual_mode_aware ? warning : error)\n            << get_error_string(result) << \" GetDisplayConfigBufferSizes failed\"\n            << (virtual_mode_aware ? \" (with QDC_VIRTUAL_MODE_AWARE)\" : \"\") << \"!\";\n          break;\n        }\n\n        // No display paths exist (e.g. headless machine before VDD creation).\n        // Return empty data instead of calling QueryDisplayConfig with 0-sized buffers.\n        if (path_count == 0 && mode_count == 0) {\n          return path_and_mode_data_t { {}, {} };\n        }\n\n        paths.resize(path_count);\n        modes.resize(mode_count);\n        result = QueryDisplayConfig(flags, &path_count, paths.data(), &mode_count, modes.data(), nullptr);\n\n        // The function may have returned fewer paths/modes than estimated\n        paths.resize(path_count);\n        modes.resize(mode_count);\n\n        // It's possible that between the call to GetDisplayConfigBufferSizes and QueryDisplayConfig\n        // that the display state changed, so loop on the case of ERROR_INSUFFICIENT_BUFFER.\n      } while (result == ERROR_INSUFFICIENT_BUFFER);\n\n      if (result == ERROR_SUCCESS) {\n        return path_and_mode_data_t { std::move(paths), std::move(modes) };\n      }\n\n      if (virtual_mode_aware) {\n        BOOST_LOG(warning) << get_error_string(result) << \" failed to query display paths and modes with QDC_VIRTUAL_MODE_AWARE, retrying without...\";\n        continue;\n      }\n\n      BOOST_LOG(error) << get_error_string(result) << \" failed to query display paths and modes!\";\n      return boost::none;\n    }\n\n    return boost::none;  // unreachable: loop always returns in both iterations\n  }\n\n  const DISPLAYCONFIG_PATH_INFO *\n  get_active_path(const std::string &device_id, const std::vector<DISPLAYCONFIG_PATH_INFO> &paths) {\n    for (const auto &path : paths) {\n      const auto device_info { get_device_info_for_valid_path(path, ACTIVE_ONLY_DEVICES) };\n      if (!device_info) {\n        continue;\n      }\n\n      if (device_info->device_id == device_id) {\n        return &path;\n      }\n    }\n\n    return nullptr;\n  }\n\n  DISPLAYCONFIG_PATH_INFO *\n  get_active_path(const std::string &device_id, std::vector<DISPLAYCONFIG_PATH_INFO> &paths) {\n    return const_cast<DISPLAYCONFIG_PATH_INFO *>(get_active_path(device_id, const_cast<const std::vector<DISPLAYCONFIG_PATH_INFO> &>(paths)));\n  }\n\n  bool\n  is_user_session_locked() {\n    LPWSTR buffer { nullptr };\n    const auto cleanup_guard {\n      util::fail_guard([&buffer]() {\n        if (buffer) {\n          WTSFreeMemory(buffer);\n        }\n      })\n    };\n\n    DWORD buffer_size_in_bytes { 0 };\n    if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, WTSGetActiveConsoleSessionId(), WTSSessionInfoEx, &buffer, &buffer_size_in_bytes)) {\n      if (buffer_size_in_bytes > 0) {\n        const auto wts_info { reinterpret_cast<const WTSINFOEXW *>(buffer) };\n        if (wts_info && wts_info->Level == 1) {\n          const bool is_locked { wts_info->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_LOCK };\n          const auto session_flags = wts_info->Data.WTSInfoExLevel1.SessionFlags;\n          \n          BOOST_LOG(info) << \"Session lock check - Locked: \" << is_locked \n                           << \", SessionFlags: \" << session_flags \n                           << \" (LOCK=\" << WTS_SESSIONSTATE_LOCK \n                           << \", UNLOCK=\" << WTS_SESSIONSTATE_UNLOCK << \")\";\n          return is_locked;\n        }\n      }\n\n      BOOST_LOG(warning) << \"Failed to get session info in is_user_session_locked.\";\n    }\n    else {\n      BOOST_LOG(error) << get_error_string(GetLastError()) << \" failed while calling WTSQuerySessionInformationW!\";\n    }\n\n    return false;\n  }\n\n  bool\n  test_no_access_to_ccd_api() {\n    auto display_data { query_display_config(w_utils::ACTIVE_ONLY_DEVICES) };\n    if (!display_data) {\n      BOOST_LOG(debug) << \"test_no_access_to_ccd_api failed in query_display_config.\";\n      return true;\n    }\n\n    // No active displays (e.g. headless machine) - can't test CCD access without paths\n    if (display_data->paths.empty()) {\n      BOOST_LOG(debug) << \"test_no_access_to_ccd_api: no active display paths, assuming no access.\";\n      return true;\n    }\n\n    // Here we are supplying the retrieved display data back to SetDisplayConfig (with VALIDATE flag only, so that we make no actual changes).\n    // Unless something is really broken on Windows, this call should never fail under normal circumstances - the configuration is 100% correct, since it was\n    // provided by Windows.\n    const UINT32 flags { SDC_VALIDATE | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_VIRTUAL_MODE_AWARE };\n    const LONG result { SetDisplayConfig(display_data->paths.size(), display_data->paths.data(), display_data->modes.size(), display_data->modes.data(), flags) };\n\n    BOOST_LOG(debug) << \"test_no_access_to_ccd_api result: \" << get_error_string(result);\n    return result == ERROR_ACCESS_DENIED;\n  }\n\n  bool\n  togglePnpDeviceByFriendlyName(const std::string &friendlyName, const bool &stat) {\n    HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, NULL, NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES);\n    if (deviceInfoSet == INVALID_HANDLE_VALUE) {\n      std::cerr << \"Failed to get device information set.\" << std::endl;\n      return false;\n    }\n\n    SP_DEVINFO_DATA deviceInfoData;\n    deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);\n\n    bool deviceFound = false;\n    for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) {\n      TCHAR deviceFriendlyName[256];\n      if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_FRIENDLYNAME, NULL, (PBYTE) deviceFriendlyName, sizeof(deviceFriendlyName), NULL)) {\n        if (friendlyName == deviceFriendlyName) {\n          deviceFound = true;\n          break;\n        }\n      }\n    }\n\n    if (!deviceFound) {\n      std::cerr << \"Device not found.\" << std::endl;\n      SetupDiDestroyDeviceInfoList(deviceInfoSet);\n      return false;\n    }\n\n    SP_PROPCHANGE_PARAMS propChangeParams;\n    propChangeParams.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);\n    propChangeParams.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;\n    propChangeParams.StateChange = stat ? DICS_ENABLE : DICS_DISABLE;\n    propChangeParams.Scope = DICS_FLAG_GLOBAL;\n    propChangeParams.HwProfile = 0;\n\n    if (!SetupDiSetClassInstallParams(deviceInfoSet, &deviceInfoData, &propChangeParams.ClassInstallHeader, sizeof(propChangeParams)) ||\n        !SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, deviceInfoSet, &deviceInfoData)) {\n      std::cerr << \"Failed to toggle device.\" << std::endl;\n      SetupDiDestroyDeviceInfoList(deviceInfoSet);\n      return false;\n    }\n\n    SetupDiDestroyDeviceInfoList(deviceInfoSet);\n    return true;\n  }\n  \n  // 检测RDP会话\n  bool is_any_rdp_session_active() {\n    PWTS_SESSION_INFO pSessionInfo = nullptr;\n    DWORD sessionCount = 0;\n\n    if (!WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &sessionCount)) {\n      BOOST_LOG(warning) << \"[Check_RDP_Session] WTSEnumerateSessions failed: \" << GetLastError();\n      return false; // 异常默认返回false\n    }\n    \n    BOOST_LOG(debug) << \"[Check_RDP_Session] Checking \" << sessionCount << \" sessions\";\n    \n    // 遍历所有会话，检测是否有rdp会话\n    for (DWORD i = 0; i < sessionCount; ++i) {\n      WTS_SESSION_INFO si = pSessionInfo[i];\n\n      if (si.State != WTSActive){\n        continue;\n      }\n\n      LPTSTR buffer = nullptr;\n      DWORD bytesReturned = 0;\n\n      if (WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, si.SessionId, WTSClientProtocolType, &buffer, &bytesReturned)) {\n        USHORT protocolType = *(USHORT*)buffer;\n        WTSFreeMemory(buffer);\n        \n        BOOST_LOG(debug) << \"[Check_RDP_Session] Session \" << si.SessionId \n                         << \" - State: \" << si.State \n                         << \", Protocol: \" << protocolType \n                         << \" (Console=0, RDP=2)\";\n        \n        if (protocolType == 2) { // RDP 协议\n          BOOST_LOG(info) << \"[Check_RDP_Session] Active RDP session detected, session ID = \" << si.SessionId;\n          WTSFreeMemory(pSessionInfo);\n          return true;\n        }\n      } else {\n        BOOST_LOG(warning) << \"[Check_RDP_Session] WTSQuerySessionInformation failed for session \" << si.SessionId << \", error = \" << GetLastError();\n      }\n    }\n    WTSFreeMemory(pSessionInfo);\n    BOOST_LOG(debug) << \"[Check_RDP_Session] No active RDP session found.\";\n    return false;\n  }\n\n  bool\n  rotate_display(int angle, const std::string &display_name) {\n    // Validate angle: 0, 90, 180, 270\n    if (angle != 0 && angle != 90 && angle != 180 && angle != 270) {\n      BOOST_LOG(error) << \"Invalid rotation angle: \" << angle << \". Must be 0, 90, 180, or 270\";\n      return false;\n    }\n\n    // Convert angle to Windows display orientation\n    static const DWORD angle_to_orientation[] = { DMDO_DEFAULT, DMDO_90, DMDO_180, DMDO_270 };\n    DWORD new_orientation = angle_to_orientation[angle / 90];\n\n    // Convert display name to wide string\n    std::wstring wdisplay_name;\n    LPCWSTR device_name = nullptr;\n    if (!display_name.empty()) {\n      wdisplay_name = std::wstring(display_name.begin(), display_name.end());\n      device_name = wdisplay_name.c_str();\n    }\n\n    const std::string target = display_name.empty() ? \"primary display\" : display_name;\n\n    // Get current display settings\n    DEVMODEW dm = {};\n    dm.dmSize = sizeof(DEVMODEW);\n    \n    if (!EnumDisplaySettingsW(device_name, ENUM_CURRENT_SETTINGS, &dm)) {\n      BOOST_LOG(error) << \"Failed to get current display settings for \" << target\n                       << \", error: \" << GetLastError();\n      return false;\n    }\n\n    BOOST_LOG(debug) << \"Current display settings: \" \n                     << dm.dmPelsWidth << \"x\" << dm.dmPelsHeight \n                     << \", orientation: \" << dm.dmDisplayOrientation;\n\n    DWORD current_orientation = dm.dmDisplayOrientation;\n\n    // Swap width/height if transitioning between landscape and portrait\n    bool current_is_portrait = (current_orientation == DMDO_90 || current_orientation == DMDO_270);\n    bool new_is_portrait = (new_orientation == DMDO_90 || new_orientation == DMDO_270);\n    \n    if (current_is_portrait != new_is_portrait) {\n      std::swap(dm.dmPelsWidth, dm.dmPelsHeight);\n      BOOST_LOG(debug) << \"Swapping dimensions to: \" << dm.dmPelsWidth << \"x\" << dm.dmPelsHeight;\n    }\n\n    dm.dmDisplayOrientation = new_orientation;\n    dm.dmFields = DM_DISPLAYORIENTATION | DM_PELSWIDTH | DM_PELSHEIGHT;\n\n    // Test if settings are valid\n    if (ChangeDisplaySettingsExW(device_name, &dm, nullptr, CDS_TEST, nullptr) != DISP_CHANGE_SUCCESSFUL) {\n      BOOST_LOG(error) << \"Display rotation test failed for \" << target;\n      return false;\n    }\n\n    // Apply settings\n    LONG result = ChangeDisplaySettingsExW(device_name, &dm, nullptr, CDS_UPDATEREGISTRY, nullptr);\n\n    if (result == DISP_CHANGE_SUCCESSFUL) {\n      BOOST_LOG(info) << \"Display rotation changed to \" << angle << \" degrees for \" << target;\n      return true;\n    }\n\n    BOOST_LOG(error) << \"Failed to change display rotation for \" << target \n                     << \": error code \" << result;\n    return false;\n  }\n}  // namespace display_device::w_utils\n"
  },
  {
    "path": "src/platform/windows/display_device/windows_utils.h",
    "content": "#pragma once\n\n// the most stupid windows include (because it needs to be first...)\n#include <windows.h>\n\n// local includes\n#include \"src/display_device/display_device.h\"\n\nnamespace display_device::w_utils {\n\n  constexpr bool ACTIVE_ONLY_DEVICES { true }; /**< The device path must be active. */\n  constexpr bool ALL_DEVICES { false }; /**< The device path can be active or inactive. */\n\n  /**\n   * @brief Contains currently available paths and associated modes.\n   */\n  struct path_and_mode_data_t {\n    std::vector<DISPLAYCONFIG_PATH_INFO> paths; /**< Available display paths. */\n    std::vector<DISPLAYCONFIG_MODE_INFO> modes; /**< Display modes for ACTIVE displays. */\n  };\n\n  /**\n   * @brief Contains the device path and the id for a VALID device.\n   * @see get_device_info_for_valid_path for what is considered a valid device.\n   * @see get_device_id for how we make the device id.\n   */\n  struct device_info_t {\n    std::string device_path; /**< Unique device path string. */\n    std::string device_id; /**< A device id (made up by us) that is identifies the device. */\n  };\n\n  /**\n   * @brief Stringify the error code from Windows API.\n   * @param error_code Error code to stringify.\n   * @returns String containing the error code in a readable format + a system message describing the code.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::string error_message = get_error_string(ERROR_NOT_SUPPORTED);\n   * ```\n   */\n  std::string\n  get_error_string(LONG error_code);\n\n  /**\n   * @brief Check if the display's source mode is primary - if the associated device is a primary display device.\n   * @param mode Mode to check.\n   * @returns True if the mode's origin point is at (0, 0) coordinate (primary), false otherwise.\n   * @note It is possible to have multiple primary source modes at the same time.\n   * @see get_source_mode on how to get the source mode.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_SOURCE_MODE mode;\n   * const bool is_primary = is_primary(mode);\n   * ```\n   */\n  bool\n  is_primary(const DISPLAYCONFIG_SOURCE_MODE &mode);\n\n  /**\n   * @brief Check if the source modes are duplicated (cloned).\n   * @param mode_a First mode to check.\n   * @param mode_b Second mode to check.\n   * @returns True if both mode have the same origin point, false otherwise.\n   * @note Windows enforces the behaviour that only the duplicate devices can\n   *       have the same origin point as otherwise the configuration is considered invalid by the OS.\n   * @see get_source_mode on how to get the source mode.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_SOURCE_MODE mode_a;\n   * DISPLAYCONFIG_SOURCE_MODE mode_b;\n   * const bool are_duplicated = are_modes_duplicated(mode_a, mode_b);\n   * ```\n   */\n  bool\n  are_modes_duplicated(const DISPLAYCONFIG_SOURCE_MODE &mode_a, const DISPLAYCONFIG_SOURCE_MODE &mode_b);\n\n  /**\n   * @brief Check if the display device path's target is available.\n   *\n   * In most cases this this would mean physically connected to the system,\n   * but it also possible force the path to persist. It is not clear if it be\n   * counted as available or not.\n   *\n   * @param path Path to check.\n   * @returns True if path's target is marked as available, false otherwise.\n   * @see query_display_config on how to get paths from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * const bool available = is_available(path);\n   * ```\n   */\n  bool\n  is_available(const DISPLAYCONFIG_PATH_INFO &path);\n\n  /**\n   * @brief Check if the display device path is marked as active.\n   * @param path Path to check.\n   * @returns True if path is marked as active, false otherwise.\n   * @see query_display_config on how to get paths from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * const bool active = is_active(path);\n   * ```\n   */\n  bool\n  is_active(const DISPLAYCONFIG_PATH_INFO &path);\n\n  /**\n   * @brief Mark the display device path as active.\n   * @param path Path to mark.\n   * @see query_display_config on how to get paths from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * if (!is_active(path)) {\n   *   set_active(path);\n   * }\n   * ```\n   */\n  void\n  set_active(DISPLAYCONFIG_PATH_INFO &path);\n\n  /**\n   * @brief Get a stable and persistent device id for the path.\n   *\n   * This function tries to generate a unique id for the path that\n   * is persistent between driver re-installs and physical unplugging and\n   * replugging of the device.\n   *\n   * The best candidate for it could have been a \"ContainerID\" from the\n   * registry, however it was found to be unstable for the virtual display\n   * (probably because it uses the EDID for the id generation and the current\n   * virtual displays have incomplete EDID information). The \"ContainerID\"\n   * also does not change if the physical device is plugged into a different\n   * port and seems to be very stable, however because of virtual displays\n   * other solution was used.\n   *\n   * The accepted solution was to use the \"InstanceID\" and EDID (just to be\n   * on the safe side). \"InstanceID\" is semi-stable, it has some parts that\n   * change between driver re-installs and it has a part that changes based\n   * on the GPU port that the display is connected to. It is most likely to\n   * be unique, but since the MS documentation is lacking we are also hashing\n   * EDID information (contains serial ids, timestamps and etc. that should\n   * guarantee that identical displays are differentiated like with the\n   * \"ContainerID\"). Most importantly this information is stable for the virtual\n   * displays.\n   *\n   * After we remove the unstable parts from the \"InstanceID\" and hash everything\n   * together, we get an id that changes only when you connect the display to\n   * a different GPU port which seems to be acceptable.\n   *\n   * As a fallback we are using a hashed device path, in case the \"InstanceID\" or\n   * EDID is not available. At least if you don't do driver re-installs often\n   * and change the GPU ports, it will be stable for a while.\n   *\n   * @param path Path to get the device id for.\n   * @returns Device id, or an empty string if it could not be generated.\n   * @see query_display_config on how to get paths from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * const std::string device_path = get_device_id(path);\n   * ```\n   */\n  std::string\n  get_device_id(const DISPLAYCONFIG_PATH_INFO &path);\n\n  std::string\n  get_device_driver_path(const DISPLAYCONFIG_PATH_INFO &path);\n\n  /**\n   * @brief Get a string that represents a path from the adapter to the display target.\n   * @param path Path to get the string for.\n   * @returns String representation, or an empty string if it's not available.\n   * @see query_display_config on how to get paths from the system.\n   * @note In the rest of the code we refer to this string representation simply as the \"device path\".\n   *       It is used as a simple way of grouping related path objects together and removing \"bad\" paths\n   *       that don't have such string representation.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * const std::string device_path = get_monitor_device_path(path);\n   * ```\n   */\n  std::string\n  get_monitor_device_path(const DISPLAYCONFIG_PATH_INFO &path);\n\n  /**\n   * @brief Get the user friendly name for the path.\n   * @param path Path to get user friendly name for.\n   * @returns User friendly name for the path if available, empty string otherwise.\n   * @see query_display_config on how to get paths from the system.\n   * @note This is usually a monitor name (like \"ROG PG279Q\") and is most likely take from EDID.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * const std::string friendly_name = get_friendly_name(path);\n   * ```\n   */\n  std::string\n  get_friendly_name(const DISPLAYCONFIG_PATH_INFO &path);\n\n  /**\n   * @brief Get the logical display name for the path.\n   *\n   * These are the \"\\\\\\\\.\\\\DISPLAY1\", \"\\\\\\\\.\\\\DISPLAY2\" and etc. display names that can\n   * change whenever Windows wants to change them.\n   *\n   * @param path Path to get user display name for.\n   * @returns Display name for the path if available, empty string otherwise.\n   * @see query_display_config on how to get paths from the system.\n   * @note Inactive paths can have these names already assigned to them, even\n   *       though they are not even in use! There can also be duplicates.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * const std::string display_name = get_display_name(path);\n   * ```\n   */\n  std::string\n  get_display_name(const DISPLAYCONFIG_PATH_INFO &path);\n\n  /**\n   * @brief Get the HDR state the path.\n   * @param path Path to get HDR state for.\n   * @returns hdr_state_e::unknown if the state could not be retrieved, or other enum values describing the state otherwise.\n   * @see query_display_config on how to get paths from the system.\n   * @see hdr_state_e\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * const auto hdr_state = get_hdr_state(path);\n   * ```\n   */\n  hdr_state_e\n  get_hdr_state(const DISPLAYCONFIG_PATH_INFO &path);\n\n  /**\n   * @brief Set the HDR state for the path.\n   * @param path Path to set HDR state for.\n   * @param enable Specify whether to enable or disable HDR state.\n   * @returns True if new HDR state was set, false otherwise.\n   * @see query_display_config on how to get paths from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * const bool success = set_hdr_state(path, false);\n   * ```\n   */\n  bool\n  set_hdr_state(const DISPLAYCONFIG_PATH_INFO &path, bool enable);\n\n  /**\n   * @brief Get the source mode index from the path.\n   *\n   * It performs sanity checks on the modes list that the index is indeed correct.\n   *\n   * @param path Path to get the source mode index for.\n   * @param modes A list of various modes (source, target, desktop and probably more in the future).\n   * @returns Valid index value if it's found in the modes list and the mode at that index is of a type \"source\" mode,\n   *          empty optional otherwise.\n   * @see query_display_config on how to get paths and modes from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * std::vector<DISPLAYCONFIG_MODE_INFO> modes;\n   * const auto source_index = get_source_index(path, modes);\n   * ```\n   */\n  boost::optional<UINT32>\n  get_source_index(const DISPLAYCONFIG_PATH_INFO &path, const std::vector<DISPLAYCONFIG_MODE_INFO> &modes);\n\n  /**\n   * @brief Set the source mode index in the path.\n   * @param path Path to modify.\n   * @param index Index value to set or empty optional to mark the index as invalid.\n   * @see query_display_config on how to get paths and modes from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * set_source_index(path, 5);\n   * set_source_index(path, boost::none);\n   * ```\n   */\n  void\n  set_source_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional<UINT32> &index);\n\n  /**\n   * @brief Set the target mode index in the path.\n   * @param path Path to modify.\n   * @param index Index value to set or empty optional to mark the index as invalid.\n   * @see query_display_config on how to get paths and modes from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * set_target_index(path, 5);\n   * set_target_index(path, boost::none);\n   * ```\n   */\n  void\n  set_target_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional<UINT32> &index);\n\n  /**\n   * @brief Set the desktop mode index in the path.\n   * @param path Path to modify.\n   * @param index Index value to set or empty optional to mark the index as invalid.\n   * @see query_display_config on how to get paths and modes from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * set_desktop_index(path, 5);\n   * set_desktop_index(path, boost::none);\n   * ```\n   */\n  void\n  set_desktop_index(DISPLAYCONFIG_PATH_INFO &path, const boost::optional<UINT32> &index);\n\n  /**\n   * @brief Set the clone group id in the path.\n   * @param path Path to modify.\n   * @param id Id value to set or empty optional to mark the id as invalid.\n   * @see query_display_config on how to get paths and modes from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * set_clone_group_id(path, 5);\n   * set_clone_group_id(path, boost::none);\n   * ```\n   */\n  void\n  set_clone_group_id(DISPLAYCONFIG_PATH_INFO &path, const boost::optional<UINT32> &id);\n\n  /**\n   * @brief Get the source mode from the list at the specified index.\n   *\n   * This function does additional sanity checks for the modes list and ensures\n   * that the mode at the specified index is indeed a source mode.\n   *\n   * @param index Index to get the mode for. It is of boost::optional type\n   *              as the function is intended to be used with get_source_index function.\n   * @param modes List to get the mode from.\n   * @returns A pointer to a valid source mode from to list at the specified index, nullptr otherwise.\n   * @see query_display_config on how to get paths and modes from the system.\n   * @see get_source_index\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * const std::vector<DISPLAYCONFIG_MODE_INFO> modes;\n   * const DISPLAYCONFIG_SOURCE_MODE* source_mode = get_source_mode(get_source_index(path, modes), modes);\n   * ```\n   */\n  const DISPLAYCONFIG_SOURCE_MODE *\n  get_source_mode(const boost::optional<UINT32> &index, const std::vector<DISPLAYCONFIG_MODE_INFO> &modes);\n\n  /**\n   * @brief Get the source mode from the list at the specified index.\n   *\n   * This function does additional sanity checks for the modes list and ensures\n   * that the mode at the specified index is indeed a source mode.\n   *\n   * @param index Index to get the mode for. It is of boost::optional type\n   *              as the function is intended to be used with get_source_index function.\n   * @param modes List to get the mode from.\n   * @returns A pointer to a valid source mode from to list at the specified index, nullptr otherwise.\n   * @see query_display_config on how to get paths and modes from the system.\n   * @see get_source_index\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * std::vector<DISPLAYCONFIG_MODE_INFO> modes;\n   * DISPLAYCONFIG_SOURCE_MODE* source_mode = get_source_mode(get_source_index(path, modes), modes);\n   * ```\n   */\n  DISPLAYCONFIG_SOURCE_MODE *\n  get_source_mode(const boost::optional<UINT32> &index, std::vector<DISPLAYCONFIG_MODE_INFO> &modes);\n\n  /**\n   * @brief Validate the path and get the commonly used information from it.\n   *\n   * This a convenience function to ensure that our concept of \"valid path\" remains the\n   * same throughout the code.\n   *\n   * Currently, for use, a valid path is:\n   *   - a path with and available display target;\n   *   - a path that is active (optional);\n   *   - a path that has a non-empty device path;\n   *   - a path that has a non-empty device id;\n   *   - a path that has a non-empty device name assigned.\n   *\n   * @param path Path to validate and get info for.\n   * @param must_be_active Optionally request that the valid path must also be active.\n   * @returns Commonly used info for the path, or empty optional if the path is invalid.\n   * @see query_display_config on how to get paths and modes from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * DISPLAYCONFIG_PATH_INFO path;\n   * const auto device_info = get_device_info_for_valid_path(path, true);\n   * ```\n   */\n  boost::optional<device_info_t>\n  get_device_info_for_valid_path(const DISPLAYCONFIG_PATH_INFO &path, bool must_be_active);\n\n  /**\n   * @brief Query Windows for the device paths and associated modes.\n   * @param active_only Specify to query for active devices only.\n   * @returns Data containing paths and modes, empty optional if we have failed to query.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const auto display_data = query_display_config(true);\n   * ```\n   */\n  boost::optional<path_and_mode_data_t>\n  query_display_config(bool active_only);\n\n  /**\n   * @brief Get the active path matching the device id.\n   * @param device_id Id to search for in the the list.\n   * @param paths List to be searched.\n   * @returns A pointer to an active path matching our id, nullptr otherwise.\n   * @see query_display_config on how to get paths and modes from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const std::vector<DISPLAYCONFIG_PATH_INFO> paths;\n   * const DISPLAYCONFIG_PATH_INFO* active_path = get_active_path(\"MY_DEVICE_ID\", paths);\n   * ```\n   */\n  const DISPLAYCONFIG_PATH_INFO *\n  get_active_path(const std::string &device_id, const std::vector<DISPLAYCONFIG_PATH_INFO> &paths);\n\n  /**\n   * @brief Get the active path matching the device id.\n   * @param device_id Id to search for in the the list.\n   * @param paths List to be searched.\n   * @returns A pointer to an active path matching our id, nullptr otherwise.\n   * @see query_display_config on how to get paths and modes from the system.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * std::vector<DISPLAYCONFIG_PATH_INFO> paths;\n   * DISPLAYCONFIG_PATH_INFO* active_path = get_active_path(\"MY_DEVICE_ID\", paths);\n   * ```\n   */\n  DISPLAYCONFIG_PATH_INFO *\n  get_active_path(const std::string &device_id, std::vector<DISPLAYCONFIG_PATH_INFO> &paths);\n\n  /**\n   * @brief Check whether the user session is locked.\n   * @returns True if it's definitely known that the session is locked, false otherwise.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const bool is_locked { is_user_session_locked() };\n   * ```\n   */\n  bool\n  is_user_session_locked();\n\n  /**\n   * @brief Check whether it is already known that the CCD API will fail to set settings.\n   * @returns True if we already known we don't have access (for now), false otherwise.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const bool no_access { test_no_access_to_ccd_api() };\n   * ```\n   */\n  bool\n  test_no_access_to_ccd_api();\n\n  bool\n  togglePnpDeviceByFriendlyName(const std::string &friendlyName, const bool &stat);\n\n  /**\n   * @brief Check whether any RDP session is active.\n   * @returns True if it's detected that any RDP session is active, false otherwise.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const bool is_rdp { is_any_rdp_session_active() };\n   * ```\n   */\n  bool\n  is_any_rdp_session_active();\n\n  /**\n   * @brief Rotate the display to the specified angle.\n   * @param angle Rotation angle in degrees. Must be 0, 90, 180, or 270.\n   * @param display_name Optional display name. If empty, rotates the primary display.\n   * @returns True if rotation was successful, false otherwise.\n   *\n   * EXAMPLES:\n   * ```cpp\n   * const bool success = rotate_display(90, \"\");\n   * const bool success2 = rotate_display(180, \"\\\\\\\\.\\\\DISPLAY1\");\n   * ```\n   */\n  bool\n  rotate_display(int angle, const std::string &display_name);\n}  // namespace display_device::w_utils\n"
  },
  {
    "path": "src/platform/windows/display_ram.cpp",
    "content": "/**\n * @file src/platform/windows/display_ram.cpp\n * @brief Definitions for handling ram.\n */\n#include \"display.h\"\n\n#include \"misc.h\"\n#include \"src/logging.h\"\n\nnamespace platf {\n  using namespace std::literals;\n}\n\nnamespace platf::dxgi {\n  struct img_t: public ::platf::img_t {\n    ~img_t() override {\n      delete[] data;\n      data = nullptr;\n    }\n  };\n\n  void\n  blend_cursor_monochrome(const cursor_t &cursor, img_t &img) {\n    int height = cursor.shape_info.Height / 2;\n    int width = cursor.shape_info.Width;\n    int pitch = cursor.shape_info.Pitch;\n\n    // img cursor.{x,y} < 0, skip parts of the cursor.img_data\n    auto cursor_skip_y = -std::min(0, cursor.y);\n    auto cursor_skip_x = -std::min(0, cursor.x);\n\n    // img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data\n    auto cursor_truncate_y = std::max(0, cursor.y - img.height);\n    auto cursor_truncate_x = std::max(0, cursor.x - img.width);\n\n    auto cursor_width = width - cursor_skip_x - cursor_truncate_x;\n    auto cursor_height = height - cursor_skip_y - cursor_truncate_y;\n\n    if (cursor_height > height || cursor_width > width) {\n      return;\n    }\n\n    auto img_skip_y = std::max(0, cursor.y);\n    auto img_skip_x = std::max(0, cursor.x);\n\n    auto cursor_img_data = cursor.img_data.data() + cursor_skip_y * pitch;\n\n    int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y));\n    int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x));\n\n    auto pixels_per_byte = width / pitch;\n    auto bytes_per_row = delta_width / pixels_per_byte;\n\n    auto img_data = (int *) img.data;\n    for (int i = 0; i < delta_height; ++i) {\n      auto and_mask = &cursor_img_data[i * pitch];\n      auto xor_mask = &cursor_img_data[(i + height) * pitch];\n\n      auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x];\n\n      auto skip_x = cursor_skip_x;\n      for (int x = 0; x < bytes_per_row; ++x) {\n        for (auto bit = 0u; bit < 8; ++bit) {\n          if (skip_x > 0) {\n            --skip_x;\n\n            continue;\n          }\n\n          int and_ = *and_mask & (1 << (7 - bit)) ? -1 : 0;\n          int xor_ = *xor_mask & (1 << (7 - bit)) ? -1 : 0;\n\n          *img_pixel_p &= and_;\n          *img_pixel_p ^= xor_;\n\n          ++img_pixel_p;\n        }\n\n        ++and_mask;\n        ++xor_mask;\n      }\n    }\n  }\n\n  void\n  apply_color_alpha(int *img_pixel_p, int cursor_pixel) {\n    auto colors_out = (std::uint8_t *) &cursor_pixel;\n    auto colors_in = (std::uint8_t *) img_pixel_p;\n\n    // TODO: When use of IDXGIOutput5 is implemented, support different color formats\n    auto alpha = colors_out[3];\n    if (alpha == 255) {\n      *img_pixel_p = cursor_pixel;\n    }\n    else {\n      colors_in[0] = colors_out[0] + (colors_in[0] * (255 - alpha) + 255 / 2) / 255;\n      colors_in[1] = colors_out[1] + (colors_in[1] * (255 - alpha) + 255 / 2) / 255;\n      colors_in[2] = colors_out[2] + (colors_in[2] * (255 - alpha) + 255 / 2) / 255;\n    }\n  }\n\n  void\n  apply_color_masked(int *img_pixel_p, int cursor_pixel) {\n    // TODO: When use of IDXGIOutput5 is implemented, support different color formats\n    auto alpha = ((std::uint8_t *) &cursor_pixel)[3];\n    if (alpha == 0xFF) {\n      *img_pixel_p ^= cursor_pixel;\n    }\n    else {\n      *img_pixel_p = cursor_pixel;\n    }\n  }\n\n  void\n  blend_cursor_color(const cursor_t &cursor, img_t &img, const bool masked) {\n    int height = cursor.shape_info.Height;\n    int width = cursor.shape_info.Width;\n    int pitch = cursor.shape_info.Pitch;\n\n    // img cursor.y < 0, skip parts of the cursor.img_data\n    auto cursor_skip_y = -std::min(0, cursor.y);\n    auto cursor_skip_x = -std::min(0, cursor.x);\n\n    // img cursor.{x,y} > img.{x,y}, truncate parts of the cursor.img_data\n    auto cursor_truncate_y = std::max(0, cursor.y - img.height);\n    auto cursor_truncate_x = std::max(0, cursor.x - img.width);\n\n    auto img_skip_y = std::max(0, cursor.y);\n    auto img_skip_x = std::max(0, cursor.x);\n\n    auto cursor_width = width - cursor_skip_x - cursor_truncate_x;\n    auto cursor_height = height - cursor_skip_y - cursor_truncate_y;\n\n    if (cursor_height > height || cursor_width > width) {\n      return;\n    }\n\n    auto cursor_img_data = (int *) &cursor.img_data[cursor_skip_y * pitch];\n\n    int delta_height = std::min(cursor_height - cursor_truncate_y, std::max(0, img.height - img_skip_y));\n    int delta_width = std::min(cursor_width - cursor_truncate_x, std::max(0, img.width - img_skip_x));\n\n    auto img_data = (int *) img.data;\n\n    for (int i = 0; i < delta_height; ++i) {\n      auto cursor_begin = &cursor_img_data[i * cursor.shape_info.Width + cursor_skip_x];\n      auto cursor_end = &cursor_begin[delta_width];\n\n      auto img_pixel_p = &img_data[(i + img_skip_y) * (img.row_pitch / img.pixel_pitch) + img_skip_x];\n      std::for_each(cursor_begin, cursor_end, [&](int cursor_pixel) {\n        if (masked) {\n          apply_color_masked(img_pixel_p, cursor_pixel);\n        }\n        else {\n          apply_color_alpha(img_pixel_p, cursor_pixel);\n        }\n        ++img_pixel_p;\n      });\n    }\n  }\n\n  void\n  blend_cursor(const cursor_t &cursor, img_t &img) {\n    switch (cursor.shape_info.Type) {\n      case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:\n        blend_cursor_color(cursor, img, false);\n        break;\n      case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME:\n        blend_cursor_monochrome(cursor, img);\n        break;\n      case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR:\n        blend_cursor_color(cursor, img, true);\n        break;\n      default:\n        BOOST_LOG(warning) << \"Unsupported cursor format [\"sv << cursor.shape_info.Type << ']';\n    }\n  }\n\n  capture_e\n  display_ddup_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) {\n    HRESULT status;\n    DXGI_OUTDUPL_FRAME_INFO frame_info;\n\n    resource_t::pointer res_p {};\n    auto capture_status = dup.next_frame(frame_info, timeout, &res_p);\n    resource_t res { res_p };\n\n    if (capture_status != capture_e::ok) {\n      return capture_status;\n    }\n\n    const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0;\n    const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0;\n    const bool update_flag = mouse_update_flag || frame_update_flag;\n\n    if (!update_flag) {\n      return capture_e::timeout;\n    }\n\n    std::optional<std::chrono::steady_clock::time_point> frame_timestamp;\n    if (auto qpc_displayed = std::max(frame_info.LastPresentTime.QuadPart, frame_info.LastMouseUpdateTime.QuadPart)) {\n      // Translate QueryPerformanceCounter() value to steady_clock time point\n      frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), qpc_displayed);\n    }\n\n    if (frame_info.PointerShapeBufferSize > 0) {\n      auto &img_data = cursor.img_data;\n\n      img_data.resize(frame_info.PointerShapeBufferSize);\n\n      UINT dummy;\n      status = dup.dup->GetFramePointerShape(img_data.size(), img_data.data(), &dummy, &cursor.shape_info);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to get new pointer shape [0x\"sv << util::hex(status).to_string_view() << ']';\n\n        return capture_e::error;\n      }\n    }\n\n    if (frame_info.LastMouseUpdateTime.QuadPart) {\n      cursor.x = frame_info.PointerPosition.Position.x;\n      cursor.y = frame_info.PointerPosition.Position.y;\n      cursor.visible = frame_info.PointerPosition.Visible;\n    }\n\n    if (frame_update_flag) {\n      {\n        texture2d_t src {};\n        status = res->QueryInterface(IID_ID3D11Texture2D, (void **) &src);\n\n        if (FAILED(status)) {\n          BOOST_LOG(error) << \"Couldn't query interface [0x\"sv << util::hex(status).to_string_view() << ']';\n          return capture_e::error;\n        }\n\n        D3D11_TEXTURE2D_DESC desc;\n        src->GetDesc(&desc);\n\n        // If we don't know the capture format yet, grab it from this texture and create the staging texture\n        if (capture_format == DXGI_FORMAT_UNKNOWN) {\n          capture_format = desc.Format;\n          BOOST_LOG(info) << \"Capture format [\"sv << dxgi_format_to_string(capture_format) << ']';\n\n          D3D11_TEXTURE2D_DESC t {};\n          t.Width = width;\n          t.Height = height;\n          t.MipLevels = 1;\n          t.ArraySize = 1;\n          t.SampleDesc.Count = 1;\n          t.Usage = D3D11_USAGE_STAGING;\n          t.Format = capture_format;\n          t.CPUAccessFlags = D3D11_CPU_ACCESS_READ;\n\n          auto status = device->CreateTexture2D(&t, nullptr, &texture);\n\n          if (FAILED(status)) {\n            BOOST_LOG(error) << \"Failed to create staging texture [0x\"sv << util::hex(status).to_string_view() << ']';\n            return capture_e::error;\n          }\n        }\n\n        // It's possible for our display enumeration to race with mode changes and result in\n        // mismatched image pool and desktop texture sizes. If this happens, just reinit again.\n        if (desc.Width != width || desc.Height != height) {\n          BOOST_LOG(info) << \"Capture size changed [\"sv << width << 'x' << height << \" -> \"sv << desc.Width << 'x' << desc.Height << ']';\n          return capture_e::reinit;\n        }\n\n        // It's also possible for the capture format to change on the fly. If that happens,\n        // reinitialize capture to try format detection again and create new images.\n        if (capture_format != desc.Format) {\n          BOOST_LOG(info) << \"Capture format changed [\"sv << dxgi_format_to_string(capture_format) << \" -> \"sv << dxgi_format_to_string(desc.Format) << ']';\n          return capture_e::reinit;\n        }\n\n        // Copy from GPU to CPU\n        device_ctx->CopyResource(texture.get(), src.get());\n      }\n    }\n\n    if (!pull_free_image_cb(img_out)) {\n      return capture_e::interrupted;\n    }\n    auto img = (img_t *) img_out.get();\n\n    // If we don't know the final capture format yet, encode a dummy image\n    if (capture_format == DXGI_FORMAT_UNKNOWN) {\n      BOOST_LOG(debug) << \"Capture format is still unknown. Encoding a blank image\"sv;\n\n      if (dummy_img(img)) {\n        return capture_e::error;\n      }\n    }\n    else {\n      // Map the staging texture for CPU access (making it inaccessible for the GPU)\n      status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to map texture [0x\"sv << util::hex(status).to_string_view() << ']';\n\n        return capture_e::error;\n      }\n\n      // Now that we know the capture format, we can finish creating the image\n      if (complete_img(img, false)) {\n        device_ctx->Unmap(texture.get(), 0);\n        img_info.pData = nullptr;\n        return capture_e::error;\n      }\n\n      std::copy_n((std::uint8_t *) img_info.pData, height * img_info.RowPitch, (std::uint8_t *) img->data);\n\n      // Unmap the staging texture to allow GPU access again\n      device_ctx->Unmap(texture.get(), 0);\n      img_info.pData = nullptr;\n    }\n\n    if (cursor_visible && cursor.visible) {\n      blend_cursor(cursor, *img);\n    }\n\n    if (img) {\n      img->frame_timestamp = frame_timestamp;\n    }\n\n    return capture_e::ok;\n  }\n\n  capture_e\n  display_ddup_ram_t::release_snapshot() {\n    return dup.release_frame();\n  }\n\n  std::shared_ptr<platf::img_t>\n  display_ram_t::alloc_img() {\n    auto img = std::make_shared<img_t>();\n\n    // Initialize fields that are format-independent\n    img->width = width;\n    img->height = height;\n\n    return img;\n  }\n\n  int\n  display_ram_t::complete_img(platf::img_t *img, bool dummy) {\n    // If this is not a dummy image, we must know the format by now\n    if (!dummy && capture_format == DXGI_FORMAT_UNKNOWN) {\n      BOOST_LOG(error) << \"display_ram_t::complete_img() called with unknown capture format!\";\n      return -1;\n    }\n\n    img->pixel_pitch = get_pixel_pitch();\n\n    if (dummy && !img->row_pitch) {\n      // Assume our dummy image will have no padding\n      img->row_pitch = img->pixel_pitch * img->width;\n    }\n\n    // Reallocate the image buffer if the pitch changes\n    if (!dummy && img->row_pitch != img_info.RowPitch) {\n      img->row_pitch = img_info.RowPitch;\n      delete img->data;\n      img->data = nullptr;\n    }\n\n    if (!img->data) {\n      // Use img->height instead of height to support window capture with different dimensions\n      img->data = new std::uint8_t[img->row_pitch * img->height];\n    }\n\n    return 0;\n  }\n\n  /**\n   * @memberof platf::dxgi::display_ram_t\n   */\n  int\n  display_ram_t::dummy_img(platf::img_t *img) {\n    if (complete_img(img, true)) {\n      return -1;\n    }\n\n    std::fill_n((std::uint8_t *) img->data, height * img->row_pitch, 0);\n    return 0;\n  }\n\n  std::vector<DXGI_FORMAT>\n  display_ram_t::get_supported_capture_formats() {\n    return { DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8X8_UNORM };\n  }\n\n  int\n  display_ddup_ram_t::init(const ::video::config_t &config, const std::string &display_name) {\n    if (display_base_t::init(config, display_name) || dup.init(this, config)) {\n      return -1;\n    }\n\n    return 0;\n  }\n\n  std::unique_ptr<avcodec_encode_device_t>\n  display_ram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) {\n    return std::make_unique<avcodec_encode_device_t>();\n  }\n\n}  // namespace platf::dxgi\n"
  },
  {
    "path": "src/platform/windows/display_vram.cpp",
    "content": "/**\n * @file src/platform/windows/display_vram.cpp\n * @brief Definitions for handling video ram.\n */\n#include <cmath>\n\n#include <d3dcompiler.h>\n#include <directxmath.h>\n#include <winuser.h>\n\nextern \"C\" {\n#include <libavcodec/avcodec.h>\n#include <libavutil/hwcontext_d3d11va.h>\n}\n\n#include \"display.h\"\n#include \"misc.h\"\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/nvenc/win/nvenc_dynamic_factory.h\"\n#include \"src/amf/amf_d3d11.h\"\n#include \"src/video.h\"\n\n#include <AMF/components/DisplayCapture.h>\n#include <AMF/core/Factory.h>\n\n#include <boost/algorithm/string/predicate.hpp>\n\n#if !defined(SUNSHINE_SHADERS_DIR)  // for testing this needs to be defined in cmake as we don't do an install\n  #define SUNSHINE_SHADERS_DIR SUNSHINE_ASSETS_DIR \"/shaders/directx\"\n#endif\nnamespace platf {\n  using namespace std::literals;\n}\n\nstatic void\nfree_frame(AVFrame *frame) {\n  av_frame_free(&frame);\n}\n\nusing frame_t = util::safe_ptr<AVFrame, free_frame>;\n\nnamespace platf::dxgi {\n\n  template <class T>\n  buf_t\n  make_buffer(device_t::pointer device, const T &t) {\n    static_assert(sizeof(T) % 16 == 0, \"Buffer needs to be aligned on a 16-byte alignment\");\n\n    D3D11_BUFFER_DESC buffer_desc {\n      sizeof(T),\n      D3D11_USAGE_IMMUTABLE,\n      D3D11_BIND_CONSTANT_BUFFER\n    };\n\n    D3D11_SUBRESOURCE_DATA init_data {\n      &t\n    };\n\n    buf_t::pointer buf_p;\n    auto status = device->CreateBuffer(&buffer_desc, &init_data, &buf_p);\n    if (status) {\n      BOOST_LOG(error) << \"Failed to create buffer: [0x\"sv << util::hex(status).to_string_view() << ']';\n      return nullptr;\n    }\n\n    return buf_t { buf_p };\n  }\n\n  blend_t\n  make_blend(device_t::pointer device, bool enable, bool invert) {\n    D3D11_BLEND_DESC bdesc {};\n    auto &rt = bdesc.RenderTarget[0];\n    rt.BlendEnable = enable;\n    rt.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;\n\n    if (enable) {\n      rt.BlendOp = D3D11_BLEND_OP_ADD;\n      rt.BlendOpAlpha = D3D11_BLEND_OP_ADD;\n\n      if (invert) {\n        // Invert colors\n        rt.SrcBlend = D3D11_BLEND_INV_DEST_COLOR;\n        rt.DestBlend = D3D11_BLEND_INV_SRC_COLOR;\n      }\n      else {\n        // Regular alpha blending\n        rt.SrcBlend = D3D11_BLEND_SRC_ALPHA;\n        rt.DestBlend = D3D11_BLEND_INV_SRC_ALPHA;\n      }\n\n      rt.SrcBlendAlpha = D3D11_BLEND_ZERO;\n      rt.DestBlendAlpha = D3D11_BLEND_ZERO;\n    }\n\n    blend_t blend;\n    auto status = device->CreateBlendState(&bdesc, &blend);\n    if (status) {\n      BOOST_LOG(error) << \"Failed to create blend state: [0x\"sv << util::hex(status).to_string_view() << ']';\n      return nullptr;\n    }\n\n    return blend;\n  }\n\n  blob_t convert_yuv420_packed_uv_type0_ps_hlsl;\n  blob_t convert_yuv420_packed_uv_type0_ps_linear_hlsl;\n  blob_t convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl;\n  blob_t convert_yuv420_packed_uv_type0_ps_hybrid_log_gamma_hlsl;\n  blob_t convert_yuv420_packed_uv_type0_vs_hlsl;\n  blob_t convert_yuv420_packed_uv_type0s_ps_hlsl;\n  blob_t convert_yuv420_packed_uv_type0s_ps_linear_hlsl;\n  blob_t convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer_hlsl;\n  blob_t convert_yuv420_packed_uv_type0s_ps_hybrid_log_gamma_hlsl;\n  blob_t convert_yuv420_packed_uv_type0s_vs_hlsl;\n  blob_t convert_yuv420_packed_uv_bicubic_ps_hlsl;\n  blob_t convert_yuv420_packed_uv_bicubic_ps_linear_hlsl;\n  blob_t convert_yuv420_packed_uv_bicubic_ps_perceptual_quantizer_hlsl;\n  blob_t convert_yuv420_packed_uv_bicubic_ps_hybrid_log_gamma_hlsl;\n  blob_t convert_yuv420_packed_uv_bicubic_vs_hlsl;\n  blob_t convert_yuv420_planar_y_ps_hlsl;\n  blob_t convert_yuv420_planar_y_ps_linear_hlsl;\n  blob_t convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl;\n  blob_t convert_yuv420_planar_y_ps_hybrid_log_gamma_hlsl;\n  blob_t convert_yuv420_planar_y_vs_hlsl;\n  blob_t convert_yuv420_planar_y_bicubic_ps_hlsl;\n  blob_t convert_yuv420_planar_y_bicubic_ps_linear_hlsl;\n  blob_t convert_yuv420_planar_y_bicubic_ps_perceptual_quantizer_hlsl;\n  blob_t convert_yuv420_planar_y_bicubic_ps_hybrid_log_gamma_hlsl;\n  blob_t convert_yuv444_packed_ayuv_ps_hlsl;\n  blob_t convert_yuv444_packed_ayuv_ps_linear_hlsl;\n  blob_t convert_yuv444_packed_vs_hlsl;\n  blob_t convert_yuv444_planar_ps_hlsl;\n  blob_t convert_yuv444_planar_ps_linear_hlsl;\n  blob_t convert_yuv444_planar_ps_perceptual_quantizer_hlsl;\n  blob_t convert_yuv444_planar_ps_hybrid_log_gamma_hlsl;\n  blob_t convert_yuv444_packed_y410_ps_hlsl;\n  blob_t convert_yuv444_packed_y410_ps_linear_hlsl;\n  blob_t convert_yuv444_packed_y410_ps_perceptual_quantizer_hlsl;\n  blob_t convert_yuv444_packed_y410_ps_hybrid_log_gamma_hlsl;\n  blob_t convert_yuv444_planar_vs_hlsl;\n  blob_t cursor_ps_hlsl;\n  blob_t cursor_ps_normalize_white_hlsl;\n  blob_t cursor_vs_hlsl;\n  blob_t simple_cursor_vs_hlsl;\n  blob_t simple_cursor_ps_hlsl;\n  blob_t hdr_luminance_analysis_cs_hlsl;\n  blob_t hdr_luminance_reduce_cs_hlsl;\n\n  struct img_d3d_t: public platf::img_t {\n    // These objects are owned by the display_t's ID3D11Device\n    texture2d_t capture_texture;\n    render_target_t capture_rt;\n    keyed_mutex_t capture_mutex;\n\n    // This is the shared handle used by hwdevice_t to open capture_texture\n    HANDLE encoder_texture_handle = {};\n\n    // Set to true if the image corresponds to a dummy texture used prior to\n    // the first successful capture of a desktop frame\n    bool dummy = false;\n\n    // Set to true if the image is blank (contains no content at all, including a cursor)\n    bool blank = true;\n\n    // Unique identifier for this image\n    uint32_t id = 0;\n\n    // DXGI format of this image texture\n    DXGI_FORMAT format;\n\n    // Whether this image's pixel data is in linear gamma (scRGB G10).\n    // When true, the conversion shader must apply sRGB transfer function.\n    // When false, the data already has sRGB gamma and should be used as-is.\n    bool linear_gamma = false;\n\n    virtual ~img_d3d_t() override {\n      if (encoder_texture_handle) {\n        CloseHandle(encoder_texture_handle);\n      }\n    };\n  };\n\n  struct texture_lock_helper {\n    keyed_mutex_t _mutex;\n    bool _locked = false;\n\n    texture_lock_helper(const texture_lock_helper &) = delete;\n    texture_lock_helper &\n    operator=(const texture_lock_helper &) = delete;\n\n    texture_lock_helper(texture_lock_helper &&other) {\n      _mutex.reset(other._mutex.release());\n      _locked = other._locked;\n      other._locked = false;\n    }\n\n    texture_lock_helper &\n    operator=(texture_lock_helper &&other) {\n      if (_locked) _mutex->ReleaseSync(0);\n      _mutex.reset(other._mutex.release());\n      _locked = other._locked;\n      other._locked = false;\n      return *this;\n    }\n\n    texture_lock_helper(IDXGIKeyedMutex *mutex):\n        _mutex(mutex) {\n      if (_mutex) _mutex->AddRef();\n    }\n\n    ~texture_lock_helper() {\n      if (_locked) _mutex->ReleaseSync(0);\n    }\n\n    bool\n    lock() {\n      if (_locked) return true;\n      HRESULT status = _mutex->AcquireSync(0, INFINITE);\n      if (status == S_OK) {\n        _locked = true;\n      }\n      else {\n        BOOST_LOG(error) << \"Failed to acquire texture mutex [0x\"sv << util::hex(status).to_string_view() << ']';\n      }\n      return _locked;\n    }\n  };\n\n  util::buffer_t<std::uint8_t>\n  make_cursor_xor_image(const util::buffer_t<std::uint8_t> &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) {\n    constexpr std::uint32_t inverted = 0xFFFFFFFF;\n    constexpr std::uint32_t transparent = 0;\n\n    switch (shape_info.Type) {\n      case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:\n        // This type doesn't require any XOR-blending\n        return {};\n      case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: {\n        util::buffer_t<std::uint8_t> cursor_img = img_data;\n        std::for_each((std::uint32_t *) std::begin(cursor_img), (std::uint32_t *) std::end(cursor_img), [](auto &pixel) {\n          auto alpha = (std::uint8_t) ((pixel >> 24) & 0xFF);\n          if (alpha == 0xFF) {\n            // Pixels with 0xFF alpha will be XOR-blended as is.\n          }\n          else if (alpha == 0x00) {\n            // Pixels with 0x00 alpha will be blended by make_cursor_alpha_image().\n            // We make them transparent for the XOR-blended cursor image.\n            pixel = transparent;\n          }\n          else {\n            // Other alpha values are illegal in masked color cursors\n            BOOST_LOG(warning) << \"Illegal alpha value in masked color cursor: \" << alpha;\n          }\n        });\n        return cursor_img;\n      }\n      case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME:\n        // Monochrome is handled below\n        break;\n      default:\n        BOOST_LOG(error) << \"Invalid cursor shape type: \" << shape_info.Type;\n        return {};\n    }\n\n    shape_info.Height /= 2;\n\n    util::buffer_t<std::uint8_t> cursor_img { shape_info.Width * shape_info.Height * 4 };\n\n    auto bytes = shape_info.Pitch * shape_info.Height;\n    auto pixel_begin = (std::uint32_t *) std::begin(cursor_img);\n    auto pixel_data = pixel_begin;\n    auto and_mask = std::begin(img_data);\n    auto xor_mask = std::begin(img_data) + bytes;\n\n    for (auto x = 0; x < bytes; ++x) {\n      for (auto c = 7; c >= 0 && ((std::uint8_t *) pixel_data) != std::end(cursor_img); --c) {\n        auto bit = 1 << c;\n        auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0);\n\n        switch (color_type) {\n          case 0:  // Opaque black (handled by alpha-blending)\n          case 2:  // Opaque white (handled by alpha-blending)\n          case 1:  // Color of screen (transparent)\n            *pixel_data = transparent;\n            break;\n          case 3:  // Inverse of screen\n            *pixel_data = inverted;\n            break;\n        }\n\n        ++pixel_data;\n      }\n      ++and_mask;\n      ++xor_mask;\n    }\n\n    return cursor_img;\n  }\n\n  util::buffer_t<std::uint8_t>\n  make_cursor_alpha_image(const util::buffer_t<std::uint8_t> &img_data, DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info) {\n    constexpr std::uint32_t black = 0xFF000000;\n    constexpr std::uint32_t white = 0xFFFFFFFF;\n    constexpr std::uint32_t transparent = 0;\n\n    switch (shape_info.Type) {\n      case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR: {\n        util::buffer_t<std::uint8_t> cursor_img = img_data;\n        std::for_each((std::uint32_t *) std::begin(cursor_img), (std::uint32_t *) std::end(cursor_img), [](auto &pixel) {\n          auto alpha = (std::uint8_t) ((pixel >> 24) & 0xFF);\n          if (alpha == 0xFF) {\n            // Pixels with 0xFF alpha will be XOR-blended by make_cursor_xor_image().\n            // We make them transparent for the alpha-blended cursor image.\n            pixel = transparent;\n          }\n          else if (alpha == 0x00) {\n            // Pixels with 0x00 alpha will be blended as opaque with the alpha-blended image.\n            pixel |= 0xFF000000;\n          }\n          else {\n            // Other alpha values are illegal in masked color cursors\n            BOOST_LOG(warning) << \"Illegal alpha value in masked color cursor: \" << alpha;\n          }\n        });\n        return cursor_img;\n      }\n      case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:\n        // Color cursors are just an ARGB bitmap which requires no processing.\n        return img_data;\n      case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME:\n        // Monochrome cursors are handled below.\n        break;\n      default:\n        BOOST_LOG(error) << \"Invalid cursor shape type: \" << shape_info.Type;\n        return {};\n    }\n\n    shape_info.Height /= 2;\n\n    util::buffer_t<std::uint8_t> cursor_img { shape_info.Width * shape_info.Height * 4 };\n\n    auto bytes = shape_info.Pitch * shape_info.Height;\n    auto pixel_begin = (std::uint32_t *) std::begin(cursor_img);\n    auto pixel_data = pixel_begin;\n    auto and_mask = std::begin(img_data);\n    auto xor_mask = std::begin(img_data) + bytes;\n\n    for (auto x = 0; x < bytes; ++x) {\n      for (auto c = 7; c >= 0 && ((std::uint8_t *) pixel_data) != std::end(cursor_img); --c) {\n        auto bit = 1 << c;\n        auto color_type = ((*and_mask & bit) ? 1 : 0) + ((*xor_mask & bit) ? 2 : 0);\n\n        switch (color_type) {\n          case 0:  // Opaque black\n            *pixel_data = black;\n            break;\n          case 2:  // Opaque white\n            *pixel_data = white;\n            break;\n          case 3:  // Inverse of screen (handled by XOR blending)\n          case 1:  // Color of screen (transparent)\n            *pixel_data = transparent;\n            break;\n        }\n\n        ++pixel_data;\n      }\n      ++and_mask;\n      ++xor_mask;\n    }\n\n    return cursor_img;\n  }\n\n  blob_t\n  compile_shader(LPCSTR file, LPCSTR entrypoint, LPCSTR shader_model) {\n    blob_t::pointer msg_p = nullptr;\n    blob_t::pointer compiled_p;\n\n    DWORD flags = D3DCOMPILE_ENABLE_STRICTNESS;\n\n#ifndef NDEBUG\n    flags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;\n#endif\n\n    auto wFile = from_utf8(file);\n    auto status = D3DCompileFromFile(wFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint, shader_model, flags, 0, &compiled_p, &msg_p);\n\n    if (msg_p) {\n      BOOST_LOG(warning) << std::string_view { (const char *) msg_p->GetBufferPointer(), msg_p->GetBufferSize() - 1 };\n      msg_p->Release();\n    }\n\n    if (status) {\n      BOOST_LOG(error) << \"Couldn't compile [\"sv << file << \"] [0x\"sv << util::hex(status).to_string_view() << ']';\n      return nullptr;\n    }\n\n    return blob_t { compiled_p };\n  }\n\n  blob_t\n  compile_pixel_shader(LPCSTR file) {\n    return compile_shader(file, \"main_ps\", \"ps_5_0\");\n  }\n\n  blob_t\n  compile_vertex_shader(LPCSTR file) {\n    return compile_shader(file, \"main_vs\", \"vs_5_0\");\n  }\n\n  blob_t\n  compile_compute_shader(LPCSTR file) {\n    return compile_shader(file, \"main_cs\", \"cs_5_0\");\n  }\n\n  class d3d_base_encode_device final {\n  public:\n    int\n    convert(platf::img_t &img_base) {\n      // Garbage collect mapped capture images whose weak references have expired\n      for (auto it = img_ctx_map.begin(); it != img_ctx_map.end();) {\n        if (it->second.img_weak.expired()) {\n          it = img_ctx_map.erase(it);\n        }\n        else {\n          it++;\n        }\n      }\n\n      auto &img = (img_d3d_t &) img_base;\n      if (!img.blank) {\n        auto &img_ctx = img_ctx_map[img.id];\n        const bool can_analyze_hdr_frame = hdr_analysis_enabled && img.linear_gamma && img.format == DXGI_FORMAT_R16G16B16A16_FLOAT;\n\n        // Open the shared capture texture with our ID3D11Device\n        if (initialize_image_context(img, img_ctx)) {\n          return -1;\n        }\n\n        // Poll the previous analysis result before taking the capture mutex.\n        if (hdr_analysis_pending) {\n          read_hdr_analysis_results();\n        }\n\n        // Acquire encoder mutex to synchronize with capture code\n        auto status = img_ctx.encoder_mutex->AcquireSync(0, INFINITE);\n        if (status != S_OK) {\n          BOOST_LOG(error) << \"Failed to acquire encoder mutex [0x\"sv << util::hex(status).to_string_view() << ']';\n          // Check if the D3D11 device is lost (TDR, driver crash, etc.)\n          if (device.get()) {\n            auto removed_reason = device->GetDeviceRemovedReason();\n            if (removed_reason != S_OK) {\n              BOOST_LOG(error) << \"D3D11 device lost during convert, reason: 0x\"sv << util::hex(removed_reason).to_string_view();\n            }\n          }\n          return -1;\n        }\n\n        auto draw = [&](auto &input, auto &y_or_yuv_viewports, auto &uv_viewport) {\n          device_ctx->PSSetShaderResources(0, 1, &input);\n\n          // Select the correct pixel shader based on image gamma type:\n          // - linear_gamma AND FP16 format: Use FP16 shader that applies transfer function\n          //   (sRGB for SDR, PQ for HDR, HLG for HLG) to convert from linear light\n          // - Otherwise: Use standard shader that assumes sRGB gamma input\n          //\n          // Both conditions are required because:\n          // 1. FP16 format + G10/G2084 colorspace = data is truly linear (scRGB)\n          // 2. B8G8R8A8 format + G10 colorspace = data was converted TO sRGB by the\n          //    capture API (e.g., WGC requests 8-bit format while display is in ACM mode)\n          //\n          // This prevents double-gamma when the display is in ACM/HDR mode but the\n          // capture is in a non-FP16 format, and also when a driver returns FP16 data\n          // that already carries sRGB gamma (G22 colorspace with FP16 format).\n          const bool use_linear_shader = img.linear_gamma && (img.format == DXGI_FORMAT_R16G16B16A16_FLOAT);\n\n          // Draw Y/YUV\n          device_ctx->OMSetRenderTargets(1, &out_Y_or_YUV_rtv, nullptr);\n          device_ctx->VSSetShader(convert_Y_or_YUV_vs.get(), nullptr, 0);\n          device_ctx->PSSetShader(use_linear_shader ? convert_Y_or_YUV_fp16_ps.get() : convert_Y_or_YUV_ps.get(), nullptr, 0);\n          auto viewport_count = (format == DXGI_FORMAT_R16_UINT) ? 3 : 1;\n          assert(viewport_count <= y_or_yuv_viewports.size());\n          device_ctx->RSSetViewports(viewport_count, y_or_yuv_viewports.data());\n          device_ctx->Draw(3 * viewport_count, 0);  // vertex shader will spread vertices across viewports\n\n          // Draw UV if needed\n          if (out_UV_rtv) {\n            assert(format == DXGI_FORMAT_NV12 || format == DXGI_FORMAT_P010);\n            device_ctx->OMSetRenderTargets(1, &out_UV_rtv, nullptr);\n            device_ctx->VSSetShader(convert_UV_vs.get(), nullptr, 0);\n            device_ctx->PSSetShader(use_linear_shader ? convert_UV_fp16_ps.get() : convert_UV_ps.get(), nullptr, 0);\n            device_ctx->RSSetViewports(1, &uv_viewport);\n            device_ctx->Draw(3, 0);\n          }\n        };\n\n        // Clear render target view(s) once so that the aspect ratio mismatch \"bars\" appear black\n        if (!rtvs_cleared) {\n          auto black = create_black_texture_for_rtv_clear();\n          if (black) draw(black, out_Y_or_YUV_viewports_for_clear, out_UV_viewport_for_clear);\n          rtvs_cleared = true;\n        }\n\n        // Draw captured frame\n        draw(img_ctx.encoder_input_res, out_Y_or_YUV_viewports, out_UV_viewport);\n\n        ID3D11ShaderResourceView *emptyShaderResourceView = nullptr;\n        device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView);\n\n        bool dispatch_hdr_after_unlock = false;\n\n        // Copy the source frame to a dedicated analysis texture while the shared\n        // texture is still locked. The expensive compute passes run after unlock.\n        if (can_analyze_hdr_frame && should_dispatch_hdr_analysis()) {\n          device_ctx->CopyResource(hdr_analysis_input_tex.get(), img_ctx.encoder_texture.get());\n          dispatch_hdr_after_unlock = true;\n        }\n\n        // Release encoder mutex to allow capture code to reuse this image\n        img_ctx.encoder_mutex->ReleaseSync(0);\n\n        if (dispatch_hdr_after_unlock) {\n          dispatch_hdr_analysis(hdr_analysis_input_srv.get());\n        }\n      }\n\n      return 0;\n    }\n\n    void apply_colorspace(const ::video::sunshine_colorspace_t &colorspace) {\n      auto color_vectors = ::video::color_vectors_from_colorspace(colorspace, true);\n\n      if (format == DXGI_FORMAT_AYUV ||\n          format == DXGI_FORMAT_R16_UINT ||\n          format == DXGI_FORMAT_Y410) {\n        color_vectors = ::video::color_vectors_from_colorspace(colorspace, false);\n      }\n\n      if (!color_vectors) {\n        BOOST_LOG(error) << \"No vector data for colorspace\"sv;\n        return;\n      }\n\n      auto color_matrix = make_buffer(device.get(), *color_vectors);\n      if (!color_matrix) {\n        BOOST_LOG(warning) << \"Failed to create color matrix\"sv;\n        return;\n      }\n\n      device_ctx->VSSetConstantBuffers(3, 1, &color_matrix);\n      device_ctx->PSSetConstantBuffers(0, 1, &color_matrix);\n      this->color_matrix = std::move(color_matrix);\n    }\n\n    int\n    init_output(ID3D11Texture2D *frame_texture, int width, int height, const ::video::sunshine_colorspace_t &colorspace, bool is_probe = false) {\n      // The underlying frame pool owns the texture, so we must reference it for ourselves\n      frame_texture->AddRef();\n      output_texture.reset(frame_texture);\n\n      HRESULT status = S_OK;\n\n#define create_vertex_shader_helper(x, y)                                                                    \\\n  if (FAILED(status = device->CreateVertexShader(x->GetBufferPointer(), x->GetBufferSize(), nullptr, &y))) { \\\n    BOOST_LOG(error) << \"Failed to create vertex shader \" << #x << \": \" << util::log_hex(status);            \\\n    return -1;                                                                                               \\\n  }\n#define create_pixel_shader_helper(x, y)                                                                    \\\n  if (FAILED(status = device->CreatePixelShader(x->GetBufferPointer(), x->GetBufferSize(), nullptr, &y))) { \\\n    BOOST_LOG(error) << \"Failed to create pixel shader \" << #x << \": \" << util::log_hex(status);            \\\n    return -1;                                                                                              \\\n  }\n\n      // Determine which HDR shader to use based on colorspace\n      const bool use_pq_shader = ::video::colorspace_is_pq(colorspace);\n      const bool use_hlg_shader = ::video::colorspace_is_hlg(colorspace);\n\n      const bool downscaling = display->width > width || display->height > height;\n      // Determine downscaling quality based on config\n      // \"fast\" = bilinear + 8pt average (original method)\n      // \"balanced\" = bicubic (default, best quality/performance balance)\n      // \"high_quality\" = reserved for future lanczos implementation\n      const bool use_bicubic = downscaling && \n                               (config::video.downscaling_quality == \"balanced\" || \n                                config::video.downscaling_quality == \"high_quality\");\n      \n      if (downscaling) {\n        if (is_probe) {\n          BOOST_LOG(debug) << \"Downscaling from \" << display->width << \"x\" << display->height\n                           << \" to \" << width << \"x\" << height\n                           << \" using quality: \" << config::video.downscaling_quality\n                           << (use_bicubic ? \" (bicubic)\" : \" (bilinear+8pt)\")\n                           << \" (encoder probe)\";\n        }\n        else {\n          BOOST_LOG(info) << \"Downscaling from \" << display->width << \"x\" << display->height\n                         << \" to \" << width << \"x\" << height\n                         << \" using quality: \" << config::video.downscaling_quality\n                         << (use_bicubic ? \" (bicubic)\" : \" (bilinear+8pt)\");\n        }\n      }\n\n      switch (format) {\n        case DXGI_FORMAT_NV12:\n          // Semi-planar 8-bit YUV 4:2:0\n          if (use_bicubic) {\n            // Use bicubic sampling for high-quality downscaling\n            create_vertex_shader_helper(convert_yuv420_planar_y_vs_hlsl, convert_Y_or_YUV_vs);\n            create_pixel_shader_helper(convert_yuv420_planar_y_bicubic_ps_hlsl, convert_Y_or_YUV_ps);\n            create_pixel_shader_helper(convert_yuv420_planar_y_bicubic_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps);\n            create_vertex_shader_helper(convert_yuv420_packed_uv_bicubic_vs_hlsl, convert_UV_vs);\n            create_pixel_shader_helper(convert_yuv420_packed_uv_bicubic_ps_hlsl, convert_UV_ps);\n            create_pixel_shader_helper(convert_yuv420_packed_uv_bicubic_ps_linear_hlsl, convert_UV_fp16_ps);\n          }\n          else {\n            create_vertex_shader_helper(convert_yuv420_planar_y_vs_hlsl, convert_Y_or_YUV_vs);\n            create_pixel_shader_helper(convert_yuv420_planar_y_ps_hlsl, convert_Y_or_YUV_ps);\n            create_pixel_shader_helper(convert_yuv420_planar_y_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps);\n            if (downscaling) {\n              create_vertex_shader_helper(convert_yuv420_packed_uv_type0s_vs_hlsl, convert_UV_vs);\n              create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hlsl, convert_UV_ps);\n              create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear_hlsl, convert_UV_fp16_ps);\n            }\n            else {\n              create_vertex_shader_helper(convert_yuv420_packed_uv_type0_vs_hlsl, convert_UV_vs);\n              create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hlsl, convert_UV_ps);\n              create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear_hlsl, convert_UV_fp16_ps);\n            }\n          }\n          break;\n\n        case DXGI_FORMAT_P010:\n          // Semi-planar 16-bit YUV 4:2:0, 10 most significant bits store the value\n          if (use_bicubic) {\n            // Use bicubic sampling for high-quality downscaling\n            create_vertex_shader_helper(convert_yuv420_planar_y_vs_hlsl, convert_Y_or_YUV_vs);\n            create_pixel_shader_helper(convert_yuv420_planar_y_bicubic_ps_hlsl, convert_Y_or_YUV_ps);\n            if (use_pq_shader) {\n              create_pixel_shader_helper(convert_yuv420_planar_y_bicubic_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps);\n            }\n            else if (use_hlg_shader) {\n              create_pixel_shader_helper(convert_yuv420_planar_y_bicubic_ps_hybrid_log_gamma_hlsl, convert_Y_or_YUV_fp16_ps);\n            }\n            else {\n              create_pixel_shader_helper(convert_yuv420_planar_y_bicubic_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps);\n            }\n            create_vertex_shader_helper(convert_yuv420_packed_uv_bicubic_vs_hlsl, convert_UV_vs);\n            create_pixel_shader_helper(convert_yuv420_packed_uv_bicubic_ps_hlsl, convert_UV_ps);\n            if (use_pq_shader) {\n              create_pixel_shader_helper(convert_yuv420_packed_uv_bicubic_ps_perceptual_quantizer_hlsl, convert_UV_fp16_ps);\n            }\n            else if (use_hlg_shader) {\n              create_pixel_shader_helper(convert_yuv420_packed_uv_bicubic_ps_hybrid_log_gamma_hlsl, convert_UV_fp16_ps);\n            }\n            else {\n              create_pixel_shader_helper(convert_yuv420_packed_uv_bicubic_ps_linear_hlsl, convert_UV_fp16_ps);\n            }\n          }\n          else {\n            create_vertex_shader_helper(convert_yuv420_planar_y_vs_hlsl, convert_Y_or_YUV_vs);\n            create_pixel_shader_helper(convert_yuv420_planar_y_ps_hlsl, convert_Y_or_YUV_ps);\n            if (use_pq_shader) {\n              create_pixel_shader_helper(convert_yuv420_planar_y_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps);\n            }\n            else if (use_hlg_shader) {\n              create_pixel_shader_helper(convert_yuv420_planar_y_ps_hybrid_log_gamma_hlsl, convert_Y_or_YUV_fp16_ps);\n            }\n            else {\n              create_pixel_shader_helper(convert_yuv420_planar_y_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps);\n            }\n            if (downscaling) {\n              create_vertex_shader_helper(convert_yuv420_packed_uv_type0s_vs_hlsl, convert_UV_vs);\n              create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hlsl, convert_UV_ps);\n              if (use_pq_shader) {\n                create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer_hlsl, convert_UV_fp16_ps);\n              }\n              else if (use_hlg_shader) {\n                create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hybrid_log_gamma_hlsl, convert_UV_fp16_ps);\n              }\n              else {\n                create_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear_hlsl, convert_UV_fp16_ps);\n              }\n            }\n            else {\n              create_vertex_shader_helper(convert_yuv420_packed_uv_type0_vs_hlsl, convert_UV_vs);\n              create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hlsl, convert_UV_ps);\n              if (use_pq_shader) {\n                create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_perceptual_quantizer_hlsl, convert_UV_fp16_ps);\n              }\n              else if (use_hlg_shader) {\n                create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hybrid_log_gamma_hlsl, convert_UV_fp16_ps);\n              }\n              else {\n                create_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear_hlsl, convert_UV_fp16_ps);\n              }\n            }\n          }\n          break;\n\n        case DXGI_FORMAT_R16_UINT:\n          // Planar 16-bit YUV 4:4:4, 10 most significant bits store the value\n          create_vertex_shader_helper(convert_yuv444_planar_vs_hlsl, convert_Y_or_YUV_vs);\n          create_pixel_shader_helper(convert_yuv444_planar_ps_hlsl, convert_Y_or_YUV_ps);\n          if (use_pq_shader) {\n            create_pixel_shader_helper(convert_yuv444_planar_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps);\n          }\n          else if (use_hlg_shader) {\n            create_pixel_shader_helper(convert_yuv444_planar_ps_hybrid_log_gamma_hlsl, convert_Y_or_YUV_fp16_ps);\n          }\n          else {\n            create_pixel_shader_helper(convert_yuv444_planar_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps);\n          }\n          break;\n\n        case DXGI_FORMAT_AYUV:\n          // Packed 8-bit YUV 4:4:4\n          create_vertex_shader_helper(convert_yuv444_packed_vs_hlsl, convert_Y_or_YUV_vs);\n          create_pixel_shader_helper(convert_yuv444_packed_ayuv_ps_hlsl, convert_Y_or_YUV_ps);\n          create_pixel_shader_helper(convert_yuv444_packed_ayuv_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps);\n          break;\n\n        case DXGI_FORMAT_Y410:\n          // Packed 10-bit YUV 4:4:4\n          create_vertex_shader_helper(convert_yuv444_packed_vs_hlsl, convert_Y_or_YUV_vs);\n          create_pixel_shader_helper(convert_yuv444_packed_y410_ps_hlsl, convert_Y_or_YUV_ps);\n          if (use_pq_shader) {\n            create_pixel_shader_helper(convert_yuv444_packed_y410_ps_perceptual_quantizer_hlsl, convert_Y_or_YUV_fp16_ps);\n          }\n          else if (use_hlg_shader) {\n            create_pixel_shader_helper(convert_yuv444_packed_y410_ps_hybrid_log_gamma_hlsl, convert_Y_or_YUV_fp16_ps);\n          }\n          else {\n            create_pixel_shader_helper(convert_yuv444_packed_y410_ps_linear_hlsl, convert_Y_or_YUV_fp16_ps);\n          }\n          break;\n\n        default:\n          BOOST_LOG(error) << \"Unable to create shaders because of the unrecognized surface format\";\n          return -1;\n      }\n\n#undef create_vertex_shader_helper\n#undef create_pixel_shader_helper\n\n      auto out_width = width;\n      auto out_height = height;\n\n      float in_width = display->width;\n      float in_height = display->height;\n\n      // Ensure aspect ratio is maintained\n      auto scalar = std::fminf(out_width / in_width, out_height / in_height);\n      auto out_width_f = in_width * scalar;\n      auto out_height_f = in_height * scalar;\n\n      // result is always positive\n      auto offsetX = (out_width - out_width_f) / 2;\n      auto offsetY = (out_height - out_height_f) / 2;\n\n      out_Y_or_YUV_viewports[0] = { offsetX, offsetY, out_width_f, out_height_f, 0.0f, 1.0f };  // Y plane\n      out_Y_or_YUV_viewports[1] = out_Y_or_YUV_viewports[0];  // U plane\n      out_Y_or_YUV_viewports[1].TopLeftY += out_height;\n      out_Y_or_YUV_viewports[2] = out_Y_or_YUV_viewports[1];  // V plane\n      out_Y_or_YUV_viewports[2].TopLeftY += out_height;\n\n      out_Y_or_YUV_viewports_for_clear[0] = { 0, 0, (float) out_width, (float) out_height, 0.0f, 1.0f };  // Y plane\n      out_Y_or_YUV_viewports_for_clear[1] = out_Y_or_YUV_viewports_for_clear[0];  // U plane\n      out_Y_or_YUV_viewports_for_clear[1].TopLeftY += out_height;\n      out_Y_or_YUV_viewports_for_clear[2] = out_Y_or_YUV_viewports_for_clear[1];  // V plane\n      out_Y_or_YUV_viewports_for_clear[2].TopLeftY += out_height;\n\n      out_UV_viewport = { offsetX / 2, offsetY / 2, out_width_f / 2, out_height_f / 2, 0.0f, 1.0f };\n      out_UV_viewport_for_clear = { 0, 0, (float) out_width / 2, (float) out_height / 2, 0.0f, 1.0f };\n\n      float subsample_offset_in[16 / sizeof(float)] { 1.0f / (float) out_width_f, 1.0f / (float) out_height_f };  // aligned to 16-byte\n      subsample_offset = make_buffer(device.get(), subsample_offset_in);\n\n      if (!subsample_offset) {\n        BOOST_LOG(error) << \"Failed to create subsample offset vertex constant buffer\";\n        return -1;\n      }\n      device_ctx->VSSetConstantBuffers(0, 1, &subsample_offset);\n\n      {\n        int32_t rotation_modifier = display->display_rotation == DXGI_MODE_ROTATION_UNSPECIFIED ? 0 : display->display_rotation - 1;\n        int32_t rotation_data[16 / sizeof(int32_t)] { -rotation_modifier };  // aligned to 16-byte\n        auto rotation = make_buffer(device.get(), rotation_data);\n        if (!rotation) {\n          BOOST_LOG(error) << \"Failed to create display rotation vertex constant buffer\";\n          return -1;\n        }\n        device_ctx->VSSetConstantBuffers(1, 1, &rotation);\n      }\n\n      DXGI_FORMAT rtv_Y_or_YUV_format = DXGI_FORMAT_UNKNOWN;\n      DXGI_FORMAT rtv_UV_format = DXGI_FORMAT_UNKNOWN;\n      bool rtv_simple_clear = false;\n\n      switch (format) {\n        case DXGI_FORMAT_NV12:\n          rtv_Y_or_YUV_format = DXGI_FORMAT_R8_UNORM;\n          rtv_UV_format = DXGI_FORMAT_R8G8_UNORM;\n          rtv_simple_clear = true;\n          break;\n\n        case DXGI_FORMAT_P010:\n          rtv_Y_or_YUV_format = DXGI_FORMAT_R16_UNORM;\n          rtv_UV_format = DXGI_FORMAT_R16G16_UNORM;\n          rtv_simple_clear = true;\n          break;\n\n        case DXGI_FORMAT_AYUV:\n          rtv_Y_or_YUV_format = DXGI_FORMAT_R8G8B8A8_UINT;\n          break;\n\n        case DXGI_FORMAT_R16_UINT:\n          rtv_Y_or_YUV_format = DXGI_FORMAT_R16_UINT;\n          break;\n\n        case DXGI_FORMAT_Y410:\n          rtv_Y_or_YUV_format = DXGI_FORMAT_R10G10B10A2_UINT;\n          break;\n\n        default:\n          BOOST_LOG(error) << \"Unable to create render target views because of the unrecognized surface format\";\n          return -1;\n      }\n\n      auto create_rtv = [&](auto &rt, DXGI_FORMAT rt_format) -> bool {\n        D3D11_RENDER_TARGET_VIEW_DESC rtv_desc = {};\n        rtv_desc.Format = rt_format;\n        rtv_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;\n\n        auto status = device->CreateRenderTargetView(output_texture.get(), &rtv_desc, &rt);\n        if (FAILED(status)) {\n          BOOST_LOG(error) << \"Failed to create render target view: \" << util::log_hex(status);\n          return false;\n        }\n\n        return true;\n      };\n\n      // Create Y/YUV render target view\n      if (!create_rtv(out_Y_or_YUV_rtv, rtv_Y_or_YUV_format)) return -1;\n\n      // Create UV render target view if needed\n      if (rtv_UV_format != DXGI_FORMAT_UNKNOWN && !create_rtv(out_UV_rtv, rtv_UV_format)) return -1;\n\n      if (rtv_simple_clear) {\n        // Clear the RTVs to ensure the aspect ratio padding is black\n        const float y_black[] = { 0.0f, 0.0f, 0.0f, 0.0f };\n        device_ctx->ClearRenderTargetView(out_Y_or_YUV_rtv.get(), y_black);\n        if (out_UV_rtv) {\n          const float uv_black[] = { 0.5f, 0.5f, 0.5f, 0.5f };\n          device_ctx->ClearRenderTargetView(out_UV_rtv.get(), uv_black);\n        }\n        rtvs_cleared = true;\n      }\n      else {\n        // Can't use ClearRenderTargetView(), will clear on first convert()\n        rtvs_cleared = false;\n      }\n\n      return 0;\n    }\n\n    int\n    init(std::shared_ptr<platf::display_t> display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) {\n      switch (pix_fmt) {\n        case pix_fmt_e::nv12:\n          format = DXGI_FORMAT_NV12;\n          break;\n\n        case pix_fmt_e::p010:\n          format = DXGI_FORMAT_P010;\n          break;\n\n        case pix_fmt_e::ayuv:\n          format = DXGI_FORMAT_AYUV;\n          break;\n\n        case pix_fmt_e::yuv444p16:\n          format = DXGI_FORMAT_R16_UINT;\n          break;\n\n        case pix_fmt_e::y410:\n          format = DXGI_FORMAT_Y410;\n          break;\n\n        default:\n          BOOST_LOG(error) << \"D3D11 backend doesn't support pixel format: \" << from_pix_fmt(pix_fmt);\n          return -1;\n      }\n\n      D3D_FEATURE_LEVEL featureLevels[] {\n        D3D_FEATURE_LEVEL_11_1,\n        D3D_FEATURE_LEVEL_11_0,\n        D3D_FEATURE_LEVEL_10_1,\n        D3D_FEATURE_LEVEL_10_0,\n        D3D_FEATURE_LEVEL_9_3,\n        D3D_FEATURE_LEVEL_9_2,\n        D3D_FEATURE_LEVEL_9_1\n      };\n\n      HRESULT status = D3D11CreateDevice(\n        adapter_p,\n        D3D_DRIVER_TYPE_UNKNOWN,\n        nullptr,\n        D3D11_CREATE_DEVICE_FLAGS | D3D11_CREATE_DEVICE_VIDEO_SUPPORT,\n        featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL),\n        D3D11_SDK_VERSION,\n        &device,\n        nullptr,\n        &device_ctx);\n\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to create encoder D3D11 device [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      dxgi::dxgi_t dxgi;\n      status = device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to query DXGI interface from device [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      status = dxgi->SetGPUThreadPriority(7);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to increase encoding GPU thread priority. Please run application as administrator for optimal performance.\";\n      }\n\n      auto default_color_vectors = ::video::color_vectors_from_colorspace({::video::colorspace_e::rec601, false, 8}, true);\n      if (!default_color_vectors) {\n        BOOST_LOG(error) << \"Missing color vectors for Rec. 601\"sv;\n        return -1;\n      }\n\n      color_matrix = make_buffer(device.get(), *default_color_vectors);\n      if (!color_matrix) {\n        BOOST_LOG(error) << \"Failed to create color matrix buffer\"sv;\n        return -1;\n      }\n      device_ctx->VSSetConstantBuffers(3, 1, &color_matrix);\n      device_ctx->PSSetConstantBuffers(0, 1, &color_matrix);\n\n      this->display = std::dynamic_pointer_cast<display_base_t>(display);\n      if (!this->display) {\n        return -1;\n      }\n      display = nullptr;\n\n      blend_disable = make_blend(device.get(), false, false);\n      if (!blend_disable) {\n        return -1;\n      }\n\n      D3D11_SAMPLER_DESC sampler_desc {};\n      sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;\n      sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;\n      sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;\n      sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;\n      sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER;\n      sampler_desc.MinLOD = 0;\n      sampler_desc.MaxLOD = D3D11_FLOAT32_MAX;\n\n      status = device->CreateSamplerState(&sampler_desc, &sampler_linear);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to create linear sampler state [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);\n      // s0 = linear (existing shaders), s1 = point (high-quality resampling shaders)\n      sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;\n      status = device->CreateSamplerState(&sampler_desc, &sampler_point);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to create point sampler state [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      ID3D11SamplerState *samplers[] = { sampler_linear.get(), sampler_point.get() };\n      device_ctx->PSSetSamplers(0, 2, samplers);\n      device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);\n\n      // Initialize HDR luminance analyzer for HDR formats (P010, Y410, R16_UINT)\n      // The analyzer is optional — if it fails, HDR will still work with static metadata only\n      if (config::video.hdr_luminance_analysis &&\n          (format == DXGI_FORMAT_P010 || format == DXGI_FORMAT_Y410 || format == DXGI_FORMAT_R16_UINT)) {\n        if (init_hdr_luminance_analyzer() != 0) {\n          BOOST_LOG(warning) << \"HDR luminance analyzer init failed, dynamic metadata will use defaults\";\n        }\n      }\n\n      return 0;\n    }\n\n    struct encoder_img_ctx_t {\n      // Used to determine if the underlying texture changes.\n      // Not safe for actual use by the encoder!\n      texture2d_t::const_pointer capture_texture_p;\n\n      texture2d_t encoder_texture;\n      shader_res_t encoder_input_res;\n      keyed_mutex_t encoder_mutex;\n\n      std::weak_ptr<const platf::img_t> img_weak;\n\n      void\n      reset() {\n        capture_texture_p = nullptr;\n        encoder_texture.reset();\n        encoder_input_res.reset();\n        encoder_mutex.reset();\n        img_weak.reset();\n      }\n    };\n\n    int\n    initialize_image_context(const img_d3d_t &img, encoder_img_ctx_t &img_ctx) {\n      // If we've already opened the shared texture, we're done\n      if (img_ctx.encoder_texture && img.capture_texture.get() == img_ctx.capture_texture_p) {\n        return 0;\n      }\n\n      // Reset this image context in case it was used before with a different texture.\n      // Textures can change when transitioning from a dummy image to a real image.\n      img_ctx.reset();\n\n      device1_t device1;\n      auto status = device->QueryInterface(__uuidof(ID3D11Device1), (void **) &device1);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to query ID3D11Device1 [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      // Open a handle to the shared texture\n      status = device1->OpenSharedResource1(img.encoder_texture_handle, __uuidof(ID3D11Texture2D), (void **) &img_ctx.encoder_texture);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to open shared image texture [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      // Get the keyed mutex to synchronize with the capture code\n      status = img_ctx.encoder_texture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **) &img_ctx.encoder_mutex);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to query IDXGIKeyedMutex [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      // Create the SRV for the encoder texture\n      status = device->CreateShaderResourceView(img_ctx.encoder_texture.get(), nullptr, &img_ctx.encoder_input_res);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to create shader resource view for encoding [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      img_ctx.capture_texture_p = img.capture_texture.get();\n\n      img_ctx.img_weak = img.weak_from_this();\n\n      return 0;\n    }\n\n    shader_res_t\n    create_black_texture_for_rtv_clear() {\n      constexpr auto width = 32;\n      constexpr auto height = 32;\n\n      D3D11_TEXTURE2D_DESC texture_desc = {};\n      texture_desc.Width = width;\n      texture_desc.Height = height;\n      texture_desc.MipLevels = 1;\n      texture_desc.ArraySize = 1;\n      texture_desc.SampleDesc.Count = 1;\n      texture_desc.Usage = D3D11_USAGE_IMMUTABLE;\n      texture_desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;\n      texture_desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;\n\n      std::vector<uint8_t> mem(4 * width * height, 0);\n      D3D11_SUBRESOURCE_DATA texture_data = { mem.data(), 4 * width, 0 };\n\n      texture2d_t texture;\n      auto status = device->CreateTexture2D(&texture_desc, &texture_data, &texture);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to create black texture: \" << util::log_hex(status);\n        return {};\n      }\n\n      shader_res_t resource_view;\n      status = device->CreateShaderResourceView(texture.get(), nullptr, &resource_view);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to create black texture resource view: \" << util::log_hex(status);\n        return {};\n      }\n\n      return resource_view;\n    }\n\n    ::video::color_t *color_p;\n\n    buf_t subsample_offset;\n    buf_t color_matrix;\n\n    blend_t blend_disable;\n    sampler_state_t sampler_linear;\n    sampler_state_t sampler_point;\n\n    render_target_t out_Y_or_YUV_rtv;\n    render_target_t out_UV_rtv;\n    bool rtvs_cleared = false;\n\n    // d3d_img_t::id -> encoder_img_ctx_t\n    // These store the encoder textures for each img_t that passes through\n    // convert(). We can't store them in the img_t itself because it is shared\n    // amongst multiple hwdevice_t objects (and therefore multiple ID3D11Devices).\n    std::map<uint32_t, encoder_img_ctx_t> img_ctx_map;\n\n    std::shared_ptr<display_base_t> display;\n\n    vs_t convert_Y_or_YUV_vs;\n    ps_t convert_Y_or_YUV_ps;\n    ps_t convert_Y_or_YUV_fp16_ps;\n\n    vs_t convert_UV_vs;\n    ps_t convert_UV_ps;\n    ps_t convert_UV_fp16_ps;\n\n    std::array<D3D11_VIEWPORT, 3> out_Y_or_YUV_viewports, out_Y_or_YUV_viewports_for_clear;\n    D3D11_VIEWPORT out_UV_viewport, out_UV_viewport_for_clear;\n\n    DXGI_FORMAT format;\n\n    device_t device;\n    device_ctx_t device_ctx;\n\n    texture2d_t output_texture;\n\n    // ===== HDR Luminance Analyzer (Two-Pass GPU Reduction) =====\n    // Pass 1: Per-tile CS — each 16x16 group produces {min, max, sum, count, histogram[128]}\n    // Pass 2: Single-group CS — reduces all groups to one final result on GPU\n    // CPU only reads 1 FinalResult (no iteration over thousands of groups)\n    cs_t hdr_pass1_cs;                     // First pass: per-tile analysis\n    cs_t hdr_pass2_cs;                     // Second pass: global reduction\n    texture2d_t hdr_analysis_input_tex;    // Dedicated copy of the HDR frame for analysis outside the keyed mutex\n    shader_res_t hdr_analysis_input_srv;   // SRV for the copied HDR frame\n    buf_t hdr_group_results_buf;           // Pass 1 output (default usage + UAV + SRV)\n    uav_t hdr_group_results_uav;           // UAV view for pass 1 output\n    shader_res_t hdr_group_results_srv;    // SRV view for pass 2 input\n    buf_t hdr_final_result_buf;            // Pass 2 output (default usage + UAV)\n    uav_t hdr_final_result_uav;            // UAV view for pass 2 output\n    buf_t hdr_staging_buf;                 // Staging buffer for CPU readback (1 FinalResult)\n    buf_t hdr_analysis_cbuf;               // Constant buffer for pass 1 (analysis resolution)\n    buf_t hdr_reduce_cbuf;                 // Constant buffer for pass 2 (numGroups)\n    uint32_t hdr_analysis_width = 0;       // Analysis grid width (downsampled from source)\n    uint32_t hdr_analysis_height = 0;      // Analysis grid height (downsampled from source)\n    uint32_t hdr_num_groups = 0;           // Number of thread groups dispatched in pass 1\n    uint64_t hdr_analysis_frame_index = 0; // Used to downsample analysis frequency\n    bool hdr_analysis_pending = false;     // Whether we have results ready to read\n    bool hdr_analysis_enabled = false;     // Whether HDR analysis is initialized\n\n    // Must match HLSL GroupResult layout exactly\n    static constexpr uint32_t HISTOGRAM_BINS = 128;\n    static constexpr uint32_t HDR_ANALYSIS_INTERVAL = 4;\n    static constexpr uint32_t HDR_ANALYSIS_MAX_WIDTH = 1920;\n    static constexpr uint32_t HDR_ANALYSIS_MAX_HEIGHT = 1080;\n\n    struct GroupResult {\n      float minMaxRGB;\n      float maxMaxRGB;\n      float sumMaxRGB;\n      uint32_t pixelCount;\n      uint32_t histogram[HISTOGRAM_BINS];\n    };\n\n    // Must match HLSL FinalResult layout exactly (same as GroupResult for merged output)\n    struct FinalResult {\n      float minMaxRGB;\n      float maxMaxRGB;\n      float sumMaxRGB;\n      uint32_t pixelCount;\n      uint32_t histogram[HISTOGRAM_BINS];\n    };\n\n    bool\n    should_dispatch_hdr_analysis() {\n      const bool should_dispatch = (hdr_analysis_frame_index % HDR_ANALYSIS_INTERVAL) == 0;\n      ++hdr_analysis_frame_index;\n      return should_dispatch;\n    }\n\n    /**\n     * @brief Initialize the two-pass HDR luminance analysis compute pipeline.\n     * Pass 1: Per-tile analysis CS (dispatched per-frame)\n     * Pass 2: Single-group reduction CS (dispatched per-frame, reduces all groups to 1 result)\n     * @return 0 on success, -1 on failure (non-fatal, analysis will be disabled)\n     */\n    int\n    init_hdr_luminance_analyzer() {\n      if (!hdr_luminance_analysis_cs_hlsl || !hdr_luminance_reduce_cs_hlsl) {\n        BOOST_LOG(warning) << \"HDR luminance analysis CS not compiled, skipping init\";\n        return -1;\n      }\n\n      // Create pass 1 compute shader (per-tile analysis)\n      HRESULT status = device->CreateComputeShader(\n        hdr_luminance_analysis_cs_hlsl->GetBufferPointer(),\n        hdr_luminance_analysis_cs_hlsl->GetBufferSize(),\n        nullptr,\n        &hdr_pass1_cs);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR pass 1 compute shader: \" << util::log_hex(status);\n        return -1;\n      }\n\n      // Create pass 2 compute shader (global reduction)\n      status = device->CreateComputeShader(\n        hdr_luminance_reduce_cs_hlsl->GetBufferPointer(),\n        hdr_luminance_reduce_cs_hlsl->GetBufferSize(),\n        nullptr,\n        &hdr_pass2_cs);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR pass 2 compute shader: \" << util::log_hex(status);\n        return -1;\n      }\n\n      // Analyze at a capped resolution to keep dynamic HDR metadata cheap enough\n      // to run in the capture path.\n      uint32_t width = display->width;\n      uint32_t height = display->height;\n      float scale_x = static_cast<float>(HDR_ANALYSIS_MAX_WIDTH) / static_cast<float>(width);\n      float scale_y = static_cast<float>(HDR_ANALYSIS_MAX_HEIGHT) / static_cast<float>(height);\n      float analysis_scale = std::fmin(1.0f, std::fmin(scale_x, scale_y));\n      hdr_analysis_width = std::max<uint32_t>(1, static_cast<uint32_t>(width * analysis_scale + 0.5f));\n      hdr_analysis_height = std::max<uint32_t>(1, static_cast<uint32_t>(height * analysis_scale + 0.5f));\n\n      uint32_t groups_x = (hdr_analysis_width + 15) / 16;\n      uint32_t groups_y = (hdr_analysis_height + 15) / 16;\n      hdr_num_groups = groups_x * groups_y;\n\n      // --- Dedicated HDR analysis input copy ---\n      D3D11_TEXTURE2D_DESC analysis_input_desc = {};\n      analysis_input_desc.Width = width;\n      analysis_input_desc.Height = height;\n      analysis_input_desc.MipLevels = 1;\n      analysis_input_desc.ArraySize = 1;\n      analysis_input_desc.SampleDesc.Count = 1;\n      analysis_input_desc.Usage = D3D11_USAGE_DEFAULT;\n      analysis_input_desc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;\n      analysis_input_desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;\n\n      status = device->CreateTexture2D(&analysis_input_desc, nullptr, &hdr_analysis_input_tex);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR analysis input texture: \" << util::log_hex(status);\n        return -1;\n      }\n\n      status = device->CreateShaderResourceView(hdr_analysis_input_tex.get(), nullptr, &hdr_analysis_input_srv);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR analysis input SRV: \" << util::log_hex(status);\n        return -1;\n      }\n\n      // --- Constant buffer for pass 1 (analysis resolution) ---\n      D3D11_BUFFER_DESC analysis_cb_desc = {};\n      analysis_cb_desc.ByteWidth = 16;  // 16-byte aligned: uint2 + padding\n      analysis_cb_desc.Usage = D3D11_USAGE_IMMUTABLE;\n      analysis_cb_desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;\n\n      struct {\n        uint32_t analysisWidth;\n        uint32_t analysisHeight;\n        uint32_t pad[2];\n      } analysis_cb_data = { hdr_analysis_width, hdr_analysis_height, {} };\n\n      D3D11_SUBRESOURCE_DATA analysis_cb_init = {};\n      analysis_cb_init.pSysMem = &analysis_cb_data;\n\n      status = device->CreateBuffer(&analysis_cb_desc, &analysis_cb_init, &hdr_analysis_cbuf);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR analysis constant buffer: \" << util::log_hex(status);\n        return -1;\n      }\n\n      // --- Pass 1 output: structured buffer with UAV + SRV ---\n      D3D11_BUFFER_DESC buf_desc = {};\n      buf_desc.ByteWidth = hdr_num_groups * sizeof(GroupResult);\n      buf_desc.Usage = D3D11_USAGE_DEFAULT;\n      buf_desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE;\n      buf_desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;\n      buf_desc.StructureByteStride = sizeof(GroupResult);\n\n      status = device->CreateBuffer(&buf_desc, nullptr, &hdr_group_results_buf);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR group results buffer: \" << util::log_hex(status);\n        return -1;\n      }\n\n      // UAV for pass 1 output\n      D3D11_UNORDERED_ACCESS_VIEW_DESC uav_desc = {};\n      uav_desc.Format = DXGI_FORMAT_UNKNOWN;\n      uav_desc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;\n      uav_desc.Buffer.NumElements = hdr_num_groups;\n\n      status = device->CreateUnorderedAccessView(hdr_group_results_buf.get(), &uav_desc, &hdr_group_results_uav);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR group UAV: \" << util::log_hex(status);\n        return -1;\n      }\n\n      // SRV for pass 2 input (read group results)\n      D3D11_SHADER_RESOURCE_VIEW_DESC srv_desc = {};\n      srv_desc.Format = DXGI_FORMAT_UNKNOWN;\n      srv_desc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER;\n      srv_desc.Buffer.NumElements = hdr_num_groups;\n\n      status = device->CreateShaderResourceView(hdr_group_results_buf.get(), &srv_desc, &hdr_group_results_srv);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR group SRV: \" << util::log_hex(status);\n        return -1;\n      }\n\n      // --- Pass 2 output: single FinalResult ---\n      D3D11_BUFFER_DESC final_desc = {};\n      final_desc.ByteWidth = sizeof(FinalResult);\n      final_desc.Usage = D3D11_USAGE_DEFAULT;\n      final_desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;\n      final_desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;\n      final_desc.StructureByteStride = sizeof(FinalResult);\n\n      status = device->CreateBuffer(&final_desc, nullptr, &hdr_final_result_buf);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR final result buffer: \" << util::log_hex(status);\n        return -1;\n      }\n\n      D3D11_UNORDERED_ACCESS_VIEW_DESC final_uav_desc = {};\n      final_uav_desc.Format = DXGI_FORMAT_UNKNOWN;\n      final_uav_desc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;\n      final_uav_desc.Buffer.NumElements = 1;\n\n      status = device->CreateUnorderedAccessView(hdr_final_result_buf.get(), &final_uav_desc, &hdr_final_result_uav);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR final UAV: \" << util::log_hex(status);\n        return -1;\n      }\n\n      // --- Constant buffer for pass 2 (numGroups) ---\n      D3D11_BUFFER_DESC cb_desc = {};\n      cb_desc.ByteWidth = 16;  // 16-byte aligned: uint numGroups + 12 bytes padding\n      cb_desc.Usage = D3D11_USAGE_IMMUTABLE;\n      cb_desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;\n\n      struct {\n        uint32_t numGroups;\n        uint32_t pad[3];\n      } cb_data = { hdr_num_groups, {} };\n\n      D3D11_SUBRESOURCE_DATA cb_init = {};\n      cb_init.pSysMem = &cb_data;\n\n      status = device->CreateBuffer(&cb_desc, &cb_init, &hdr_reduce_cbuf);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR reduce constant buffer: \" << util::log_hex(status);\n        return -1;\n      }\n\n      // --- Staging buffer for async CPU readback (1 FinalResult only) ---\n      D3D11_BUFFER_DESC staging_desc = {};\n      staging_desc.ByteWidth = sizeof(FinalResult);\n      staging_desc.Usage = D3D11_USAGE_STAGING;\n      staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;\n\n      status = device->CreateBuffer(&staging_desc, nullptr, &hdr_staging_buf);\n      if (FAILED(status)) {\n        BOOST_LOG(warning) << \"Failed to create HDR staging buffer: \" << util::log_hex(status);\n        return -1;\n      }\n\n      hdr_analysis_enabled = true;\n      BOOST_LOG(info) << \"HDR luminance analyzer initialized (two-pass): \" << width << \"x\" << height\n                      << \", analysis \" << hdr_analysis_width << \"x\" << hdr_analysis_height\n                      << \", \" << hdr_num_groups << \" groups (\" << groups_x << \"x\" << groups_y << \")\"\n                      << \", interval 1/\" << HDR_ANALYSIS_INTERVAL\n                      << \", staging: \" << sizeof(FinalResult) << \" bytes\";\n      return 0;\n    }\n\n    /**\n     * @brief Dispatch the two-pass luminance analysis for the current frame.\n     * Pass 1: Per-tile analysis — reads scRGB texture, writes per-group results\n     * Pass 2: Global reduction — reads per-group results, writes 1 final result\n     * Then copies final result to staging for async CPU readback next frame.\n     * @param input_srv SRV of the scRGB FP16 capture texture\n     */\n    void\n    dispatch_hdr_analysis(ID3D11ShaderResourceView *input_srv) {\n      if (!hdr_analysis_enabled || !input_srv) return;\n\n      // Unbind render targets to avoid resource hazard (SRV vs RTV conflict)\n      ID3D11RenderTargetView *null_rtv = nullptr;\n      device_ctx->OMSetRenderTargets(1, &null_rtv, nullptr);\n\n      // ===== Pass 1: Per-tile analysis =====\n      device_ctx->CSSetShader(hdr_pass1_cs.get(), nullptr, 0);\n      device_ctx->CSSetShaderResources(0, 1, &input_srv);\n      ID3D11SamplerState *cs_sampler = sampler_linear.get();\n      device_ctx->CSSetSamplers(0, 1, &cs_sampler);\n      ID3D11UnorderedAccessView *uav1 = hdr_group_results_uav.get();\n      device_ctx->CSSetUnorderedAccessViews(0, 1, &uav1, nullptr);\n      ID3D11Buffer *analysis_cbuf = hdr_analysis_cbuf.get();\n      device_ctx->CSSetConstantBuffers(0, 1, &analysis_cbuf);\n\n      uint32_t groups_x = (hdr_analysis_width + 15) / 16;\n      uint32_t groups_y = (hdr_analysis_height + 15) / 16;\n      device_ctx->Dispatch(groups_x, groups_y, 1);\n\n      // Unbind pass 1 resources\n      ID3D11ShaderResourceView *null_srv = nullptr;\n      ID3D11UnorderedAccessView *null_uav = nullptr;\n      device_ctx->CSSetShaderResources(0, 1, &null_srv);\n      device_ctx->CSSetUnorderedAccessViews(0, 1, &null_uav, nullptr);\n      ID3D11SamplerState *null_sampler = nullptr;\n      device_ctx->CSSetSamplers(0, 1, &null_sampler);\n\n      // ===== Pass 2: Global reduction =====\n      device_ctx->CSSetShader(hdr_pass2_cs.get(), nullptr, 0);\n      ID3D11ShaderResourceView *group_srv = hdr_group_results_srv.get();\n      device_ctx->CSSetShaderResources(0, 1, &group_srv);\n      ID3D11UnorderedAccessView *uav2 = hdr_final_result_uav.get();\n      device_ctx->CSSetUnorderedAccessViews(0, 1, &uav2, nullptr);\n      ID3D11Buffer *cbuf = hdr_reduce_cbuf.get();\n      device_ctx->CSSetConstantBuffers(0, 1, &cbuf);\n\n      device_ctx->Dispatch(1, 1, 1);  // Single group of 256 threads\n\n      // Unbind all CS resources\n      device_ctx->CSSetShaderResources(0, 1, &null_srv);\n      device_ctx->CSSetUnorderedAccessViews(0, 1, &null_uav, nullptr);\n      ID3D11Buffer *null_cb = nullptr;\n      device_ctx->CSSetConstantBuffers(0, 1, &null_cb);\n      device_ctx->CSSetShader(nullptr, nullptr, 0);\n\n      // Copy final result to staging buffer for CPU readback next frame\n      device_ctx->CopyResource(hdr_staging_buf.get(), hdr_final_result_buf.get());\n\n      hdr_analysis_pending = true;\n    }\n\n    /**\n     * @brief Read HDR analysis results from the staging buffer (previous frame).\n     * GPU has already reduced all groups to one FinalResult — CPU just reads it\n     * and computes percentiles from the histogram.\n     */\n    void\n    read_hdr_analysis_results() {\n      D3D11_MAPPED_SUBRESOURCE mapped = {};\n      HRESULT status = device_ctx->Map(hdr_staging_buf.get(), 0, D3D11_MAP_READ, D3D11_MAP_FLAG_DO_NOT_WAIT, &mapped);\n\n      if (status == DXGI_ERROR_WAS_STILL_DRAWING) {\n        // GPU hasn't finished yet — skip this readback, try next frame\n        return;\n      }\n\n      if (FAILED(status)) {\n        BOOST_LOG(debug) << \"HDR staging Map failed: \" << util::log_hex(status);\n        return;\n      }\n\n      auto *result = reinterpret_cast<const FinalResult *>(mapped.pData);\n\n      if (result->pixelCount > 0) {\n        hdr_luminance_stats_out.min_maxrgb = result->minMaxRGB;\n        hdr_luminance_stats_out.max_maxrgb = result->maxMaxRGB;\n        hdr_luminance_stats_out.avg_maxrgb = result->sumMaxRGB / static_cast<float>(result->pixelCount);\n\n        // Copy histogram to stats\n        for (uint32_t i = 0; i < HISTOGRAM_BINS; i++) {\n          hdr_luminance_stats_out.histogram[i] = result->histogram[i];\n        }\n\n        // Compute P95 and P99 percentiles from histogram\n        uint32_t total = result->pixelCount;\n        uint32_t target_95 = static_cast<uint32_t>(total * 0.95);\n        uint32_t target_99 = static_cast<uint32_t>(total * 0.99);\n        uint32_t cumulative = 0;\n        bool found_95 = false, found_99 = false;\n\n        for (uint32_t i = 0; i < HISTOGRAM_BINS; i++) {\n          cumulative += result->histogram[i];\n          if (!found_95 && cumulative >= target_95) {\n            // P95 is the upper edge of this bin\n            hdr_luminance_stats_out.percentile_95 = (i + 1) * platf::HDR_NITS_PER_BIN;\n            found_95 = true;\n          }\n          if (!found_99 && cumulative >= target_99) {\n            hdr_luminance_stats_out.percentile_99 = (i + 1) * platf::HDR_NITS_PER_BIN;\n            found_99 = true;\n            break;\n          }\n        }\n\n        hdr_luminance_stats_out.valid = true;\n      }\n\n      device_ctx->Unmap(hdr_staging_buf.get(), 0);\n      hdr_analysis_pending = false;\n    }\n\n    // Intermediate storage for luminance stats (written by readback, consumed by convert caller)\n    platf::hdr_frame_luminance_stats_t hdr_luminance_stats_out;\n  };\n\n  class d3d_avcodec_encode_device_t: public avcodec_encode_device_t {\n  public:\n    int\n    init(std::shared_ptr<platf::display_t> display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) {\n      int result = base.init(display, adapter_p, pix_fmt);\n      data = base.device.get();\n      return result;\n    }\n\n    int\n    convert(platf::img_t &img_base) override {\n      int result = base.convert(img_base);\n      // Propagate per-frame luminance stats from GPU analyzer to encode device\n      hdr_luminance_stats = base.hdr_luminance_stats_out;\n      return result;\n    }\n\n    void\n    apply_colorspace() override {\n      base.apply_colorspace(colorspace);\n    }\n\n    void\n    init_hwframes(AVHWFramesContext *frames) override {\n      // We may be called with a QSV or D3D11VA context\n      if (frames->device_ctx->type == AV_HWDEVICE_TYPE_D3D11VA) {\n        auto d3d11_frames = (AVD3D11VAFramesContext *) frames->hwctx;\n\n        // The encoder requires textures with D3D11_BIND_RENDER_TARGET set\n        d3d11_frames->BindFlags = D3D11_BIND_RENDER_TARGET;\n        d3d11_frames->MiscFlags = 0;\n      }\n\n      // We require a single texture\n      frames->initial_pool_size = 1;\n    }\n\n    int\n    prepare_to_derive_context(int hw_device_type) override {\n      // QuickSync requires our device to be multithread-protected\n      if (hw_device_type == AV_HWDEVICE_TYPE_QSV) {\n        multithread_t mt;\n\n        auto status = base.device->QueryInterface(IID_ID3D11Multithread, (void **) &mt);\n        if (FAILED(status)) {\n          BOOST_LOG(warning) << \"Failed to query ID3D11Multithread interface from device [0x\"sv << util::hex(status).to_string_view() << ']';\n          return -1;\n        }\n\n        mt->SetMultithreadProtected(TRUE);\n      }\n\n      return 0;\n    }\n\n    int\n    set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {\n      this->hwframe.reset(frame);\n      this->frame = frame;\n\n      // Populate this frame with a hardware buffer if one isn't there already\n      if (!frame->buf[0]) {\n        auto err = av_hwframe_get_buffer(hw_frames_ctx, frame, 0);\n        if (err) {\n          char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n          BOOST_LOG(error) << \"Failed to get hwframe buffer: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);\n          return -1;\n        }\n      }\n\n      // If this is a frame from a derived context, we'll need to map it to D3D11\n      ID3D11Texture2D *frame_texture;\n      if (frame->format != AV_PIX_FMT_D3D11) {\n        frame_t d3d11_frame { av_frame_alloc() };\n\n        d3d11_frame->format = AV_PIX_FMT_D3D11;\n\n        auto err = av_hwframe_map(d3d11_frame.get(), frame, AV_HWFRAME_MAP_WRITE | AV_HWFRAME_MAP_OVERWRITE);\n        if (err) {\n          char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n          BOOST_LOG(error) << \"Failed to map D3D11 frame: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);\n          return -1;\n        }\n\n        // Get the texture from the mapped frame\n        frame_texture = (ID3D11Texture2D *) d3d11_frame->data[0];\n      }\n      else {\n        // Otherwise, we can just use the texture inside the original frame\n        frame_texture = (ID3D11Texture2D *) frame->data[0];\n      }\n\n      return base.init_output(frame_texture, frame->width, frame->height, colorspace);\n    }\n\n  private:\n    d3d_base_encode_device base;\n    frame_t hwframe;\n  };\n\n  class d3d_nvenc_encode_device_t: public nvenc_encode_device_t {\n  public:\n    bool\n    init_device(std::shared_ptr<platf::display_t> display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) {\n      if (base.init(display, adapter_p, pix_fmt)) return false;\n\n      auto factory = nvenc::nvenc_dynamic_factory::get();\n      if (!factory) return false;\n\n      if (pix_fmt == pix_fmt_e::yuv444p16) {\n        nvenc_d3d = factory->create_nvenc_d3d11_on_cuda(base.device.get());\n      }\n      else {\n        nvenc_d3d = factory->create_nvenc_d3d11_native(base.device.get());\n      }\n\n      if (!nvenc_d3d) return false;\n\n      buffer_format = pix_fmt;\n      nvenc = nvenc_d3d.get();\n\n      return true;\n    }\n\n    bool\n    init_encoder(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &colorspace, bool is_probe = false) override {\n      if (!nvenc_d3d) return false;\n\n      if (!nvenc_d3d->create_encoder(config::video.nv, client_config, colorspace, buffer_format)) return false;\n\n      base.apply_colorspace(colorspace);\n      return base.init_output(nvenc_d3d->get_input_texture(), client_config.width, client_config.height, colorspace, is_probe) == 0;\n    }\n\n    int\n    convert(platf::img_t &img_base) override {\n      int result = base.convert(img_base);\n      // Propagate per-frame luminance stats from GPU analyzer to encode device\n      hdr_luminance_stats = base.hdr_luminance_stats_out;\n      return result;\n    }\n\n  private:\n    d3d_base_encode_device base;\n    std::unique_ptr<nvenc::nvenc_d3d11> nvenc_d3d;\n    platf::pix_fmt_e buffer_format = platf::pix_fmt_e::unknown;\n  };\n\n  class d3d_amf_encode_device_t: public amf_encode_device_t {\n  public:\n    bool\n    init_device(std::shared_ptr<platf::display_t> display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) {\n      if (base.init(display, adapter_p, pix_fmt)) return false;\n\n      amf_d3d = ::amf::create_amf_d3d11(base.device.get());\n      if (!amf_d3d) return false;\n\n      buffer_format = pix_fmt;\n      amf = amf_d3d.get();\n\n      return true;\n    }\n\n    bool\n    init_encoder(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &colorspace, bool is_probe = false) override {\n      if (!amf_d3d) return false;\n\n      ::amf::amf_config amf_cfg;\n\n      // Pass AMF SDK integer values directly from config\n      if (client_config.videoFormat == 0) {\n        amf_cfg.usage = config::video.amd.amd_usage_h264;\n        amf_cfg.quality_preset = config::video.amd.amd_quality_h264;\n        amf_cfg.rc_mode = config::video.amd.amd_rc_h264;\n      }\n      else if (client_config.videoFormat == 1) {\n        amf_cfg.usage = config::video.amd.amd_usage_hevc;\n        amf_cfg.quality_preset = config::video.amd.amd_quality_hevc;\n        amf_cfg.rc_mode = config::video.amd.amd_rc_hevc;\n      }\n      else {\n        amf_cfg.usage = config::video.amd.amd_usage_av1;\n        amf_cfg.quality_preset = config::video.amd.amd_quality_av1;\n        amf_cfg.rc_mode = config::video.amd.amd_rc_av1;\n      }\n\n      amf_cfg.preanalysis = config::video.amd.amd_preanalysis;\n      amf_cfg.vbaq = config::video.amd.amd_vbaq;\n      amf_cfg.enforce_hrd = config::video.amd.amd_enforce_hrd;\n      amf_cfg.h264_cabac = (config::video.amd.amd_coder != 2);  // 2 = CAVLC\n      amf_cfg.max_ltr_frames = config::video.amd.amd_ltr_frames;\n      amf_cfg.qvbr_quality_level = config::video.amd.amd_qvbr_quality;\n\n      // Pre-Analysis sub-system defaults: enable PAQ + TAQ for better quality at same bitrate\n      if (amf_cfg.preanalysis && *amf_cfg.preanalysis) {\n        amf_cfg.pa_paq_mode = 1;    // CAQ (Content Adaptive Quantization)\n        amf_cfg.pa_taq_mode = 2;    // TAQ mode 2 (more aggressive temporal AQ)\n        amf_cfg.pa_caq_strength = 1;  // Medium strength\n        amf_cfg.pa_activity_type = 1; // YUV activity (better than Y-only)\n        amf_cfg.pa_high_motion_quality_boost = 1;  // Auto\n      }\n\n      // High motion quality boost at encoder level\n      amf_cfg.high_motion_quality_boost_enable = true;\n\n      // Apply server-side slices per frame override if configured\n      auto effective_config = client_config;\n      if (config::video.amd.amd_slices_per_frame > 0) {\n        effective_config.slicesPerFrame = std::max(effective_config.slicesPerFrame, config::video.amd.amd_slices_per_frame);\n      }\n\n      if (!amf_d3d->create_encoder(amf_cfg, effective_config, colorspace, buffer_format)) return false;\n\n      base.apply_colorspace(colorspace);\n      return base.init_output(static_cast<ID3D11Texture2D *>(amf_d3d->get_input_texture()), client_config.width, client_config.height, colorspace, is_probe) == 0;\n    }\n\n    int\n    convert(platf::img_t &img_base) override {\n      int result = base.convert(img_base);\n      hdr_luminance_stats = base.hdr_luminance_stats_out;\n      return result;\n    }\n\n  private:\n    d3d_base_encode_device base;\n    std::unique_ptr<::amf::amf_d3d11> amf_d3d;\n    platf::pix_fmt_e buffer_format = platf::pix_fmt_e::unknown;\n  };\n\n  bool\n  set_cursor_texture(device_t::pointer device, gpu_cursor_t &cursor, util::buffer_t<std::uint8_t> &&cursor_img, DXGI_OUTDUPL_POINTER_SHAPE_INFO &shape_info) {\n    // This cursor image may not be used\n    if (cursor_img.size() == 0) {\n      cursor.input_res.reset();\n      cursor.set_texture(0, 0, nullptr);\n      return true;\n    }\n\n    D3D11_SUBRESOURCE_DATA data {\n      std::begin(cursor_img),\n      4 * shape_info.Width,\n      0\n    };\n\n    // Create texture for cursor\n    D3D11_TEXTURE2D_DESC t {};\n    t.Width = shape_info.Width;\n    t.Height = cursor_img.size() / data.SysMemPitch;\n    t.MipLevels = 1;\n    t.ArraySize = 1;\n    t.SampleDesc.Count = 1;\n    t.Usage = D3D11_USAGE_IMMUTABLE;\n    t.Format = DXGI_FORMAT_B8G8R8A8_UNORM;\n    t.BindFlags = D3D11_BIND_SHADER_RESOURCE;\n\n    texture2d_t texture;\n    auto status = device->CreateTexture2D(&t, &data, &texture);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create mouse texture [0x\"sv << util::hex(status).to_string_view() << ']';\n      return false;\n    }\n\n    // Free resources before allocating on the next line.\n    cursor.input_res.reset();\n    status = device->CreateShaderResourceView(texture.get(), nullptr, &cursor.input_res);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create cursor shader resource view [0x\"sv << util::hex(status).to_string_view() << ']';\n      return false;\n    }\n\n    cursor.set_texture(t.Width, t.Height, std::move(texture));\n    return true;\n  }\n\n  capture_e\n  display_ddup_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) {\n    HRESULT status;\n    DXGI_OUTDUPL_FRAME_INFO frame_info;\n\n    resource_t::pointer res_p {};\n    auto capture_status = dup.next_frame(frame_info, timeout, &res_p);\n    resource_t res { res_p };\n\n    if (capture_status != capture_e::ok) {\n      return capture_status;\n    }\n\n    const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0;\n    const bool frame_update_flag = frame_info.LastPresentTime.QuadPart != 0;\n    const bool update_flag = mouse_update_flag || frame_update_flag;\n\n    if (!update_flag) {\n      return capture_e::timeout;\n    }\n\n    std::optional<std::chrono::steady_clock::time_point> frame_timestamp;\n    if (auto qpc_displayed = std::max(frame_info.LastPresentTime.QuadPart, frame_info.LastMouseUpdateTime.QuadPart)) {\n      // Translate QueryPerformanceCounter() value to steady_clock time point\n      frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), qpc_displayed);\n    }\n\n    if (frame_info.PointerShapeBufferSize > 0) {\n      DXGI_OUTDUPL_POINTER_SHAPE_INFO shape_info {};\n\n      util::buffer_t<std::uint8_t> img_data { frame_info.PointerShapeBufferSize };\n\n      UINT dummy;\n      status = dup.dup->GetFramePointerShape(img_data.size(), std::begin(img_data), &dummy, &shape_info);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to get new pointer shape [0x\"sv << util::hex(status).to_string_view() << ']';\n\n        return capture_e::error;\n      }\n\n      auto alpha_cursor_img = make_cursor_alpha_image(img_data, shape_info);\n      auto xor_cursor_img = make_cursor_xor_image(img_data, shape_info);\n\n      if (!set_cursor_texture(device.get(), cursor_alpha, std::move(alpha_cursor_img), shape_info) ||\n          !set_cursor_texture(device.get(), cursor_xor, std::move(xor_cursor_img), shape_info)) {\n        return capture_e::error;\n      }\n    }\n\n    if (frame_info.LastMouseUpdateTime.QuadPart) {\n      cursor_alpha.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y,\n        width, height, display_rotation, frame_info.PointerPosition.Visible);\n\n      cursor_xor.set_pos(frame_info.PointerPosition.Position.x, frame_info.PointerPosition.Position.y,\n        width, height, display_rotation, frame_info.PointerPosition.Visible);\n    }\n\n    const bool blend_mouse_cursor_flag = (cursor_alpha.visible || cursor_xor.visible) && cursor_visible;\n\n    texture2d_t src {};\n    if (frame_update_flag) {\n      // Get the texture object from this frame\n      status = res->QueryInterface(IID_ID3D11Texture2D, (void **) &src);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Couldn't query interface [0x\"sv << util::hex(status).to_string_view() << ']';\n        return capture_e::error;\n      }\n\n      D3D11_TEXTURE2D_DESC desc;\n      src->GetDesc(&desc);\n\n      // It's possible for our display enumeration to race with mode changes and result in\n      // mismatched image pool and desktop texture sizes. If this happens, just reinit again.\n      if (desc.Width != width_before_rotation || desc.Height != height_before_rotation) {\n        BOOST_LOG(info) << \"Capture size changed [\"sv << width << 'x' << height << \" -> \"sv << desc.Width << 'x' << desc.Height << ']';\n        return capture_e::reinit;\n      }\n\n      // If we don't know the capture format yet, grab it from this texture\n      if (capture_format == DXGI_FORMAT_UNKNOWN) {\n        capture_format = desc.Format;\n        BOOST_LOG(info) << \"Capture format [\"sv << dxgi_format_to_string(capture_format) << ']';\n      }\n\n      // It's also possible for the capture format to change on the fly. If that happens,\n      // reinitialize capture to try format detection again and create new images.\n      if (capture_format != desc.Format) {\n        BOOST_LOG(info) << \"Capture format changed [\"sv << dxgi_format_to_string(capture_format) << \" -> \"sv << dxgi_format_to_string(desc.Format) << ']';\n        return capture_e::reinit;\n      }\n    }\n\n    enum class lfa {\n      nothing,\n      replace_surface_with_img,\n      replace_img_with_surface,\n      copy_src_to_img,\n      copy_src_to_surface,\n    };\n\n    enum class ofa {\n      forward_last_img,\n      copy_last_surface_and_blend_cursor,\n      dummy_fallback,\n    };\n\n    auto last_frame_action = lfa::nothing;\n    auto out_frame_action = ofa::dummy_fallback;\n\n    if (capture_format == DXGI_FORMAT_UNKNOWN) {\n      // We don't know the final capture format yet, so we will encode a black dummy image\n      last_frame_action = lfa::nothing;\n      out_frame_action = ofa::dummy_fallback;\n    }\n    else {\n      if (src) {\n        // We got a new frame from DesktopDuplication...\n        if (blend_mouse_cursor_flag) {\n          // ...and we need to blend the mouse cursor onto it.\n          // Optimization: Directly copy to target image and blend cursor, avoiding intermediate surface copy.\n          // This reduces one texture copy operation when we have a new frame.\n          // We still use intermediate surface for cursor-only updates (no new frame) to avoid\n          // memory overhead from sharing images between direct3d devices.\n          last_frame_action = lfa::copy_src_to_img;\n          // Blend cursor directly onto the image we just copied to.\n          out_frame_action = ofa::copy_last_surface_and_blend_cursor;  // Reused for direct blend\n        }\n        else {\n          // ...and we don't need to blend the mouse cursor.\n          // Copy the frame to a new image from pull_free_image_cb and save the shared pointer to the image\n          // in case the mouse cursor appears without a new frame from DesktopDuplication.\n          last_frame_action = lfa::copy_src_to_img;\n          // Use saved last image shared pointer as output image evading copy.\n          out_frame_action = ofa::forward_last_img;\n        }\n      }\n      else if (!std::holds_alternative<std::monostate>(last_frame_variant)) {\n        // We didn't get a new frame from DesktopDuplication...\n        if (blend_mouse_cursor_flag) {\n          // ...but we need to blend the mouse cursor.\n          if (std::holds_alternative<std::shared_ptr<platf::img_t>>(last_frame_variant)) {\n            // We have the shared pointer of the last image, replace it with intermediate surface\n            // while copying contents so we can blend this and future mouse cursor updates.\n            last_frame_action = lfa::replace_img_with_surface;\n          }\n          // Copy the intermediate surface which contains last DesktopDuplication frame\n          // to a new image from pull_free_image_cb and blend the mouse cursor onto it.\n          out_frame_action = ofa::copy_last_surface_and_blend_cursor;\n        }\n        else {\n          // ...and we don't need to blend the mouse cursor.\n          // This happens when the mouse cursor disappears from screen,\n          // or there's mouse cursor on screen, but its drawing is disabled in sunshine.\n          if (std::holds_alternative<texture2d_t>(last_frame_variant)) {\n            // We have the intermediate surface that was used as the mouse cursor blending base.\n            // Replace it with an image from pull_free_image_cb copying contents and freeing up the surface memory.\n            // Save the shared pointer to the image in case the mouse cursor reappears.\n            last_frame_action = lfa::replace_surface_with_img;\n          }\n          // Use saved last image shared pointer as output image evading copy.\n          out_frame_action = ofa::forward_last_img;\n        }\n      }\n    }\n\n    auto create_surface = [&](texture2d_t &surface) -> bool {\n      // Try to reuse the old surface if it hasn't been destroyed yet.\n      if (old_surface_delayed_destruction) {\n        surface.reset(old_surface_delayed_destruction.release());\n        return true;\n      }\n\n      // Otherwise create a new surface.\n      D3D11_TEXTURE2D_DESC t {};\n      t.Width = width_before_rotation;\n      t.Height = height_before_rotation;\n      t.MipLevels = 1;\n      t.ArraySize = 1;\n      t.SampleDesc.Count = 1;\n      t.Usage = D3D11_USAGE_DEFAULT;\n      t.Format = capture_format;\n      t.BindFlags = 0;\n      status = device->CreateTexture2D(&t, nullptr, &surface);\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to create frame copy texture [0x\"sv << util::hex(status).to_string_view() << ']';\n        return false;\n      }\n\n      return true;\n    };\n\n    auto get_locked_d3d_img = [&](std::shared_ptr<platf::img_t> &img, bool dummy = false) -> std::tuple<std::shared_ptr<img_d3d_t>, texture_lock_helper> {\n      auto d3d_img = std::static_pointer_cast<img_d3d_t>(img);\n\n      // Finish creating the image (if it hasn't happened already),\n      // also creates synchronization primitives for shared access from multiple direct3d devices.\n      if (complete_img(d3d_img.get(), dummy)) return { nullptr, nullptr };\n\n      // This image is shared between capture direct3d device and encoders direct3d devices,\n      // we must acquire lock before doing anything to it.\n      texture_lock_helper lock_helper(d3d_img->capture_mutex.get());\n      if (!lock_helper.lock()) {\n        BOOST_LOG(error) << \"Failed to lock capture texture\";\n        return { nullptr, nullptr };\n      }\n\n      // Clear the blank flag now that we're ready to capture into the image\n      d3d_img->blank = false;\n\n      return { std::move(d3d_img), std::move(lock_helper) };\n    };\n\n    switch (last_frame_action) {\n      case lfa::nothing: {\n        break;\n      }\n\n      case lfa::replace_surface_with_img: {\n        auto p_surface = std::get_if<texture2d_t>(&last_frame_variant);\n        if (!p_surface) {\n          BOOST_LOG(error) << \"Logical error at \" << __FILE__ << \":\" << __LINE__;\n          return capture_e::error;\n        }\n\n        std::shared_ptr<platf::img_t> img;\n        if (!pull_free_image_cb(img)) return capture_e::interrupted;\n\n        auto [d3d_img, lock] = get_locked_d3d_img(img);\n        if (!d3d_img) return capture_e::error;\n\n        device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get());\n\n        // We delay the destruction of intermediate surface in case the mouse cursor reappears shortly.\n        old_surface_delayed_destruction.reset(p_surface->release());\n        old_surface_timestamp = std::chrono::steady_clock::now();\n\n        last_frame_variant = img;\n        break;\n      }\n\n      case lfa::replace_img_with_surface: {\n        auto p_img = std::get_if<std::shared_ptr<platf::img_t>>(&last_frame_variant);\n        if (!p_img) {\n          BOOST_LOG(error) << \"Logical error at \" << __FILE__ << \":\" << __LINE__;\n          return capture_e::error;\n        }\n        auto [d3d_img, lock] = get_locked_d3d_img(*p_img);\n        if (!d3d_img) return capture_e::error;\n\n        p_img = nullptr;\n        last_frame_variant = texture2d_t {};\n        auto &surface = std::get<texture2d_t>(last_frame_variant);\n        if (!create_surface(surface)) return capture_e::error;\n\n        device_ctx->CopyResource(surface.get(), d3d_img->capture_texture.get());\n        break;\n      }\n\n      case lfa::copy_src_to_img: {\n        last_frame_variant = {};\n\n        std::shared_ptr<platf::img_t> img;\n        if (!pull_free_image_cb(img)) return capture_e::interrupted;\n\n        auto [d3d_img, lock] = get_locked_d3d_img(img);\n        if (!d3d_img) return capture_e::error;\n\n        device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get());\n        last_frame_variant = img;\n        break;\n      }\n\n      case lfa::copy_src_to_surface: {\n        auto p_surface = std::get_if<texture2d_t>(&last_frame_variant);\n        if (!p_surface) {\n          last_frame_variant = texture2d_t {};\n          p_surface = std::get_if<texture2d_t>(&last_frame_variant);\n          if (!create_surface(*p_surface)) return capture_e::error;\n        }\n        device_ctx->CopyResource(p_surface->get(), src.get());\n        break;\n      }\n    }\n\n    auto blend_cursor = [&](img_d3d_t &d3d_img) {\n      device_ctx->VSSetShader(cursor_vs.get(), nullptr, 0);\n      device_ctx->PSSetShader(cursor_ps.get(), nullptr, 0);\n      device_ctx->OMSetRenderTargets(1, &d3d_img.capture_rt, nullptr);\n\n      if (cursor_alpha.texture.get()) {\n        // Perform an alpha blending operation\n        device_ctx->OMSetBlendState(blend_alpha.get(), nullptr, 0xFFFFFFFFu);\n\n        device_ctx->PSSetShaderResources(0, 1, &cursor_alpha.input_res);\n        device_ctx->RSSetViewports(1, &cursor_alpha.cursor_view);\n        device_ctx->Draw(3, 0);\n      }\n\n      if (cursor_xor.texture.get()) {\n        // Perform an invert blending without touching alpha values\n        device_ctx->OMSetBlendState(blend_invert.get(), nullptr, 0x00FFFFFFu);\n\n        device_ctx->PSSetShaderResources(0, 1, &cursor_xor.input_res);\n        device_ctx->RSSetViewports(1, &cursor_xor.cursor_view);\n        device_ctx->Draw(3, 0);\n      }\n\n      device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);\n\n      ID3D11RenderTargetView *emptyRenderTarget = nullptr;\n      device_ctx->OMSetRenderTargets(1, &emptyRenderTarget, nullptr);\n      device_ctx->RSSetViewports(0, nullptr);\n      ID3D11ShaderResourceView *emptyShaderResourceView = nullptr;\n      device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView);\n    };\n\n    switch (out_frame_action) {\n      case ofa::forward_last_img: {\n        auto p_img = std::get_if<std::shared_ptr<platf::img_t>>(&last_frame_variant);\n        if (!p_img) {\n          BOOST_LOG(error) << \"Logical error at \" << __FILE__ << \":\" << __LINE__;\n          return capture_e::error;\n        }\n        img_out = *p_img;\n        break;\n      }\n\n      case ofa::copy_last_surface_and_blend_cursor: {\n        if (!blend_mouse_cursor_flag) {\n          BOOST_LOG(error) << \"Logical error at \" << __FILE__ << \":\" << __LINE__;\n          return capture_e::error;\n        }\n\n        // Optimization: Check if we have an image (from direct copy) or surface (from previous frame)\n        auto p_img = std::get_if<std::shared_ptr<platf::img_t>>(&last_frame_variant);\n        auto p_surface = std::get_if<texture2d_t>(&last_frame_variant);\n        \n        if (p_img && p_img->use_count() == 1) {\n          // Optimization: We have a direct image copy that's not yet used by encoder,\n          // blend cursor directly onto it to avoid an extra texture copy.\n          img_out = *p_img;\n          auto d3d_img = std::static_pointer_cast<img_d3d_t>(img_out);\n          texture_lock_helper lock_helper(d3d_img->capture_mutex.get());\n          if (!lock_helper.lock()) {\n            BOOST_LOG(error) << \"Failed to lock capture texture for cursor blend\";\n            return capture_e::error;\n          }\n          blend_cursor(*d3d_img);\n        }\n        else if (p_surface) {\n          // We have an intermediate surface, copy it first then blend\n          if (!pull_free_image_cb(img_out)) return capture_e::interrupted;\n\n          auto [d3d_img, lock] = get_locked_d3d_img(img_out);\n          if (!d3d_img) return capture_e::error;\n\n          device_ctx->CopyResource(d3d_img->capture_texture.get(), p_surface->get());\n          blend_cursor(*d3d_img);\n        }\n        else if (p_img) {\n          // Image is already in use by encoder, we need to get a new one\n          // This case is rare and indicates the image was consumed before cursor blend\n          // Fall back to creating a surface and copying (original behavior)\n          if (!pull_free_image_cb(img_out)) return capture_e::interrupted;\n\n          auto [d3d_img, lock] = get_locked_d3d_img(img_out);\n          if (!d3d_img) return capture_e::error;\n\n          // Copy from the image that's in use (it should still be valid for reading)\n          // Note: This creates an extra copy, but it's a rare edge case\n          auto d3d_img_src = std::static_pointer_cast<img_d3d_t>(*p_img);\n          texture_lock_helper src_lock_helper(d3d_img_src->capture_mutex.get());\n          if (src_lock_helper.lock()) {\n            device_ctx->CopyResource(d3d_img->capture_texture.get(), d3d_img_src->capture_texture.get());\n          }\n          blend_cursor(*d3d_img);\n        }\n        else {\n          BOOST_LOG(error) << \"Logical error at \" << __FILE__ << \":\" << __LINE__;\n          return capture_e::error;\n        }\n        break;\n      }\n\n      case ofa::dummy_fallback: {\n        if (!pull_free_image_cb(img_out)) return capture_e::interrupted;\n\n        // Clear the image if it has been used as a dummy.\n        // It can have the mouse cursor blended onto it.\n        auto old_d3d_img = (img_d3d_t *) img_out.get();\n        bool reclear_dummy = !old_d3d_img->blank && old_d3d_img->capture_texture;\n\n        auto [d3d_img, lock] = get_locked_d3d_img(img_out, true);\n        if (!d3d_img) return capture_e::error;\n\n        if (reclear_dummy) {\n          const float rgb_black[] = { 0.0f, 0.0f, 0.0f, 0.0f };\n          device_ctx->ClearRenderTargetView(d3d_img->capture_rt.get(), rgb_black);\n        }\n\n        if (blend_mouse_cursor_flag) {\n          blend_cursor(*d3d_img);\n        }\n\n        break;\n      }\n    }\n\n    // Perform delayed destruction of the unused surface if the time is due.\n    if (old_surface_delayed_destruction && old_surface_timestamp + 10s < std::chrono::steady_clock::now()) {\n      old_surface_delayed_destruction.reset();\n    }\n\n    if (img_out) {\n      img_out->frame_timestamp = frame_timestamp;\n    }\n\n    return capture_e::ok;\n  }\n\n  capture_e\n  display_ddup_vram_t::release_snapshot() {\n    return dup.release_frame();\n  }\n\n  int\n  display_ddup_vram_t::init(const ::video::config_t &config, const std::string &display_name) {\n    if (display_base_t::init(config, display_name) || dup.init(this, config)) {\n      return -1;\n    }\n\n    D3D11_SAMPLER_DESC sampler_desc {};\n    sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;\n    sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;\n    sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;\n    sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;\n    sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER;\n    sampler_desc.MinLOD = 0;\n    sampler_desc.MaxLOD = D3D11_FLOAT32_MAX;\n\n    auto status = device->CreateSamplerState(&sampler_desc, &sampler_linear);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create linear sampler state [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n\n    sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;\n    status = device->CreateSamplerState(&sampler_desc, &sampler_point);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create point sampler state [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n\n    status = device->CreateVertexShader(cursor_vs_hlsl->GetBufferPointer(), cursor_vs_hlsl->GetBufferSize(), nullptr, &cursor_vs);\n    if (status) {\n      BOOST_LOG(error) << \"Failed to create scene vertex shader [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n\n    {\n      int32_t rotation_modifier = display_rotation == DXGI_MODE_ROTATION_UNSPECIFIED ? 0 : display_rotation - 1;\n      int32_t rotation_data[16 / sizeof(int32_t)] { rotation_modifier };  // aligned to 16-byte\n      auto rotation = make_buffer(device.get(), rotation_data);\n      if (!rotation) {\n        BOOST_LOG(error) << \"Failed to create display rotation vertex constant buffer\";\n        return -1;\n      }\n      device_ctx->VSSetConstantBuffers(2, 1, &rotation);\n    }\n\n    if (config.dynamicRange && is_hdr()) {\n      // This shader will normalize scRGB white levels to a user-defined white level\n      status = device->CreatePixelShader(cursor_ps_normalize_white_hlsl->GetBufferPointer(), cursor_ps_normalize_white_hlsl->GetBufferSize(), nullptr, &cursor_ps);\n      if (status) {\n        BOOST_LOG(error) << \"Failed to create cursor blending (normalized white) pixel shader [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n\n      // Use a 300 nit target for the mouse cursor. We should really get\n      // the user's SDR white level in nits, but there is no API that\n      // provides that information to Win32 apps.\n      float white_multiplier_data[16 / sizeof(float)] { 300.0f / 80.f };  // aligned to 16-byte\n      auto white_multiplier = make_buffer(device.get(), white_multiplier_data);\n      if (!white_multiplier) {\n        BOOST_LOG(warning) << \"Failed to create cursor blending (normalized white) white multiplier constant buffer\";\n        return -1;\n      }\n\n      device_ctx->PSSetConstantBuffers(1, 1, &white_multiplier);\n    }\n    else {\n      status = device->CreatePixelShader(cursor_ps_hlsl->GetBufferPointer(), cursor_ps_hlsl->GetBufferSize(), nullptr, &cursor_ps);\n      if (status) {\n        BOOST_LOG(error) << \"Failed to create cursor blending pixel shader [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n    }\n\n    blend_alpha = make_blend(device.get(), true, false);\n    blend_invert = make_blend(device.get(), true, true);\n    blend_disable = make_blend(device.get(), false, false);\n\n    if (!blend_disable || !blend_alpha || !blend_invert) {\n      return -1;\n    }\n\n    device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0xFFFFFFFFu);\n    ID3D11SamplerState *samplers[] = { sampler_linear.get(), sampler_point.get() };\n    device_ctx->PSSetSamplers(0, 2, samplers);\n    device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);\n\n    return 0;\n  }\n\n  int\n  display_amd_vram_t::init(const ::video::config_t &config, const std::string &display_name) {\n    if (display_base_t::init(config, display_name) || dup.init(this, config, output_index)) {\n      BOOST_LOG(error) << \"AMD VRAM() failed\";\n      return -1;\n    }\n    \n    auto status = device->CreateVertexShader(simple_cursor_vs_hlsl->GetBufferPointer(), simple_cursor_vs_hlsl->GetBufferSize(), nullptr, &cursor_vs);\n    if (status) {\n      BOOST_LOG(error) << \"Failed to create simple cursor vertex shader [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n    status = device->CreatePixelShader(simple_cursor_ps_hlsl->GetBufferPointer(), simple_cursor_ps_hlsl->GetBufferSize(), nullptr, &cursor_ps);\n    if (status) {\n      BOOST_LOG(error) << \"Failed to create simple cursor pixel shader [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n    \n    blend_invert = make_blend(device.get(), true, true);\n    blend_disable = make_blend(device.get(), false, false);\n\n    if (!blend_disable || !blend_invert) {\n      return -1;\n    }\n    \n    D3D11_BUFFER_DESC buffer_desc {\n      sizeof(float[16 / sizeof(float)]),\n      D3D11_USAGE_DEFAULT,\n      D3D11_BIND_CONSTANT_BUFFER,\n      0\n    };\n\n    buf_t::pointer cursor_info_p;\n    status = device->CreateBuffer(&buffer_desc, nullptr, &cursor_info_p);\n    if (status) {\n      BOOST_LOG(error) << \"Failed to create cursor position buffer: [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n    cursor_info = buf_t { cursor_info_p };\n\n    return 0;\n  }\n\n  /**\n   * @brief Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture.\n   * @param pull_free_image_cb call this to get a new free image from the video subsystem.\n   * @param img_out the captured frame is returned here\n   * @param timeout how long to wait for the next frame\n   * @param cursor_visible whether to capture the cursor\n   */\n  capture_e\n  display_amd_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) {\n    amf::AMFSurfacePtr output;\n    D3D11_TEXTURE2D_DESC desc;\n\n    CURSORINFO pt;\n    pt.cbSize = sizeof(CURSORINFO);\n\n    // Check for display configuration change\n    auto capture_status = dup.next_frame(timeout, (amf::AMFData **) &output);\n    if (capture_status != capture_e::ok) {\n      return capture_status;\n    }\n    dup.capturedSurface = output;\n\n    texture2d_t src = (ID3D11Texture2D *) dup.capturedSurface->GetPlaneAt(0)->GetNative();\n    src->GetDesc(&desc);\n\n    // It's possible for our display enumeration to race with mode changes and result in\n    // mismatched image pool and desktop texture sizes. If this happens, just reinit again.\n    if (desc.Width != width_before_rotation || desc.Height != height_before_rotation) {\n      BOOST_LOG(info) << \"Capture size changed [\"sv << width << 'x' << height << \" -> \"sv << desc.Width << 'x' << desc.Height << ']';\n      return capture_e::reinit;\n    }\n\n    // If we don't know the capture format yet, grab it from this texture\n    if (capture_format == DXGI_FORMAT_UNKNOWN) {\n      capture_format = desc.Format;\n      BOOST_LOG(info) << \"AMD Capture format [\"sv << dxgi_format_to_string(capture_format) << ']';\n    }\n\n    // It's also possible for the capture format to change on the fly. If that happens,\n    // reinitialize capture to try format detection again and create new images.\n    if (capture_format != desc.Format) {\n      BOOST_LOG(info) << \"AMD Capture format changed [\"sv << dxgi_format_to_string(capture_format) << \" -> \"sv << dxgi_format_to_string(desc.Format) << ']';\n      return capture_e::reinit;\n    }\n\n    std::shared_ptr<platf::img_t> img;\n    if (!pull_free_image_cb(img))\n      return capture_e::interrupted;\n    \n    auto blend_cursor = [&](img_d3d_t &d3d_img) {\n      float new_cursor_data[16/ sizeof(float)] = { (float)pt.ptScreenPos.x, (float)pt.ptScreenPos.y, (float)width, (float)height };\n      device_ctx->UpdateSubresource(cursor_info.get(), 0, nullptr, &new_cursor_data, 0, 0);\n      \n      device_ctx->VSSetConstantBuffers(0, 1, &cursor_info);\n      device_ctx->VSSetShader(cursor_vs.get(), nullptr, 0);\n      device_ctx->PSSetShader(cursor_ps.get(), nullptr, 0);\n      device_ctx->OMSetRenderTargets(1, &d3d_img.capture_rt, nullptr);\n      device_ctx->IASetInputLayout(nullptr);\n      device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);\n      device_ctx->OMSetBlendState(blend_invert.get(), nullptr, 0x00FFFFFFu);\n\n      device_ctx->Draw(3, 0);\n\n      ID3D11RenderTargetView *emptyRenderTarget = nullptr;\n      device_ctx->OMSetRenderTargets(1, &emptyRenderTarget, nullptr);\n      device_ctx->RSSetViewports(0, nullptr);\n      ID3D11ShaderResourceView *emptyShaderResourceView = nullptr;\n      device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView);\n        device_ctx->OMSetBlendState(blend_disable.get(), nullptr, 0x00FFFFFFu);\n    };\n\n    auto d3d_img = std::static_pointer_cast<img_d3d_t>(img);\n    d3d_img->blank = false;  // image is always ready for capture\n    if (complete_img(d3d_img.get(), false) == 0) {\n      texture_lock_helper lock_helper(d3d_img->capture_mutex.get());\n      if (lock_helper.lock()) {\n        device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get());\n        if (config::input.amf_draw_mouse_cursor) {\n          GetCursorInfo(&pt);\n          if (pt.flags == CURSOR_SHOWING) {\n            blend_cursor(*d3d_img);\n          }\n        }\n      \n      }\n      else {\n        return capture_e::error;\n      }\n    }\n    else {\n      return capture_e::error;\n    }\n    \n    img_out = img;\n    if (img_out) {\n      img_out->frame_timestamp = std::chrono::steady_clock::now();\n    }\n\n    src.release();\n    return capture_e::ok;\n  }\n\n  capture_e\n  display_amd_vram_t::release_snapshot() {\n    dup.release_frame();\n    return capture_e::ok;\n  }\n\n  /**\n   * Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture.\n   * @param pull_free_image_cb call this to get a new free image from the video subsystem.\n   * @param img_out the captured frame is returned here\n   * @param timeout how long to wait for the next frame\n   * @param cursor_visible\n   */\n  capture_e\n  display_wgc_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) {\n    // Check if window is still valid (if capturing a window)\n    // If window becomes invalid (closed, minimized, hidden), fall back to display capture\n    if (!dup.is_window_valid()) {\n      BOOST_LOG(warning) << \"Captured window is no longer valid (closed, minimized, or hidden), falling back to display capture\"sv;\n      return capture_e::reinit;\n    }\n    \n    texture2d_t src;\n    uint64_t frame_qpc;\n    dup.set_cursor_visible(cursor_visible);\n    auto capture_status = dup.next_frame(timeout, &src, frame_qpc);\n    if (capture_status != capture_e::ok) {\n      // If we're capturing a window and getting timeouts/errors, check if window is still valid\n      if (dup.captured_window_hwnd != nullptr) {\n        // Simplified: Any error or timeout means window might have changed, check validity\n        if (!dup.is_window_valid()) {\n          BOOST_LOG(warning) << \"Captured window is no longer valid, reinitializing capture\"sv;\n          return capture_e::reinit;\n        }\n      }\n      return capture_status;\n    }\n\n    auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc);\n    D3D11_TEXTURE2D_DESC desc;\n    src->GetDesc(&desc);\n\n    // Get the actual captured frame dimensions\n    int frame_width = static_cast<int>(desc.Width);\n    int frame_height = static_cast<int>(desc.Height);\n    \n    // For window capture, check if size changed and handle it\n    if (dup.captured_window_hwnd != nullptr) {\n      int expected_width = dup.window_capture_width > 0 ? dup.window_capture_width : width_before_rotation;\n      int expected_height = dup.window_capture_height > 0 ? dup.window_capture_height : height_before_rotation;\n      \n      if (frame_width != expected_width || frame_height != expected_height) {\n        BOOST_LOG(info) << \"Window capture size changed [\"sv << expected_width << 'x' << expected_height \n                         << \" -> \"sv << frame_width << 'x' << frame_height << ']';\n        // Update stored dimensions\n        dup.window_capture_width = frame_width;\n        dup.window_capture_height = frame_height;\n        // Trigger reinit to recreate all resources (images, textures, etc.) with new size\n        return capture_e::reinit;\n      }\n    }\n    else {\n      // For display capture with WGC, the frame dimensions are in \"display orientation\"\n      // (i.e., after rotation). Our `width`/`height` are derived from DesktopCoordinates\n      // and match that orientation. Using width_before_rotation/height_before_rotation\n      // here can cause an infinite reinit loop on rotation.\n      if (frame_width != width || frame_height != height) {\n        BOOST_LOG(info) << \"Capture size changed [\"sv << width << 'x' << height << \" -> \"sv << frame_width << 'x' << frame_height << ']';\n        return capture_e::reinit;\n      }\n    }\n\n    // It's also possible for the capture format to change on the fly. If that happens,\n    // reinitialize capture to try format detection again and create new images.\n    if (capture_format != desc.Format) {\n      BOOST_LOG(info) << \"Capture format changed [\"sv << dxgi_format_to_string(capture_format) << \" -> \"sv << dxgi_format_to_string(desc.Format) << ']';\n      return capture_e::reinit;\n    }\n\n    std::shared_ptr<platf::img_t> img;\n    if (!pull_free_image_cb(img))\n      return capture_e::interrupted;\n\n    auto d3d_img = std::static_pointer_cast<img_d3d_t>(img);\n    d3d_img->blank = false;  // image is always ready for capture\n    if (complete_img(d3d_img.get(), false) == 0) {\n      texture_lock_helper lock_helper(d3d_img->capture_mutex.get());\n      if (lock_helper.lock()) {\n        device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get());\n      }\n      else {\n        BOOST_LOG(error) << \"Failed to lock capture texture\";\n        return capture_e::error;\n      }\n    }\n    else {\n      return capture_e::error;\n    }\n    img_out = img;\n    if (img_out) {\n      img_out->frame_timestamp = frame_timestamp;\n    }\n\n    return capture_e::ok;\n  }\n\n  capture_e\n  display_wgc_vram_t::release_snapshot() {\n    return dup.release_frame();\n  }\n\n  std::shared_ptr<platf::img_t>\n  display_wgc_vram_t::alloc_img() {\n    auto img = std::make_shared<img_d3d_t>();\n    \n    // For window capture, use window capture dimensions; for display capture, use display dimensions\n    int img_width = dup.window_capture_width > 0 ? dup.window_capture_width : width;\n    int img_height = dup.window_capture_height > 0 ? dup.window_capture_height : height;\n    \n    img->width = img_width;\n    img->height = img_height;\n    img->id = next_image_id++;\n    img->blank = true;\n\n    return img;\n  }\n\n  int\n  display_wgc_vram_t::init(const ::video::config_t &config, const std::string &display_name) {\n    if (display_base_t::init(config, display_name) || dup.init(this, config))\n      return -1;\n\n    // WGC frames are typically delivered in the current display orientation.\n    // The DXGI rotation flag comes from the output descriptor and is needed for DDX,\n    // but for WGC it can lead to applying rotation twice (client sees flipped/stretched).\n    if (display_rotation != DXGI_MODE_ROTATION_UNSPECIFIED &&\n        display_rotation != DXGI_MODE_ROTATION_IDENTITY) {\n      BOOST_LOG(info) << \"WGC: disabling DXGI rotation handling for oriented frames\";\n      display_rotation = DXGI_MODE_ROTATION_UNSPECIFIED;\n      width_before_rotation = width;\n      height_before_rotation = height;\n    }\n\n    return 0;\n  }\n\n  std::shared_ptr<platf::img_t>\n  display_vram_t::alloc_img() {\n    auto img = std::make_shared<img_d3d_t>();\n\n    // Initialize format-independent fields\n    img->width = width_before_rotation;\n    img->height = height_before_rotation;\n    img->id = next_image_id++;\n    img->blank = true;\n\n    return img;\n  }\n\n  // This cannot use ID3D11DeviceContext because it can be called concurrently by the encoding thread\n  int\n  display_vram_t::complete_img(platf::img_t *img_base, bool dummy) {\n    auto img = (img_d3d_t *) img_base;\n\n    // If this already has a capture texture and it's not switching dummy state, nothing to do\n    if (img->capture_texture && img->dummy == dummy) {\n      return 0;\n    }\n\n    // If this is not a dummy image, we must know the format by now\n    if (!dummy && capture_format == DXGI_FORMAT_UNKNOWN) {\n      BOOST_LOG(error) << \"display_vram_t::complete_img() called with unknown capture format!\";\n      return -1;\n    }\n\n    // Reset the image (in case this was previously a dummy)\n    img->capture_texture.reset();\n    img->capture_rt.reset();\n    img->capture_mutex.reset();\n    img->data = nullptr;\n    if (img->encoder_texture_handle) {\n      CloseHandle(img->encoder_texture_handle);\n      img->encoder_texture_handle = NULL;\n    }\n\n    // Initialize format-dependent fields\n    img->pixel_pitch = get_pixel_pitch();\n    img->row_pitch = img->pixel_pitch * img->width;\n    img->dummy = dummy;\n    img->format = (capture_format == DXGI_FORMAT_UNKNOWN) ? DXGI_FORMAT_B8G8R8A8_UNORM : capture_format;\n    img->linear_gamma = capture_linear_gamma;\n\n    D3D11_TEXTURE2D_DESC t {};\n    t.Width = img->width;\n    t.Height = img->height;\n    t.MipLevels = 1;\n    t.ArraySize = 1;\n    t.SampleDesc.Count = 1;\n    t.Usage = D3D11_USAGE_DEFAULT;\n    t.Format = img->format;\n    t.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;\n    t.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;\n\n    auto status = device->CreateTexture2D(&t, nullptr, &img->capture_texture);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create img buf texture [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n\n    status = device->CreateRenderTargetView(img->capture_texture.get(), nullptr, &img->capture_rt);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create render target view [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n\n    // Get the keyed mutex to synchronize with the encoding code\n    status = img->capture_texture->QueryInterface(__uuidof(IDXGIKeyedMutex), (void **) &img->capture_mutex);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to query IDXGIKeyedMutex [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n\n    resource1_t resource;\n    status = img->capture_texture->QueryInterface(__uuidof(IDXGIResource1), (void **) &resource);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to query IDXGIResource1 [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n\n    // Create a handle for the encoder device to use to open this texture\n    status = resource->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ, nullptr, &img->encoder_texture_handle);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to create shared texture handle [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n\n    img->data = (std::uint8_t *) img->capture_texture.get();\n\n    return 0;\n  }\n\n  // This cannot use ID3D11DeviceContext because it can be called concurrently by the encoding thread\n  /**\n   * @memberof platf::dxgi::display_vram_t\n   */\n  int\n  display_vram_t::dummy_img(platf::img_t *img_base) {\n    return complete_img(img_base, true);\n  }\n\n  std::vector<DXGI_FORMAT>\n  display_vram_t::get_supported_capture_formats() {\n    return {\n      // scRGB FP16 is the ideal format for Wide Color Gamut and Advanced Color\n      // displays (both SDR and HDR). This format uses linear gamma, so we will\n      // use a linear->PQ shader for HDR and a linear->sRGB shader for SDR.\n      DXGI_FORMAT_R16G16B16A16_FLOAT,\n\n      // DXGI_FORMAT_R10G10B10A2_UNORM seems like it might give us frames already\n      // converted to SMPTE 2084 PQ, however it seems to actually just clamp the\n      // scRGB FP16 values that DWM is using when the desktop format is scRGB FP16.\n      //\n      // If there is a case where the desktop format is really SMPTE 2084 PQ, it\n      // might make sense to support capturing it without conversion to scRGB,\n      // but we avoid it for now.\n\n      // We include the 8-bit modes too for when the display is in SDR mode,\n      // while the client stream is HDR-capable. These UNORM formats can\n      // use our normal pixel shaders that expect sRGB input.\n      DXGI_FORMAT_B8G8R8A8_UNORM,\n      DXGI_FORMAT_B8G8R8X8_UNORM,\n      DXGI_FORMAT_R8G8B8A8_UNORM,\n    };\n  }\n\n  /**\n   * @brief Check that a given codec is supported by the display device.\n   * @param name The FFmpeg codec name (or similar for non-FFmpeg codecs).\n   * @param config The codec configuration.\n   * @return `true` if supported, `false` otherwise.\n   */\n  bool\n  display_vram_t::is_codec_supported(std::string_view name, const ::video::config_t &config) {\n    DXGI_ADAPTER_DESC adapter_desc;\n    adapter->GetDesc(&adapter_desc);\n\n    if (adapter_desc.VendorId == 0x1002) {  // AMD\n      // If it's not an AMF encoder, it's not compatible with an AMD GPU\n      if (!boost::algorithm::ends_with(name, \"_amf\")) {\n        return false;\n      }\n\n      // Perform AMF version checks if we're using an AMD GPU. This check is placed in display_vram_t\n      // to avoid hitting the display_ram_t path which uses software encoding and doesn't touch AMF.\n      HMODULE amfrt = LoadLibraryW(AMF_DLL_NAME);\n      if (amfrt) {\n        auto unload_amfrt = util::fail_guard([amfrt]() {\n          FreeLibrary(amfrt);\n        });\n\n        auto fnAMFQueryVersion = (AMFQueryVersion_Fn) GetProcAddress(amfrt, AMF_QUERY_VERSION_FUNCTION_NAME);\n        if (fnAMFQueryVersion) {\n          amf_uint64 version;\n          auto result = fnAMFQueryVersion(&version);\n          if (result == AMF_OK) {\n            if (config.videoFormat == 2 && version < AMF_MAKE_FULL_VERSION(1, 4, 30, 0)) {\n              // AMF 1.4.30 adds ultra low latency mode for AV1. Don't use AV1 on earlier versions.\n              // This corresponds to driver version 23.5.2 (23.10.01.45) or newer.\n              BOOST_LOG(warning) << \"AV1 encoding is disabled on AMF version \"sv\n                                 << AMF_GET_MAJOR_VERSION(version) << '.'\n                                 << AMF_GET_MINOR_VERSION(version) << '.'\n                                 << AMF_GET_SUBMINOR_VERSION(version) << '.'\n                                 << AMF_GET_BUILD_VERSION(version);\n              BOOST_LOG(warning) << \"If your AMD GPU supports AV1 encoding, update your graphics drivers!\"sv;\n              return false;\n            }\n            else if (config.dynamicRange && version < AMF_MAKE_FULL_VERSION(1, 4, 23, 0)) {\n              // Older versions of the AMD AMF runtime can crash when fed P010 surfaces.\n              // Fail if AMF version is below 1.4.23 where HEVC Main10 encoding was introduced.\n              // AMF 1.4.23 corresponds to driver version 21.12.1 (21.40.11.03) or newer.\n              BOOST_LOG(warning) << \"HDR encoding is disabled on AMF version \"sv\n                                 << AMF_GET_MAJOR_VERSION(version) << '.'\n                                 << AMF_GET_MINOR_VERSION(version) << '.'\n                                 << AMF_GET_SUBMINOR_VERSION(version) << '.'\n                                 << AMF_GET_BUILD_VERSION(version);\n              BOOST_LOG(warning) << \"If your AMD GPU supports HEVC Main10 encoding, update your graphics drivers!\"sv;\n              return false;\n            }\n          }\n          else {\n            BOOST_LOG(warning) << \"AMFQueryVersion() failed: \"sv << result;\n          }\n        }\n        else {\n          BOOST_LOG(warning) << \"AMF DLL missing export: \"sv << AMF_QUERY_VERSION_FUNCTION_NAME;\n        }\n      }\n      else {\n        BOOST_LOG(warning) << \"Detected AMD GPU but AMF failed to load\"sv;\n      }\n    }\n    else if (adapter_desc.VendorId == 0x8086) {  // Intel\n      // If it's not a QSV encoder, it's not compatible with an Intel GPU\n      if (!boost::algorithm::ends_with(name, \"_qsv\")) {\n        return false;\n      }\n      if (config.chromaSamplingType == 1) {\n        if (config.videoFormat == 0 || config.videoFormat == 2) {\n          // QSV doesn't support 4:4:4 in H.264 or AV1\n          return false;\n        }\n        // TODO: Blacklist HEVC 4:4:4 based on adapter model\n      }\n    }\n    else if (adapter_desc.VendorId == 0x10de) {  // Nvidia\n      // If it's not an NVENC encoder, it's not compatible with an Nvidia GPU\n      if (!boost::algorithm::ends_with(name, \"_nvenc\")) {\n        return false;\n      }\n    }\n    else {\n      BOOST_LOG(warning) << \"Unknown GPU vendor ID: \" << util::hex(adapter_desc.VendorId).to_string_view();\n    }\n\n    return true;\n  }\n\n  std::unique_ptr<avcodec_encode_device_t>\n  display_vram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) {\n    auto device = std::make_unique<d3d_avcodec_encode_device_t>();\n    if (device->init(shared_from_this(), adapter.get(), pix_fmt) != 0) {\n      return nullptr;\n    }\n    return device;\n  }\n\n  std::unique_ptr<nvenc_encode_device_t>\n  display_vram_t::make_nvenc_encode_device(pix_fmt_e pix_fmt) {\n    // For hybrid graphics laptops, NVENC encoder requires NVIDIA GPU,\n    // but display capture may use integrated graphics (built-in screen).\n    // We need to find the NVIDIA adapter for encoding, not the capture adapter.\n    adapter_t::pointer nvenc_adapter_p = nullptr;\n    adapter_t nvenc_adapter;  // Smart pointer to manage adapter lifetime if we find a different one\n    \n    // Check if current adapter is NVIDIA\n    DXGI_ADAPTER_DESC adapter_desc;\n    adapter->GetDesc(&adapter_desc);\n    \n    if (adapter_desc.VendorId == 0x10de) {  // NVIDIA\n      // Current adapter is already NVIDIA, use it\n      nvenc_adapter_p = adapter.get();\n    }\n    else {\n      // Current adapter is not NVIDIA (likely integrated graphics),\n      // find the NVIDIA adapter for encoding\n      factory1_t factory;\n      HRESULT status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory);\n      if (SUCCEEDED(status)) {\n        adapter_t::pointer adapter_p;\n        for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {\n          dxgi::adapter_t adapter_tmp { adapter_p };\n          DXGI_ADAPTER_DESC1 adapter_desc1;\n          adapter_tmp->GetDesc1(&adapter_desc1);\n          \n          if (adapter_desc1.VendorId == 0x10de) {  // NVIDIA\n            // Found NVIDIA adapter, use it\n            nvenc_adapter = std::move(adapter_tmp);\n            nvenc_adapter_p = nvenc_adapter.get();\n            BOOST_LOG(info) << \"Found NVIDIA GPU for NVENC encoding: \" << platf::to_utf8(adapter_desc1.Description)\n                            << \" (display capture uses: \" << platf::to_utf8(adapter_desc.Description) << \")\";\n            break;\n          }\n        }\n      }\n      \n      if (!nvenc_adapter_p) {\n        BOOST_LOG(error) << \"Failed to find NVIDIA GPU adapter for NVENC encoding. \"\n                         << \"Current adapter (VendorId: 0x\" << util::hex(adapter_desc.VendorId).to_string_view()\n                         << \") does not support NVENC.\";\n        return nullptr;\n      }\n    }\n    \n    auto device = std::make_unique<d3d_nvenc_encode_device_t>();\n    if (!device->init_device(shared_from_this(), nvenc_adapter_p, pix_fmt)) {\n      return nullptr;\n    }\n    \n    return device;\n  }\n\n  std::unique_ptr<amf_encode_device_t>\n  display_vram_t::make_amf_encode_device(pix_fmt_e pix_fmt) {\n    // Find AMD adapter for AMF encoding\n    adapter_t::pointer amf_adapter_p = nullptr;\n    adapter_t amf_adapter;\n\n    DXGI_ADAPTER_DESC adapter_desc;\n    adapter->GetDesc(&adapter_desc);\n\n    if (adapter_desc.VendorId == 0x1002) {  // AMD\n      amf_adapter_p = adapter.get();\n    }\n    else {\n      factory1_t factory;\n      HRESULT status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory);\n      if (SUCCEEDED(status)) {\n        adapter_t::pointer adapter_p;\n        for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {\n          dxgi::adapter_t adapter_tmp { adapter_p };\n          DXGI_ADAPTER_DESC1 adapter_desc1;\n          adapter_tmp->GetDesc1(&adapter_desc1);\n\n          if (adapter_desc1.VendorId == 0x1002) {  // AMD\n            amf_adapter = std::move(adapter_tmp);\n            amf_adapter_p = amf_adapter.get();\n            BOOST_LOG(info) << \"Found AMD GPU for AMF encoding: \" << platf::to_utf8(adapter_desc1.Description);\n            break;\n          }\n        }\n      }\n\n      if (!amf_adapter_p) {\n        BOOST_LOG(error) << \"Failed to find AMD GPU adapter for AMF encoding.\";\n        return nullptr;\n      }\n    }\n\n    auto device = std::make_unique<d3d_amf_encode_device_t>();\n    if (!device->init_device(shared_from_this(), amf_adapter_p, pix_fmt)) {\n      return nullptr;\n    }\n\n    return device;\n  }\n\n  int\n  init() {\n    BOOST_LOG(debug) << \"Compiling shaders...\"sv;\n\n#define compile_vertex_shader_helper(x) \\\n  if (!(x##_hlsl = compile_vertex_shader(SUNSHINE_SHADERS_DIR \"/\" #x \".hlsl\"))) return -1;\n#define compile_pixel_shader_helper(x) \\\n  if (!(x##_hlsl = compile_pixel_shader(SUNSHINE_SHADERS_DIR \"/\" #x \".hlsl\"))) return -1;\n\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_linear);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_perceptual_quantizer);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_type0_ps_hybrid_log_gamma);\n    compile_vertex_shader_helper(convert_yuv420_packed_uv_type0_vs);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_linear);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_type0s_ps_hybrid_log_gamma);\n    compile_vertex_shader_helper(convert_yuv420_packed_uv_type0s_vs);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_bicubic_ps);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_bicubic_ps_linear);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_bicubic_ps_perceptual_quantizer);\n    compile_pixel_shader_helper(convert_yuv420_packed_uv_bicubic_ps_hybrid_log_gamma);\n    compile_vertex_shader_helper(convert_yuv420_packed_uv_bicubic_vs);\n    compile_pixel_shader_helper(convert_yuv420_planar_y_ps);\n    compile_pixel_shader_helper(convert_yuv420_planar_y_ps_linear);\n    compile_pixel_shader_helper(convert_yuv420_planar_y_ps_perceptual_quantizer);\n    compile_pixel_shader_helper(convert_yuv420_planar_y_ps_hybrid_log_gamma);\n    compile_vertex_shader_helper(convert_yuv420_planar_y_vs);\n    compile_pixel_shader_helper(convert_yuv420_planar_y_bicubic_ps);\n    compile_pixel_shader_helper(convert_yuv420_planar_y_bicubic_ps_linear);\n    compile_pixel_shader_helper(convert_yuv420_planar_y_bicubic_ps_perceptual_quantizer);\n    compile_pixel_shader_helper(convert_yuv420_planar_y_bicubic_ps_hybrid_log_gamma);\n    compile_pixel_shader_helper(convert_yuv444_packed_ayuv_ps);\n    compile_pixel_shader_helper(convert_yuv444_packed_ayuv_ps_linear);\n    compile_vertex_shader_helper(convert_yuv444_packed_vs);\n    compile_pixel_shader_helper(convert_yuv444_planar_ps);\n    compile_pixel_shader_helper(convert_yuv444_planar_ps_linear);\n    compile_pixel_shader_helper(convert_yuv444_planar_ps_perceptual_quantizer);\n    compile_pixel_shader_helper(convert_yuv444_planar_ps_hybrid_log_gamma);\n    compile_pixel_shader_helper(convert_yuv444_packed_y410_ps);\n    compile_pixel_shader_helper(convert_yuv444_packed_y410_ps_linear);\n    compile_pixel_shader_helper(convert_yuv444_packed_y410_ps_perceptual_quantizer);\n    compile_pixel_shader_helper(convert_yuv444_packed_y410_ps_hybrid_log_gamma);\n    compile_vertex_shader_helper(convert_yuv444_planar_vs);\n    compile_pixel_shader_helper(cursor_ps);\n    compile_pixel_shader_helper(cursor_ps_normalize_white);\n    compile_vertex_shader_helper(cursor_vs);\n    compile_pixel_shader_helper(simple_cursor_ps);\n    compile_vertex_shader_helper(simple_cursor_vs);\n\n    // Compile HDR luminance analysis compute shaders (optional, non-fatal if fails)\n    hdr_luminance_analysis_cs_hlsl = compile_compute_shader(SUNSHINE_SHADERS_DIR \"/hdr_luminance_analysis_cs.hlsl\");\n    if (!hdr_luminance_analysis_cs_hlsl) {\n      BOOST_LOG(warning) << \"Failed to compile HDR luminance analysis CS, per-frame HDR metadata will use defaults\";\n    }\n    hdr_luminance_reduce_cs_hlsl = compile_compute_shader(SUNSHINE_SHADERS_DIR \"/hdr_luminance_reduce_cs.hlsl\");\n    if (!hdr_luminance_reduce_cs_hlsl) {\n      BOOST_LOG(warning) << \"Failed to compile HDR luminance reduce CS, per-frame HDR metadata will use defaults\";\n    }\n\n    BOOST_LOG(debug) << \"Compiled shaders\"sv;\n\n#undef compile_vertex_shader_helper\n#undef compile_pixel_shader_helper\n\n    return 0;\n  }\n}  // namespace platf::dxgi"
  },
  {
    "path": "src/platform/windows/display_wgc.cpp",
    "content": "/**\n * @file src/platform/windows/display_wgc.cpp\n * @brief Definitions for WinRT Windows.Graphics.Capture API\n */\n// platform includes\n#include <winsock2.h>\n#include <windows.h>\n#include <algorithm>\n#include <atomic>\n#include <chrono>\n#include <dxgi1_2.h>\n#include <filesystem>\n#include <fstream>\n#include <mutex>\n#include <thread>\n\n// local includes\n#include \"display.h\"\n#include \"misc.h\"\n#include \"src/config.h\"\n#include \"src/globals.h\"\n#include \"src/logging.h\"\n#include \"src/process.h\"\n#include <boost/program_options/parsers.hpp>\n#include <vector>\n\n// Gross hack to work around MINGW-packages#22160\n#define ____FIReference_1_boolean_INTERFACE_DEFINED__\n\n#include <Windows.Graphics.Capture.Interop.h>\n#include <winrt/windows.foundation.h>\n#include <winrt/windows.foundation.metadata.h>\n#include <winrt/windows.graphics.directx.direct3d11.h>\n\nnamespace platf {\n  using namespace std::literals;\n}\n\nnamespace winrt {\n  using namespace Windows::Foundation;\n  using namespace Windows::Foundation::Metadata;\n  using namespace Windows::Graphics::Capture;\n  using namespace Windows::Graphics::DirectX::Direct3D11;\n\n  extern \"C\" {\n  HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(::IDXGIDevice *dxgiDevice, ::IInspectable **graphicsDevice);\n  }\n\n  /**\n   * Windows structures sometimes have compile-time GUIDs. GCC supports this, but in a roundabout way.\n   * If WINRT_IMPL_HAS_DECLSPEC_UUID is true, then the compiler supports adding this attribute to a struct. For example, Visual Studio.\n   * If not, then MinGW GCC has a workaround to assign a GUID to a structure.\n   */\n  struct\n#if WINRT_IMPL_HAS_DECLSPEC_UUID\n    __declspec(uuid(\"A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1\"))\n#endif\n    IDirect3DDxgiInterfaceAccess: ::IUnknown {\n    virtual HRESULT __stdcall GetInterface(REFIID id, void **object) = 0;\n  };\n}  // namespace winrt\n#if !WINRT_IMPL_HAS_DECLSPEC_UUID\nstatic constexpr GUID GUID__IDirect3DDxgiInterfaceAccess = {\n  0xA9B3D012,\n  0x3DF2,\n  0x4EE3,\n  { 0xB8, 0xD1, 0x86, 0x95, 0xF4, 0x57, 0xD3, 0xC1 }\n  // compare with __declspec(uuid(...)) for the struct above.\n};\n\ntemplate <>\nconstexpr auto\n__mingw_uuidof<winrt::IDirect3DDxgiInterfaceAccess>() -> GUID const & {\n  return GUID__IDirect3DDxgiInterfaceAccess;\n}\n#endif\n\nnamespace platf::dxgi {\n\n  // UAC bypass for WGC capture mode.\n  // WGC cannot capture UAC prompts, so we temporarily set ConsentPromptBehaviorAdmin=0\n  // to auto-elevate without prompting during capture, and restore the original value when capture ends.\n  namespace secure_desktop {\n    static constexpr const wchar_t *kPoliciesKey = L\"SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\System\";\n    static constexpr const wchar_t *kValueName = L\"ConsentPromptBehaviorAdmin\";\n\n    static std::mutex state_mutex;\n    static int wgc_ref_count = 0;\n    static DWORD original_value = 5;  // Default: prompt for consent on secure desktop\n    static bool modified = false;\n    static std::once_flag recovery_flag;\n\n    static std::filesystem::path\n    backup_file_path() {\n      wchar_t temp[MAX_PATH];\n      GetTempPathW(MAX_PATH, temp);\n      return std::filesystem::path(temp) / L\"sunshine_uac_consent_backup\";\n    }\n\n    /**\n     * @brief Valid range for ConsentPromptBehaviorAdmin: 0-5.\n     */\n    static bool\n    is_valid_consent_value(DWORD value) {\n      return value <= 5;\n    }\n\n    /**\n     * @brief Save original value to a temp file for crash recovery.\n     * @return true if backup was successfully written and flushed.\n     */\n    static bool\n    save_backup(DWORD value) {\n      try {\n        auto path = backup_file_path();\n        std::ofstream f(path, std::ios::trunc);\n        if (f) {\n          f << value;\n          f.flush();\n          if (f.good()) {\n            return true;\n          }\n        }\n        BOOST_LOG(error) << \"Failed to write crash recovery backup file\"sv;\n      }\n      catch (...) {\n        BOOST_LOG(error) << \"Exception writing crash recovery backup file\"sv;\n      }\n      return false;\n    }\n\n    /**\n     * @brief Remove the backup file.\n     */\n    static void\n    remove_backup() {\n      try {\n        std::filesystem::remove(backup_file_path());\n      }\n      catch (...) {\n      }\n    }\n\n    /**\n     * @brief Restore from crash recovery backup if it exists.\n     */\n    static void\n    recover_from_crash() {\n      try {\n        auto path = backup_file_path();\n        if (!std::filesystem::exists(path)) return;\n\n        std::ifstream f(path);\n        DWORD saved_value;\n        if (!(f >> saved_value)) {\n          BOOST_LOG(warning) << \"Corrupt crash recovery backup file, removing\"sv;\n          remove_backup();\n          return;\n        }\n\n        if (!is_valid_consent_value(saved_value)) {\n          BOOST_LOG(error) << \"Invalid ConsentPromptBehaviorAdmin value \"sv << saved_value << \" in backup, rejecting\"sv;\n          remove_backup();\n          return;\n        }\n\n        HKEY hkey;\n        if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, kPoliciesKey, 0, KEY_WRITE, &hkey) == ERROR_SUCCESS) {\n          auto result = RegSetValueExW(hkey, kValueName, 0, REG_DWORD, (const BYTE *) &saved_value, sizeof(saved_value));\n          RegCloseKey(hkey);\n          if (result == ERROR_SUCCESS) {\n            BOOST_LOG(info) << \"Restored ConsentPromptBehaviorAdmin to \"sv << saved_value << \" from crash recovery backup\"sv;\n            remove_backup();\n          }\n          else {\n            BOOST_LOG(error) << \"Failed to restore ConsentPromptBehaviorAdmin from crash backup, will retry next startup\"sv;\n          }\n        }\n        else {\n          BOOST_LOG(error) << \"Failed to open registry for crash recovery, will retry next startup\"sv;\n        }\n      }\n      catch (...) {\n      }\n    }\n\n    /**\n     * @brief Disable UAC prompts by setting ConsentPromptBehaviorAdmin to 0 (elevate without prompting).\n     * @return true if the value was modified.\n     */\n    static bool\n    disable() {\n      std::lock_guard<std::mutex> lock(state_mutex);\n\n      if (wgc_ref_count++ > 0) {\n        // Another WGC instance already disabled it\n        return false;\n      }\n\n      // If a prior restore() failed, the registry is still at 0 and original_value\n      // holds the real pre-modification value. Don't re-read or overwrite it.\n      if (modified) {\n        BOOST_LOG(info) << \"ConsentPromptBehaviorAdmin still modified from prior session (pending restore), reusing original value \"sv << original_value;\n        return false;\n      }\n\n      DWORD value, size = sizeof(value);\n      auto read_result = RegGetValueW(HKEY_LOCAL_MACHINE, kPoliciesKey, kValueName, RRF_RT_REG_DWORD, nullptr, &value, &size);\n      if (read_result != ERROR_SUCCESS) {\n        BOOST_LOG(warning) << \"Failed to read ConsentPromptBehaviorAdmin (error 0x\"sv << util::hex(read_result).to_string_view() << \"), aborting UAC disable\"sv;\n        return false;\n      }\n      original_value = value;\n\n      if (value == 0) {\n        BOOST_LOG(info) << \"ConsentPromptBehaviorAdmin is already 0 (auto-elevate)\"sv;\n        return false;\n      }\n\n      // Persist backup BEFORE modifying registry — ensures crash recovery is possible\n      if (!save_backup(original_value)) {\n        BOOST_LOG(error) << \"Cannot persist backup file, aborting UAC disable to prevent irrecoverable state\"sv;\n        return false;\n      }\n\n      HKEY hkey;\n      if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, kPoliciesKey, 0, KEY_WRITE, &hkey) != ERROR_SUCCESS) {\n        BOOST_LOG(warning) << \"Cannot disable UAC prompts: insufficient privileges. Run Sunshine elevated to enable this feature.\"sv;\n        remove_backup();\n        return false;\n      }\n\n      DWORD new_value = 0;\n      auto result = RegSetValueExW(hkey, kValueName, 0, REG_DWORD, (const BYTE *) &new_value, sizeof(new_value));\n      RegCloseKey(hkey);\n\n      if (result == ERROR_SUCCESS) {\n        modified = true;\n        BOOST_LOG(info) << \"Set ConsentPromptBehaviorAdmin to 0 for WGC capture (original value: \"sv << original_value << \")\"sv;\n        return true;\n      }\n\n      BOOST_LOG(warning) << \"Failed to set ConsentPromptBehaviorAdmin [0x\"sv << util::hex(result).to_string_view() << ']';\n      remove_backup();\n      return false;\n    }\n\n    /**\n     * @brief Restore the original ConsentPromptBehaviorAdmin setting.\n     */\n    static void\n    restore() {\n      std::lock_guard<std::mutex> lock(state_mutex);\n\n      if (--wgc_ref_count > 0) {\n        // Other WGC instances still active\n        return;\n      }\n\n      if (!modified) {\n        return;\n      }\n\n      HKEY hkey;\n      if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, kPoliciesKey, 0, KEY_WRITE, &hkey) == ERROR_SUCCESS) {\n        auto result = RegSetValueExW(hkey, kValueName, 0, REG_DWORD, (const BYTE *) &original_value, sizeof(original_value));\n        RegCloseKey(hkey);\n        if (result == ERROR_SUCCESS) {\n          BOOST_LOG(info) << \"Restored ConsentPromptBehaviorAdmin to \"sv << original_value;\n          modified = false;\n          remove_backup();\n        }\n        else {\n          BOOST_LOG(error) << \"Failed to restore ConsentPromptBehaviorAdmin, backup preserved for next startup\"sv;\n        }\n      }\n      else {\n        BOOST_LOG(error) << \"Failed to open registry for restore, backup preserved for next startup\"sv;\n      }\n    }\n  }  // namespace secure_desktop\n\n  void\n  recover_secure_desktop() {\n    std::call_once(secure_desktop::recovery_flag, secure_desktop::recover_from_crash);\n  }\n\n  /**\n   * @brief Find a window by title (case-insensitive fuzzy matching).\n   * @param window_title The window title to search for.\n   * @return HWND of the found window, or nullptr if not found.\n   *\n   * @note This function uses multiple matching strategies:\n   *       1. Direct substring match\n   *       2. Match after removing spaces\n   *       3. Word-based match (all search words appear in window title)\n   *       4. Fuzzy character sequence match (characters appear in order)\n   *       It skips invisible windows and windows without titles.\n   */\n  static HWND\n  find_window_by_title(const std::string &window_title) {\n    if (window_title.empty()) {\n      return nullptr;\n    }\n\n    std::wstring search_title = platf::from_utf8(window_title);\n\n    // Convert to lowercase for case-insensitive comparison\n    std::transform(search_title.begin(), search_title.end(), search_title.begin(), ::towlower);\n\n    // Split search title into words for word-based matching\n    std::vector<std::wstring> search_words = platf::split_words(search_title);\n    std::wstring search_title_no_spaces = search_title;\n    search_title_no_spaces.erase(std::remove(search_title_no_spaces.begin(), search_title_no_spaces.end(), L' '), search_title_no_spaces.end());\n\n    struct EnumData {\n      std::wstring search_title;\n      std::wstring search_title_no_spaces;\n      std::vector<std::wstring> search_words;\n      HWND found_hwnd;\n      HWND best_match_hwnd;  // Best fuzzy match found so far\n      int best_match_score;  // Higher is better\n    } enum_data { search_title, search_title_no_spaces, search_words, nullptr, nullptr, 0 };\n\n    EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL {\n      auto *data = reinterpret_cast<EnumData *>(lParam);\n\n      // Check if window is visible\n      if (!IsWindowVisible(hwnd)) {\n        return TRUE;  // Continue enumeration\n      }\n\n      // Skip minimized windows as they may not capture properly\n      if (IsIconic(hwnd)) {\n        return TRUE;  // Continue enumeration\n      }\n\n      // Get window title length first\n      int title_length = GetWindowTextLengthW(hwnd);\n      if (title_length == 0) {\n        return TRUE;  // Continue enumeration - no title\n      }\n\n      // Get window title\n      std::wstring window_text(title_length + 1, L'\\0');\n      if (GetWindowTextW(hwnd, &window_text[0], title_length + 1) == 0) {\n        return TRUE;  // Continue enumeration\n      }\n      window_text.resize(title_length);  // Remove null terminator from string\n\n      // Convert to lowercase for case-insensitive comparison\n      std::transform(window_text.begin(), window_text.end(), window_text.begin(), ::towlower);\n\n      // Strategy 1: Direct substring match (highest priority)\n      if (window_text.find(data->search_title) != std::wstring::npos) {\n        data->found_hwnd = hwnd;\n        return FALSE;  // Stop enumeration - exact match found\n      }\n\n      // Strategy 2: Match after removing spaces\n      std::wstring window_text_no_spaces = window_text;\n      window_text_no_spaces.erase(std::remove(window_text_no_spaces.begin(), window_text_no_spaces.end(), L' '), window_text_no_spaces.end());\n      if (window_text_no_spaces.find(data->search_title_no_spaces) != std::wstring::npos) {\n        data->found_hwnd = hwnd;\n        return FALSE;  // Stop enumeration - good match found\n      }\n\n      // Strategy 3: Word-based matching (check if all search words appear in window title)\n      if (!data->search_words.empty()) {\n        bool all_words_found = true;\n        for (const auto &word : data->search_words) {\n          if (word.length() < 2) {\n            continue;  // Skip very short words\n          }\n          if (window_text.find(word) == std::wstring::npos &&\n              window_text_no_spaces.find(word) == std::wstring::npos) {\n            all_words_found = false;\n            break;\n          }\n        }\n        if (all_words_found && data->search_words.size() > 0) {\n          // Calculate a simple score based on word matches\n          int score = static_cast<int>(data->search_words.size() * 10);\n          if (score > data->best_match_score) {\n            data->best_match_hwnd = hwnd;\n            data->best_match_score = score;\n          }\n        }\n      }\n\n      // Strategy 4: Fuzzy character sequence matching (lowest priority, but still useful)\n      if (platf::fuzzy_match(window_text, data->search_title)) {\n        // Calculate score based on how close the match is\n        int score = static_cast<int>(data->search_title.length() * 5);\n        if (score > data->best_match_score) {\n          data->best_match_hwnd = hwnd;\n          data->best_match_score = score;\n        }\n      }\n\n      return TRUE;  // Continue enumeration\n    },\n      reinterpret_cast<LPARAM>(&enum_data));\n\n    // If exact match found, return it\n    if (enum_data.found_hwnd != nullptr) {\n      return enum_data.found_hwnd;\n    }\n\n    // Otherwise, return the best fuzzy match if we found one\n    if (enum_data.best_match_hwnd != nullptr && enum_data.best_match_score > 0) {\n      wchar_t actual_title[256] = { 0 };\n      GetWindowTextW(enum_data.best_match_hwnd, actual_title, sizeof(actual_title) / sizeof(actual_title[0]));\n      BOOST_LOG(debug) << \"Using fuzzy match: [\"sv << platf::to_utf8(actual_title) << \"] for search [\"sv << window_title << \"] (score: \"sv << enum_data.best_match_score << \")\";\n      return enum_data.best_match_hwnd;\n    }\n\n    return nullptr;\n  }\n\n  wgc_capture_t::wgc_capture_t() {\n    InitializeConditionVariable(&frame_present_cv);\n  }\n\n  wgc_capture_t::~wgc_capture_t() {\n    if (capture_session) {\n      capture_session.Close();\n    }\n    if (frame_pool) {\n      frame_pool.Close();\n    }\n    item = nullptr;\n    capture_session = nullptr;\n    frame_pool = nullptr;\n\n    if (config::video.wgc_disable_secure_desktop) {\n      secure_desktop::restore();\n    }\n  }\n\n  /**\n   * @brief Initialize the Windows.Graphics.Capture backend.\n   * @return 0 on success, -1 on failure.\n   */\n  int\n  wgc_capture_t::init(display_base_t *display, const ::video::config_t &config) {\n    // WGC is not supported in service mode (running as SYSTEM user)\n    // Fail fast to avoid unnecessary attempts and potential deadlocks\n    if (is_running_as_system_user) {\n      BOOST_LOG(error) << \"WGC capture is not available in service mode (running as SYSTEM user). Use DDX capture instead.\"sv;\n      return -1;\n    }\n\n    if (!winrt::GraphicsCaptureSession::IsSupported()) {\n      BOOST_LOG(error) << \"Screen capture is not supported on this device for this release of Windows!\"sv;\n      return -1;\n    }\n\n    HRESULT status;\n    dxgi::dxgi_t dxgi;\n    winrt::com_ptr<::IInspectable> d3d_comhandle;\n\n    if (FAILED(status = display->device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi))) {\n      BOOST_LOG(error) << \"Failed to query DXGI interface from device [0x\"sv << util::hex(status).to_string_view() << ']';\n      return -1;\n    }\n    try {\n      if (FAILED(status = winrt::CreateDirect3D11DeviceFromDXGIDevice(*&dxgi, d3d_comhandle.put()))) {\n        BOOST_LOG(error) << \"Failed to query WinRT DirectX interface from device [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n    }\n    catch (winrt::hresult_error &e) {\n      BOOST_LOG(error) << \"Screen capture is not supported on this device for this release of Windows: failed to acquire device: [0x\"sv << util::hex(e.code()).to_string_view() << ']';\n      return -1;\n    }\n\n    uwp_device = d3d_comhandle.as<winrt::IDirect3DDevice>();\n\n    auto capture_factory = winrt::get_activation_factory<winrt::GraphicsCaptureItem, IGraphicsCaptureItemInterop>();\n    if (capture_factory == nullptr) {\n      BOOST_LOG(error) << \"Failed to get GraphicsCaptureItem factory\"sv;\n      return -1;\n    }\n\n    // Determine capture target: window or display\n    bool capture_window = false;\n    std::string window_title;\n\n    // Priority 1: Check capture_target configuration from global config\n    if (config::video.capture_target == \"window\") {\n      capture_window = true;\n      window_title = config::video.window_title;\n      desired_window_title = window_title;\n\n      // If window_title is empty, try to derive from running app\n      if (window_title.empty()) {\n        int running_app_id = proc::proc.running();\n        if (running_app_id > 0) {\n          // Try to extract from the app command (executable path)\n          std::string app_cmd = proc::proc.get_app_cmd(running_app_id);\n          if (!app_cmd.empty()) {\n            std::vector<std::string> parts;\n            try {\n              parts = boost::program_options::split_winmain(app_cmd);\n            }\n            catch (...) {\n              // Ignore parsing errors\n            }\n\n            if (!parts.empty() && parts[0].find(\"://\") == std::string::npos) {\n              std::string exe_path = parts[0];\n              size_t last_slash = exe_path.find_last_of(\"/\\\\\");\n              std::string filename = (last_slash != std::string::npos) ? exe_path.substr(last_slash + 1) : exe_path;\n              size_t last_dot = filename.find_last_of('.');\n              window_title = (last_dot != std::string::npos) ? filename.substr(0, last_dot) : filename;\n\n              if (!window_title.empty()) {\n                BOOST_LOG(info) << \"Window title not specified, using executable filename: [\"sv << window_title << \"] (from: [\"sv << app_cmd << \"])\";\n              }\n            }\n          }\n\n          // Fallback to app name if still empty\n          if (window_title.empty()) {\n            window_title = proc::proc.get_app_name(running_app_id);\n            if (!window_title.empty()) {\n              BOOST_LOG(info) << \"Window title not specified, using app name: [\"sv << window_title << \"]\";\n            }\n          }\n\n          if (!window_title.empty()) {\n            desired_window_title = window_title;\n          }\n        }\n      }\n    }\n    // Priority 2: Check \"window:\" prefix in display_name (backward compatibility)\n    else if (config.display_name.length() > 7 && config.display_name.substr(0, 7) == \"window:\") {\n      capture_window = true;\n      window_title = config.display_name.substr(7);\n      desired_window_title = window_title;\n    }\n    else {\n      desired_window_title.clear();\n    }\n\n    if (capture_window) {\n      if (window_title.empty()) {\n        BOOST_LOG(warning) << \"Window capture requested but window_title is empty and no app is running. Falling back to display capture.\"sv;\n        capture_window = false;\n      }\n      else {\n        // Retry window lookup with timeout - window might be starting up\n        constexpr int max_retries = 20;\n        constexpr int retry_interval_ms = 500;\n        HWND target_hwnd = nullptr;\n\n        for (int retry = 0; retry < max_retries; ++retry) {\n          target_hwnd = find_window_by_title(window_title);\n          if (target_hwnd && IsWindow(target_hwnd) && IsWindowVisible(target_hwnd) && !IsIconic(target_hwnd)) {\n            break;\n          }\n          target_hwnd = nullptr;\n\n          if (retry < max_retries - 1) {\n            BOOST_LOG(info) << \"Window not found yet: [\"sv << window_title << \"], retrying in \"sv << retry_interval_ms << \"ms (\"sv << (retry + 1) << \"/\"sv << max_retries << \")...\"sv;\n            Sleep(retry_interval_ms);\n          }\n        }\n\n        if (!target_hwnd) {\n          BOOST_LOG(warning) << \"Window not found or invalid after \"sv << max_retries << \" attempts: [\"sv << window_title << \"]. Falling back to display capture.\"sv;\n          capture_window = false;\n        }\n        else {\n          wchar_t actual_title[256] = {};\n          GetWindowTextW(target_hwnd, actual_title, std::size(actual_title));\n          BOOST_LOG(info) << \"Capturing window: [\"sv << platf::to_utf8(actual_title) << \"] (searched for: [\"sv << window_title << \"])\";\n\n          // Maximize the window first for better capture experience\n          if (!IsZoomed(target_hwnd)) {\n            BOOST_LOG(info) << \"Maximizing window for capture...\"sv;\n            ShowWindow(target_hwnd, SW_MAXIMIZE);\n            Sleep(500);\n          }\n\n          SetForegroundWindow(target_hwnd);\n          Sleep(100);\n\n          if (FAILED(status = capture_factory->CreateForWindow(target_hwnd, winrt::guid_of<winrt::IGraphicsCaptureItem>(), winrt::put_abi(item)))) {\n            BOOST_LOG(error) << \"Failed to create capture item for window [0x\"sv << util::hex(status).to_string_view() << ']';\n            return -1;\n          }\n\n          captured_window_hwnd = target_hwnd;\n\n          auto window_size = item.Size();\n          window_capture_width = static_cast<int>(window_size.Width);\n          window_capture_height = static_cast<int>(window_size.Height);\n\n          // Log window details for debugging\n          RECT window_rect = {}, client_rect = {};\n          if (GetWindowRect(target_hwnd, &window_rect) && GetClientRect(target_hwnd, &client_rect)) {\n            BOOST_LOG(info) << \"Window geometry - Window: \"sv\n                            << (window_rect.right - window_rect.left) << 'x' << (window_rect.bottom - window_rect.top)\n                            << \", Client: \"sv << (client_rect.right - client_rect.left) << 'x' << (client_rect.bottom - client_rect.top)\n                            << \", WGC initial: \"sv << window_capture_width << 'x' << window_capture_height\n                            << \", Display: \"sv << display->width << 'x' << display->height;\n          }\n\n          BOOST_LOG(info) << \"Window capture initialized with size: \"sv << window_capture_width << 'x' << window_capture_height;\n        }\n      }\n    }\n\n    // If not capturing window (either not requested or fallback), capture display\n    if (!capture_window) {\n      captured_window_hwnd = nullptr;  // Not capturing a window\n      window_capture_width = 0;\n      window_capture_height = 0;\n      if (display->output == nullptr) {\n        BOOST_LOG(error) << \"Display output is null, cannot capture monitor\"sv;\n        return -1;\n      }\n      DXGI_OUTPUT_DESC output_desc;\n      display->output->GetDesc(&output_desc);\n      BOOST_LOG(info) << \"Capturing display: [\"sv << platf::to_utf8(output_desc.DeviceName) << ']';\n      if (FAILED(status = capture_factory->CreateForMonitor(output_desc.Monitor, winrt::guid_of<winrt::IGraphicsCaptureItem>(), winrt::put_abi(item)))) {\n        BOOST_LOG(error) << \"Screen capture is not supported on this device for this release of Windows: failed to acquire display: [0x\"sv << util::hex(status).to_string_view() << ']';\n        return -1;\n      }\n    }\n\n    display->capture_format = config.dynamicRange ? DXGI_FORMAT_R16G16B16A16_FLOAT : DXGI_FORMAT_B8G8R8A8_UNORM;\n\n    // Use the actual capture item size for frame pool creation\n    auto item_size = item.Size();\n\n    try {\n      frame_pool = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(uwp_device, static_cast<winrt::Windows::Graphics::DirectX::DirectXPixelFormat>(display->capture_format), 2, item_size);\n      capture_session = frame_pool.CreateCaptureSession(item);\n      frame_pool.FrameArrived({ this, &wgc_capture_t::on_frame_arrived });\n    }\n    catch (winrt::hresult_error &e) {\n      BOOST_LOG(error) << \"Screen capture is not supported on this device for this release of Windows: failed to create capture session: [0x\"sv << util::hex(e.code()).to_string_view() << ']';\n      return -1;\n    }\n\n    try {\n      if (winrt::ApiInformation::IsPropertyPresent(L\"Windows.Graphics.Capture.GraphicsCaptureSession\", L\"IsBorderRequired\")) {\n        capture_session.IsBorderRequired(false);\n      }\n      else {\n        BOOST_LOG(warning) << \"Can't disable colored border around capture area on this version of Windows\";\n      }\n    }\n    catch (winrt::hresult_error &e) {\n      BOOST_LOG(warning) << \"Screen capture may not be fully supported on this device for this release of Windows: failed to disable border around capture area: [0x\"sv << util::hex(e.code()).to_string_view() << ']';\n    }\n\n    try {\n      if (winrt::ApiInformation::IsPropertyPresent(L\"Windows.Graphics.Capture.GraphicsCaptureSession\", L\"MinUpdateInterval\")) {\n        capture_session.MinUpdateInterval(4ms);\n      }\n      else {\n        BOOST_LOG(warning) << \"Can't set MinUpdateInterval on this version of Windows\";\n      }\n    }\n    catch (winrt::hresult_error &e) {\n      BOOST_LOG(warning) << \"Screen capture may be capped to 60fps on this device for this release of Windows: failed to set MinUpdateInterval: [0x\"sv << util::hex(e.code()).to_string_view() << ']';\n    }\n\n    try {\n      capture_session.StartCapture();\n    }\n    catch (winrt::hresult_error &e) {\n      BOOST_LOG(error) << \"Screen capture is not supported on this device for this release of Windows: failed to start capture: [0x\"sv << util::hex(e.code()).to_string_view() << ']';\n      return -1;\n    }\n\n    // Disable UAC prompts so WGC can operate without interruption (if enabled in config)\n    if (config::video.wgc_disable_secure_desktop) {\n      secure_desktop::disable();\n    }\n    else {\n      BOOST_LOG(info) << \"WGC capture active. UAC prompts will not be auto-elevated. \"\n                         \"Set wgc_disable_secure_desktop=true in config to auto-elevate UAC during WGC capture.\"sv;\n    }\n\n    return 0;\n  }\n\n  /**\n   * This function runs in a separate thread spawned by the frame pool and is a producer of frames.\n   * To maintain parity with the original display interface, this frame will be consumed by the capture thread.\n   * Acquire a read-write lock, make the produced frame available to the capture thread, then wake the capture thread.\n   */\n  void\n  wgc_capture_t::on_frame_arrived(winrt::Direct3D11CaptureFramePool const &sender, winrt::IInspectable const &) {\n    winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame frame { nullptr };\n    try {\n      frame = sender.TryGetNextFrame();\n    }\n    catch (winrt::hresult_error &e) {\n      BOOST_LOG(warning) << \"Failed to capture frame: \"sv << e.code();\n      return;\n    }\n    if (frame != nullptr) {\n      AcquireSRWLockExclusive(&frame_lock);\n      if (produced_frame) {\n        produced_frame.Close();\n      }\n\n      produced_frame = frame;\n      ReleaseSRWLockExclusive(&frame_lock);\n      WakeConditionVariable(&frame_present_cv);\n    }\n  }\n\n  /**\n   * @brief Get the next frame from the producer thread.\n   * If not available, the capture thread blocks until one is, or the wait times out.\n   * @param timeout how long to wait for the next frame\n   * @param out a texture containing the frame just captured\n   * @param out_time the timestamp of the frame just captured\n   */\n  capture_e\n  wgc_capture_t::next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time) {\n    // this CONSUMER runs in the capture thread\n    release_frame();\n\n    AcquireSRWLockExclusive(&frame_lock);\n    if (produced_frame == nullptr && SleepConditionVariableSRW(&frame_present_cv, &frame_lock, timeout.count(), 0) == 0) {\n      ReleaseSRWLockExclusive(&frame_lock);\n      if (GetLastError() == ERROR_TIMEOUT) {\n        return capture_e::timeout;\n      }\n      else {\n        return capture_e::error;\n      }\n    }\n    if (produced_frame) {\n      consumed_frame = produced_frame;\n      produced_frame = nullptr;\n    }\n    ReleaseSRWLockExclusive(&frame_lock);\n    if (consumed_frame == nullptr) {  // spurious wakeup\n      return capture_e::timeout;\n    }\n\n    auto capture_access = consumed_frame.Surface().as<winrt::IDirect3DDxgiInterfaceAccess>();\n    if (capture_access == nullptr) {\n      return capture_e::error;\n    }\n    capture_access->GetInterface(IID_ID3D11Texture2D, (void **) out);\n    out_time = consumed_frame.SystemRelativeTime().count();  // raw ticks from query performance counter\n    return capture_e::ok;\n  }\n\n  capture_e\n  wgc_capture_t::release_frame() {\n    if (consumed_frame != nullptr) {\n      consumed_frame.Close();\n      consumed_frame = nullptr;\n    }\n    return capture_e::ok;\n  }\n\n  int\n  wgc_capture_t::set_cursor_visible(bool x) {\n    try {\n      if (capture_session.IsCursorCaptureEnabled() != x) {\n        capture_session.IsCursorCaptureEnabled(x);\n      }\n      return 0;\n    }\n    catch (winrt::hresult_error &) {\n      return -1;\n    }\n  }\n\n  bool\n  wgc_capture_t::is_window_valid() const {\n    // If not capturing a window, always return true\n    if (captured_window_hwnd == nullptr) {\n      return true;\n    }\n    // Check if window is still valid\n    if (IsWindow(captured_window_hwnd) == FALSE) {\n      return false;\n    }\n    // Check if window is minimized - minimized windows may not capture properly\n    if (IsIconic(captured_window_hwnd) != FALSE) {\n      return false;\n    }\n    // Check if window is visible - invisible windows may not capture properly\n    if (IsWindowVisible(captured_window_hwnd) == FALSE) {\n      return false;\n    }\n    return true;\n  }\n\n  std::shared_ptr<img_t>\n  display_wgc_ram_t::alloc_img() {\n    auto img = std::make_shared<img_t>();\n\n    // For window capture, use window capture dimensions; for display capture, use display dimensions\n    int img_width = dup.window_capture_width > 0 ? dup.window_capture_width : width;\n    int img_height = dup.window_capture_height > 0 ? dup.window_capture_height : height;\n\n    img->width = img_width;\n    img->height = img_height;\n    img->pixel_pitch = get_pixel_pitch();\n    img->row_pitch = img->pixel_pitch * img->width;\n    img->data = nullptr;\n\n    return img;\n  }\n\n  int\n  display_wgc_ram_t::init(const ::video::config_t &config, const std::string &display_name) {\n    if (display_base_t::init(config, display_name) || dup.init(this, config)) {\n      return -1;\n    }\n\n    // WGC frames are typically delivered in the current display orientation.\n    // If we keep DXGI rotation enabled here, later stages may apply an extra rotation,\n    // causing flipped/upside-down/stretched output on client after a monitor rotation.\n    if (display_rotation != DXGI_MODE_ROTATION_UNSPECIFIED &&\n        display_rotation != DXGI_MODE_ROTATION_IDENTITY) {\n      BOOST_LOG(info) << \"WGC: disabling DXGI rotation handling for oriented frames\";\n      display_rotation = DXGI_MODE_ROTATION_UNSPECIFIED;\n      width_before_rotation = width;\n      height_before_rotation = height;\n    }\n\n    texture.reset();\n    return 0;\n  }\n\n  /**\n   * @brief Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture.\n   * @param pull_free_image_cb call this to get a new free image from the video subsystem.\n   * @param img_out the captured frame is returned here\n   * @param timeout how long to wait for the next frame\n   * @param cursor_visible whether to capture the cursor\n   */\n  capture_e\n  display_wgc_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) {\n    // Check if window is still valid (if capturing a window)\n    if (!dup.is_window_valid()) {\n      BOOST_LOG(warning) << \"Captured window is no longer valid, reinitializing capture\"sv;\n      return capture_e::reinit;\n    }\n\n    HRESULT status;\n    texture2d_t src;\n    uint64_t frame_qpc;\n    dup.set_cursor_visible(cursor_visible);\n    auto capture_status = dup.next_frame(timeout, &src, frame_qpc);\n    if (capture_status != capture_e::ok) {\n      // If we're capturing a window and getting timeouts/errors, check if window is still valid\n      if (dup.captured_window_hwnd != nullptr) {\n        // Simplified: Any error or timeout means window might have changed, check validity\n        if (!dup.is_window_valid()) {\n          BOOST_LOG(warning) << \"Captured window is no longer valid, reinitializing capture\"sv;\n          return capture_e::reinit;\n        }\n      }\n      return capture_status;\n    }\n\n    auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc);\n    D3D11_TEXTURE2D_DESC desc;\n    src->GetDesc(&desc);\n\n    // Get the actual captured frame dimensions\n    int frame_width = static_cast<int>(desc.Width);\n    int frame_height = static_cast<int>(desc.Height);\n\n    // For window capture, update stored dimensions if they changed\n    if (dup.captured_window_hwnd != nullptr) {\n      if (dup.window_capture_width != frame_width || dup.window_capture_height != frame_height) {\n        BOOST_LOG(info) << \"Window capture size changed: \"sv << dup.window_capture_width << 'x' << dup.window_capture_height\n                        << \" -> \"sv << frame_width << 'x' << frame_height;\n        dup.window_capture_width = frame_width;\n        dup.window_capture_height = frame_height;\n        // Reset texture to force recreation with new size\n        texture.reset();\n      }\n    }\n    else {\n      // For display capture, check against display dimensions\n      // WGC frames are typically in display orientation (after rotation),\n      // so compare against `width`/`height` to avoid reinit loops on rotation.\n      if (frame_width != width || frame_height != height) {\n        BOOST_LOG(info) << \"Capture size changed [\"sv << width << 'x' << height << \" -> \"sv << frame_width << 'x' << frame_height << ']';\n        return capture_e::reinit;\n      }\n    }\n\n    // Create the staging texture if it doesn't exist. It should match the source in size and format.\n    if (texture == nullptr) {\n      capture_format = desc.Format;\n      BOOST_LOG(info) << \"Capture format [\"sv << dxgi_format_to_string(capture_format) << ']';\n      BOOST_LOG(info) << \"Creating staging texture: \"sv << frame_width << 'x' << frame_height;\n\n      D3D11_TEXTURE2D_DESC t {};\n      t.Width = frame_width;\n      t.Height = frame_height;\n      t.MipLevels = 1;\n      t.ArraySize = 1;\n      t.SampleDesc.Count = 1;\n      t.Usage = D3D11_USAGE_STAGING;\n      t.Format = capture_format;\n      t.CPUAccessFlags = D3D11_CPU_ACCESS_READ;\n\n      auto status = device->CreateTexture2D(&t, nullptr, &texture);\n\n      if (FAILED(status)) {\n        BOOST_LOG(error) << \"Failed to create staging texture [0x\"sv << util::hex(status).to_string_view() << ']';\n        return capture_e::error;\n      }\n    }\n\n    // Check if the captured frame size matches our staging texture\n    D3D11_TEXTURE2D_DESC staging_desc;\n    texture->GetDesc(&staging_desc);\n\n    if (frame_width != static_cast<int>(staging_desc.Width) || frame_height != static_cast<int>(staging_desc.Height)) {\n      BOOST_LOG(info) << \"Capture size mismatch - frame: \"sv << frame_width << 'x' << frame_height\n                      << \", staging: \"sv << staging_desc.Width << 'x' << staging_desc.Height\n                      << \", recreating staging texture\"sv;\n      // Reset texture to force recreation with new size on next iteration\n      texture.reset();\n      // Don't reinit the whole capture, just recreate the staging texture next frame\n      return capture_e::timeout;\n    }\n\n    // It's also possible for the capture format to change on the fly. If that happens,\n    // reinitialize capture to try format detection again and create new images.\n    if (capture_format != desc.Format) {\n      BOOST_LOG(info) << \"Capture format changed [\"sv << dxgi_format_to_string(capture_format) << \" -> \"sv << dxgi_format_to_string(desc.Format) << ']';\n      return capture_e::reinit;\n    }\n\n    // Copy from GPU to CPU\n    device_ctx->CopyResource(texture.get(), src.get());\n\n    if (!pull_free_image_cb(img_out)) {\n      return capture_e::interrupted;\n    }\n    auto img = (img_t *) img_out.get();\n\n    // Map the staging texture for CPU access (making it inaccessible for the GPU)\n    if (FAILED(status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info))) {\n      BOOST_LOG(error) << \"Failed to map texture [0x\"sv << util::hex(status).to_string_view() << ']';\n\n      return capture_e::error;\n    }\n\n    // Now that we know the capture format, we can finish creating the image\n    if (complete_img(img, false)) {\n      device_ctx->Unmap(texture.get(), 0);\n      img_info.pData = nullptr;\n      return capture_e::error;\n    }\n\n    // Copy data using actual frame dimensions\n    std::copy_n((std::uint8_t *) img_info.pData, frame_height * img_info.RowPitch, (std::uint8_t *) img->data);\n\n    // Unmap the staging texture to allow GPU access again\n    device_ctx->Unmap(texture.get(), 0);\n    img_info.pData = nullptr;\n\n    if (img) {\n      img->frame_timestamp = frame_timestamp;\n    }\n\n    return capture_e::ok;\n  }\n\n  capture_e\n  display_wgc_ram_t::release_snapshot() {\n    return dup.release_frame();\n  }\n}  // namespace platf::dxgi\n"
  },
  {
    "path": "src/platform/windows/dsu_server.cpp",
    "content": "/**\n * @file src/platform/windows/dsu_server.cpp\n * @brief DSU Server实现文件，用于接收客户端连接并发送Switch Pro手柄的运动传感器数据\n */\n\n#include \"dsu_server.h\"\n#include \"src/logging.h\"\n#include <iomanip>\n#include <sstream>\n\n#ifdef _WIN32\n  #include <winsock2.h>\n  #include <ws2tcpip.h>\n  #ifdef _MSC_VER\n    #pragma comment(lib, \"ws2_32.lib\")\n  #endif\n#endif\n\nnamespace platf {\n\n  dsu_server_t::dsu_server_t(uint16_t port):\n      socket_(io_context_), recv_buffer_(MAX_PACKET_SIZE), running_(false), port_(port), packet_counter_(0) {\n  }\n\n  dsu_server_t::~dsu_server_t() {\n    stop();\n  }\n\n  int\n  dsu_server_t::start() {\n    if (running_) {\n      BOOST_LOG(warning) << \"DSU服务器已经在运行中\";\n      return 0;\n    }\n\n    try {\n      // 检查端口是否可用\n      BOOST_LOG(info) << \"DSU服务器正在启动，端口: \" << port_;\n\n      if (!is_port_available(port_)) {\n        BOOST_LOG(warning) << \"端口 \" << port_ << \" 可能被占用，尝试继续启动...\";\n      }\n\n      // 绑定到指定端口\n      socket_.open(boost::asio::ip::udp::v4());\n\n      // 设置socket选项\n      socket_.set_option(boost::asio::ip::udp::socket::reuse_address(true));\n\n      // 设置socket为非阻塞模式，匹配cemuhook的行为\n      socket_.non_blocking(true);\n\n      // 修复Windows UDP socket的10054错误（远程主机强制关闭连接）\n      // 这是Windows的已知bug，需要禁用连接重置\n      BOOL bNewBehavior = FALSE;\n      DWORD dwBytesReturned = 0;\n      SOCKET native_socket = socket_.native_handle();\n      WSAIoctl(native_socket, SIO_UDP_CONNRESET, &bNewBehavior, sizeof(bNewBehavior),\n        NULL, 0, &dwBytesReturned, NULL, NULL);\n      BOOST_LOG(debug) << \"DSU服务器已禁用Windows UDP连接重置 (SIO_UDP_CONNRESET)\";\n\n      // 尝试绑定端口\n      boost::asio::ip::udp::endpoint endpoint(boost::asio::ip::udp::v4(), port_);\n      socket_.bind(endpoint);\n\n      running_ = true;\n\n      // 启动服务器线程\n      server_thread_ = std::thread(&dsu_server_t::server_loop, this);\n\n      BOOST_LOG(info) << \"DSU服务器启动成功，监听端口: \" << port_\n                      << \" (IP: \" << endpoint.address().to_string() << \")\";\n      return 0;\n    }\n    catch (const boost::system::system_error &e) {\n      BOOST_LOG(error) << \"DSU服务器启动失败: \" << e.what()\n                       << \" (错误代码: \" << e.code().value() << \")\";\n\n      if (e.code() == boost::asio::error::address_in_use) {\n        BOOST_LOG(error) << \"端口 \" << port_ << \" 已被占用，请尝试使用其他端口\";\n      }\n      else if (e.code() == boost::asio::error::access_denied) {\n        BOOST_LOG(error) << \"访问被拒绝，请检查防火墙设置或管理员权限\";\n      }\n\n      return -1;\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(error) << \"DSU服务器启动失败: \" << e.what();\n      return -1;\n    }\n  }\n\n  void\n  dsu_server_t::stop() {\n    if (!running_) {\n      return;\n    }\n\n    running_ = false;\n\n    // 关闭socket以中断接收操作\n    if (socket_.is_open()) {\n      socket_.close();\n    }\n\n    // 等待服务器线程结束\n    if (server_thread_.joinable()) {\n      server_thread_.join();\n    }\n\n    // 清理客户端列表\n    clients_.clear();\n\n    BOOST_LOG(info) << \"DSU服务器已停止\";\n  }\n\n  void\n  dsu_server_t::server_loop() {\n    auto last_cleanup = std::chrono::steady_clock::now();\n    const auto cleanup_interval = std::chrono::milliseconds(500);  // 每500ms清理一次，匹配cemuhook的MAIN_SLEEP_TIME_M\n\n    BOOST_LOG(debug) << \"DSU服务器主循环开始\";\n\n    while (running_) {\n      try {\n        // 使用同步接收方式，匹配cemuhook的行为\n        boost::system::error_code ec;\n        std::size_t bytes_transferred = socket_.receive_from(\n          boost::asio::buffer(recv_buffer_), remote_endpoint_, 0, ec);\n\n        if (!ec) {\n          // 处理接收到的数据包\n          handle_receive_sync(ec, bytes_transferred);\n        }\n        else if (ec != boost::asio::error::would_block) {\n          // 忽略Windows UDP socket的10054错误（远程主机强制关闭连接）\n          // 这是Windows的已知bug，客户端断开连接时会触发此错误\n          if (ec.value() != 10054) {\n            BOOST_LOG(warning) << \"DSU服务器接收数据错误: \" << ec.message()\n                               << \" (错误代码: \" << ec.value() << \")\";\n          }\n          else {\n            BOOST_LOG(debug) << \"DSU服务器忽略Windows UDP连接重置错误 (10054)\";\n          }\n        }\n\n        // 定期清理超时客户端\n        auto now = std::chrono::steady_clock::now();\n        if (now - last_cleanup > cleanup_interval) {\n          cleanup_timeout_clients();\n          last_cleanup = now;\n        }\n\n        // 短暂休眠，避免CPU占用过高\n        std::this_thread::sleep_for(std::chrono::milliseconds(5));\n      }\n      catch (const std::exception &e) {\n        if (running_) {\n          BOOST_LOG(error) << \"DSU服务器异常: \" << e.what();\n        }\n      }\n    }\n\n    BOOST_LOG(debug) << \"DSU服务器主循环结束\";\n  }\n\n  void\n  dsu_server_t::handle_receive_sync(const boost::system::error_code &ec, std::size_t bytes_transferred) {\n    if (!running_) {\n      return;\n    }\n\n    if (ec) {\n      if (ec != boost::asio::error::operation_aborted) {\n        BOOST_LOG(warning) << \"DSU服务器接收数据错误: \" << ec.message()\n                           << \" (错误代码: \" << ec.value() << \")\";\n      }\n      return;\n    }\n\n    if (bytes_transferred < 4) {\n      BOOST_LOG(warning) << \"DSU服务器收到过小的数据包: \" << bytes_transferred << \" 字节\";\n      return;\n    }\n\n    // 解析Header（前16字节）\n    if (bytes_transferred < 16) {\n      BOOST_LOG(warning) << \"DSU服务器收到过小的数据包: \" << bytes_transferred << \" 字节\";\n      return;\n    }\n\n    // 解析消息类型（第16字节开始）\n    uint32_t message_type = *reinterpret_cast<const uint32_t *>(recv_buffer_.data() + 16);\n\n    switch (message_type) {\n      case DSU_MESSAGE_TYPE_INFO:\n        handle_info_request(remote_endpoint_, recv_buffer_.data(), bytes_transferred);\n        break;\n\n      case DSU_MESSAGE_TYPE_DATA:\n        handle_data_request(remote_endpoint_, recv_buffer_.data(), bytes_transferred);\n        break;\n\n      default:\n        BOOST_LOG(debug) << \"DSU服务器收到未知消息类型: 0x\" << std::hex << message_type;\n        break;\n    }\n  }\n\n  void\n  dsu_server_t::handle_info_request(const boost::asio::ip::udp::endpoint &client_endpoint,\n    const uint8_t *data, std::size_t size) {\n    if (size < 20) {  // 至少需要16字节Header + 4字节消息类型\n      BOOST_LOG(warning) << \"DSU服务器收到过小的INFO请求: \" << size << \" 字节\";\n      return;\n    }\n\n    // 解析客户端ID\n    uint32_t client_id = parse_client_id(data);\n\n    // 解析ControllerInfoRequest中的槽位（第16字节后）\n    uint8_t slot = *(data + 16 + 4);  // 跳过消息类型，读取槽位\n\n    // INFO请求不管理客户端连接，只响应信息（匹配cemuhook行为）\n    BOOST_LOG(debug) << \"DSU服务器收到INFO请求 - 客户端ID: \" << client_id\n                     << \", 槽位: \" << (int) slot\n                     << \", 当前客户端总数: \" << clients_.size();\n\n    memset(&info_packet_, 0, sizeof(info_packet_));\n\n    // 设置DSU协议头部\n    info_packet_.header.magic = 0x53555344;  // \"DSUS\" 魔数\n    info_packet_.header.version = DSU_PROTOCOL_VERSION;\n    info_packet_.header.length = sizeof(info_packet_) - sizeof(dsu_header);  // 总长度减去头部长度\n    info_packet_.header.client_id = client_id;\n\n    // 设置SharedResponse结构\n    info_packet_.shared.message_type = DSU_MESSAGE_TYPE_INFO;  // MessageType\n    info_packet_.shared.slot = slot;  // Slot\n\n    info_packet_.shared.slot_state = 2;  // SlotState.Connected\n    info_packet_.shared.device_model = 2;  // DeviceModelType.FullGyro (Switch Pro)\n    info_packet_.shared.connection_type = 2;  // ConnectionType.Bluetooth\n\n    // 设置MAC地址（6字节数组，全部为0）\n    memset(info_packet_.shared.mac_address, 0, 6);\n\n    // 兼容东哥助手\n    info_packet_.shared.mac_address[0] = 1;\n\n    // 设置电池状态\n    info_packet_.shared.battery_status = 2;  // BatteryStatus.Charging\n\n    info_packet_.padding = 0;\n\n    // 使用通用函数计算CRC32并发送\n    send_packet_with_crc(client_endpoint, &info_packet_, sizeof(info_packet_));\n\n    BOOST_LOG(debug) << \"DSU服务器发送INFO响应 - 客户端ID: \" << client_id\n                     << \", 槽位: \" << (int) slot\n                     << \", 槽位状态: \" << (int) info_packet_.shared.slot_state\n                     << \", 设备型号: \" << (int) info_packet_.shared.device_model\n                     << \", 连接类型: \" << (int) info_packet_.shared.connection_type\n                     << \", 电池状态: \" << (int) info_packet_.shared.battery_status\n                     << \", 响应大小: \" << sizeof(info_packet_) << \" 字节\";\n  }\n\n  void\n  dsu_server_t::handle_data_request(const boost::asio::ip::udp::endpoint &client_endpoint,\n    const uint8_t *data, std::size_t size) {\n    if (size < 20) {  // 至少需要16字节Header + 4字节消息类型\n      BOOST_LOG(warning) << \"DSU服务器收到过小的数据包请求: \" << size << \" 字节\";\n      return;\n    }\n\n    // 使用通用函数解析客户端ID\n    uint32_t client_id = parse_client_id(data);\n\n    // 解析ControllerDataRequest中的槽位（第16字节后）\n    uint8_t slot = *(data + 16 + 4);  // 跳过消息类型，读取槽位\n    uint32_t controller_id = slot;  // 使用槽位作为控制器ID\n\n    // 匹配cemuhook的客户端管理逻辑：只在DATA请求时管理客户端\n    std::string client_key = generate_client_key(client_endpoint);\n    auto it = clients_.find(client_key);\n\n    if (it == clients_.end()) {\n      // 新客户端\n      clients_[client_key] = client_info_t(client_endpoint, controller_id, client_id);\n      BOOST_LOG(debug) << \"DSU服务器新客户端订阅数据 - 客户端ID: \" << client_id\n                       << \", 槽位: \" << (int) slot\n                       << \", 客户端: \" << client_endpoint.address().to_string()\n                       << \":\" << client_endpoint.port()\n                       << \", 当前客户端总数: \" << clients_.size();\n    }\n    else {\n      // 现有客户端，重置超时计数器（匹配cemuhook行为）\n      it->second.sendTimeout = 0;\n    }\n  }\n\n  void\n  dsu_server_t::send_packet_to_client(const boost::asio::ip::udp::endpoint &client_endpoint,\n    const uint8_t *data, size_t size) {\n    try {\n      socket_.send_to(boost::asio::buffer(data, size), client_endpoint);\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(warning) << \"DSU服务器发送数据包失败: \" << e.what();\n    }\n  }\n\n  // 检查端口是否可用\n  bool\n  dsu_server_t::is_port_available(uint16_t port) {\n    try {\n      boost::asio::io_context io_context;\n      boost::asio::ip::udp::socket test_socket(io_context);\n      test_socket.open(boost::asio::ip::udp::v4());\n      test_socket.bind(boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), port));\n      return true;\n    }\n    catch (const std::exception &) {\n      return false;\n    }\n  }\n\n  // CRC32计算函数 - 根据cemuhook.cpp实现\n  uint32_t\n  dsu_server_t::crc32(const unsigned char *s, size_t n) {\n    uint32_t crc = 0xFFFFFFFF;\n\n    int k;\n    while (n--) {\n      crc ^= *s++;\n      for (k = 0; k < 8; k++) {\n        crc = crc & 1 ? (crc >> 1) ^ 0xedb88320 : crc >> 1;\n      }\n    }\n    return ~crc;\n  }\n\n  // 解析客户端ID的通用函数\n  uint32_t\n  dsu_server_t::parse_client_id(const uint8_t *data) {\n    return *reinterpret_cast<const uint32_t *>(data + 8);\n  }\n\n  // 计算CRC32并发送数据包的通用函数\n  void\n  dsu_server_t::send_packet_with_crc(const boost::asio::ip::udp::endpoint &client_endpoint,\n    void *packet, size_t packet_size) {\n    // 计算CRC32校验\n    uint32_t *crc32_ptr = reinterpret_cast<uint32_t *>(static_cast<uint8_t *>(packet) + 8);\n    *crc32_ptr = 0;\n    *crc32_ptr = crc32(reinterpret_cast<const unsigned char *>(packet), packet_size);\n\n    // 发送数据包\n    send_packet_to_client(client_endpoint, reinterpret_cast<const uint8_t *>(packet), packet_size);\n  }\n\n  void\n  dsu_server_t::send_motion_data(uint32_t controller_id,\n    float accel_x, float accel_y, float accel_z,\n    float gyro_x, float gyro_y, float gyro_z) {\n    if (!running_ || clients_.empty()) {\n      return;\n    }\n\n    // 累积运动数据\n    auto &motion = motion_data_[controller_id];\n    motion.last_update = std::chrono::steady_clock::now();\n\n    // 总是更新加速度数据（如果提供了非零值）\n    if (accel_x != 0.0f || accel_y != 0.0f || accel_z != 0.0f) {\n      motion.accel_x = accel_x;\n      motion.accel_y = accel_y;\n      motion.accel_z = accel_z;\n      motion.has_accel = true;\n      BOOST_LOG(debug) << \"DSU服务器更新加速度数据 - 控制器ID: \" << controller_id\n                       << \", 加速度: (\" << accel_x << \", \" << accel_y << \", \" << accel_z << \")\";\n    }\n\n    // 总是更新陀螺仪数据（如果提供了非零值）\n    if (gyro_x != 0.0f || gyro_y != 0.0f || gyro_z != 0.0f) {\n      motion.gyro_x = gyro_x;\n      motion.gyro_y = gyro_y;\n      motion.gyro_z = gyro_z;\n      motion.has_gyro = true;\n      BOOST_LOG(debug) << \"DSU服务器更新陀螺仪数据 - 控制器ID: \" << controller_id\n                       << \", 角速度: (\" << gyro_x << \", \" << gyro_y << \", \" << gyro_z << \")\";\n    }\n\n    // 只有当有运动数据时才发送\n    if (!motion.has_accel && !motion.has_gyro) {\n      return;\n    }\n\n    // 使用预分配的成员变量，避免栈内存分配\n    // 初始化数据包\n    memset(&data_packet_, 0, sizeof(data_packet_));\n\n    // 设置DSU协议头部 - 匹配Ryujinx Header结构\n    data_packet_.header.magic = 0x53555344;  // \"DSUS\" 魔数\n    data_packet_.header.version = DSU_PROTOCOL_VERSION;\n    data_packet_.header.length = sizeof(data_packet_) - sizeof(dsu_header);  // 总长度减去头部长度\n    data_packet_.header.crc32 = 0;  // 稍后计算\n    data_packet_.header.client_id = 0;  // 稍后设置\n\n    // 设置SharedResponse结构 - 匹配Ryujinx期望\n    data_packet_.shared.message_type = DSU_MESSAGE_TYPE_DATA;  // 消息类型在SharedResponse内部\n    data_packet_.shared.slot = controller_id;\n    data_packet_.shared.slot_state = 2;  // Connected\n    data_packet_.shared.device_model = 2;  // FullGyro\n    data_packet_.shared.connection_type = 1;  // USB\n    memset(data_packet_.shared.mac_address, 0, 6);  // MAC地址设为0\n    data_packet_.shared.battery_status = 0;  // NA\n\n    // 设置ControllerDataResponse结构\n    data_packet_.connected = 1;  // 已连接\n    data_packet_.packet_id = 0;  // 稍后设置\n    data_packet_.extra_buttons = 0;\n    data_packet_.main_buttons = 0;\n    data_packet_.ps_extra_input = 0;\n    data_packet_.left_stick_xy = 0;\n    data_packet_.right_stick_xy = 0;\n    data_packet_.dpad_analog = 0;\n    data_packet_.main_buttons_analog = 0;\n    memset(data_packet_.touch1, 0, 6);\n    memset(data_packet_.touch2, 0, 6);\n\n    data_packet_.motion.motion_timestamp = std::chrono::duration_cast<std::chrono::microseconds>(\n      motion.last_update.time_since_epoch())\n                                             .count();\n    // 坐标映射 - 匹配Ryujinx的期望转换\n    // Ryujinx: X = -AccelerometerX, Y = AccelerometerZ, Z = -AccelerometerY\n    data_packet_.motion.accelerometer_x = -motion.accel_x;  // 取反，让Ryujinx得到正确的X\n    data_packet_.motion.accelerometer_y = -motion.accel_z;  // 取反Z，让Ryujinx得到正确的Y\n    data_packet_.motion.accelerometer_z = motion.accel_y;   // 直接映射Y，让Ryujinx得到正确的Z\n    \n    // Ryujinx: X = GyroscopePitch, Y = GyroscopeRoll, Z = -GyroscopeYaw\n    data_packet_.motion.gyroscope_pitch = motion.gyro_x;    // pitch对应gyro_x\n    data_packet_.motion.gyroscope_yaw = -motion.gyro_y;     // yaw取反，让Ryujinx得到正确的Y\n    data_packet_.motion.gyroscope_roll = motion.gyro_z;     // roll对应gyro_z\n\n    if (clients_.empty()) {\n      BOOST_LOG(debug) << \"DSU服务器没有连接的客户端，跳过运动数据发送\";\n      return;\n    }\n\n    // 批量发送到所有客户端\n    for (const auto &[client_key, client_info] : clients_) {\n      // 设置客户端ID和数据包编号\n      data_packet_.header.client_id = client_info.client_id;\n      data_packet_.packet_id = ++packet_counter_;\n\n      // 使用通用函数计算CRC32并发送\n      send_packet_with_crc(client_info.endpoint, &data_packet_, sizeof(data_packet_));\n    }\n  }\n\n  void\n  dsu_server_t::cleanup_timeout_clients() {\n    auto it = clients_.begin();\n\n    while (it != clients_.end()) {\n      it->second.sendTimeout++;\n      if (it->second.sendTimeout >= CLIENT_TIMEOUT) {\n        BOOST_LOG(debug) << \"DSU服务器清理超时客户端: \" << it->first;\n        it = clients_.erase(it);\n      }\n      else {\n        ++it;\n      }\n    }\n  }\n\n  std::string\n  dsu_server_t::generate_client_key(const boost::asio::ip::udp::endpoint &client_endpoint) const {\n    return client_endpoint.address().to_string() + \":\" + std::to_string(client_endpoint.port());\n  }\n\n  void\n  dsu_server_t::update_clients_activity(const std::vector<boost::asio::ip::udp::endpoint> &client_endpoints) {\n    auto now = std::chrono::steady_clock::now();\n\n    for (const auto &endpoint : client_endpoints) {\n      std::string client_key = generate_client_key(endpoint);\n      auto it = clients_.find(client_key);\n\n      if (it != clients_.end()) {\n        it->second.last_seen = now;\n      }\n    }\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/windows/dsu_server.h",
    "content": "/**\n * @file src/platform/windows/dsu_server.h\n * @brief DSU Server头文件，用于接收客户端连接并发送Switch Pro手柄的运动传感器数据\n */\n\n#pragma once\n\n#include <cstdint>\n#include <string>\n#include <vector>\n#include <map>\n#include <thread>\n#include <atomic>\n#include <chrono>\n#include <boost/asio.hpp>\n\nnamespace platf {\n\n  // DSU协议基础结构\n  #pragma pack(push, 1)  // 确保字节对齐\n  \n  // DSU协议头部 (16字节) - 匹配cemuhook标准\n  struct dsu_header {\n    uint32_t magic;          // 0x53555344 (DSUS)\n    uint16_t version;        // 协议版本 (1001)\n    uint16_t length;         // 数据长度\n    uint32_t crc32;          // CRC32校验\n    uint32_t client_id;      // 客户端ID\n  };\n\n  // SharedResponse结构\n  struct dsu_shared_response {\n    uint32_t message_type;   // MessageType (0x100001 INFO, 0x100002 DATA)\n    uint8_t slot;            // Slot\n    uint8_t slot_state;      // SlotState (0=Disconnected, 1=Reserved, 2=Connected)\n    uint8_t device_model;    // DeviceModelType (0=None, 1=PartialGyro, 2=FullGyro)\n    uint8_t connection_type; // ConnectionType (0=None, 1=USB, 2=Bluetooth)\n    uint8_t mac_address[6];  // Array6<byte> MacAddress (6字节数组)\n    uint8_t battery_status;  // BatteryStatus (0=NA, 1=Dying, 2=Low, 3=Medium, 4=High, 5=Full, 6=Charging, 7=Charged)\n  };\n\n  // 运动数据结构\n  struct dsu_motion_data {\n    uint64_t motion_timestamp; // 运动时间戳\n    float accelerometer_x;   // X轴加速度\n    float accelerometer_y;   // Y轴加速度\n    float accelerometer_z;   // Z轴加速度\n    float gyroscope_pitch;   // X轴角速度\n    float gyroscope_yaw;     // Y轴角速度 (注意：Yaw在Roll之前)\n    float gyroscope_roll;    // Z轴角速度\n  };\n\n  // INFO响应结构 - 组合头部和共享响应\n  struct dsu_info_response {\n    dsu_header header;           // DSU协议头部\n    dsu_shared_response shared;  // 共享响应部分\n    uint8_t padding;             // 1字节填充，期望的32字节总大小\n  };\n\n  // DATA响应结构 - 组合头部、共享响应和控制器数据\n  struct dsu_data_packet {\n    dsu_header header;           // DSU协议头部\n    dsu_shared_response shared;  // 共享响应部分\n    \n    // ControllerDataResponse结构\n    uint8_t connected;       // 连接状态\n    uint32_t packet_id;      // 数据包ID\n    uint8_t extra_buttons;   // 额外按钮\n    uint8_t main_buttons;    // 主要按钮\n    uint16_t ps_extra_input; // PS额外输入\n    uint16_t left_stick_xy;  // 左摇杆XY\n    uint16_t right_stick_xy; // 右摇杆XY\n    uint32_t dpad_analog;    // 方向键模拟\n    uint64_t main_buttons_analog; // 主要按钮模拟\n    \n    uint8_t touch1[6];       // 触摸1数据\n    uint8_t touch2[6];       // 触摸2数据\n    \n    // 运动数据 - 复用运动数据结构\n    dsu_motion_data motion;  // 运动数据部分\n  };\n  #pragma pack(pop)  // 恢复默认字节对齐\n\n  /**\n   * @brief DSU Server，用于接收客户端连接并发送Switch Pro手柄的运动传感器数据\n   * @details 实现DSU (cemuhook protocol) 服务器，接收客户端连接请求并发送运动数据\n   */\n  class dsu_server_t {\n  public:\n    /**\n     * @brief 构造函数\n     * @param port 服务器监听端口，默认为26760（DSU标准端口）\n     */\n    explicit dsu_server_t(uint16_t port = 26760);\n\n    /**\n     * @brief 析构函数\n     */\n    ~dsu_server_t();\n\n    /**\n     * @brief 启动DSU服务器\n     * @return 0表示成功，-1表示失败\n     */\n    int start();\n\n    /**\n     * @brief 停止DSU服务器\n     */\n    void stop();\n\n    /**\n     * @brief 发送运动传感器数据到所有连接的客户端\n     * @param controller_id 控制器ID\n     * @param accel_x X轴加速度 (m/s²)\n     * @param accel_y Y轴加速度 (m/s²)\n     * @param accel_z Z轴加速度 (m/s²)\n     * @param gyro_x X轴角速度 (deg/s)\n     * @param gyro_y Y轴角速度 (deg/s)\n     * @param gyro_z Z轴角速度 (deg/s)\n     */\n    void send_motion_data(uint32_t controller_id,\n                          float accel_x, float accel_y, float accel_z,\n                          float gyro_x, float gyro_y, float gyro_z);\n\n\n    /**\n     * @brief 生成客户端键（IP:端口格式）\n     * @param client_endpoint 客户端端点\n     * @return 客户端键字符串\n     */\n    std::string generate_client_key(const boost::asio::ip::udp::endpoint &client_endpoint) const;\n\n    /**\n     * @brief 批量更新客户端最后活动时间\n     * @param client_endpoints 需要更新的客户端端点列表\n     */\n    void update_clients_activity(const std::vector<boost::asio::ip::udp::endpoint> &client_endpoints);\n\n    /**\n     * @brief 检查服务器是否正在运行\n     * @return true表示正在运行，false表示已停止\n     */\n    bool is_running() const { return running_; }\n\n    /**\n     * @brief 获取连接的客户端数量\n     * @return 连接的客户端数量\n     */\n    size_t get_client_count() const { return clients_.size(); }\n\n  private:\n    /**\n     * @brief 客户端连接信息\n     */\n    struct client_info_t {\n      boost::asio::ip::udp::endpoint endpoint;\n      std::chrono::steady_clock::time_point last_seen;\n      uint32_t controller_id;\n      uint32_t client_id;\n      int sendTimeout;  // 超时计数器，匹配cemuhook行为\n      \n      // 构造函数，便于初始化\n      client_info_t() = default;\n      client_info_t(const boost::asio::ip::udp::endpoint &ep, uint32_t ctrl_id, uint32_t cli_id)\n        : endpoint(ep), last_seen(std::chrono::steady_clock::now()), \n          controller_id(ctrl_id), client_id(cli_id), sendTimeout(0) {}\n    };\n\n    /**\n     * @brief 运动数据结构\n     */\n    struct motion_data_t {\n      float accel_x = 0.0f;\n      float accel_y = 0.0f;\n      float accel_z = 0.0f;\n      float gyro_x = 0.0f;\n      float gyro_y = 0.0f;\n      float gyro_z = 0.0f;\n      std::chrono::steady_clock::time_point last_update = std::chrono::steady_clock::now();\n      bool has_accel = false;\n      bool has_gyro = false;\n    };\n\n    /**\n     * @brief 启动接收循环\n     */\n    void start_receive();\n\n    /**\n     * @brief 处理接收到的数据包\n     * @param error 错误信息\n     * @param bytes_transferred 传输的字节数\n     */\n    void handle_receive_sync(const boost::system::error_code& error, std::size_t bytes_transferred);\n\n    /**\n     * @brief 处理控制器信息请求\n     * @param client_endpoint 客户端端点\n     * @param data 数据包内容\n     * @param size 数据包大小\n     */\n    void handle_info_request(const boost::asio::ip::udp::endpoint& client_endpoint,\n                            const uint8_t* data, std::size_t size);\n\n    /**\n     * @brief 处理数据请求\n     * @param client_endpoint 客户端端点\n     * @param data 数据包内容\n     * @param size 数据包大小\n     */\n    void handle_data_request(const boost::asio::ip::udp::endpoint& client_endpoint,\n                            const uint8_t* data, std::size_t size);\n\n\n    /**\n     * @brief 发送数据包到指定客户端\n     * @param client_endpoint 客户端端点\n     * @param data 数据指针\n     * @param size 数据大小\n     */\n    void send_packet_to_client(const boost::asio::ip::udp::endpoint& client_endpoint,\n                               const uint8_t* data, size_t size);\n\n    /**\n     * @brief 清理超时的客户端连接\n     */\n    void cleanup_timeout_clients();\n\n    /**\n     * @brief 计算CRC32校验\n     * @param s 数据指针\n     * @param n 数据长度\n     * @return CRC32值\n     */\n    uint32_t crc32(const unsigned char *s, size_t n);\n\n    /**\n     * @brief 解析客户端ID的通用函数\n     * @param data 数据包指针\n     * @return 客户端ID\n     */\n    uint32_t parse_client_id(const uint8_t *data);\n\n    /**\n     * @brief 计算CRC32并发送数据包的通用函数\n     * @param client_endpoint 客户端端点\n     * @param packet 数据包指针\n     * @param packet_size 数据包大小\n     */\n    void send_packet_with_crc(const boost::asio::ip::udp::endpoint &client_endpoint,\n                             void *packet, size_t packet_size);\n\n    /**\n     * @brief 检查端口是否可用\n     * @param port 要检查的端口号\n     * @return true如果端口可用，false如果被占用\n     */\n    bool is_port_available(uint16_t port);\n\n    /**\n     * @brief 服务器主循环\n     */\n    void server_loop();\n\n    boost::asio::io_context io_context_;\n    boost::asio::ip::udp::socket socket_;\n    boost::asio::ip::udp::endpoint remote_endpoint_;\n    \n    std::vector<uint8_t> recv_buffer_;\n    std::map<std::string, client_info_t> clients_;\n    std::map<uint32_t, motion_data_t> motion_data_;  // 控制器ID -> 运动数据\n    \n    // 性能优化：预分配数据包结构，避免每次函数调用都分配栈内存\n    dsu_info_response info_packet_;  // 预分配的INFO响应结构\n    dsu_data_packet data_packet_;    // 预分配的DATA响应结构\n    \n    std::thread server_thread_;\n    std::atomic<bool> running_;\n    uint16_t port_;\n    uint32_t packet_counter_;\n    \n    static constexpr size_t MAX_PACKET_SIZE = 100;\n    static constexpr int CLIENT_TIMEOUT = 40;  // 匹配cemuhook的超时阈值\n    \n    // DSU协议常量 - 根据cemuhook\n    static constexpr uint32_t DSU_PROTOCOL_VERSION = 1001;\n    static constexpr uint32_t DSU_MESSAGE_TYPE_INFO = 0x100001;     // 控制器信息请求\n    static constexpr uint32_t DSU_MESSAGE_TYPE_DATA = 0x100002;     // 数据请求\n  };\n\n} // namespace platf\n"
  },
  {
    "path": "src/platform/windows/ftime_compat.cpp",
    "content": "#include <sys/timeb.h>\n\n#if defined(_WIN32) && !defined(_MSC_VER)\nextern \"C\" void ftime64(struct __timeb64* timeptr) {\n  _ftime64(timeptr);\n}\n#endif\n\n\n\n\n"
  },
  {
    "path": "src/platform/windows/input.cpp",
    "content": "/**\n * @file src/platform/windows/input.cpp\n * @brief Definitions for input handling on Windows.\n */\n#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#define WINVER 0x0A00\n#include <windows.h>\n\n#include <cmath>\n#include <thread>\n\n#include <ViGEm/Client.h>\n\n#include \"dsu_server.h\"\n#include \"keylayout.h\"\n#include \"misc.h\"\n#include \"virtual_mouse.h\"\n#include \"src/config.h\"\n#include \"src/globals.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n\n// Note: HSYNTHETICPOINTERDEVICE and related functions are now included in\n// modern MinGW-w64 SDK (UCRT64). The declarations below are only needed for\n// very old MinGW versions that predate Windows 10 SDK support.\n// \n// If you encounter \"undefined\" errors on old MinGW, uncomment the following:\n// #define SUNSHINE_NEED_SYNTHETIC_POINTER_DECL\n#ifdef SUNSHINE_NEED_SYNTHETIC_POINTER_DECL\nDECLARE_HANDLE(HSYNTHETICPOINTERDEVICE);\nWINUSERAPI HSYNTHETICPOINTERDEVICE WINAPI\nCreateSyntheticPointerDevice(POINTER_INPUT_TYPE pointerType, ULONG maxCount, POINTER_FEEDBACK_MODE mode);\nWINUSERAPI BOOL WINAPI\nInjectSyntheticPointerInput(HSYNTHETICPOINTERDEVICE device, CONST POINTER_TYPE_INFO *pointerInfo, UINT32 count);\nWINUSERAPI VOID WINAPI\nDestroySyntheticPointerDevice(HSYNTHETICPOINTERDEVICE device);\n#endif\n\nnamespace platf {\n  using namespace std::literals;\n\n  thread_local HDESK _lastKnownInputDesktop = nullptr;\n\n  constexpr touch_port_t target_touch_port {\n    0, 0,\n    65535, 65535\n  };\n\n  using client_t = util::safe_ptr<_VIGEM_CLIENT_T, vigem_free>;\n  using target_t = util::safe_ptr<_VIGEM_TARGET_T, vigem_target_free>;\n\n  void CALLBACK\n  x360_notify(\n    client_t::pointer client,\n    target_t::pointer target,\n    std::uint8_t largeMotor, std::uint8_t smallMotor,\n    std::uint8_t /* led_number */,\n    void *userdata);\n\n  void CALLBACK\n  ds4_notify(\n    client_t::pointer client,\n    target_t::pointer target,\n    std::uint8_t largeMotor, std::uint8_t smallMotor,\n    DS4_LIGHTBAR_COLOR /* led_color */,\n    void *userdata);\n\n  struct gp_touch_context_t {\n    uint8_t pointerIndex;\n    uint16_t x;\n    uint16_t y;\n  };\n\n  struct gamepad_context_t {\n    target_t gp;\n    feedback_queue_t feedback_queue;\n\n    union {\n      XUSB_REPORT x360;\n      DS4_REPORT_EX ds4;\n    } report;\n\n    // Map from pointer ID to pointer index\n    std::map<uint32_t, uint8_t> pointer_id_map;\n    uint8_t available_pointers;\n\n    uint8_t client_relative_index;\n\n    thread_pool_util::ThreadPool::task_id_t repeat_task {};\n    std::chrono::steady_clock::time_point last_report_ts;\n\n    gamepad_feedback_msg_t last_rumble;\n    gamepad_feedback_msg_t last_rgb_led;\n  };\n\n  constexpr float EARTH_G = 9.80665f;\n\n#define MPS2_TO_DS4_ACCEL(x) (int32_t) (((x) / EARTH_G) * 8192)\n#define DPS_TO_DS4_GYRO(x) (int32_t) ((x) * (1024 / 64))\n\n#define APPLY_CALIBRATION(val, bias, scale) (int32_t) (((float) (val) + (bias)) / (scale))\n\n  constexpr DS4_TOUCH ds4_touch_unused = {\n    .bPacketCounter = 0,\n    .bIsUpTrackingNum1 = 0x80,\n    .bTouchData1 = { 0x00, 0x00, 0x00 },\n    .bIsUpTrackingNum2 = 0x80,\n    .bTouchData2 = { 0x00, 0x00, 0x00 },\n  };\n\n  // See https://github.com/ViGEm/ViGEmBus/blob/22835473d17fbf0c4d4bb2f2d42fd692b6e44df4/sys/Ds4Pdo.cpp#L153-L164\n  constexpr DS4_REPORT_EX ds4_report_init_ex = {\n    { { .bThumbLX = 0x80,\n      .bThumbLY = 0x80,\n      .bThumbRX = 0x80,\n      .bThumbRY = 0x80,\n      .wButtons = DS4_BUTTON_DPAD_NONE,\n      .bSpecial = 0,\n      .bTriggerL = 0,\n      .bTriggerR = 0,\n      .wTimestamp = 0,\n      .bBatteryLvl = 0xFF,\n      .wGyroX = 0,\n      .wGyroY = 0,\n      .wGyroZ = 0,\n      .wAccelX = 0,\n      .wAccelY = 0,\n      .wAccelZ = 0,\n      ._bUnknown1 = { 0x00, 0x00, 0x00, 0x00, 0x00 },\n      .bBatteryLvlSpecial = 0x1A,  // Wired - Full battery\n      ._bUnknown2 = { 0x00, 0x00 },\n      .bTouchPacketsN = 1,\n      .sCurrentTouch = ds4_touch_unused,\n      .sPreviousTouch = { ds4_touch_unused, ds4_touch_unused } } }\n  };\n\n  /**\n   * @brief Updates the DS4 input report with the provided motion data.\n   * @details Acceleration is in m/s^2 and gyro is in deg/s.\n   * @param gamepad The gamepad to update.\n   * @param motion_type The type of motion data.\n   * @param x X component of motion.\n   * @param y Y component of motion.\n   * @param z Z component of motion.\n   */\n  static void\n  ds4_update_motion(gamepad_context_t &gamepad, uint8_t motion_type, float x, float y, float z) {\n    auto &report = gamepad.report.ds4.Report;\n\n    // Use int32 to process this data, so we can clamp if needed.\n    int32_t intX, intY, intZ;\n\n    switch (motion_type) {\n      case LI_MOTION_TYPE_ACCEL:\n        // Convert to the DS4's accelerometer scale\n        intX = MPS2_TO_DS4_ACCEL(x);\n        intY = MPS2_TO_DS4_ACCEL(y);\n        intZ = MPS2_TO_DS4_ACCEL(z);\n\n        // Apply the inverse of ViGEmBus's calibration data\n        intX = APPLY_CALIBRATION(intX, -297, 1.010796f);\n        intY = APPLY_CALIBRATION(intY, -42, 1.014614f);\n        intZ = APPLY_CALIBRATION(intZ, -512, 1.024768f);\n        break;\n      case LI_MOTION_TYPE_GYRO:\n        // Convert to the DS4's gyro scale\n        intX = DPS_TO_DS4_GYRO(x);\n        intY = DPS_TO_DS4_GYRO(y);\n        intZ = DPS_TO_DS4_GYRO(z);\n\n        // Apply the inverse of ViGEmBus's calibration data\n        intX = APPLY_CALIBRATION(intX, 1, 0.977596f);\n        intY = APPLY_CALIBRATION(intY, 0, 0.972370f);\n        intZ = APPLY_CALIBRATION(intZ, 0, 0.971550f);\n        break;\n      default:\n        return;\n    }\n\n    // Clamp the values to the range of the data type\n    intX = std::clamp(intX, INT16_MIN, INT16_MAX);\n    intY = std::clamp(intY, INT16_MIN, INT16_MAX);\n    intZ = std::clamp(intZ, INT16_MIN, INT16_MAX);\n\n    // Populate the report\n    switch (motion_type) {\n      case LI_MOTION_TYPE_ACCEL:\n        report.wAccelX = (int16_t) intX;\n        report.wAccelY = (int16_t) intY;\n        report.wAccelZ = (int16_t) intZ;\n        break;\n      case LI_MOTION_TYPE_GYRO:\n        report.wGyroX = (int16_t) intX;\n        report.wGyroY = (int16_t) intY;\n        report.wGyroZ = (int16_t) intZ;\n        break;\n      default:\n        return;\n    }\n  }\n\n  class vigem_t {\n  public:\n    int\n    init() {\n      // Probe ViGEm during startup to see if we can successfully attach gamepads. This will allow us to\n      // immediately display the error message in the web UI even before the user tries to stream.\n      client_t client { vigem_alloc() };\n      VIGEM_ERROR status = vigem_connect(client.get());\n      if (!VIGEM_SUCCESS(status)) {\n        // Log a special fatal message for this case to show the error in the web UI\n        BOOST_LOG(fatal) << \"ViGEmBus is not installed or running. You must install ViGEmBus for gamepad support! (If you don't need gamepad support, you can ignore this message.)\"sv;\n        BOOST_LOG(fatal) << \"ViGEmBus 没有安装或运行。您必须安装 ViGEmBus 才能支持游戏手柄！如果不需要使用游戏手柄，可以忽略此提示。\"sv;\n      }\n      else {\n        vigem_disconnect(client.get());\n      }\n\n      gamepads.resize(MAX_GAMEPADS);\n\n      return 0;\n    }\n\n    /**\n     * @brief Attaches a new gamepad.\n     * @param id The gamepad ID.\n     * @param feedback_queue The queue for posting messages back to the client.\n     * @param gp_type The type of gamepad.\n     * @return 0 on success.\n     */\n    int\n    alloc_gamepad_internal(const gamepad_id_t &id, feedback_queue_t &feedback_queue, VIGEM_TARGET_TYPE gp_type) {\n      auto &gamepad = gamepads[id.globalIndex];\n      assert(!gamepad.gp);\n\n      gamepad.client_relative_index = id.clientRelativeIndex;\n      gamepad.last_report_ts = std::chrono::steady_clock::now();\n\n      // Establish a connect to the ViGEm driver if we don't have one yet\n      if (!client) {\n        BOOST_LOG(debug) << \"Connecting to ViGEmBus driver\"sv;\n        client.reset(vigem_alloc());\n\n        auto status = vigem_connect(client.get());\n        if (!VIGEM_SUCCESS(status)) {\n          BOOST_LOG(warning) << \"Couldn't setup connection to ViGEm for gamepad support [\"sv << util::hex(status).to_string_view() << ']';\n          client.reset();\n          return -1;\n        }\n      }\n\n      if (gp_type == Xbox360Wired) {\n        gamepad.gp.reset(vigem_target_x360_alloc());\n        XUSB_REPORT_INIT(&gamepad.report.x360);\n      }\n      else {\n        gamepad.gp.reset(vigem_target_ds4_alloc());\n\n        // There is no equivalent DS4_REPORT_EX_INIT()\n        gamepad.report.ds4 = ds4_report_init_ex;\n\n        // Set initial accelerometer and gyro state\n        ds4_update_motion(gamepad, LI_MOTION_TYPE_ACCEL, 0.0f, EARTH_G, 0.0f);\n        ds4_update_motion(gamepad, LI_MOTION_TYPE_GYRO, 0.0f, 0.0f, 0.0f);\n\n        // Request motion events from the client at 100 Hz\n        feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(gamepad.client_relative_index, LI_MOTION_TYPE_ACCEL, 100));\n        feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(gamepad.client_relative_index, LI_MOTION_TYPE_GYRO, 100));\n\n        // We support pointer index 0 and 1\n        gamepad.available_pointers = 0x3;\n      }\n\n      auto status = vigem_target_add(client.get(), gamepad.gp.get());\n      if (!VIGEM_SUCCESS(status)) {\n        BOOST_LOG(error) << \"Couldn't add Gamepad to ViGEm connection [\"sv << util::hex(status).to_string_view() << ']';\n\n        return -1;\n      }\n\n      gamepad.feedback_queue = std::move(feedback_queue);\n\n      if (gp_type == Xbox360Wired) {\n        status = vigem_target_x360_register_notification(client.get(), gamepad.gp.get(), x360_notify, this);\n      }\n      else {\n        status = vigem_target_ds4_register_notification(client.get(), gamepad.gp.get(), ds4_notify, this);\n      }\n\n      if (!VIGEM_SUCCESS(status)) {\n        BOOST_LOG(warning) << \"Couldn't register notifications for rumble support [\"sv << util::hex(status).to_string_view() << ']';\n      }\n\n      return 0;\n    }\n\n    /**\n     * @brief Detaches the specified gamepad\n     * @param nr The gamepad.\n     */\n    void\n    free_target(int nr) {\n      auto &gamepad = gamepads[nr];\n\n      if (gamepad.repeat_task) {\n        task_pool.cancel(gamepad.repeat_task);\n        gamepad.repeat_task = 0;\n      }\n\n      if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) {\n        auto status = vigem_target_remove(client.get(), gamepad.gp.get());\n        if (!VIGEM_SUCCESS(status)) {\n          BOOST_LOG(warning) << \"Couldn't detach gamepad from ViGEm [\"sv << util::hex(status).to_string_view() << ']';\n        }\n      }\n\n      gamepad.gp.reset();\n\n      // Disconnect from ViGEm if we just removed the last gamepad\n      bool disconnect = true;\n      for (auto &gamepad : gamepads) {\n        if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) {\n          disconnect = false;\n          break;\n        }\n      }\n      if (disconnect) {\n        BOOST_LOG(debug) << \"Disconnecting from ViGEmBus driver\"sv;\n        vigem_disconnect(client.get());\n        client.reset();\n      }\n    }\n\n    /**\n     * @brief Pass rumble data back to the client.\n     * @param target The gamepad.\n     * @param largeMotor The large motor.\n     * @param smallMotor The small motor.\n     */\n    void\n    rumble(target_t::pointer target, std::uint8_t largeMotor, std::uint8_t smallMotor) {\n      for (int x = 0; x < gamepads.size(); ++x) {\n        auto &gamepad = gamepads[x];\n\n        if (gamepad.gp.get() == target) {\n          // Convert from 8-bit to 16-bit values\n          uint16_t normalizedLargeMotor = largeMotor << 8;\n          uint16_t normalizedSmallMotor = smallMotor << 8;\n\n          // Don't resend duplicate rumble data\n          if (normalizedSmallMotor != gamepad.last_rumble.data.rumble.highfreq ||\n              normalizedLargeMotor != gamepad.last_rumble.data.rumble.lowfreq) {\n            // We have to use the client-relative index when communicating back to the client\n            gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rumble(\n              gamepad.client_relative_index, normalizedLargeMotor, normalizedSmallMotor);\n            gamepad.feedback_queue->raise(msg);\n            gamepad.last_rumble = msg;\n          }\n          return;\n        }\n      }\n    }\n\n    /**\n     * @brief Pass RGB LED data back to the client.\n     * @param target The gamepad.\n     * @param r The red channel.\n     * @param g The red channel.\n     * @param b The red channel.\n     */\n    void\n    set_rgb_led(target_t::pointer target, std::uint8_t r, std::uint8_t g, std::uint8_t b) {\n      for (int x = 0; x < gamepads.size(); ++x) {\n        auto &gamepad = gamepads[x];\n\n        if (gamepad.gp.get() == target) {\n          // Don't resend duplicate RGB data\n          if (r != gamepad.last_rgb_led.data.rgb_led.r ||\n              g != gamepad.last_rgb_led.data.rgb_led.g ||\n              b != gamepad.last_rgb_led.data.rgb_led.b) {\n            // We have to use the client-relative index when communicating back to the client\n            gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rgb_led(gamepad.client_relative_index, r, g, b);\n            gamepad.feedback_queue->raise(msg);\n            gamepad.last_rgb_led = msg;\n          }\n          return;\n        }\n      }\n    }\n\n    /**\n     * @brief vigem_t destructor.\n     */\n    ~vigem_t() {\n      if (client) {\n        for (auto &gamepad : gamepads) {\n          if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) {\n            auto status = vigem_target_remove(client.get(), gamepad.gp.get());\n            if (!VIGEM_SUCCESS(status)) {\n              BOOST_LOG(warning) << \"Couldn't detach gamepad from ViGEm [\"sv << util::hex(status).to_string_view() << ']';\n            }\n          }\n        }\n\n        vigem_disconnect(client.get());\n      }\n    }\n\n    std::vector<gamepad_context_t> gamepads;\n\n    client_t client;\n  };\n\n  void CALLBACK\n  x360_notify(\n    client_t::pointer client,\n    target_t::pointer target,\n    std::uint8_t largeMotor, std::uint8_t smallMotor,\n    std::uint8_t /* led_number */,\n    void *userdata) {\n    BOOST_LOG(debug)\n      << \"largeMotor: \"sv << (int) largeMotor << std::endl\n      << \"smallMotor: \"sv << (int) smallMotor;\n\n    task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, largeMotor, smallMotor);\n  }\n\n  void CALLBACK\n  ds4_notify(\n    client_t::pointer client,\n    target_t::pointer target,\n    std::uint8_t largeMotor, std::uint8_t smallMotor,\n    DS4_LIGHTBAR_COLOR led_color,\n    void *userdata) {\n    BOOST_LOG(debug)\n      << \"largeMotor: \"sv << (int) largeMotor << std::endl\n      << \"smallMotor: \"sv << (int) smallMotor << std::endl\n      << \"LED: \"sv << util::hex(led_color.Red).to_string_view() << ' '\n      << util::hex(led_color.Green).to_string_view() << ' '\n      << util::hex(led_color.Blue).to_string_view() << std::endl;\n\n    task_pool.push(&vigem_t::rumble, (vigem_t *) userdata, target, largeMotor, smallMotor);\n    task_pool.push(&vigem_t::set_rgb_led, (vigem_t *) userdata, target, led_color.Red, led_color.Green, led_color.Blue);\n  }\n\n  // Per-app mouse mode: 0=auto (use global config), 1=force virtual mouse, 2=force SendInput\n  static std::atomic<int> current_mouse_mode { 0 };\n\n  void\n  set_mouse_mode(int mode) {\n    current_mouse_mode.store(mode, std::memory_order_relaxed);\n    BOOST_LOG(info) << \"Mouse mode set to: \"sv << (mode == 0 ? \"auto\" : mode == 1 ? \"virtual mouse\" : \"SendInput\");\n  }\n\n  struct input_raw_t {\n    ~input_raw_t() {\n      delete vigem;\n      delete vmouse_dev;\n      if (dsu_server) {\n        dsu_server->stop();\n        delete dsu_server;\n        dsu_server = nullptr;\n      }\n    }\n\n    // 初始化DSU服务器（延迟初始化）\n    void\n    init_dsu_server() {\n      if (dsu_server != nullptr) return;\n\n      // 获取DSU服务器端口\n      uint16_t server_port = config::input.dsu_server_port;\n\n      dsu_server = new dsu_server_t { server_port };\n      if (dsu_server->start()) {\n        dsu_server->stop();\n        delete dsu_server;\n        dsu_server = nullptr;\n      }\n    }\n\n    vigem_t *vigem;\n    dsu_server_t *dsu_server;\n    vmouse::device_t *vmouse_dev;\n    int vmouse_vscroll_accum = 0;\n    int vmouse_hscroll_accum = 0;\n\n    decltype(CreateSyntheticPointerDevice) *fnCreateSyntheticPointerDevice;\n    decltype(InjectSyntheticPointerInput) *fnInjectSyntheticPointerInput;\n    decltype(DestroySyntheticPointerDevice) *fnDestroySyntheticPointerDevice;\n  };\n\n  input_t\n  input() {\n    input_t result { new input_raw_t {} };\n    auto &raw = *(input_raw_t *) result.get();\n\n    raw.vigem = new vigem_t {};\n    if (raw.vigem->init()) {\n      delete raw.vigem;\n      raw.vigem = nullptr;\n    }\n\n    // 初始化DSU服务器（延迟初始化，只在需要时创建）\n    raw.dsu_server = nullptr;\n\n    // 初始化虚拟鼠标设备\n    if (config::input.virtual_mouse) {\n      raw.vmouse_dev = new vmouse::device_t(vmouse::create());\n      if (raw.vmouse_dev->is_available()) {\n        BOOST_LOG(info) << \"Virtual mouse driver connected\"sv;\n      }\n      else {\n        BOOST_LOG(info) << \"Virtual mouse driver not available, using SendInput\"sv;\n      }\n    }\n    else {\n      raw.vmouse_dev = nullptr;\n      BOOST_LOG(info) << \"Virtual mouse driver disabled by config\"sv;\n    }\n\n    // Get pointers to virtual touch/pen input functions (Win10 1809+)\n    raw.fnCreateSyntheticPointerDevice = (decltype(CreateSyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA(\"user32.dll\"), \"CreateSyntheticPointerDevice\");\n    raw.fnInjectSyntheticPointerInput = (decltype(InjectSyntheticPointerInput) *) GetProcAddress(GetModuleHandleA(\"user32.dll\"), \"InjectSyntheticPointerInput\");\n    raw.fnDestroySyntheticPointerDevice = (decltype(DestroySyntheticPointerDevice) *) GetProcAddress(GetModuleHandleA(\"user32.dll\"), \"DestroySyntheticPointerDevice\");\n\n    return result;\n  }\n\n  /**\n   * @brief Calls SendInput() and switches input desktops if required.\n   * @param i The `INPUT` struct to send.\n   */\n  void\n  send_input(INPUT &i) {\n  retry:\n    auto send = SendInput(1, &i, sizeof(INPUT));\n    if (send != 1) {\n      auto hDesk = syncThreadDesktop();\n      if (_lastKnownInputDesktop != hDesk) {\n        _lastKnownInputDesktop = hDesk;\n        goto retry;\n      }\n      BOOST_LOG(error) << \"Couldn't send input\"sv;\n    }\n  }\n\n  /**\n   * @brief Calls InjectSyntheticPointerInput() and switches input desktops if required.\n   * @details Must only be called if InjectSyntheticPointerInput() is available.\n   * @param input The global input context.\n   * @param device The synthetic pointer device handle.\n   * @param pointerInfo An array of `POINTER_TYPE_INFO` structs.\n   * @param count The number of elements in `pointerInfo`.\n   * @return true if input was successfully injected.\n   */\n  bool\n  inject_synthetic_pointer_input(input_raw_t *input, HSYNTHETICPOINTERDEVICE device, const POINTER_TYPE_INFO *pointerInfo, UINT32 count) {\n  retry:\n    if (!input->fnInjectSyntheticPointerInput(device, pointerInfo, count)) {\n      auto hDesk = syncThreadDesktop();\n      if (_lastKnownInputDesktop != hDesk) {\n        _lastKnownInputDesktop = hDesk;\n        goto retry;\n      }\n      return false;\n    }\n    return true;\n  }\n\n  void\n  abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {\n    INPUT i {};\n\n    i.type = INPUT_MOUSE;\n    auto &mi = i.mi;\n\n    mi.dwFlags =\n      MOUSEEVENTF_MOVE |\n      MOUSEEVENTF_ABSOLUTE |\n\n      // MOUSEEVENTF_VIRTUALDESK maps to the entirety of the desktop rather than the primary desktop\n      MOUSEEVENTF_VIRTUALDESK;\n\n    auto scaled_x = std::lround((x + touch_port.offset_x) * ((float) target_touch_port.width / (float) touch_port.width));\n    auto scaled_y = std::lround((y + touch_port.offset_y) * ((float) target_touch_port.height / (float) touch_port.height));\n\n    mi.dx = scaled_x;\n    mi.dy = scaled_y;\n\n    send_input(i);\n  }\n\n  void\n  move_mouse(input_t &input, int deltaX, int deltaY) {\n    auto &raw = *(input_raw_t *) input.get();\n    if (current_mouse_mode.load(std::memory_order_relaxed) != 2 && raw.vmouse_dev && raw.vmouse_dev->is_available()) {\n      raw.vmouse_dev->move((int16_t) deltaX, (int16_t) deltaY);\n      return;\n    }\n\n    INPUT i {};\n\n    i.type = INPUT_MOUSE;\n    auto &mi = i.mi;\n\n    mi.dwFlags = MOUSEEVENTF_MOVE;\n    mi.dx = deltaX;\n    mi.dy = deltaY;\n\n    send_input(i);\n  }\n\n  util::point_t\n  get_mouse_loc(input_t &input) {\n    throw std::runtime_error(\"not implemented yet, has to pass tests\");\n    // TODO: Tests are failing, something wrong here?\n    POINT p;\n    if (!GetCursorPos(&p)) {\n      return util::point_t { 0.0, 0.0 };\n    }\n\n    return util::point_t {\n      (double) p.x,\n      (double) p.y\n    };\n  }\n\n  void\n  button_mouse(input_t &input, int button, bool release) {\n    auto &raw = *(input_raw_t *) input.get();\n    if (current_mouse_mode.load(std::memory_order_relaxed) != 2 && raw.vmouse_dev && raw.vmouse_dev->is_available()) {\n      uint8_t mask = 0;\n      switch (button) {\n        case 1: mask = vmouse::BTN_LEFT; break;\n        case 2: mask = vmouse::BTN_MIDDLE; break;\n        case 3: mask = vmouse::BTN_RIGHT; break;\n        case 4: mask = vmouse::BTN_SIDE; break;\n        default: mask = vmouse::BTN_EXTRA; break;\n      }\n      raw.vmouse_dev->button(mask, release);\n      return;\n    }\n\n    INPUT i {};\n\n    i.type = INPUT_MOUSE;\n    auto &mi = i.mi;\n\n    if (button == 1) {\n      mi.dwFlags = release ? MOUSEEVENTF_LEFTUP : MOUSEEVENTF_LEFTDOWN;\n    }\n    else if (button == 2) {\n      mi.dwFlags = release ? MOUSEEVENTF_MIDDLEUP : MOUSEEVENTF_MIDDLEDOWN;\n    }\n    else if (button == 3) {\n      mi.dwFlags = release ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_RIGHTDOWN;\n    }\n    else if (button == 4) {\n      mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;\n      mi.mouseData = XBUTTON1;\n    }\n    else {\n      mi.dwFlags = release ? MOUSEEVENTF_XUP : MOUSEEVENTF_XDOWN;\n      mi.mouseData = XBUTTON2;\n    }\n\n    send_input(i);\n  }\n\n  void\n  scroll(input_t &input, int distance) {\n    auto &raw = *(input_raw_t *) input.get();\n    if (current_mouse_mode.load(std::memory_order_relaxed) != 2 && raw.vmouse_dev && raw.vmouse_dev->is_available()) {\n      // HID wheel uses notch units; distance is in WHEEL_DELTA (120) units.\n      // Accumulate sub-notch deltas for high-resolution scrolling support.\n      raw.vmouse_vscroll_accum += distance;\n      int notches = raw.vmouse_vscroll_accum / WHEEL_DELTA;\n      if (notches != 0) {\n        raw.vmouse_vscroll_accum -= notches * WHEEL_DELTA;\n        raw.vmouse_dev->scroll((int8_t) std::clamp(notches, -127, 127));\n      }\n      return;\n    }\n\n    INPUT i {};\n\n    i.type = INPUT_MOUSE;\n    auto &mi = i.mi;\n\n    mi.dwFlags = MOUSEEVENTF_WHEEL;\n    mi.mouseData = distance;\n\n    send_input(i);\n  }\n\n  void\n  hscroll(input_t &input, int distance) {\n    auto &raw = *(input_raw_t *) input.get();\n    if (current_mouse_mode.load(std::memory_order_relaxed) != 2 && raw.vmouse_dev && raw.vmouse_dev->is_available()) {\n      raw.vmouse_hscroll_accum += distance;\n      int notches = raw.vmouse_hscroll_accum / WHEEL_DELTA;\n      if (notches != 0) {\n        raw.vmouse_hscroll_accum -= notches * WHEEL_DELTA;\n        raw.vmouse_dev->hscroll((int8_t) std::clamp(notches, -127, 127));\n      }\n      return;\n    }\n\n    INPUT i {};\n\n    i.type = INPUT_MOUSE;\n    auto &mi = i.mi;\n\n    mi.dwFlags = MOUSEEVENTF_HWHEEL;\n    mi.mouseData = distance;\n\n    send_input(i);\n  }\n\n  void\n  keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) {\n    INPUT i {};\n    i.type = INPUT_KEYBOARD;\n    auto &ki = i.ki;\n\n    // If the client did not normalize this VK code to a US English layout, we can't accurately convert it to a scancode.\n    // If we're set to always send scancodes, we will use the current keyboard layout to convert to a scancode. This will\n    // assume the client and host have the same keyboard layout, but it's probably better than always using US English.\n    if (!(flags & SS_KBE_FLAG_NON_NORMALIZED)) {\n      // Mask off the extended key byte\n      ki.wScan = VK_TO_SCANCODE_MAP[modcode & 0xFF];\n    }\n    else if (config::input.always_send_scancodes && modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) {\n      // For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/\n      ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC);\n    }\n\n    // If we can map this to a scancode, send it as a scancode for maximum game compatibility.\n    if (ki.wScan) {\n      ki.dwFlags = KEYEVENTF_SCANCODE;\n    }\n    else {\n      // If there is no scancode mapping or it's non-normalized, send it as a regular VK event.\n      ki.wVk = modcode;\n    }\n\n    // https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#keystroke-message-flags\n    switch (modcode) {\n      case VK_LWIN:\n      case VK_RWIN:\n      case VK_RMENU:\n      case VK_RCONTROL:\n      case VK_INSERT:\n      case VK_DELETE:\n      case VK_HOME:\n      case VK_END:\n      case VK_PRIOR:\n      case VK_NEXT:\n      case VK_UP:\n      case VK_DOWN:\n      case VK_LEFT:\n      case VK_RIGHT:\n      case VK_DIVIDE:\n      case VK_APPS:\n        ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;\n        break;\n      default:\n        break;\n    }\n\n    if (release) {\n      ki.dwFlags |= KEYEVENTF_KEYUP;\n    }\n\n    send_input(i);\n  }\n\n  struct client_input_raw_t: public client_input_t {\n    client_input_raw_t(input_t &input) {\n      global = (input_raw_t *) input.get();\n    }\n\n    ~client_input_raw_t() override {\n      if (penRepeatTask) {\n        task_pool.cancel(penRepeatTask);\n      }\n      if (touchRepeatTask) {\n        task_pool.cancel(touchRepeatTask);\n      }\n\n      if (pen) {\n        global->fnDestroySyntheticPointerDevice(pen);\n      }\n      if (touch) {\n        global->fnDestroySyntheticPointerDevice(touch);\n      }\n    }\n\n    input_raw_t *global;\n\n    // Device state and handles for pen and touch input must be stored in the per-client\n    // input context, because each connected client may be sending their own independent\n    // pen/touch events. To maintain separation, we expose separate pen and touch devices\n    // for each client.\n\n    HSYNTHETICPOINTERDEVICE pen {};\n    POINTER_TYPE_INFO penInfo {};\n    thread_pool_util::ThreadPool::task_id_t penRepeatTask {};\n\n    HSYNTHETICPOINTERDEVICE touch {};\n    POINTER_TYPE_INFO touchInfo[10] {};\n    UINT32 activeTouchSlots {};\n    thread_pool_util::ThreadPool::task_id_t touchRepeatTask {};\n  };\n\n  /**\n   * @brief Allocates a context to store per-client input data.\n   * @param input The global input context.\n   * @return A unique pointer to a per-client input data context.\n   */\n  std::unique_ptr<client_input_t>\n  allocate_client_input_context(input_t &input) {\n    return std::make_unique<client_input_raw_t>(input);\n  }\n\n  /**\n   * @brief Compacts the touch slots into a contiguous block and updates the active count.\n   * @details Since this swaps entries around, all slot pointers/references are invalid after compaction.\n   * @param raw The client-specific input context.\n   */\n  void\n  perform_touch_compaction(client_input_raw_t *raw) {\n    // Windows requires all active touches be contiguous when fed into InjectSyntheticPointerInput().\n    UINT32 i;\n    for (i = 0; i < ARRAYSIZE(raw->touchInfo); i++) {\n      if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) {\n        // This is an empty slot. Look for a later entry to move into this slot.\n        for (UINT32 j = i + 1; j < ARRAYSIZE(raw->touchInfo); j++) {\n          if (raw->touchInfo[j].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {\n            std::swap(raw->touchInfo[i], raw->touchInfo[j]);\n            break;\n          }\n        }\n\n        // If we didn't find anything, we've reached the end of active slots.\n        if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) {\n          break;\n        }\n      }\n    }\n\n    // Update the number of active touch slots\n    raw->activeTouchSlots = i;\n  }\n\n  /**\n   * @brief Gets a pointer slot by client-relative pointer ID, claiming a new one if necessary.\n   * @param raw The raw client-specific input context.\n   * @param pointerId The client's pointer ID.\n   * @param eventType The LI_TOUCH_EVENT value from the client.\n   * @return A pointer to the slot entry.\n   */\n  POINTER_TYPE_INFO *\n  pointer_by_id(client_input_raw_t *raw, uint32_t pointerId, uint8_t eventType) {\n    // Compact active touches into a single contiguous block\n    perform_touch_compaction(raw);\n\n    // Try to find a matching pointer ID\n    for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) {\n      if (raw->touchInfo[i].touchInfo.pointerInfo.pointerId == pointerId &&\n          raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {\n        if (eventType == LI_TOUCH_EVENT_DOWN && (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT)) {\n          BOOST_LOG(warning) << \"Pointer \"sv << pointerId << \" already down. Did the client drop an up/cancel event?\"sv;\n        }\n\n        return &raw->touchInfo[i];\n      }\n    }\n\n    if (eventType != LI_TOUCH_EVENT_HOVER && eventType != LI_TOUCH_EVENT_DOWN) {\n      BOOST_LOG(warning) << \"Unexpected new pointer \"sv << pointerId << \" for event \"sv << (uint32_t) eventType << \". Did the client drop a down/hover event?\"sv;\n    }\n\n    // If there was none, grab an unused entry and increment the active slot count\n    for (UINT32 i = 0; i < ARRAYSIZE(raw->touchInfo); i++) {\n      if (raw->touchInfo[i].touchInfo.pointerInfo.pointerFlags == POINTER_FLAG_NONE) {\n        raw->touchInfo[i].touchInfo.pointerInfo.pointerId = pointerId;\n        raw->activeTouchSlots = i + 1;\n        return &raw->touchInfo[i];\n      }\n    }\n\n    return nullptr;\n  }\n\n  /**\n   * @brief Populate common `POINTER_INFO` members shared between pen and touch events.\n   * @param pointerInfo The pointer info to populate.\n   * @param touchPort The current viewport for translating to screen coordinates.\n   * @param eventType The type of touch/pen event.\n   * @param x The normalized 0.0-1.0 X coordinate.\n   * @param y The normalized 0.0-1.0 Y coordinate.\n   */\n  void\n  populate_common_pointer_info(POINTER_INFO &pointerInfo, const touch_port_t &touchPort, uint8_t eventType, float x, float y) {\n    switch (eventType) {\n      case LI_TOUCH_EVENT_HOVER:\n        pointerInfo.pointerFlags &= ~POINTER_FLAG_INCONTACT;\n        pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE;\n        pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;\n        pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;\n        break;\n      case LI_TOUCH_EVENT_DOWN:\n        pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN;\n        pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;\n        pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;\n        break;\n      case LI_TOUCH_EVENT_UP:\n        // We expect to get another LI_TOUCH_EVENT_HOVER if the pointer remains in range\n        pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);\n        pointerInfo.pointerFlags |= POINTER_FLAG_UP;\n        break;\n      case LI_TOUCH_EVENT_MOVE:\n        pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_UPDATE;\n        pointerInfo.ptPixelLocation.x = x * touchPort.width + touchPort.offset_x;\n        pointerInfo.ptPixelLocation.y = y * touchPort.height + touchPort.offset_y;\n        break;\n      case LI_TOUCH_EVENT_CANCEL:\n      case LI_TOUCH_EVENT_CANCEL_ALL:\n        // If we were in contact with the touch surface at the time of the cancellation,\n        // we'll set POINTER_FLAG_UP, otherwise set POINTER_FLAG_UPDATE.\n        if (pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) {\n          pointerInfo.pointerFlags |= POINTER_FLAG_UP;\n        }\n        else {\n          pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;\n        }\n        pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);\n        pointerInfo.pointerFlags |= POINTER_FLAG_CANCELED;\n        break;\n      case LI_TOUCH_EVENT_HOVER_LEAVE:\n        pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_INRANGE);\n        pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;\n        break;\n      case LI_TOUCH_EVENT_BUTTON_ONLY:\n        // On Windows, we can only pass buttons if we have an active pointer\n        if (pointerInfo.pointerFlags != POINTER_FLAG_NONE) {\n          pointerInfo.pointerFlags |= POINTER_FLAG_UPDATE;\n        }\n        break;\n      default:\n        BOOST_LOG(warning) << \"Unknown touch event: \"sv << (uint32_t) eventType;\n        break;\n    }\n  }\n\n  // Active pointer interactions sent via InjectSyntheticPointerInput() seem to be automatically\n  // cancelled by Windows if not repeated/updated within about a second. To avoid this, refresh\n  // the injected input periodically.\n  constexpr auto ISPI_REPEAT_INTERVAL = 50ms;\n\n  /**\n   * @brief Repeats the current touch state to avoid the interactions timing out.\n   * @param raw The raw client-specific input context.\n   */\n  void\n  repeat_touch(client_input_raw_t *raw) {\n    if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) {\n      auto err = GetLastError();\n      BOOST_LOG(warning) << \"Failed to refresh virtual touch input: \"sv << err;\n    }\n\n    raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id;\n  }\n\n  /**\n   * @brief Repeats the current pen state to avoid the interactions timing out.\n   * @param raw The raw client-specific input context.\n   */\n  void\n  repeat_pen(client_input_raw_t *raw) {\n    if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) {\n      auto err = GetLastError();\n      BOOST_LOG(warning) << \"Failed to refresh virtual pen input: \"sv << err;\n    }\n\n    raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id;\n  }\n\n  /**\n   * @brief Cancels all active touches.\n   * @param raw The raw client-specific input context.\n   */\n  void\n  cancel_all_active_touches(client_input_raw_t *raw) {\n    // Cancel touch repeat callbacks\n    if (raw->touchRepeatTask) {\n      task_pool.cancel(raw->touchRepeatTask);\n      raw->touchRepeatTask = nullptr;\n    }\n\n    // Compact touches to update activeTouchSlots\n    perform_touch_compaction(raw);\n\n    // If we have active slots, cancel them all\n    if (raw->activeTouchSlots > 0) {\n      for (UINT32 i = 0; i < raw->activeTouchSlots; i++) {\n        populate_common_pointer_info(raw->touchInfo[i].touchInfo.pointerInfo, {}, LI_TOUCH_EVENT_CANCEL_ALL, 0.0f, 0.0f);\n        raw->touchInfo[i].touchInfo.touchMask = TOUCH_MASK_NONE;\n      }\n      if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) {\n        auto err = GetLastError();\n        BOOST_LOG(warning) << \"Failed to cancel all virtual touch input: \"sv << err;\n      }\n    }\n\n    // Zero all touch state\n    std::memset(raw->touchInfo, 0, sizeof(raw->touchInfo));\n    raw->activeTouchSlots = 0;\n  }\n\n  // These are edge-triggered pointer state flags that should always be cleared next frame\n  constexpr auto EDGE_TRIGGERED_POINTER_FLAGS = POINTER_FLAG_DOWN | POINTER_FLAG_UP | POINTER_FLAG_CANCELED | POINTER_FLAG_UPDATE;\n\n  /**\n   * @brief Sends a touch event to the OS.\n   * @param input The client-specific input context.\n   * @param touch_port The current viewport for translating to screen coordinates.\n   * @param touch The touch event.\n   */\n  void\n  touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) {\n    auto raw = (client_input_raw_t *) input;\n\n    // Bail if we're not running on an OS that supports virtual touch input\n    if (!raw->global->fnCreateSyntheticPointerDevice ||\n        !raw->global->fnInjectSyntheticPointerInput ||\n        !raw->global->fnDestroySyntheticPointerDevice) {\n      BOOST_LOG(warning) << \"Touch input requires Windows 10 1809 or later\"sv;\n      return;\n    }\n\n    // If there's not already a virtual touch device, create one now\n    if (!raw->touch) {\n      if (touch.eventType != LI_TOUCH_EVENT_CANCEL_ALL) {\n        BOOST_LOG(info) << \"Creating virtual touch input device\"sv;\n        raw->touch = raw->global->fnCreateSyntheticPointerDevice(PT_TOUCH, ARRAYSIZE(raw->touchInfo), POINTER_FEEDBACK_DEFAULT);\n        if (!raw->touch) {\n          auto err = GetLastError();\n          BOOST_LOG(warning) << \"Failed to create virtual touch device: \"sv << err;\n          return;\n        }\n      }\n      else {\n        // No need to cancel anything if we had no touch input device\n        return;\n      }\n    }\n\n    // Cancel touch repeat callbacks\n    if (raw->touchRepeatTask) {\n      task_pool.cancel(raw->touchRepeatTask);\n      raw->touchRepeatTask = nullptr;\n    }\n\n    // If this is a special request to cancel all touches, do that and return\n    if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) {\n      cancel_all_active_touches(raw);\n      return;\n    }\n\n    // Find or allocate an entry for this touch pointer ID\n    auto pointer = pointer_by_id(raw, touch.pointerId, touch.eventType);\n    if (!pointer) {\n      BOOST_LOG(error) << \"No unused pointer entries! Cancelling all active touches!\"sv;\n      cancel_all_active_touches(raw);\n      pointer = pointer_by_id(raw, touch.pointerId, touch.eventType);\n    }\n\n    pointer->type = PT_TOUCH;\n\n    auto &touchInfo = pointer->touchInfo;\n    touchInfo.pointerInfo.pointerType = PT_TOUCH;\n\n    // Populate shared pointer info fields\n    populate_common_pointer_info(touchInfo.pointerInfo, touch_port, touch.eventType, touch.x, touch.y);\n\n    touchInfo.touchMask = TOUCH_MASK_NONE;\n\n    // Pressure and contact area only apply to in-contact pointers.\n    //\n    // The clients also pass distance and tool size for hovers, but Windows doesn't\n    // provide APIs to receive that data.\n    if (touchInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) {\n      if (touch.pressureOrDistance != 0.0f) {\n        touchInfo.touchMask |= TOUCH_MASK_PRESSURE;\n\n        // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses\n        touchInfo.pressure = (UINT32) (touch.pressureOrDistance * 1024);\n      }\n      else {\n        // The default touch pressure is 512\n        touchInfo.pressure = 512;\n      }\n\n      if (touch.contactAreaMajor != 0.0f && touch.contactAreaMinor != 0.0f) {\n        // For the purposes of contact area calculation, we will assume the touches\n        // are at a 45 degree angle if rotation is unknown. This will scale the major\n        // axis value by width and height equally.\n        float rotationAngleDegs = touch.rotation == LI_ROT_UNKNOWN ? 45 : touch.rotation;\n\n        float majorAxisAngle = rotationAngleDegs * (M_PI / 180);\n        float minorAxisAngle = majorAxisAngle + (M_PI / 2);\n\n        // Estimate the contact rectangle\n        float contactWidth = (std::cos(majorAxisAngle) * touch.contactAreaMajor) + (std::cos(minorAxisAngle) * touch.contactAreaMinor);\n        float contactHeight = (std::sin(majorAxisAngle) * touch.contactAreaMajor) + (std::sin(minorAxisAngle) * touch.contactAreaMinor);\n\n        // Convert into screen coordinates centered at the touch location and constrained by screen dimensions\n        touchInfo.rcContact.left = std::max<LONG>(touch_port.offset_x, touchInfo.pointerInfo.ptPixelLocation.x - std::floor(contactWidth / 2));\n        touchInfo.rcContact.right = std::min<LONG>(touch_port.offset_x + touch_port.width, touchInfo.pointerInfo.ptPixelLocation.x + std::ceil(contactWidth / 2));\n        touchInfo.rcContact.top = std::max<LONG>(touch_port.offset_y, touchInfo.pointerInfo.ptPixelLocation.y - std::floor(contactHeight / 2));\n        touchInfo.rcContact.bottom = std::min<LONG>(touch_port.offset_y + touch_port.height, touchInfo.pointerInfo.ptPixelLocation.y + std::ceil(contactHeight / 2));\n\n        touchInfo.touchMask |= TOUCH_MASK_CONTACTAREA;\n      }\n    }\n    else {\n      touchInfo.pressure = 0;\n      touchInfo.rcContact = {};\n    }\n\n    if (touch.rotation != LI_ROT_UNKNOWN) {\n      touchInfo.touchMask |= TOUCH_MASK_ORIENTATION;\n      touchInfo.orientation = touch.rotation;\n    }\n    else {\n      touchInfo.orientation = 0;\n    }\n\n    if (!inject_synthetic_pointer_input(raw->global, raw->touch, raw->touchInfo, raw->activeTouchSlots)) {\n      auto err = GetLastError();\n      BOOST_LOG(warning) << \"Failed to inject virtual touch input: \"sv << err;\n      return;\n    }\n\n    // Clear pointer flags that should only remain set for one frame\n    touchInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS;\n\n    // If we still have an active touch, refresh the touch state periodically\n    if (raw->activeTouchSlots > 1 || touchInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {\n      raw->touchRepeatTask = task_pool.pushDelayed(repeat_touch, ISPI_REPEAT_INTERVAL, raw).task_id;\n    }\n  }\n\n  /**\n   * @brief Sends a pen event to the OS.\n   * @param input The client-specific input context.\n   * @param touch_port The current viewport for translating to screen coordinates.\n   * @param pen The pen event.\n   */\n  void\n  pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) {\n    auto raw = (client_input_raw_t *) input;\n\n    // Bail if we're not running on an OS that supports virtual pen input\n    if (!raw->global->fnCreateSyntheticPointerDevice ||\n        !raw->global->fnInjectSyntheticPointerInput ||\n        !raw->global->fnDestroySyntheticPointerDevice) {\n      BOOST_LOG(warning) << \"Pen input requires Windows 10 1809 or later\"sv;\n      return;\n    }\n\n    // If there's not already a virtual pen device, create one now\n    if (!raw->pen) {\n      if (pen.eventType != LI_TOUCH_EVENT_CANCEL_ALL) {\n        BOOST_LOG(info) << \"Creating virtual pen input device\"sv;\n        raw->pen = raw->global->fnCreateSyntheticPointerDevice(PT_PEN, 1, POINTER_FEEDBACK_DEFAULT);\n        if (!raw->pen) {\n          auto err = GetLastError();\n          BOOST_LOG(warning) << \"Failed to create virtual pen device: \"sv << err;\n          return;\n        }\n      }\n      else {\n        // No need to cancel anything if we had no pen input device\n        return;\n      }\n    }\n\n    // Cancel pen repeat callbacks\n    if (raw->penRepeatTask) {\n      task_pool.cancel(raw->penRepeatTask);\n      raw->penRepeatTask = nullptr;\n    }\n\n    raw->penInfo.type = PT_PEN;\n\n    auto &penInfo = raw->penInfo.penInfo;\n    penInfo.pointerInfo.pointerType = PT_PEN;\n    penInfo.pointerInfo.pointerId = 0;\n\n    // Populate shared pointer info fields\n    populate_common_pointer_info(penInfo.pointerInfo, touch_port, pen.eventType, pen.x, pen.y);\n\n    // Windows only supports a single pen button, so send all buttons as the barrel button\n    if (pen.penButtons) {\n      penInfo.penFlags |= PEN_FLAG_BARREL;\n    }\n    else {\n      penInfo.penFlags &= ~PEN_FLAG_BARREL;\n    }\n\n    switch (pen.toolType) {\n      default:\n      case LI_TOOL_TYPE_PEN:\n        penInfo.penFlags &= ~PEN_FLAG_ERASER;\n        break;\n      case LI_TOOL_TYPE_ERASER:\n        penInfo.penFlags |= PEN_FLAG_ERASER;\n        break;\n      case LI_TOOL_TYPE_UNKNOWN:\n        // Leave tool flags alone\n        break;\n    }\n\n    penInfo.penMask = PEN_MASK_NONE;\n\n    // Windows doesn't support hover distance, so only pass pressure/distance when the pointer is in contact\n    if ((penInfo.pointerInfo.pointerFlags & POINTER_FLAG_INCONTACT) && pen.pressureOrDistance != 0.0f) {\n      penInfo.penMask |= PEN_MASK_PRESSURE;\n\n      // Convert the 0.0f..1.0f float to the 0..1024 range that Windows uses\n      penInfo.pressure = (UINT32) (pen.pressureOrDistance * 1024);\n    }\n    else {\n      // The default pen pressure is 0\n      penInfo.pressure = 0;\n    }\n\n    if (pen.rotation != LI_ROT_UNKNOWN) {\n      penInfo.penMask |= PEN_MASK_ROTATION;\n      penInfo.rotation = pen.rotation;\n    }\n    else {\n      penInfo.rotation = 0;\n    }\n\n    // We require rotation and tilt to perform the conversion to X and Y tilt angles\n    if (pen.tilt != LI_TILT_UNKNOWN && pen.rotation != LI_ROT_UNKNOWN) {\n      auto rotationRads = pen.rotation * (M_PI / 180.f);\n      auto tiltRads = pen.tilt * (M_PI / 180.f);\n      auto r = std::sin(tiltRads);\n      auto z = std::cos(tiltRads);\n\n      // Convert polar coordinates into X and Y tilt angles\n      penInfo.penMask |= PEN_MASK_TILT_X | PEN_MASK_TILT_Y;\n      penInfo.tiltX = (INT32) (std::atan2(std::sin(-rotationRads) * r, z) * 180.f / M_PI);\n      penInfo.tiltY = (INT32) (std::atan2(std::cos(-rotationRads) * r, z) * 180.f / M_PI);\n    }\n    else {\n      penInfo.tiltX = 0;\n      penInfo.tiltY = 0;\n    }\n\n    if (!inject_synthetic_pointer_input(raw->global, raw->pen, &raw->penInfo, 1)) {\n      auto err = GetLastError();\n      BOOST_LOG(warning) << \"Failed to inject virtual pen input: \"sv << err;\n      return;\n    }\n\n    // Clear pointer flags that should only remain set for one frame\n    penInfo.pointerInfo.pointerFlags &= ~EDGE_TRIGGERED_POINTER_FLAGS;\n\n    // If we still have an active pen interaction, refresh the pen state periodically\n    if (penInfo.pointerInfo.pointerFlags != POINTER_FLAG_NONE) {\n      raw->penRepeatTask = task_pool.pushDelayed(repeat_pen, ISPI_REPEAT_INTERVAL, raw).task_id;\n    }\n  }\n\n  void\n  unicode(input_t &input, char *utf8, int size) {\n    // We can do no worse than one UTF-16 character per byte of UTF-8\n    WCHAR wide[size];\n\n    int chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, size, wide, size);\n    if (chars <= 0) {\n      return;\n    }\n\n    // Send all key down events\n    for (int i = 0; i < chars; i++) {\n      INPUT input {};\n      input.type = INPUT_KEYBOARD;\n      input.ki.wScan = wide[i];\n      input.ki.dwFlags = KEYEVENTF_UNICODE;\n      send_input(input);\n    }\n\n    // Send all key up events\n    for (int i = 0; i < chars; i++) {\n      INPUT input {};\n      input.type = INPUT_KEYBOARD;\n      input.ki.wScan = wide[i];\n      input.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;\n      send_input(input);\n    }\n  }\n\n  int\n  alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) {\n    auto raw = (input_raw_t *) input.get();\n\n    if (!raw->vigem) {\n      return 0;\n    }\n\n    VIGEM_TARGET_TYPE selectedGamepadType;\n\n    if (config::input.gamepad == \"x360\"sv) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be Xbox 360 controller (manual selection)\"sv;\n      selectedGamepadType = Xbox360Wired;\n    }\n    else if (config::input.gamepad == \"ds4\"sv) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be DualShock 4 controller (manual selection)\"sv;\n      selectedGamepadType = DualShock4Wired;\n    }\n    else if (metadata.type == LI_CTYPE_PS) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be DualShock 4 controller (auto-selected by client-reported type)\"sv;\n      selectedGamepadType = DualShock4Wired;\n    }\n    else if (metadata.type == LI_CTYPE_XBOX) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be Xbox 360 controller (auto-selected by client-reported type)\"sv;\n      selectedGamepadType = Xbox360Wired;\n    }\n    else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be DualShock 4 controller (auto-selected by motion sensor presence)\"sv;\n      selectedGamepadType = DualShock4Wired;\n    }\n    else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be DualShock 4 controller (auto-selected by touchpad presence)\"sv;\n      selectedGamepadType = DualShock4Wired;\n    }\n    else if (config::input.enable_dsu_server && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be DualShock 4 controller (auto-selected by DSU server enabled and motion sensor presence)\"sv;\n      selectedGamepadType = DualShock4Wired;\n    }\n    else {\n      BOOST_LOG(info) << \"Gamepad \" << id.globalIndex << \" will be Xbox 360 controller (default)\"sv;\n      selectedGamepadType = Xbox360Wired;\n    }\n\n    if (selectedGamepadType == Xbox360Wired) {\n      if (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) {\n        BOOST_LOG(warning) << \"Gamepad \" << id.globalIndex << \" has motion sensors, but they are not usable when emulating an Xbox 360 controller\"sv;\n      }\n      if (metadata.capabilities & LI_CCAP_TOUCHPAD) {\n        BOOST_LOG(warning) << \"Gamepad \" << id.globalIndex << \" has a touchpad, but it is not usable when emulating an Xbox 360 controller\"sv;\n      }\n      if (metadata.capabilities & LI_CCAP_RGB_LED) {\n        BOOST_LOG(warning) << \"Gamepad \" << id.globalIndex << \" has an RGB LED, but it is not usable when emulating an Xbox 360 controller\"sv;\n      }\n    }\n    else if (selectedGamepadType == DualShock4Wired) {\n      if (!(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) {\n        BOOST_LOG(warning) << \"Gamepad \" << id.globalIndex << \" is emulating a DualShock 4 controller, but the client gamepad doesn't have motion sensors active\"sv;\n      }\n      if (!(metadata.capabilities & LI_CCAP_TOUCHPAD)) {\n        BOOST_LOG(warning) << \"Gamepad \" << id.globalIndex << \" is emulating a DualShock 4 controller, but the client gamepad doesn't have a touchpad\"sv;\n      }\n    }\n\n    return raw->vigem->alloc_gamepad_internal(id, feedback_queue, selectedGamepadType);\n  }\n\n  void\n  free_gamepad(input_t &input, int nr) {\n    auto raw = (input_raw_t *) input.get();\n\n    if (!raw->vigem) {\n      return;\n    }\n\n    raw->vigem->free_target(nr);\n  }\n\n  /**\n   * @brief Converts the standard button flags into X360 format.\n   * @param gamepad_state The gamepad button/axis state sent from the client.\n   * @return XUSB_BUTTON flags.\n   */\n  static XUSB_BUTTON\n  x360_buttons(const gamepad_state_t &gamepad_state) {\n    int buttons {};\n\n    auto flags = gamepad_state.buttonFlags;\n    if (flags & DPAD_UP) buttons |= XUSB_GAMEPAD_DPAD_UP;\n    if (flags & DPAD_DOWN) buttons |= XUSB_GAMEPAD_DPAD_DOWN;\n    if (flags & DPAD_LEFT) buttons |= XUSB_GAMEPAD_DPAD_LEFT;\n    if (flags & DPAD_RIGHT) buttons |= XUSB_GAMEPAD_DPAD_RIGHT;\n    if (flags & START) buttons |= XUSB_GAMEPAD_START;\n    if (flags & BACK) buttons |= XUSB_GAMEPAD_BACK;\n    if (flags & LEFT_STICK) buttons |= XUSB_GAMEPAD_LEFT_THUMB;\n    if (flags & RIGHT_STICK) buttons |= XUSB_GAMEPAD_RIGHT_THUMB;\n    if (flags & LEFT_BUTTON) buttons |= XUSB_GAMEPAD_LEFT_SHOULDER;\n    if (flags & RIGHT_BUTTON) buttons |= XUSB_GAMEPAD_RIGHT_SHOULDER;\n    if (flags & (HOME | MISC_BUTTON)) buttons |= XUSB_GAMEPAD_GUIDE;\n    if (flags & A) buttons |= XUSB_GAMEPAD_A;\n    if (flags & B) buttons |= XUSB_GAMEPAD_B;\n    if (flags & X) buttons |= XUSB_GAMEPAD_X;\n    if (flags & Y) buttons |= XUSB_GAMEPAD_Y;\n\n    return (XUSB_BUTTON) buttons;\n  }\n\n  /**\n   * @brief Updates the X360 input report with the provided gamepad state.\n   * @param gamepad The gamepad to update.\n   * @param gamepad_state The gamepad button/axis state sent from the client.\n   */\n  static void\n  x360_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) {\n    auto &report = gamepad.report.x360;\n\n    report.wButtons = x360_buttons(gamepad_state);\n    report.bLeftTrigger = gamepad_state.lt;\n    report.bRightTrigger = gamepad_state.rt;\n    report.sThumbLX = gamepad_state.lsX;\n    report.sThumbLY = gamepad_state.lsY;\n    report.sThumbRX = gamepad_state.rsX;\n    report.sThumbRY = gamepad_state.rsY;\n  }\n\n  static DS4_DPAD_DIRECTIONS\n  ds4_dpad(const gamepad_state_t &gamepad_state) {\n    auto flags = gamepad_state.buttonFlags;\n    if (flags & DPAD_UP) {\n      if (flags & DPAD_RIGHT) {\n        return DS4_BUTTON_DPAD_NORTHEAST;\n      }\n      else if (flags & DPAD_LEFT) {\n        return DS4_BUTTON_DPAD_NORTHWEST;\n      }\n      else {\n        return DS4_BUTTON_DPAD_NORTH;\n      }\n    }\n\n    else if (flags & DPAD_DOWN) {\n      if (flags & DPAD_RIGHT) {\n        return DS4_BUTTON_DPAD_SOUTHEAST;\n      }\n      else if (flags & DPAD_LEFT) {\n        return DS4_BUTTON_DPAD_SOUTHWEST;\n      }\n      else {\n        return DS4_BUTTON_DPAD_SOUTH;\n      }\n    }\n\n    else if (flags & DPAD_RIGHT) {\n      return DS4_BUTTON_DPAD_EAST;\n    }\n\n    else if (flags & DPAD_LEFT) {\n      return DS4_BUTTON_DPAD_WEST;\n    }\n\n    return DS4_BUTTON_DPAD_NONE;\n  }\n\n  /**\n   * @brief Converts the standard button flags into DS4 format.\n   * @param gamepad_state The gamepad button/axis state sent from the client.\n   * @return DS4_BUTTONS flags.\n   */\n  static DS4_BUTTONS\n  ds4_buttons(const gamepad_state_t &gamepad_state) {\n    int buttons {};\n\n    auto flags = gamepad_state.buttonFlags;\n    if (flags & LEFT_STICK) buttons |= DS4_BUTTON_THUMB_LEFT;\n    if (flags & RIGHT_STICK) buttons |= DS4_BUTTON_THUMB_RIGHT;\n    if (flags & LEFT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_LEFT;\n    if (flags & RIGHT_BUTTON) buttons |= DS4_BUTTON_SHOULDER_RIGHT;\n    if (flags & START) buttons |= DS4_BUTTON_OPTIONS;\n    if (flags & BACK) buttons |= DS4_BUTTON_SHARE;\n    if (flags & A) buttons |= DS4_BUTTON_CROSS;\n    if (flags & B) buttons |= DS4_BUTTON_CIRCLE;\n    if (flags & X) buttons |= DS4_BUTTON_SQUARE;\n    if (flags & Y) buttons |= DS4_BUTTON_TRIANGLE;\n\n    if (gamepad_state.lt > 0) buttons |= DS4_BUTTON_TRIGGER_LEFT;\n    if (gamepad_state.rt > 0) buttons |= DS4_BUTTON_TRIGGER_RIGHT;\n\n    return (DS4_BUTTONS) buttons;\n  }\n\n  static DS4_SPECIAL_BUTTONS\n  ds4_special_buttons(const gamepad_state_t &gamepad_state) {\n    int buttons {};\n\n    if (gamepad_state.buttonFlags & HOME) buttons |= DS4_SPECIAL_BUTTON_PS;\n\n    // Allow either PS4/PS5 clickpad button or Xbox Series X share button to activate DS4 clickpad\n    if (gamepad_state.buttonFlags & (TOUCHPAD_BUTTON | MISC_BUTTON)) buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD;\n\n    // Manual DS4 emulation: check if BACK button should also trigger DS4 touchpad click\n    if (config::input.gamepad == \"ds4\"sv && config::input.ds4_back_as_touchpad_click && (gamepad_state.buttonFlags & BACK)) buttons |= DS4_SPECIAL_BUTTON_TOUCHPAD;\n\n    return (DS4_SPECIAL_BUTTONS) buttons;\n  }\n\n  static std::uint8_t\n  to_ds4_triggerX(std::int16_t v) {\n    return (v + std::numeric_limits<std::uint16_t>::max() / 2 + 1) / 257;\n  }\n\n  static std::uint8_t\n  to_ds4_triggerY(std::int16_t v) {\n    auto new_v = -((std::numeric_limits<std::uint16_t>::max() / 2 + v - 1)) / 257;\n\n    return new_v == 0 ? 0xFF : (std::uint8_t) new_v;\n  }\n\n  /**\n   * @brief Updates the DS4 input report with the provided gamepad state.\n   * @param gamepad The gamepad to update.\n   * @param gamepad_state The gamepad button/axis state sent from the client.\n   */\n  static void\n  ds4_update_state(gamepad_context_t &gamepad, const gamepad_state_t &gamepad_state) {\n    auto &report = gamepad.report.ds4.Report;\n\n    report.wButtons = static_cast<uint16_t>(ds4_buttons(gamepad_state)) | static_cast<uint16_t>(ds4_dpad(gamepad_state));\n    report.bSpecial = ds4_special_buttons(gamepad_state);\n\n    report.bTriggerL = gamepad_state.lt;\n    report.bTriggerR = gamepad_state.rt;\n\n    report.bThumbLX = to_ds4_triggerX(gamepad_state.lsX);\n    report.bThumbLY = to_ds4_triggerY(gamepad_state.lsY);\n\n    report.bThumbRX = to_ds4_triggerX(gamepad_state.rsX);\n    report.bThumbRY = to_ds4_triggerY(gamepad_state.rsY);\n  }\n\n  /**\n   * @brief Sends DS4 input with updated timestamps and repeats to keep timestamp updated.\n   * @details Some applications require updated timestamps values to register DS4 input.\n   * @param vigem The global ViGEm context object.\n   * @param nr The global gamepad index.\n   */\n  void\n  ds4_update_ts_and_send(vigem_t *vigem, int nr) {\n    auto &gamepad = vigem->gamepads[nr];\n\n    // Cancel any pending updates. We will requeue one here when we're finished.\n    if (gamepad.repeat_task) {\n      task_pool.cancel(gamepad.repeat_task);\n      gamepad.repeat_task = 0;\n    }\n\n    if (gamepad.gp && vigem_target_is_attached(gamepad.gp.get())) {\n      auto now = std::chrono::steady_clock::now();\n      auto delta_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(now - gamepad.last_report_ts);\n\n      // Timestamp is reported in 5.333us units\n      gamepad.report.ds4.Report.wTimestamp += (uint16_t) (delta_ns.count() / 5333);\n\n      // Send the report to the virtual device\n      auto status = vigem_target_ds4_update_ex(vigem->client.get(), gamepad.gp.get(), gamepad.report.ds4);\n      if (!VIGEM_SUCCESS(status)) {\n        BOOST_LOG(warning) << \"Couldn't send gamepad input to ViGEm [\"sv << util::hex(status).to_string_view() << ']';\n        return;\n      }\n\n      // Repeat at least every 100ms to keep the 16-bit timestamp field from overflowing\n      gamepad.last_report_ts = now;\n      gamepad.repeat_task = task_pool.pushDelayed(ds4_update_ts_and_send, 100ms, vigem, nr).task_id;\n    }\n  }\n\n  /**\n   * @brief Updates virtual gamepad with the provided gamepad state.\n   * @param input The input context.\n   * @param nr The gamepad index to update.\n   * @param gamepad_state The gamepad button/axis state sent from the client.\n   */\n  void\n  gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) {\n    auto raw = (input_raw_t *) input.get();\n    auto vigem = raw->vigem;\n\n    // If there is no gamepad support\n    if (!vigem) {\n      return;\n    }\n\n    auto &gamepad = vigem->gamepads[nr];\n    if (!gamepad.gp) {\n      return;\n    }\n\n    VIGEM_ERROR status;\n\n    if (vigem_target_get_type(gamepad.gp.get()) == Xbox360Wired) {\n      x360_update_state(gamepad, gamepad_state);\n      status = vigem_target_x360_update(vigem->client.get(), gamepad.gp.get(), gamepad.report.x360);\n      if (!VIGEM_SUCCESS(status)) {\n        BOOST_LOG(warning) << \"Couldn't send gamepad input to ViGEm [\"sv << util::hex(status).to_string_view() << ']';\n      }\n    }\n    else {\n      ds4_update_state(gamepad, gamepad_state);\n      ds4_update_ts_and_send(vigem, nr);\n    }\n  }\n\n  /**\n   * @brief Sends a gamepad touch event to the OS.\n   * @param input The global input context.\n   * @param touch The touch event.\n   */\n  void\n  gamepad_touch(input_t &input, const gamepad_touch_t &touch) {\n    auto vigem = ((input_raw_t *) input.get())->vigem;\n\n    // If there is no gamepad support\n    if (!vigem) {\n      return;\n    }\n\n    auto &gamepad = vigem->gamepads[touch.id.globalIndex];\n    if (!gamepad.gp) {\n      return;\n    }\n\n    // Touch is only supported on DualShock 4 controllers\n    if (vigem_target_get_type(gamepad.gp.get()) != DualShock4Wired) {\n      return;\n    }\n\n    auto &report = gamepad.report.ds4.Report;\n\n    uint8_t pointerIndex;\n    if (touch.eventType == LI_TOUCH_EVENT_DOWN) {\n      if (gamepad.available_pointers & 0x1) {\n        // Reserve pointer index 0 for this touch\n        gamepad.pointer_id_map[touch.pointerId] = pointerIndex = 0;\n        gamepad.available_pointers &= ~(1 << pointerIndex);\n\n        // Set pointer 0 down\n        report.sCurrentTouch.bIsUpTrackingNum1 &= ~0x80;\n        report.sCurrentTouch.bIsUpTrackingNum1++;\n      }\n      else if (gamepad.available_pointers & 0x2) {\n        // Reserve pointer index 1 for this touch\n        gamepad.pointer_id_map[touch.pointerId] = pointerIndex = 1;\n        gamepad.available_pointers &= ~(1 << pointerIndex);\n\n        // Set pointer 1 down\n        report.sCurrentTouch.bIsUpTrackingNum2 &= ~0x80;\n        report.sCurrentTouch.bIsUpTrackingNum2++;\n      }\n      else {\n        BOOST_LOG(warning) << \"No more free pointer indices! Did the client miss an touch up event?\"sv;\n        return;\n      }\n    }\n    else if (touch.eventType == LI_TOUCH_EVENT_CANCEL_ALL) {\n      // Raise both pointers\n      report.sCurrentTouch.bIsUpTrackingNum1 |= 0x80;\n      report.sCurrentTouch.bIsUpTrackingNum2 |= 0x80;\n\n      // Remove all pointer index mappings\n      gamepad.pointer_id_map.clear();\n\n      // All pointers are now available\n      gamepad.available_pointers = 0x3;\n    }\n    else {\n      auto i = gamepad.pointer_id_map.find(touch.pointerId);\n      if (i == gamepad.pointer_id_map.end()) {\n        BOOST_LOG(warning) << \"Pointer ID not found! Did the client miss a touch down event?\"sv;\n        return;\n      }\n\n      pointerIndex = (*i).second;\n\n      if (touch.eventType == LI_TOUCH_EVENT_UP || touch.eventType == LI_TOUCH_EVENT_CANCEL) {\n        // Remove the pointer index mapping\n        gamepad.pointer_id_map.erase(i);\n\n        // Set pointer up\n        if (pointerIndex == 0) {\n          report.sCurrentTouch.bIsUpTrackingNum1 |= 0x80;\n        }\n        else {\n          report.sCurrentTouch.bIsUpTrackingNum2 |= 0x80;\n        }\n\n        // Free the pointer index\n        gamepad.available_pointers |= (1 << pointerIndex);\n      }\n      else if (touch.eventType != LI_TOUCH_EVENT_MOVE) {\n        BOOST_LOG(warning) << \"Unsupported touch event for gamepad: \"sv << (uint32_t) touch.eventType;\n        return;\n      }\n    }\n\n    // Touchpad is 1920x943 according to ViGEm\n    uint16_t x = touch.x * 1920;\n    uint16_t y = touch.y * 943;\n    uint8_t touchData[] = {\n      (uint8_t) (x & 0xFF),  // Low 8 bits of X\n      (uint8_t) (((x >> 8) & 0x0F) | ((y & 0x0F) << 4)),  // High 4 bits of X and low 4 bits of Y\n      (uint8_t) (((y >> 4) & 0xFF))  // High 8 bits of Y\n    };\n\n    report.sCurrentTouch.bPacketCounter++;\n    if (touch.eventType != LI_TOUCH_EVENT_CANCEL_ALL) {\n      if (pointerIndex == 0) {\n        memcpy(report.sCurrentTouch.bTouchData1, touchData, sizeof(touchData));\n      }\n      else {\n        memcpy(report.sCurrentTouch.bTouchData2, touchData, sizeof(touchData));\n      }\n    }\n\n    ds4_update_ts_and_send(vigem, touch.id.globalIndex);\n  }\n\n  /**\n   * @brief Sends a gamepad motion event to the OS.\n   * @param input The global input context.\n   * @param motion The motion event.\n   */\n  void\n  gamepad_motion(input_t &input, const gamepad_motion_t &motion) {\n    auto raw = (input_raw_t *) input.get();\n    auto vigem = raw->vigem;\n\n    // If there is no gamepad support\n    if (!vigem) {\n      return;\n    }\n\n    auto &gamepad = vigem->gamepads[motion.id.globalIndex];\n    if (!gamepad.gp) {\n      return;\n    }\n\n    // Motion is only supported on DualShock 4 controllers\n    if (vigem_target_get_type(gamepad.gp.get()) != DualShock4Wired) {\n      return;\n    }\n\n    ds4_update_motion(gamepad, motion.motionType, motion.x, motion.y, motion.z);\n    ds4_update_ts_and_send(vigem, motion.id.globalIndex);\n\n    if (!raw->dsu_server && config::input.enable_dsu_server) {\n      raw->init_dsu_server();\n    }\n\n    if (raw->dsu_server) {\n      if (motion.motionType == LI_MOTION_TYPE_ACCEL) {\n        // 发送加速度数据\n        BOOST_LOG(debug) << \"发送加速度数据到DSU服务器\";\n        raw->dsu_server->send_motion_data(motion.id.globalIndex,\n          motion.x, motion.y, motion.z,\n          0.0f, 0.0f, 0.0f);\n      }\n      else if (motion.motionType == LI_MOTION_TYPE_GYRO) {\n        // 发送陀螺仪数据\n        BOOST_LOG(debug) << \"发送陀螺仪数据到DSU服务器\";\n        raw->dsu_server->send_motion_data(motion.id.globalIndex,\n          0.0f, 0.0f, 0.0f,\n          motion.x, motion.y, motion.z);\n      }\n      else {\n        BOOST_LOG(debug) << \"未知的运动数据类型: \" << (int) motion.motionType;\n      }\n    }\n    else {\n      BOOST_LOG(warning) << \"DSU服务器未初始化，无法发送运动数据\";\n    }\n  }\n\n  /**\n   * @brief Sends a gamepad battery event to the OS.\n   * @param input The global input context.\n   * @param battery The battery event.\n   */\n  void\n  gamepad_battery(input_t &input, const gamepad_battery_t &battery) {\n    auto vigem = ((input_raw_t *) input.get())->vigem;\n\n    // If there is no gamepad support\n    if (!vigem) {\n      return;\n    }\n\n    auto &gamepad = vigem->gamepads[battery.id.globalIndex];\n    if (!gamepad.gp) {\n      return;\n    }\n\n    // Battery is only supported on DualShock 4 controllers\n    if (vigem_target_get_type(gamepad.gp.get()) != DualShock4Wired) {\n      return;\n    }\n\n    // For details on the report format of these battery level fields, see:\n    // https://github.com/torvalds/linux/blob/946c6b59c56dc6e7d8364a8959cb36bf6d10bc37/drivers/hid/hid-playstation.c#L2305-L2314\n\n    auto &report = gamepad.report.ds4.Report;\n\n    // Update the battery state if it is known\n    switch (battery.state) {\n      case LI_BATTERY_STATE_CHARGING:\n      case LI_BATTERY_STATE_DISCHARGING:\n        if (battery.state == LI_BATTERY_STATE_CHARGING) {\n          report.bBatteryLvlSpecial |= 0x10;  // Connected via USB\n        }\n        else {\n          report.bBatteryLvlSpecial &= ~0x10;  // Not connected via USB\n        }\n\n        // If there was a special battery status set before, clear that and\n        // initialize the battery level to 50%. It will be overwritten below\n        // if the actual percentage is known.\n        if ((report.bBatteryLvlSpecial & 0xF) > 0xA) {\n          report.bBatteryLvlSpecial = (report.bBatteryLvlSpecial & ~0xF) | 0x5;\n        }\n        break;\n\n      case LI_BATTERY_STATE_FULL:\n        report.bBatteryLvlSpecial = 0x1B;  // USB + Battery Full\n        report.bBatteryLvl = 0xFF;\n        break;\n\n      case LI_BATTERY_STATE_NOT_PRESENT:\n      case LI_BATTERY_STATE_NOT_CHARGING:\n        report.bBatteryLvlSpecial = 0x1F;  // USB + Charging Error\n        break;\n\n      default:\n        break;\n    }\n\n    // Update the battery level if it is known\n    if (battery.percentage != LI_BATTERY_PERCENTAGE_UNKNOWN) {\n      report.bBatteryLvl = battery.percentage * 255 / 100;\n\n      // Don't overwrite low nibble if there's a special status there (see above)\n      if ((report.bBatteryLvlSpecial & 0x10) && (report.bBatteryLvlSpecial & 0xF) <= 0xA) {\n        report.bBatteryLvlSpecial = (report.bBatteryLvlSpecial & ~0xF) | ((battery.percentage + 5) / 10);\n      }\n    }\n\n    ds4_update_ts_and_send(vigem, battery.id.globalIndex);\n  }\n\n  void\n  freeInput(void *p) {\n    auto input = (input_raw_t *) p;\n\n    delete input;\n  }\n\n  std::vector<supported_gamepad_t> &\n  supported_gamepads(input_t *input) {\n    if (!input) {\n      static std::vector gps {\n        supported_gamepad_t { \"auto\", true, \"\" },\n        supported_gamepad_t { \"x360\", false, \"\" },\n        supported_gamepad_t { \"ds4\", false, \"\" },\n        supported_gamepad_t { \"switch\", true, \"\" },\n      };\n\n      return gps;\n    }\n\n    auto vigem = ((input_raw_t *) input)->vigem;\n    auto dsu_server = ((input_raw_t *) input)->dsu_server;\n    auto enabled = vigem != nullptr;\n    auto switch_enabled = dsu_server != nullptr;\n    auto reason = enabled ? \"\" : \"gamepads.vigem-not-available\";\n    auto switch_reason = switch_enabled ? \"\" : \"gamepads.motion-server-not-available\";\n\n    // ds4 == ps4\n    static std::vector gps {\n      supported_gamepad_t { \"auto\", true, reason },\n      supported_gamepad_t { \"x360\", enabled, reason },\n      supported_gamepad_t { \"ds4\", enabled, reason },\n      supported_gamepad_t { \"switch\", switch_enabled, switch_reason }\n    };\n\n    for (auto &[name, is_enabled, reason_disabled] : gps) {\n      if (!is_enabled) {\n        BOOST_LOG(warning) << \"Gamepad \" << name << \" is disabled due to \" << reason_disabled;\n      }\n    }\n\n    return gps;\n  }\n\n  /**\n   * @brief Returns the supported platform capabilities to advertise to the client.\n   * @return Capability flags.\n   */\n  platform_caps::caps_t\n  get_capabilities() {\n    platform_caps::caps_t caps = 0;\n\n    // We support controller touchpad input as long as we're not emulating X360\n    if (config::input.gamepad != \"x360\"sv) {\n      caps |= platform_caps::controller_touch;\n    }\n\n    // We support pen and touch input on Win10 1809+\n    if (GetProcAddress(GetModuleHandleA(\"user32.dll\"), \"CreateSyntheticPointerDevice\") != nullptr) {\n      if (config::input.native_pen_touch) {\n        caps |= platform_caps::pen_touch;\n      }\n    }\n    else {\n      BOOST_LOG(warning) << \"Touch input requires Windows 10 1809 or later\"sv;\n    }\n\n    return caps;\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/windows/keylayout.h",
    "content": "/**\n * @file src/platform/windows/keylayout.h\n * @brief Keyboard layout mapping for scancode translation\n */\n#pragma once\n\n#include <array>\n#include <cstdint>\n\nnamespace platf {\n  // Virtual Key to Scan Code mapping for the US English layout (00000409).\n  // GameStream uses this as the canonical key layout for scancode conversion.\n  constexpr std::array<std::uint8_t, std::numeric_limits<std::uint8_t>::max() + 1> VK_TO_SCANCODE_MAP {\n    0, /* 0x00 */\n    0, /* 0x01 */\n    0, /* 0x02 */\n    70, /* 0x03 */\n    0, /* 0x04 */\n    0, /* 0x05 */\n    0, /* 0x06 */\n    0, /* 0x07 */\n    14, /* 0x08 */\n    15, /* 0x09 */\n    0, /* 0x0a */\n    0, /* 0x0b */\n    76, /* 0x0c */\n    28, /* 0x0d */\n    0, /* 0x0e */\n    0, /* 0x0f */\n    42, /* 0x10 */\n    29, /* 0x11 */\n    56, /* 0x12 */\n    0, /* 0x13 */\n    58, /* 0x14 */\n    0, /* 0x15 */\n    0, /* 0x16 */\n    0, /* 0x17 */\n    0, /* 0x18 */\n    0, /* 0x19 */\n    0, /* 0x1a */\n    1, /* 0x1b */\n    0, /* 0x1c */\n    0, /* 0x1d */\n    0, /* 0x1e */\n    0, /* 0x1f */\n    57, /* 0x20 */\n    73, /* 0x21 */\n    81, /* 0x22 */\n    79, /* 0x23 */\n    71, /* 0x24 */\n    75, /* 0x25 */\n    72, /* 0x26 */\n    77, /* 0x27 */\n    80, /* 0x28 */\n    0, /* 0x29 */\n    0, /* 0x2a */\n    0, /* 0x2b */\n    84, /* 0x2c */\n    82, /* 0x2d */\n    83, /* 0x2e */\n    99, /* 0x2f */\n    11, /* 0x30 */\n    2, /* 0x31 */\n    3, /* 0x32 */\n    4, /* 0x33 */\n    5, /* 0x34 */\n    6, /* 0x35 */\n    7, /* 0x36 */\n    8, /* 0x37 */\n    9, /* 0x38 */\n    10, /* 0x39 */\n    0, /* 0x3a */\n    0, /* 0x3b */\n    0, /* 0x3c */\n    0, /* 0x3d */\n    0, /* 0x3e */\n    0, /* 0x3f */\n    0, /* 0x40 */\n    30, /* 0x41 */\n    48, /* 0x42 */\n    46, /* 0x43 */\n    32, /* 0x44 */\n    18, /* 0x45 */\n    33, /* 0x46 */\n    34, /* 0x47 */\n    35, /* 0x48 */\n    23, /* 0x49 */\n    36, /* 0x4a */\n    37, /* 0x4b */\n    38, /* 0x4c */\n    50, /* 0x4d */\n    49, /* 0x4e */\n    24, /* 0x4f */\n    25, /* 0x50 */\n    16, /* 0x51 */\n    19, /* 0x52 */\n    31, /* 0x53 */\n    20, /* 0x54 */\n    22, /* 0x55 */\n    47, /* 0x56 */\n    17, /* 0x57 */\n    45, /* 0x58 */\n    21, /* 0x59 */\n    44, /* 0x5a */\n    91, /* 0x5b */\n    92, /* 0x5c */\n    93, /* 0x5d */\n    0, /* 0x5e */\n    95, /* 0x5f */\n    82, /* 0x60 */\n    79, /* 0x61 */\n    80, /* 0x62 */\n    81, /* 0x63 */\n    75, /* 0x64 */\n    76, /* 0x65 */\n    77, /* 0x66 */\n    71, /* 0x67 */\n    72, /* 0x68 */\n    73, /* 0x69 */\n    55, /* 0x6a */\n    78, /* 0x6b */\n    0, /* 0x6c */\n    74, /* 0x6d */\n    83, /* 0x6e */\n    53, /* 0x6f */\n    59, /* 0x70 */\n    60, /* 0x71 */\n    61, /* 0x72 */\n    62, /* 0x73 */\n    63, /* 0x74 */\n    64, /* 0x75 */\n    65, /* 0x76 */\n    66, /* 0x77 */\n    67, /* 0x78 */\n    68, /* 0x79 */\n    87, /* 0x7a */\n    88, /* 0x7b */\n    100, /* 0x7c */\n    101, /* 0x7d */\n    102, /* 0x7e */\n    103, /* 0x7f */\n    104, /* 0x80 */\n    105, /* 0x81 */\n    106, /* 0x82 */\n    107, /* 0x83 */\n    108, /* 0x84 */\n    109, /* 0x85 */\n    110, /* 0x86 */\n    118, /* 0x87 */\n    0, /* 0x88 */\n    0, /* 0x89 */\n    0, /* 0x8a */\n    0, /* 0x8b */\n    0, /* 0x8c */\n    0, /* 0x8d */\n    0, /* 0x8e */\n    0, /* 0x8f */\n    69, /* 0x90 */\n    70, /* 0x91 */\n    0, /* 0x92 */\n    0, /* 0x93 */\n    0, /* 0x94 */\n    0, /* 0x95 */\n    0, /* 0x96 */\n    0, /* 0x97 */\n    0, /* 0x98 */\n    0, /* 0x99 */\n    0, /* 0x9a */\n    0, /* 0x9b */\n    0, /* 0x9c */\n    0, /* 0x9d */\n    0, /* 0x9e */\n    0, /* 0x9f */\n    42, /* 0xa0 */\n    54, /* 0xa1 */\n    29, /* 0xa2 */\n    29, /* 0xa3 */\n    56, /* 0xa4 */\n    56, /* 0xa5 */\n    106, /* 0xa6 */\n    105, /* 0xa7 */\n    103, /* 0xa8 */\n    104, /* 0xa9 */\n    101, /* 0xaa */\n    102, /* 0xab */\n    50, /* 0xac */\n    32, /* 0xad */\n    46, /* 0xae */\n    48, /* 0xaf */\n    25, /* 0xb0 */\n    16, /* 0xb1 */\n    36, /* 0xb2 */\n    34, /* 0xb3 */\n    108, /* 0xb4 */\n    109, /* 0xb5 */\n    107, /* 0xb6 */\n    33, /* 0xb7 */\n    0, /* 0xb8 */\n    0, /* 0xb9 */\n    39, /* 0xba */\n    13, /* 0xbb */\n    51, /* 0xbc */\n    12, /* 0xbd */\n    52, /* 0xbe */\n    53, /* 0xbf */\n    41, /* 0xc0 */\n    115, /* 0xc1 */\n    126, /* 0xc2 */\n    0, /* 0xc3 */\n    0, /* 0xc4 */\n    0, /* 0xc5 */\n    0, /* 0xc6 */\n    0, /* 0xc7 */\n    0, /* 0xc8 */\n    0, /* 0xc9 */\n    0, /* 0xca */\n    0, /* 0xcb */\n    0, /* 0xcc */\n    0, /* 0xcd */\n    0, /* 0xce */\n    0, /* 0xcf */\n    0, /* 0xd0 */\n    0, /* 0xd1 */\n    0, /* 0xd2 */\n    0, /* 0xd3 */\n    0, /* 0xd4 */\n    0, /* 0xd5 */\n    0, /* 0xd6 */\n    0, /* 0xd7 */\n    0, /* 0xd8 */\n    0, /* 0xd9 */\n    0, /* 0xda */\n    26, /* 0xdb */\n    43, /* 0xdc */\n    27, /* 0xdd */\n    40, /* 0xde */\n    0, /* 0xdf */\n    0, /* 0xe0 */\n    0, /* 0xe1 */\n    86, /* 0xe2 */\n    0, /* 0xe3 */\n    0, /* 0xe4 */\n    0, /* 0xe5 */\n    0, /* 0xe6 */\n    0, /* 0xe7 */\n    0, /* 0xe8 */\n    113, /* 0xe9 */\n    92, /* 0xea */\n    123, /* 0xeb */\n    0, /* 0xec */\n    111, /* 0xed */\n    90, /* 0xee */\n    0, /* 0xef */\n    0, /* 0xf0 */\n    91, /* 0xf1 */\n    0, /* 0xf2 */\n    95, /* 0xf3 */\n    0, /* 0xf4 */\n    94, /* 0xf5 */\n    0, /* 0xf6 */\n    0, /* 0xf7 */\n    0, /* 0xf8 */\n    93, /* 0xf9 */\n    0, /* 0xfa */\n    98, /* 0xfb */\n    0, /* 0xfc */\n    0, /* 0xfd */\n    0, /* 0xfe */\n    0, /* 0xff */\n  };\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/windows/mic_write.cpp",
    "content": "/**\n * @file src/platform/windows/mic_write.cpp\n * @brief Implementation for Windows microphone write functionality.\n */\n#define INITGUID\n\n// Platform includes\n#include <audioclient.h>\n#include <avrt.h>\n#include <filesystem>\n#include <mmdeviceapi.h>\n#include <roapi.h>\n#include <synchapi.h>\n#include <urlmon.h>\n#include <winreg.h>\n\n// Local includes\n#include \"mic_write.h\"\n#include \"misc.h\"\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n\n// Lib includes\n#include <opus/opus.h>\n\n// Must be the last included file\n// clang-format off\n#include \"PolicyConfig.h\"\n// clang-format on\n\n// Property key definitions\nDEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2);\nDEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);\nDEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2);\n\nnamespace platf::audio {\n\n  template <class T>\n  void\n  co_task_free(T *p) {\n    if (p) {\n      CoTaskMemFree(p);\n    }\n  }\n\n  using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;\n  using device_t = util::safe_ptr<IMMDevice, Release<IMMDevice>>;\n  using collection_t = util::safe_ptr<IMMDeviceCollection, Release<IMMDeviceCollection>>;\n  using audio_client_t = util::safe_ptr<IAudioClient, Release<IAudioClient>>;\n  using wstring_t = util::safe_ptr<WCHAR, co_task_free<WCHAR>>;\n  using policy_t = util::safe_ptr<IPolicyConfig, Release<IPolicyConfig>>;\n  using prop_t = util::safe_ptr<IPropertyStore, Release<IPropertyStore>>;\n\n  class prop_var_t {\n  public:\n    prop_var_t() {\n      PropVariantInit(&prop);\n    }\n\n    ~prop_var_t() {\n      PropVariantClear(&prop);\n    }\n\n    PROPVARIANT prop;\n  };\n\n  // mic_write_wasapi_t implementation\n  mic_write_wasapi_t::~mic_write_wasapi_t() {\n    cleanup();\n  }\n\n  void\n  mic_write_wasapi_t::cleanup() {\n    is_cleaning_up.store(true);\n\n    // 等待音频处理完成\n    if (audio_client) {\n      // 停止音频客户端\n      audio_client->Stop();\n\n      // 等待缓冲区清空\n      UINT32 bufferFrameCount = 0;\n      UINT32 padding = 0;\n      HRESULT status = audio_client->GetBufferSize(&bufferFrameCount);\n      if (SUCCEEDED(status)) {\n        // 等待缓冲区完全清空，最多等待 500ms\n        int max_wait = 50;\n        while (SUCCEEDED(audio_client->GetCurrentPadding(&padding)) && padding > 0 && max_wait-- > 0) {\n          Sleep(10);\n        }\n      }\n    }\n\n    // COM 接口释放顺序很重要：\n    // 1. audio_render (从 audio_client 获取的子接口)\n    // 2. audio_client\n    // 3. device_enum\n    if (audio_render) {\n      audio_render->Release();\n      audio_render = nullptr;\n    }\n\n    // 显式释放 audio_client 和 device_enum，确保正确的释放顺序\n    audio_client.reset();\n    device_enum.reset();\n\n    if (opus_decoder) {\n      opus_decoder_destroy(opus_decoder);\n      opus_decoder = nullptr;\n    }\n\n    if (mmcss_task_handle) {\n      AvRevertMmThreadCharacteristics(mmcss_task_handle);\n      mmcss_task_handle = nullptr;\n    }\n\n    // 注意: 不在析构函数的 cleanup 中使用 BOOST_LOG，避免静态对象析构顺序问题\n  }\n\n  capture_e\n  mic_write_wasapi_t::sample(std::vector<float> &sample_out) {\n    BOOST_LOG(error) << \"mic_write_wasapi_t::sample() should not be called\";\n    return capture_e::error;\n  }\n\n  int\n  mic_write_wasapi_t::init() {\n    last_seq = 0;\n    first_packet = true;\n    total_packets = 0;\n    packet_loss_count = 0;\n    fec_recovered_packets = 0;\n\n    // 初始化OPUS解码器\n    int opus_error;\n    opus_decoder = opus_decoder_create(48000, 1, &opus_error);  // 48kHz, 单声道\n    if (opus_error != OPUS_OK) {\n      BOOST_LOG(error) << \"Failed to create OPUS decoder: \" << opus_strerror(opus_error);\n      return -1;\n    }\n\n    // 初始化设备枚举器\n    HRESULT hr = CoCreateInstance(\n      CLSID_MMDeviceEnumerator,\n      nullptr,\n      CLSCTX_ALL,\n      IID_IMMDeviceEnumerator,\n      (void **) &device_enum);\n\n    if (FAILED(hr)) {\n      BOOST_LOG(error) << \"Couldn't create Device Enumerator for mic write: [0x\" << util::hex(hr).to_string_view() << \"]\";\n      cleanup();\n      return -1;\n    }\n\n    // 存储原始音频设备设置\n    store_original_audio_settings();\n\n    // 尝试创建或使用虚拟音频设备\n    if (create_virtual_audio_device() != 0) {\n      BOOST_LOG(warning) << \"Virtual audio device not available, microphone redirection may not work\";\n    }\n\n    // 设置loopback\n    if (setup_virtual_mic_loopback() != 0) {\n      BOOST_LOG(warning) << \"Failed to setup virtual microphone loopback\";\n    }\n\n    // 对于麦克风重定向，我们需要使用虚拟音频输出设备\n    device_t device;\n\n    auto vb_matched = find_device_id({ { match_field_e::adapter_friendly_name, L\"VB-Audio Virtual Cable\" } });\n    if (vb_matched) {\n      hr = device_enum->GetDevice(vb_matched->second.c_str(), &device);\n      if (SUCCEEDED(hr) && device) {\n        BOOST_LOG(info) << \"Using VB-Audio Virtual Cable for client mic redirection\";\n      }\n    }\n\n    // 最后尝试使用默认的扬声器设备\n    if (FAILED(hr) || !device) {\n      hr = device_enum->GetDefaultAudioEndpoint(eRender, eConsole, &device);\n      if (SUCCEEDED(hr) && device) {\n        BOOST_LOG(info) << \"Using default console audio output device for client mic redirection\";\n      }\n    }\n\n    if (FAILED(hr) || !device) {\n      BOOST_LOG(error) << \"No suitable audio output device available for client mic redirection\";\n      cleanup();\n      return -1;\n    }\n\n    // 激活 IAudioClient\n    auto status = device->Activate(\n      IID_IAudioClient,\n      CLSCTX_ALL,\n      nullptr,\n      (void **) &audio_client);\n    if (FAILED(status) || !audio_client) {\n      BOOST_LOG(error) << \"Failed to activate IAudioClient for mic write: [0x\" << util::hex(status).to_string_view() << \"]\";\n\n      // 获取设备信息以便调试\n      wstring_t device_id;\n      device->GetId(&device_id);\n      BOOST_LOG(error) << \"Device ID: \" << platf::to_utf8(device_id.get());\n\n      cleanup();\n      return -1;\n    }\n\n    // 尝试多种音频格式，从最兼容的开始\n    std::vector<WAVEFORMATEX> formats_to_try = {\n      // 16位单声道，48kHz\n      { WAVE_FORMAT_PCM, 1, 48000, 96000, 2, 16, 0 },\n      // 16位单声道，44.1kHz\n      { WAVE_FORMAT_PCM, 1, 44100, 88200, 2, 16, 0 },\n      // 16位立体声，48kHz\n      { WAVE_FORMAT_PCM, 2, 48000, 192000, 4, 16, 0 },\n      // 16位立体声，44.1kHz\n      { WAVE_FORMAT_PCM, 2, 44100, 176400, 4, 16, 0 },\n    };\n\n    HRESULT init_status = E_FAIL;\n    WAVEFORMATEX *used_format = nullptr;\n\n    for (const auto &format : formats_to_try) {\n      BOOST_LOG(debug) << \"Trying audio format: \" << format.nChannels << \" channels, \"\n                       << format.nSamplesPerSec << \" Hz, \" << format.wBitsPerSample << \" bits\";\n\n      init_status = audio_client->Initialize(\n        AUDCLNT_SHAREMODE_SHARED,\n        0,  // 不使用特殊标志\n        1000000,  // 100ms buffer (10000000 was 10 seconds)\n        0,\n        &format,\n        nullptr);\n\n      if (SUCCEEDED(init_status)) {\n        used_format = const_cast<WAVEFORMATEX *>(&format);\n        BOOST_LOG(info) << \"Successfully initialized with format: \" << format.nChannels << \" channels, \"\n                        << format.nSamplesPerSec << \" Hz, \" << format.wBitsPerSample << \" bits\";\n        break;\n      }\n      else {\n        BOOST_LOG(debug) << \"Format failed: [0x\" << util::hex(init_status).to_string_view() << \"]\";\n      }\n    }\n\n    if (FAILED(init_status)) {\n      BOOST_LOG(error) << \"Failed to initialize IAudioClient with any supported format: [0x\" << util::hex(init_status).to_string_view() << \"]\";\n      cleanup();\n      return -1;\n    }\n\n    // 保存使用的格式信息\n    current_format = *used_format;\n\n    // 启动音频客户端\n    status = audio_client->Start();\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Failed to start IAudioClient for mic write: [0x\" << util::hex(status).to_string_view() << \"]\";\n      cleanup();\n      return -1;\n    }\n\n    // 获取 IAudioRenderClient - 用于写入音频数据\n    status = audio_client->GetService(IID_IAudioRenderClient, (void **) &audio_render);\n    if (FAILED(status) || !audio_render) {\n      BOOST_LOG(error) << \"Failed to get IAudioRenderClient for mic write: [0x\" << util::hex(status).to_string_view() << \"]\";\n      audio_client->Stop();\n      cleanup();\n      return -1;\n    }\n\n    // 设置MMCSS优先级\n    {\n      DWORD task_index = 0;\n      mmcss_task_handle = AvSetMmThreadCharacteristics(\"Pro Audio\", &task_index);\n      if (!mmcss_task_handle) {\n        BOOST_LOG(warning) << \"Couldn't associate mic write thread with Pro Audio MMCSS task [0x\" << util::hex(GetLastError()).to_string_view() << ']';\n      }\n    }\n\n    BOOST_LOG(info) << \"Successfully initialized mic write device with OPUS decoder\";\n    return 0;\n  }\n\n  int\n  mic_write_wasapi_t::write_data(const char *data, size_t len, uint16_t seq) {\n    if (!audio_client || !audio_render || !opus_decoder) {\n      BOOST_LOG(error) << \"Mic write device not initialized\";\n      return -1;\n    }\n\n    std::vector<int16_t> pcm_mono_buffer;\n    ++total_packets;\n    // FEC recovery: check for packet loss using sequence number\n    if (seq != 0 && !first_packet) {\n      uint16_t expected_seq = last_seq + 1;\n      if (seq != expected_seq && seq > expected_seq) {\n        // Packet loss detected, try to recover using FEC from current packet\n        uint16_t lost_count = seq - expected_seq;\n        packet_loss_count += lost_count;\n        BOOST_LOG(verbose) << \"Mic packet loss detected: expected \" << expected_seq << \", got \" << seq << \" (lost \" << lost_count << \")\";\n        \n        // Use FEC to recover the previous lost packet from current packet's redundancy data\n        // FEC can only recover one packet (the immediately preceding one)\n        if (lost_count == 1) {\n          int fec_frame_size = opus_decoder_get_nb_samples(opus_decoder, (const unsigned char *) data, len);\n          if (fec_frame_size > 0) {\n            std::vector<int16_t> fec_buffer(fec_frame_size);\n            int fec_samples = opus_decode(\n              opus_decoder,\n              (const unsigned char *) data,\n              len,\n              fec_buffer.data(),\n              fec_frame_size,\n              1  // FEC recovery mode\n            );\n            if (fec_samples > 0) {\n              BOOST_LOG(verbose) << \"FEC recovered \" << fec_samples << \" samples for lost packet\";\n              ++fec_recovered_packets;\n              // Write recovered audio (will be done together with current packet below)\n              pcm_mono_buffer = std::move(fec_buffer);\n            }\n          }\n        }\n      }\n    }\n\n    // Update sequence tracking\n    if (seq != 0) {\n      last_seq = seq;\n      first_packet = false;\n    }\n\n    // 解码OPUS数据\n    int frame_size = opus_decoder_get_nb_samples(opus_decoder, (const unsigned char *) data, len);\n    if (frame_size < 0) {\n      BOOST_LOG(error) << \"Failed to get OPUS frame size: \" << opus_strerror(frame_size);\n      return -1;\n    }\n\n    // If we recovered FEC data, append current frame; otherwise just decode current\n    size_t fec_offset = pcm_mono_buffer.size();\n    pcm_mono_buffer.resize(fec_offset + frame_size);\n\n    int samples_decoded = opus_decode(\n      opus_decoder,\n      (const unsigned char *) data,\n      len,\n      pcm_mono_buffer.data() + fec_offset,\n      frame_size,\n      0  // Normal decode\n    );\n\n    if (samples_decoded < 0) {\n      BOOST_LOG(error) << \"Failed to decode OPUS data: \" << opus_strerror(samples_decoded);\n      return -1;\n    }\n\n    // Handle channel conversion if necessary\n    std::vector<int16_t> pcm_output_buffer;\n    UINT32 framesToWrite;\n\n    if (current_format.nChannels == 1) {\n      // Mono output, direct copy\n      pcm_output_buffer = std::move(pcm_mono_buffer);\n      framesToWrite = samples_decoded;\n    }\n    else if (current_format.nChannels == 2) {\n      // Stereo output, duplicate mono samples\n      pcm_output_buffer.resize(samples_decoded * 2);\n      for (int i = 0; i < samples_decoded; ++i) {\n        pcm_output_buffer[i * 2] = pcm_mono_buffer[i];  // Left channel\n        pcm_output_buffer[i * 2 + 1] = pcm_mono_buffer[i];  // Right channel\n      }\n      framesToWrite = samples_decoded;  // Each original mono sample becomes one stereo frame\n    }\n    else {\n      BOOST_LOG(error) << \"Unsupported channel count for mic write: \" << current_format.nChannels;\n      return -1;\n    }\n\n    // 获取缓冲区大小和当前填充的帧数\n    UINT32 bufferFrameCount = 0;\n    UINT32 padding = 0;\n    auto status = audio_client->GetBufferSize(&bufferFrameCount);\n    if (FAILED(status)) {\n      if (status == AUDCLNT_E_DEVICE_INVALIDATED) {\n        BOOST_LOG(warning) << \"Audio device invalidated during mic write (GetBufferSize)\";\n        return -2;  // Special return value indicating device invalidated\n      }\n      BOOST_LOG(error) << \"Failed to get buffer size for mic write: [0x\" << util::hex(status).to_string_view() << \"]\";\n      return -1;\n    }\n    status = audio_client->GetCurrentPadding(&padding);\n    if (FAILED(status)) {\n      if (status == AUDCLNT_E_DEVICE_INVALIDATED) {\n        BOOST_LOG(warning) << \"Audio device invalidated during mic write (GetCurrentPadding)\";\n        return -2;  // Special return value indicating device invalidated\n      }\n      BOOST_LOG(error) << \"Failed to get current padding for mic write: [0x\" << util::hex(status).to_string_view() << \"]\";\n      return -1;\n    }\n\n    // 确保padding不超过缓冲区大小\n    if (padding > bufferFrameCount) {\n      BOOST_LOG(warning) << \"Invalid padding value: \" << padding << \" > \" << bufferFrameCount << \", using 0\";\n      padding = 0;\n    }\n\n    UINT32 availableFrames = bufferFrameCount - padding;\n\n    // 如果缓冲区空间不足，进行多次等待尝试\n    if (framesToWrite > availableFrames) {\n      BOOST_LOG(verbose) << \"Buffer full, waiting for space. Need: \" << framesToWrite << \", Available: \" << availableFrames;\n\n      // 最多尝试3次，每次等待时间递增\n      const int max_retries = 3;\n      for (int retry = 0; retry < max_retries && framesToWrite > availableFrames; ++retry) {\n        // 根据需要的帧数计算等待时间：帧数 / 48000 * 1000 (ms)\n        // 保守估计，等待所需时间的 80%\n        DWORD wait_ms = static_cast<DWORD>((framesToWrite - availableFrames) * 1000 / 48000 * 0.8);\n        wait_ms = std::max(wait_ms, 5ul);  // 最少等待 5ms\n        wait_ms = std::min(wait_ms, 50ul); // 最多等待 50ms\n        \n        Sleep(wait_ms);\n\n        // 重新检查可用空间\n        status = audio_client->GetCurrentPadding(&padding);\n        if (FAILED(status)) {\n          BOOST_LOG(error) << \"Failed to get current padding after wait: [0x\" << util::hex(status).to_string_view() << \"]\";\n          return -1;\n        }\n\n        if (padding > bufferFrameCount) {\n          padding = 0;\n        }\n\n        availableFrames = bufferFrameCount - padding;\n        \n        if (framesToWrite <= availableFrames) {\n          BOOST_LOG(verbose) << \"Buffer space available after \" << (retry + 1) << \" retries\";\n          break;\n        }\n      }\n\n      // 如果仍然没有足够空间，降级为 debug 日志并截断\n      if (framesToWrite > availableFrames) {\n        BOOST_LOG(warning) << \"Mic write buffer still full after retries: \" << framesToWrite << \" frames requested, \" << availableFrames << \" available. Truncating.\";\n        framesToWrite = availableFrames;\n      }\n    }\n\n    if (framesToWrite == 0) {\n      return 0;\n    }\n\n    // 获取渲染缓冲区\n    BYTE *pData = nullptr;\n    status = audio_render->GetBuffer(framesToWrite, &pData);\n    if (FAILED(status)) {\n      if (status == AUDCLNT_E_DEVICE_INVALIDATED) {\n        BOOST_LOG(warning) << \"Audio device invalidated during mic write (GetBuffer)\";\n        return -2;  // Special return value indicating device invalidated\n      }\n      BOOST_LOG(error) << \"Failed to get render buffer for mic write: [0x\" << util::hex(status).to_string_view() << \"]\";\n      return -1;\n    }\n\n    // 拷贝解码后的PCM数据到缓冲区\n    memcpy(pData, pcm_output_buffer.data(), framesToWrite * current_format.nBlockAlign);\n\n    // 释放缓冲区\n    status = audio_render->ReleaseBuffer(framesToWrite, 0);\n    if (FAILED(status)) {\n      if (status == AUDCLNT_E_DEVICE_INVALIDATED) {\n        BOOST_LOG(warning) << \"Audio device invalidated during mic write (ReleaseBuffer)\";\n        return -2;  // Special return value indicating device invalidated\n      }\n      BOOST_LOG(error) << \"Failed to release render buffer for mic write: [0x\" << util::hex(status).to_string_view() << \"]\";\n      return -1;\n    }\n\n    return framesToWrite * current_format.nBlockAlign;  // 返回实际写入的字节数\n  }\n\n  int\n  mic_write_wasapi_t::test_write() {\n    if (!audio_client || !audio_render || !opus_decoder) {\n      BOOST_LOG(error) << \"Mic write device not initialized for test\";\n      return -1;\n    }\n\n    // 创建一个简单的测试音频数据（静音）\n    const int test_frames = 480;  // 10ms at 48kHz\n    const int test_bytes = test_frames * current_format.nBlockAlign;\n    std::vector<BYTE> test_data(test_bytes, 0);  // 全零数据（静音）\n\n    BOOST_LOG(info) << \"Testing client mic redirection with \" << test_frames << \" frames, \" << test_bytes << \" bytes\";\n\n    return write_data(reinterpret_cast<const char *>(test_data.data()), test_bytes);\n  }\n\n  int\n  mic_write_wasapi_t::create_virtual_audio_device() {\n    BOOST_LOG(info) << \"Attempting to create/use virtual audio device for client mic redirection\";\n\n    // 检查VB-Cable虚拟设备\n    auto vb_matched = find_device_id({ { match_field_e::adapter_friendly_name, L\"VB-Audio Virtual Cable\" } });\n    if (vb_matched) {\n      BOOST_LOG(info) << \"Found existing VB-Audio Virtual Cable device\";\n      virtual_device_type = VirtualDeviceType::VB_CABLE;\n      return 0;  // 设备已存在\n    }\n\n    BOOST_LOG(debug) << \"VB-Cable not found, attempting to download...\";\n\n    // 检查是否已安装VB-Cable驱动程序\n    HKEY hKey;\n    if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L\"SOFTWARE\\\\VB\\\\VBAudioVAC\", 0, KEY_READ, &hKey) == ERROR_SUCCESS) {\n      RegCloseKey(hKey);\n      BOOST_LOG(info) << \"VB-Cable driver is already installed\";\n      return -1;  // 已安装但未找到设备，可能是未启用\n    }\n\n    // 检查是否已经下载并解压过（防止重复下载）\n    std::wstring extract_path = std::filesystem::temp_directory_path().wstring() + L\"\\\\VBCABLE_Install\";\n    std::wstring installer_path = extract_path + L\"\\\\VBCABLE_Setup_x64.exe\";\n    if (std::filesystem::exists(installer_path)) {\n      // 安装程序已存在，只需提示用户安装\n      BOOST_LOG(warning) << \"VB-Cable already downloaded to: \" << platf::to_utf8(extract_path) << \" ; Please run 'VBCABLE_Setup_x64.exe' as administrator to install, then restart Sunshine\";\n      return -1;\n    }\n\n    // 下载VB-Cable\n    BOOST_LOG(debug) << \"Downloading VB-Cable...\";\n\n    // 下载VB-Cable安装程序\n    std::wstring download_url = L\"https://download.vb-audio.com/Download_CABLE/VBCABLE_Driver_Pack43.zip\";\n    std::wstring temp_path = std::filesystem::temp_directory_path().wstring() + L\"\\\\VBCABLE_Driver_Pack43.zip\";\n\n    HMODULE urlmon = LoadLibraryW(L\"urlmon.dll\");\n    if (!urlmon) {\n      BOOST_LOG(warning) << \"VB-Cable is required for microphone streaming. Please download from: https://vb-audio.com/Cable/\";\n      return -1;\n    }\n\n    auto URLDownloadToFileW_ptr = (decltype(URLDownloadToFileW) *) GetProcAddress(urlmon, \"URLDownloadToFileW\");\n    if (!URLDownloadToFileW_ptr || URLDownloadToFileW_ptr(nullptr, download_url.c_str(), temp_path.c_str(), 0, nullptr) != S_OK) {\n      BOOST_LOG(warning) << \"Failed to download VB-Cable. Please download manually from: https://vb-audio.com/Cable/\";\n      FreeLibrary(urlmon);\n      return -1;\n    }\n    FreeLibrary(urlmon);\n\n    // 解压安装包到用户可访问的位置\n    std::error_code ec;\n    std::filesystem::create_directories(extract_path, ec);\n    if (ec && ec != std::errc::file_exists) {\n      BOOST_LOG(error) << \"Failed to create extraction directory: \" << ec.message();\n      return -1;\n    }\n\n    // 解压VB-Cable\n    BOOST_LOG(debug) << \"Extracting VB-Cable...\";\n    std::wstring extract_cmd = L\"powershell -command \\\"Expand-Archive -Path '\" + temp_path + L\"' -DestinationPath '\" + extract_path + L\"' -Force\\\"\";\n\n    if (_wsystem(extract_cmd.c_str()) != 0) {\n      BOOST_LOG(error) << \"Failed to extract VB-Cable installer\";\n      return -1;\n    }\n\n    // 引导用户手动安装\n    BOOST_LOG(warning) << \"VB-Cable downloaded to: \" << platf::to_utf8(extract_path) << \" ; Please run 'VBCABLE_Setup_x64.exe' as administrator to install, then restart Sunshine\";\n\n    return -1;\n  }\n\n  std::optional<matched_field_t>\n  mic_write_wasapi_t::find_device_id(const match_fields_list_t &match_list) {\n    if (match_list.empty() || !device_enum) {\n      return std::nullopt;\n    }\n\n    collection_t collection;\n    auto status = device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Couldn't enumerate render devices: [0x\"sv << util::hex(status).to_string_view() << ']';\n      return std::nullopt;\n    }\n\n    return find_device_in_collection(collection.get(), match_list);\n  }\n\n  std::optional<matched_field_t>\n  mic_write_wasapi_t::find_capture_device_id(const match_fields_list_t &match_list) {\n    if (match_list.empty() || !device_enum) {\n      return std::nullopt;\n    }\n\n    collection_t collection;\n    auto status = device_enum->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &collection);\n    if (FAILED(status)) {\n      BOOST_LOG(error) << \"Couldn't enumerate capture devices: [0x\"sv << util::hex(status).to_string_view() << ']';\n      return std::nullopt;\n    }\n\n    return find_device_in_collection(collection.get(), match_list);\n  }\n\n  std::optional<matched_field_t>\n  mic_write_wasapi_t::find_device_in_collection(void *collection_ptr, const match_fields_list_t &match_list) {\n    auto collection = static_cast<IMMDeviceCollection *>(collection_ptr);\n    UINT count = 0;\n    collection->GetCount(&count);\n\n    std::vector<std::wstring> matched(match_list.size());\n    for (auto x = 0; x < count; ++x) {\n      device_t device;\n      collection->Item(x, &device);\n\n      wstring_t wstring_id;\n      device->GetId(&wstring_id);\n      std::wstring device_id = wstring_id.get();\n\n      prop_t prop;\n      device->OpenPropertyStore(STGM_READ, &prop);\n\n      prop_var_t adapter_friendly_name;\n      prop_var_t device_friendly_name;\n      prop_var_t device_desc;\n\n      prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop);\n      prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop);\n      prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop);\n\n      for (size_t i = 0; i < match_list.size(); i++) {\n        if (matched[i].empty()) {\n          const wchar_t *match_value = nullptr;\n          switch (match_list[i].first) {\n            case match_field_e::device_id:\n              match_value = device_id.c_str();\n              break;\n\n            case match_field_e::device_friendly_name:\n              match_value = device_friendly_name.prop.pwszVal;\n              break;\n\n            case match_field_e::adapter_friendly_name:\n              match_value = adapter_friendly_name.prop.pwszVal;\n              break;\n\n            case match_field_e::device_description:\n              match_value = device_desc.prop.pwszVal;\n              break;\n          }\n          if (match_value && std::wcscmp(match_value, match_list[i].second.c_str()) == 0) {\n            matched[i] = device_id;\n          }\n        }\n      }\n    }\n\n    for (size_t i = 0; i < match_list.size(); i++) {\n      if (!matched[i].empty()) {\n        return matched_field_t(match_list[i].first, matched[i]);\n      }\n    }\n\n    return std::nullopt;\n  }\n\n  HRESULT\n  mic_write_wasapi_t::set_default_device_all_roles(const std::wstring &device_id) {\n    IPolicyConfig *policy_raw = nullptr;\n    HRESULT hr = CoCreateInstance(\n      CLSID_CPolicyConfigClient,\n      nullptr,\n      CLSCTX_ALL,\n      IID_IPolicyConfig,\n      (void **) &policy_raw);\n\n    if (FAILED(hr) || !policy_raw) {\n      BOOST_LOG(error) << \"Couldn't create PolicyConfig instance: [0x\" << util::hex(hr).to_string_view() << \"]\";\n      return hr;\n    }\n\n    policy_t policy(policy_raw);\n    hr = policy->SetDefaultEndpoint(device_id.c_str(), eCommunications);\n    if (FAILED(hr)) {\n      BOOST_LOG(error) << \"Failed to set device as default communications device: [0x\" << util::hex(hr).to_string_view() << \"]\";\n      return hr;\n    }\n\n    hr = policy->SetDefaultEndpoint(device_id.c_str(), eConsole);\n    if (FAILED(hr)) {\n      BOOST_LOG(error) << \"Failed to set device as default console device: [0x\" << util::hex(hr).to_string_view() << \"]\";\n      return hr;\n    }\n\n    return S_OK;\n  }\n\n  int\n  mic_write_wasapi_t::setup_virtual_mic_loopback() {\n    if (virtual_device_type == VirtualDeviceType::NONE) {\n      BOOST_LOG(warning) << \"No virtual device available for loopback setup\";\n      return -1;\n    }\n\n    BOOST_LOG(info) << \"Setting up virtual microphone loopback for client mic redirection\";\n\n    // 根据虚拟设备类型设置循环\n    switch (virtual_device_type) {\n      case VirtualDeviceType::STEAM:\n        return setup_steam_mic_loopback();\n      case VirtualDeviceType::VB_CABLE:\n        return setup_vb_cable_mic_loopback();\n      default:\n        BOOST_LOG(warning) << \"Unknown virtual device type for loopback setup\";\n        return -1;\n    }\n  }\n\n  int\n  mic_write_wasapi_t::setup_steam_mic_loopback() {\n    BOOST_LOG(info) << \"Setting up Steam virtual microphone loopback\";\n\n    // Steam Streaming Speakers 会自动循环到 Steam Streaming Microphone\n    // 我们需要确保Steam Streaming Microphone被设置为默认录音设备\n    if (auto steam_mic = find_capture_device_id({ { match_field_e::adapter_friendly_name, L\"Steam Streaming Microphone\" } })) {\n      HRESULT hr = set_default_device_all_roles(steam_mic->second);\n      if (FAILED(hr)) {\n        BOOST_LOG(error) << \"Failed to set Steam Streaming Microphone as default device: [0x\" << util::hex(hr).to_string_view() << \"]\";\n        return -1;\n      }\n    }\n    return 0;\n  }\n\n  int\n  mic_write_wasapi_t::setup_vb_cable_mic_loopback() {\n    BOOST_LOG(info) << \"Setting up VB-Cable virtual microphone loopback\";\n\n    // 1. 检查VB-Cable输入设备是否存在\n    auto vb_input = find_capture_device_id({ { match_field_e::adapter_friendly_name, L\"VB-Audio Virtual Cable\" } });\n    if (!vb_input) {\n      BOOST_LOG(warning) << \"VB-Cable Input device not found\";\n      return -1;\n    }\n\n    // 2. 设置VB-Cable为默认录音设备\n    HRESULT hr = set_default_device_all_roles(vb_input->second);\n    if (FAILED(hr)) {\n      BOOST_LOG(error) << \"Failed to set VB-Cable as default device: [0x\" << util::hex(hr).to_string_view() << \"]\";\n      return -1;\n    }\n    restoration_state.input_device_changed = true;\n    BOOST_LOG(info) << \"Successfully set VB-Cable as default recording device\";\n\n    // 3. 检查VB-Cable输出设备\n    auto vb_output = find_device_id({ { match_field_e::adapter_friendly_name, L\"VB-Audio Virtual Cable\" } });\n    if (!vb_output) {\n      BOOST_LOG(info) << \"VB-Cable output device not found, skipping output device check\";\n      return 0;\n    }\n\n    // 4. 检查VB-Cable是否是默认播放设备\n    device_t default_device;\n    if (FAILED(device_enum->GetDefaultAudioEndpoint(eRender, eConsole, &default_device)) || !default_device) {\n      BOOST_LOG(warning) << \"Failed to get default playback device\";\n      return 0;\n    }\n\n    wstring_t default_id;\n    if (FAILED(default_device->GetId(&default_id))) {\n      BOOST_LOG(warning) << \"Failed to get default playback device ID\";\n      return 0;\n    }\n\n    if (default_id.get() != vb_output->second) {\n      BOOST_LOG(info) << \"VB-Cable is not the default playback device, no need to switch\";\n      return 0;\n    }\n\n    // 5. 寻找替代播放设备\n    BOOST_LOG(info) << \"VB-Cable is currently the default playback device, switching to alternative...\";\n    collection_t collection;\n    if (FAILED(device_enum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection))) {\n      BOOST_LOG(error) << \"Failed to enumerate audio endpoints\";\n      return -1;\n    }\n\n    UINT count = 0;\n    collection->GetCount(&count);\n\n    for (UINT i = 0; i < count; ++i) {\n      device_t device;\n      if (FAILED(collection->Item(i, &device))) {\n        continue;\n      }\n\n      wstring_t device_id;\n      if (FAILED(device->GetId(&device_id))) {\n        continue;\n      }\n\n      if (device_id.get() != vb_output->second) {\n        if (SUCCEEDED(set_default_device_all_roles(device_id.get()))) {\n          BOOST_LOG(info) << \"Successfully changed default playback device to: \" << platf::to_utf8(device_id.get());\n          // restoration_state.output_device_changed = true;\n          BOOST_LOG(info) << \"VB-Cable virtual microphone loopback successfully configured\";\n          return 0;\n        }\n      }\n    }\n\n    BOOST_LOG(error) << \"No alternative playback device available\";\n    return -1;\n  }\n\n  void\n  mic_write_wasapi_t::store_original_audio_settings() {\n    if (restoration_state.settings_stored) {\n      return;\n    }\n\n    if (!device_enum) {\n      BOOST_LOG(warning) << \"Device enumerator not available, skipping audio settings storage\";\n      return;\n    }\n\n    // 获取并存储当前默认输入设备ID\n    device_t default_input;\n    if (SUCCEEDED(device_enum->GetDefaultAudioEndpoint(eCapture, eConsole, &default_input)) && default_input) {\n      wstring_t device_id;\n      if (SUCCEEDED(default_input->GetId(&device_id))) {\n        restoration_state.original_input_device_id = device_id.get();\n        BOOST_LOG(debug) << \"已存储原始输入设备: \" << platf::to_utf8(restoration_state.original_input_device_id);\n      }\n      else {\n        BOOST_LOG(warning) << \"获取输入设备ID失败\";\n      }\n    }\n    else {\n      BOOST_LOG(warning) << \"获取默认输入设备失败\";\n    }\n\n    restoration_state.settings_stored = true;\n    BOOST_LOG(info) << \"原始音频设备设置存储完成\";\n  }\n\n  int\n  mic_write_wasapi_t::restore_audio_devices() {\n    if (!restoration_state.settings_stored) {\n      BOOST_LOG(debug) << \"No audio device settings to restore\";\n      return 0;\n    }\n\n    BOOST_LOG(info) << \"Restoring audio devices to original state\";\n\n    // Log FEC statistics\n    if (total_packets > 0) {\n      double loss_rate = (double)packet_loss_count / (total_packets + packet_loss_count) * 100.0;\n      BOOST_LOG(info) << \"Microphone Audio Stats, Total Audio Packets: \" << total_packets\n                      << \", Packet Loss: \" << packet_loss_count << \" (\" << std::fixed << std::setprecision(1) << loss_rate << \"%)\"\n                      << \", FEC Recovered: \" << fec_recovered_packets;\n    }\n\n    int result = 0;\n\n    // 恢复输入设备\n    if (restoration_state.input_device_changed) {\n      if (restore_original_input_device() != 0) {\n        result = -1;\n      }\n    }\n\n    // 重置恢复状态\n    restoration_state.input_device_changed = false;\n    restoration_state.settings_stored = false;\n\n    BOOST_LOG(info) << \"Audio device restoration \" << (result == 0 ? \"completed successfully\" : \"completed with errors\");\n    return result;\n  }\n\n  int\n  mic_write_wasapi_t::restore_original_input_device() {\n    if (restoration_state.original_input_device_id.empty()) {\n      BOOST_LOG(warning) << \"No original input device ID stored\";\n      return -1;\n    }\n\n    BOOST_LOG(info) << \"Restoring original input device: \" << platf::to_utf8(restoration_state.original_input_device_id);\n\n    HRESULT hr = set_default_device_all_roles(restoration_state.original_input_device_id);\n    if (FAILED(hr)) {\n      BOOST_LOG(error) << \"Failed to restore original input device: [0x\" << util::hex(hr).to_string_view() << \"]\";\n      return -1;\n    }\n\n    BOOST_LOG(info) << \"Successfully restored original input device\";\n    return 0;\n  }\n\n}  // namespace platf::audio"
  },
  {
    "path": "src/platform/windows/mic_write.h",
    "content": "/**\n * @file src/platform/windows/mic_write.h\n * @brief Declarations for Windows microphone write functionality.\n */\n#pragma once\n\n#include <memory>\n#include <optional>\n#include <vector>\n\n#include \"src/platform/common.h\"\n\n// Windows includes\n#include <audioclient.h>\n#include <mmdeviceapi.h>\n#include <windows.h>\n\n// Forward declarations\nstruct OpusDecoder;\n\nnamespace platf::audio {\n  \n  // COM interface Release helper for safe_ptr\n  template<typename T>\n  inline void Release(T *p) {\n    if (p) p->Release();\n  }\n  \n  // COM interface smart pointer types\n  using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;\n  using audio_client_t = util::safe_ptr<IAudioClient, Release<IAudioClient>>;\n\n  // Forward declarations for types used in mic_write_wasapi_t\n  enum class match_field_e {\n    device_id,  ///< Match device_id\n    device_friendly_name,  ///< Match endpoint friendly name\n    adapter_friendly_name,  ///< Match adapter friendly name\n    device_description,  ///< Match endpoint description\n  };\n  using match_fields_list_t = std::vector<std::pair<match_field_e, std::wstring>>;\n  using matched_field_t = std::pair<match_field_e, std::wstring>;\n\n  /**\n   * @brief Windows WASAPI microphone write class for client mic redirection\n   * \n   * This class handles writing client microphone data to virtual audio devices\n   * for redirection purposes. It supports OPUS decoding and various audio formats.\n   */\n  class mic_write_wasapi_t: public mic_t {\n  public:\n    mic_write_wasapi_t() = default;\n    ~mic_write_wasapi_t() override;\n\n    std::atomic<bool> is_cleaning_up = false;\n\n    // This class is not for sampling, only for writing\n    capture_e\n    sample(std::vector<float> &sample_out) override;\n\n    /**\n     * @brief Initialize the microphone write device\n     * @return 0 on success, -1 on failure\n     */\n    int\n    init();\n\n    /**\n     * @brief Write audio data to the virtual audio device\n     * @param data Pointer to the audio data (OPUS encoded)\n     * @param len Length of the audio data in bytes\n     * @param seq Sequence number for FEC recovery (0 = unknown)\n     * @return Number of bytes written, or -1 on error\n     */\n    int\n    write_data(const char *data, size_t len, uint16_t seq = 0);\n\n    /**\n     * @brief Test write functionality with silent audio\n     * @return Number of bytes written, or -1 on error\n     */\n    int\n    test_write();\n\n    /**\n     * @brief Restore audio devices to their original state\n     * @return 0 on success, -1 on error\n     */\n    int\n    restore_audio_devices();\n\n    /**\n     * @brief Cleanup and release resources\n     */\n    void\n    cleanup();\n\n  private:\n    // Virtual device type enumeration\n    enum class VirtualDeviceType {\n      NONE,\n      STEAM,\n      VB_CABLE,\n    };\n\n    /**\n     * @brief Create or use virtual audio device\n     * @return 0 on success, -1 on failure\n     */\n    int\n    create_virtual_audio_device();\n\n    /**\n     * @brief Setup virtual microphone loopback\n     * @return 0 on success, -1 on failure\n     */\n    int\n    setup_virtual_mic_loopback();\n\n    /**\n     * @brief Setup Steam virtual microphone loopback\n     * @return 0 on success, -1 on failure\n     */\n    int\n    setup_steam_mic_loopback();\n\n    /**\n     * @brief Setup VB-Cable virtual microphone loopback\n     * @return 0 on success, -1 on failure\n     */\n    int\n    setup_vb_cable_mic_loopback();\n\n    /**\n     * @brief Find device ID by matching criteria\n     * @param match_list List of match criteria\n     * @return Optional matched field if found\n     */\n    std::optional<matched_field_t>\n    find_device_id(const match_fields_list_t &match_list);\n\n    /**\n     * @brief Find capture device ID by matching criteria\n     * @param match_list List of match criteria\n     * @return Optional matched field if found\n     */\n    std::optional<matched_field_t>\n    find_capture_device_id(const match_fields_list_t &match_list);\n\n    /**\n     * @brief Find device in collection by matching criteria\n     * @param collection Device collection to search\n     * @param match_list List of match criteria\n     * @return Optional matched field if found\n     */\n    std::optional<matched_field_t>\n    find_device_in_collection(void *collection, const match_fields_list_t &match_list);\n\n    /**\n     * @brief Set default device for all roles\n     * @param device_id Device ID to set as default\n     */\n    HRESULT\n    set_default_device_all_roles(const std::wstring &device_id);\n\n    /**\n     * @brief Store original audio device settings for restoration\n     */\n    void\n    store_original_audio_settings();\n\n    /**\n     * @brief Restore original default audio output device\n     * @return 0 on success, -1 on error\n     */\n    int\n    restore_original_output_device();\n\n    /**\n     * @brief Restore original default audio input device\n     * @return 0 on success, -1 on error\n     */\n    int\n    restore_original_input_device();\n\n    // Member variables\n    device_enum_t device_enum;\n    audio_client_t audio_client;\n    IAudioRenderClient *audio_render = nullptr;\n    OpusDecoder *opus_decoder = nullptr;\n    HANDLE mmcss_task_handle = nullptr;\n    WAVEFORMATEX current_format = {};\n    VirtualDeviceType virtual_device_type = VirtualDeviceType::NONE;\n\n    // Audio device restoration state\n    struct {\n      std::wstring original_input_device_id;\n      bool input_device_changed = false;\n      bool settings_stored = false;\n    } restoration_state;\n\n    // FEC recovery state\n    uint16_t last_seq = 0;\n    bool first_packet = true;\n    \n    // Statistics\n    uint64_t total_packets = 0;\n    uint64_t packet_loss_count = 0;\n    uint64_t fec_recovered_packets = 0;\n  };\n\n  extern std::unique_ptr<mic_write_wasapi_t> mic_redirect_device;\n}  // namespace platf::audio "
  },
  {
    "path": "src/platform/windows/misc.cpp",
    "content": "/**\n * @file src/platform/windows/misc.cpp\n * @brief Miscellaneous definitions for Windows.\n */\n#include <csignal>\n#include <filesystem>\n#include <iomanip>\n#include <set>\n#include <sstream>\n#include <vector>\n\n#include <boost/algorithm/string.hpp>\n#include <boost/asio/ip/address.hpp>\n#include <boost/process/v1.hpp>\n#include <boost/program_options/parsers.hpp>\n\n// prevent clang format from \"optimizing\" the header include order\n// clang-format off\n#include <dwmapi.h>\n#include <iphlpapi.h>\n#include <iterator>\n#include <powrprof.h>\n#include <timeapi.h>\n#include <userenv.h>\n#include <winsock2.h>\n#include <windows.h>\n#include <winuser.h>\n#include <wlanapi.h>\n#include <ws2tcpip.h>\n#include <wtsapi32.h>\n#include <sddl.h>\n// clang-format on\n\n// Boost overrides NTDDI_VERSION, so we re-override it here\n#undef NTDDI_VERSION\n#define NTDDI_VERSION NTDDI_WIN10\n#include <Shlwapi.h>\n\n#include \"misc.h\"\n\n#include \"src/entry_handler.h\"\n#include \"src/globals.h\"\n#include \"src/logging.h\"\n#include \"src/platform/common.h\"\n#include \"src/platform/run_command.h\"\n#include \"src/utility.h\"\n#include <iterator>\n\n#include \"nvprefs/nvprefs_interface.h\"\n\n// UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK\n#ifndef UDP_SEND_MSG_SIZE\n  #define UDP_SEND_MSG_SIZE 2\n#endif\n\n// PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers\n#ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST\n  #define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE)\n#endif\n\n#include <qos2.h>\n\n#ifndef WLAN_API_MAKE_VERSION\n  #define WLAN_API_MAKE_VERSION(_major, _minor) (((DWORD) (_minor)) << 16 | (_major))\n#endif\n\n#include <winternl.h>\nextern \"C\" {\nNTSTATUS NTAPI\nNtSetTimerResolution(ULONG DesiredResolution, BOOLEAN SetResolution, PULONG CurrentResolution);\n}\n\nnamespace {\n\n  std::atomic<bool> used_nt_set_timer_resolution = false;\n\n  bool\n  nt_set_timer_resolution_max() {\n    ULONG minimum, maximum, current;\n    if (!NT_SUCCESS(NtQueryTimerResolution(&minimum, &maximum, &current)) ||\n        !NT_SUCCESS(NtSetTimerResolution(maximum, TRUE, &current))) {\n      return false;\n    }\n    return true;\n  }\n\n  bool\n  nt_set_timer_resolution_min() {\n    ULONG minimum, maximum, current;\n    if (!NT_SUCCESS(NtQueryTimerResolution(&minimum, &maximum, &current)) ||\n        !NT_SUCCESS(NtSetTimerResolution(minimum, TRUE, &current))) {\n      return false;\n    }\n    return true;\n  }\n\n}  // namespace\n\nnamespace bp = boost::process::v1;\n\nusing namespace std::literals;\nnamespace platf {\n  using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;\n\n  bool enabled_mouse_keys = false;\n  MOUSEKEYS previous_mouse_keys_state;\n\n  // Away Mode state tracking\n  std::atomic<bool> away_mode_active = false;\n\n  HANDLE qos_handle = nullptr;\n\n  decltype(QOSCreateHandle) *fn_QOSCreateHandle = nullptr;\n  decltype(QOSAddSocketToFlow) *fn_QOSAddSocketToFlow = nullptr;\n  decltype(QOSRemoveSocketFromFlow) *fn_QOSRemoveSocketFromFlow = nullptr;\n\n  HANDLE wlan_handle = nullptr;\n\n  decltype(WlanOpenHandle) *fn_WlanOpenHandle = nullptr;\n  decltype(WlanCloseHandle) *fn_WlanCloseHandle = nullptr;\n  decltype(WlanFreeMemory) *fn_WlanFreeMemory = nullptr;\n  decltype(WlanEnumInterfaces) *fn_WlanEnumInterfaces = nullptr;\n  decltype(WlanSetInterface) *fn_WlanSetInterface = nullptr;\n\n  std::filesystem::path\n  appdata() {\n    WCHAR sunshine_path[MAX_PATH];\n    GetModuleFileNameW(NULL, sunshine_path, _countof(sunshine_path));\n    return std::filesystem::path { sunshine_path }.remove_filename() / L\"config\"sv;\n  }\n\n  std::string\n  from_sockaddr(const sockaddr *const socket_address) {\n    char data[INET6_ADDRSTRLEN] = {};\n\n    auto family = socket_address->sa_family;\n    if (family == AF_INET6) {\n      inet_ntop(AF_INET6, &((sockaddr_in6 *) socket_address)->sin6_addr, data, INET6_ADDRSTRLEN);\n    }\n    else if (family == AF_INET) {\n      inet_ntop(AF_INET, &((sockaddr_in *) socket_address)->sin_addr, data, INET_ADDRSTRLEN);\n    }\n\n    return std::string { data };\n  }\n\n  std::pair<std::uint16_t, std::string>\n  from_sockaddr_ex(const sockaddr *const ip_addr) {\n    char data[INET6_ADDRSTRLEN] = {};\n\n    auto family = ip_addr->sa_family;\n    std::uint16_t port = 0;\n    if (family == AF_INET6) {\n      inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN);\n      port = ((sockaddr_in6 *) ip_addr)->sin6_port;\n    }\n    else if (family == AF_INET) {\n      inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN);\n      port = ((sockaddr_in *) ip_addr)->sin_port;\n    }\n\n    return { port, std::string { data } };\n  }\n\n  adapteraddrs_t\n  get_adapteraddrs() {\n    adapteraddrs_t info { nullptr };\n    ULONG size = 0;\n\n    while (GetAdaptersAddresses(AF_UNSPEC, 0, nullptr, info.get(), &size) == ERROR_BUFFER_OVERFLOW) {\n      info.reset((PIP_ADAPTER_ADDRESSES) malloc(size));\n    }\n\n    return info;\n  }\n\n  std::string\n  get_mac_address(const std::string_view &address) {\n    adapteraddrs_t info = get_adapteraddrs();\n    for (auto adapter_pos = info.get(); adapter_pos != nullptr; adapter_pos = adapter_pos->Next) {\n      for (auto addr_pos = adapter_pos->FirstUnicastAddress; addr_pos != nullptr; addr_pos = addr_pos->Next) {\n        if (adapter_pos->PhysicalAddressLength != 0 && address == from_sockaddr(addr_pos->Address.lpSockaddr)) {\n          std::stringstream mac_addr;\n          mac_addr << std::hex;\n          for (int i = 0; i < adapter_pos->PhysicalAddressLength; i++) {\n            if (i > 0) {\n              mac_addr << ':';\n            }\n            mac_addr << std::setw(2) << std::setfill('0') << (int) adapter_pos->PhysicalAddress[i];\n          }\n          return mac_addr.str();\n        }\n      }\n    }\n    BOOST_LOG(warning) << \"Unable to find MAC address for \"sv << address;\n    return \"00:00:00:00:00:00\"s;\n  }\n\n  HDESK\n  syncThreadDesktop() {\n    auto hDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL);\n    if (!hDesk) {\n      auto err = GetLastError();\n      BOOST_LOG(error) << \"Failed to Open Input Desktop [0x\"sv << util::hex(err).to_string_view() << ']';\n\n      return nullptr;\n    }\n\n    if (!SetThreadDesktop(hDesk)) {\n      auto err = GetLastError();\n      // Error 0xAA (ERROR_BUSY) can occur when a virtual display is being created\n      // This is not a critical error, so we just log it as a warning\n      if (err == ERROR_BUSY) {\n        BOOST_LOG(warning) << \"Failed to sync desktop to thread [0x\"sv << util::hex(err).to_string_view() << \"] - desktop is busy, retrying later\";\n      }\n      else {\n        BOOST_LOG(error) << \"Failed to sync desktop to thread [0x\"sv << util::hex(err).to_string_view() << ']';\n      }\n    }\n\n    CloseDesktop(hDesk);\n\n    return hDesk;\n  }\n\n  void\n  print_status(const std::string_view &prefix, HRESULT status) {\n    char err_string[1024];\n\n    DWORD bytes = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,\n      nullptr,\n      status,\n      MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),\n      err_string,\n      sizeof(err_string),\n      nullptr);\n\n    BOOST_LOG(error) << prefix << \": \"sv << std::string_view { err_string, bytes };\n  }\n\n  bool\n  IsUserAdmin(HANDLE user_token) {\n    WINBOOL ret;\n    SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;\n    PSID AdministratorsGroup;\n    ret = AllocateAndInitializeSid(\n      &NtAuthority,\n      2,\n      SECURITY_BUILTIN_DOMAIN_RID,\n      DOMAIN_ALIAS_RID_ADMINS,\n      0, 0, 0, 0, 0, 0,\n      &AdministratorsGroup);\n    if (ret) {\n      if (!CheckTokenMembership(user_token, AdministratorsGroup, &ret)) {\n        ret = false;\n        BOOST_LOG(error) << \"Failed to verify token membership for administrative access: \" << GetLastError();\n      }\n      FreeSid(AdministratorsGroup);\n    }\n    else {\n      BOOST_LOG(error) << \"Unable to allocate SID to check administrative access: \" << GetLastError();\n    }\n\n    return ret;\n  }\n\n  /**\n   * @brief Obtain the current sessions user's primary token with elevated privileges.\n   * @return The user's token. If user has admin capability it will be elevated, otherwise it will be a limited token. On error, `nullptr`.\n   */\n  HANDLE\n  retrieve_users_token(bool elevated) {\n    DWORD consoleSessionId;\n    HANDLE userToken;\n    TOKEN_ELEVATION_TYPE elevationType;\n    DWORD dwSize;\n\n    // Get the session ID of the active console session\n    consoleSessionId = WTSGetActiveConsoleSessionId();\n    if (0xFFFFFFFF == consoleSessionId) {\n      // If there is no active console session, log a warning and return null\n      BOOST_LOG(warning) << \"There isn't an active user session, therefore it is not possible to execute commands under the users profile.\";\n      return nullptr;\n    }\n\n    // Get the user token for the active console session\n    if (!WTSQueryUserToken(consoleSessionId, &userToken)) {\n      BOOST_LOG(debug) << \"QueryUserToken failed, this would prevent commands from launching under the users profile.\";\n      return nullptr;\n    }\n\n    // We need to know if this is an elevated token or not.\n    // Get the elevation type of the user token\n    // Elevation - Default: User is not an admin, UAC enabled/disabled does not matter.\n    // Elevation - Limited: User is an admin, has UAC enabled.\n    // Elevation - Full:    User is an admin, has UAC disabled.\n    if (!GetTokenInformation(userToken, TokenElevationType, &elevationType, sizeof(TOKEN_ELEVATION_TYPE), &dwSize)) {\n      BOOST_LOG(debug) << \"Retrieving token information failed: \" << GetLastError();\n      CloseHandle(userToken);\n      return nullptr;\n    }\n\n    // User is currently not an administrator\n    // The documentation for this scenario is conflicting, so we'll double check to see if user is actually an admin.\n    if (elevated && (elevationType == TokenElevationTypeDefault && !IsUserAdmin(userToken))) {\n      // We don't have to strip the token or do anything here, but let's give the user a warning so they're aware what is happening.\n      BOOST_LOG(warning) << \"This command requires elevation and the current user account logged in does not have administrator rights. \"\n                         << \"For security reasons Sunshine will retain the same access level as the current user and will not elevate it.\";\n    }\n\n    // User has a limited token, this means they have UAC enabled and is an Administrator\n    if (elevated && elevationType == TokenElevationTypeLimited) {\n      TOKEN_LINKED_TOKEN linkedToken;\n      // Retrieve the administrator token that is linked to the limited token\n      if (!GetTokenInformation(userToken, TokenLinkedToken, reinterpret_cast<void *>(&linkedToken), sizeof(TOKEN_LINKED_TOKEN), &dwSize)) {\n        // If the retrieval failed, log an error message and return null\n        BOOST_LOG(error) << \"Retrieving linked token information failed: \" << GetLastError();\n        CloseHandle(userToken);\n\n        // There is no scenario where this should be hit, except for an actual error.\n        return nullptr;\n      }\n\n      // Since we need the elevated token, we'll replace it with their administrative token.\n      CloseHandle(userToken);\n      userToken = linkedToken.LinkedToken;\n    }\n\n    // We don't need to do anything for TokenElevationTypeFull users here, because they're already elevated.\n    return userToken;\n  }\n\n  bool\n  merge_user_environment_block(bp::environment &env, HANDLE shell_token) {\n    // Get the target user's environment block\n    PVOID env_block;\n    if (!CreateEnvironmentBlock(&env_block, shell_token, FALSE)) {\n      return false;\n    }\n\n    // Parse the environment block and populate env\n    for (auto c = (PWCHAR) env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) {\n      // Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry.\n      std::string env_tuple = to_utf8(std::wstring { c });\n      std::string env_name = env_tuple.substr(0, env_tuple.find('='));\n      std::string env_val = env_tuple.substr(env_tuple.find('=') + 1);\n\n      // Perform a case-insensitive search to see if this variable name already exists\n      auto itr = std::find_if(env.cbegin(), env.cend(),\n        [&](const auto &e) { return boost::iequals(e.get_name(), env_name); });\n      if (itr != env.cend()) {\n        // Use this existing name if it is already present to ensure we merge properly\n        env_name = itr->get_name();\n      }\n\n      // For the PATH variable, we will merge the values together\n      if (boost::iequals(env_name, \"PATH\")) {\n        env[env_name] = env_val + \";\" + env[env_name].to_string();\n      }\n      else {\n        // Other variables will be superseded by those in the user's environment block\n        env[env_name] = env_val;\n      }\n    }\n\n    DestroyEnvironmentBlock(env_block);\n    return true;\n  }\n\n  /**\n   * @brief Check if the current process is running with system-level privileges.\n   * @return `true` if the current process has system-level privileges, `false` otherwise.\n   */\n  bool\n  is_running_as_system() {\n    BOOL ret;\n    PSID SystemSid;\n    DWORD dwSize = SECURITY_MAX_SID_SIZE;\n\n    // Allocate memory for the SID structure\n    SystemSid = LocalAlloc(LMEM_FIXED, dwSize);\n    if (SystemSid == nullptr) {\n      BOOST_LOG(error) << \"Failed to allocate memory for the SID structure: \" << GetLastError();\n      return false;\n    }\n\n    // Create a SID for the local system account\n    ret = CreateWellKnownSid(WinLocalSystemSid, nullptr, SystemSid, &dwSize);\n    if (ret) {\n      // Check if the current process token contains this SID\n      if (!CheckTokenMembership(nullptr, SystemSid, &ret)) {\n        BOOST_LOG(error) << \"Failed to check token membership: \" << GetLastError();\n        ret = false;\n      }\n    }\n    else {\n      BOOST_LOG(error) << \"Failed to create a SID for the local system account. This may happen if the system is out of memory or if the SID buffer is too small: \" << GetLastError();\n    }\n\n    // Free the memory allocated for the SID structure\n    LocalFree(SystemSid);\n    return ret;\n  }\n\n  // Note: This does NOT append a null terminator\n  void\n  append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) {\n    std::memcpy(&env_block[offset], wstr.data(), wstr.length() * sizeof(wchar_t));\n    offset += wstr.length();\n  }\n\n  std::wstring\n  create_environment_block(bp::environment &env) {\n    int size = 0;\n    for (const auto &entry : env) {\n      auto name = entry.get_name();\n      auto value = entry.to_string();\n      size += from_utf8(name).length() + 1 /* L'=' */ + from_utf8(value).length() + 1 /* L'\\0' */;\n    }\n\n    size += 1 /* L'\\0' */;\n\n    wchar_t env_block[size];\n    int offset = 0;\n    for (const auto &entry : env) {\n      auto name = entry.get_name();\n      auto value = entry.to_string();\n\n      // Construct the NAME=VAL\\0 string\n      append_string_to_environment_block(env_block, offset, from_utf8(name));\n      env_block[offset++] = L'=';\n      append_string_to_environment_block(env_block, offset, from_utf8(value));\n      env_block[offset++] = L'\\0';\n    }\n\n    // Append a final null terminator\n    env_block[offset++] = L'\\0';\n\n    return std::wstring(env_block, offset);\n  }\n\n  LPPROC_THREAD_ATTRIBUTE_LIST\n  allocate_proc_thread_attr_list(DWORD attribute_count) {\n    SIZE_T size;\n    InitializeProcThreadAttributeList(NULL, attribute_count, 0, &size);\n\n    auto list = (LPPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc(GetProcessHeap(), 0, size);\n    if (list == NULL) {\n      return NULL;\n    }\n\n    if (!InitializeProcThreadAttributeList(list, attribute_count, 0, &size)) {\n      HeapFree(GetProcessHeap(), 0, list);\n      return NULL;\n    }\n\n    return list;\n  }\n\n  void\n  free_proc_thread_attr_list(LPPROC_THREAD_ATTRIBUTE_LIST list) {\n    DeleteProcThreadAttributeList(list);\n    HeapFree(GetProcessHeap(), 0, list);\n  }\n\n  /**\n   * @brief Create a `bp::child` object from the results of launching a process.\n   * @param process_launched A boolean indicating if the launch was successful.\n   * @param cmd The command that was used to launch the process.\n   * @param ec A reference to an `std::error_code` object that will store any error that occurred during the launch.\n   * @param process_info A reference to a `PROCESS_INFORMATION` structure that contains information about the new process.\n   * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch failed.\n   */\n  bp::child\n  create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info) {\n    // Use RAII to ensure the process is closed when we're done with it, even if there was an error.\n    auto close_process_handles = util::fail_guard([process_launched, process_info]() {\n      if (process_launched) {\n        CloseHandle(process_info.hThread);\n        CloseHandle(process_info.hProcess);\n      }\n    });\n\n    if (ec) {\n      // If there was an error, return an empty bp::child object\n      return bp::child();\n    }\n\n    if (process_launched) {\n      // If the launch was successful, create a new bp::child object representing the new process\n      auto child = bp::child((bp::pid_t) process_info.dwProcessId);\n      BOOST_LOG(info) << cmd << \" running with PID \"sv << child.id();\n      return child;\n    }\n    else {\n      auto winerror = GetLastError();\n      BOOST_LOG(error) << \"Failed to launch process: \"sv << winerror;\n      ec = std::make_error_code(std::errc::invalid_argument);\n      // We must NOT attach the failed process here, since this case can potentially be induced by ACL\n      // manipulation (denying yourself execute permission) to cause an escalation of privilege.\n      // So to protect ourselves against that, we'll return an empty child process instead.\n      return bp::child();\n    }\n  }\n\n  /**\n   * @brief Impersonate the current user and invoke the callback function.\n   * @param user_token A handle to the user's token that was obtained from the shell.\n   * @param callback A function that will be executed while impersonating the user.\n   * @return Object that will store any error that occurred during the impersonation\n   */\n  std::error_code\n  impersonate_current_user(HANDLE user_token, std::function<void()> callback) {\n    std::error_code ec;\n    // Impersonate the user when launching the process. This will ensure that appropriate access\n    // checks are done against the user token, not our SYSTEM token. It will also allow network\n    // shares and mapped network drives to be used as launch targets, since those credentials\n    // are stored per-user.\n    if (!ImpersonateLoggedOnUser(user_token)) {\n      auto winerror = GetLastError();\n      // Log the failure of impersonating the user and its error code\n      BOOST_LOG(error) << \"Failed to impersonate user: \"sv << winerror;\n      ec = std::make_error_code(std::errc::permission_denied);\n      return ec;\n    }\n\n    // Execute the callback function while impersonating the user\n    callback();\n\n    // End impersonation of the logged on user. If this fails (which is extremely unlikely),\n    // we will be running with an unknown user token. The only safe thing to do in that case\n    // is terminate ourselves.\n    if (!RevertToSelf()) {\n      auto winerror = GetLastError();\n      // Log the failure of reverting to self and its error code\n      BOOST_LOG(fatal) << \"Failed to revert to self after impersonation: \"sv << winerror;\n      DebugBreak();\n    }\n\n    return ec;\n  }\n\n  /**\n   * @brief Create a `STARTUPINFOEXW` structure for launching a process.\n   * @param file A pointer to a `FILE` object that will be used as the standard output and error for the new process, or null if not needed.\n   * @param job A job object handle to insert the new process into. This pointer must remain valid for the life of this startup info!\n   * @param ec A reference to a `std::error_code` object that will store any error that occurred during the creation of the structure.\n   * @return A structure that contains information about how to launch the new process.\n   */\n  STARTUPINFOEXW\n  create_startup_info(FILE *file, HANDLE *job, std::error_code &ec) {\n    // Initialize a zeroed-out STARTUPINFOEXW structure and set its size\n    STARTUPINFOEXW startup_info = {};\n    startup_info.StartupInfo.cb = sizeof(startup_info);\n\n    // Allocate a process attribute list with space for 2 elements\n    startup_info.lpAttributeList = allocate_proc_thread_attr_list(2);\n    if (startup_info.lpAttributeList == NULL) {\n      // If the allocation failed, set ec to an appropriate error code and return the structure\n      ec = std::make_error_code(std::errc::not_enough_memory);\n      return startup_info;\n    }\n\n    if (file) {\n      // If a file was provided, get its handle and use it as the standard output and error for the new process\n      HANDLE log_file_handle = (HANDLE) _get_osfhandle(_fileno(file));\n\n      // Populate std handles if the caller gave us a log file to use\n      startup_info.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;\n      startup_info.StartupInfo.hStdInput = NULL;\n      startup_info.StartupInfo.hStdOutput = log_file_handle;\n      startup_info.StartupInfo.hStdError = log_file_handle;\n\n      // Allow the log file handle to be inherited by the child process (without inheriting all of\n      // our inheritable handles, such as our own log file handle created by SunshineSvc).\n      //\n      // Note: The value we point to here must be valid for the lifetime of the attribute list,\n      // so we need to point into the STARTUPINFO instead of our log_file_variable on the stack.\n      UpdateProcThreadAttribute(startup_info.lpAttributeList,\n        0,\n        PROC_THREAD_ATTRIBUTE_HANDLE_LIST,\n        &startup_info.StartupInfo.hStdOutput,\n        sizeof(startup_info.StartupInfo.hStdOutput),\n        NULL,\n        NULL);\n    }\n\n    if (job) {\n      // Atomically insert the new process into the specified job.\n      //\n      // Note: The value we point to here must be valid for the lifetime of the attribute list,\n      // so we take a HANDLE* instead of just a HANDLE to use the caller's stack storage.\n      UpdateProcThreadAttribute(startup_info.lpAttributeList,\n        0,\n        PROC_THREAD_ATTRIBUTE_JOB_LIST,\n        job,\n        sizeof(*job),\n        NULL,\n        NULL);\n    }\n\n    return startup_info;\n  }\n\n  /**\n   * @brief This function overrides HKEY_CURRENT_USER and HKEY_CLASSES_ROOT using the provided token.\n   * @param token The primary token identifying the user to use, or `NULL` to restore original keys.\n   * @return `true` if the override or restore operation was successful.\n   */\n  bool\n  override_per_user_predefined_keys(HANDLE token) {\n    HKEY user_classes_root = NULL;\n    if (token) {\n      auto err = RegOpenUserClassesRoot(token, 0, GENERIC_ALL, &user_classes_root);\n      if (err != ERROR_SUCCESS) {\n        BOOST_LOG(error) << \"Failed to open classes root for target user: \"sv << err;\n        return false;\n      }\n    }\n    auto close_classes_root = util::fail_guard([user_classes_root]() {\n      if (user_classes_root) {\n        RegCloseKey(user_classes_root);\n      }\n    });\n\n    HKEY user_key = NULL;\n    if (token) {\n      impersonate_current_user(token, [&]() {\n        // RegOpenCurrentUser() doesn't take a token. It assumes we're impersonating the desired user.\n        auto err = RegOpenCurrentUser(GENERIC_ALL, &user_key);\n        if (err != ERROR_SUCCESS) {\n          BOOST_LOG(error) << \"Failed to open user key for target user: \"sv << err;\n          user_key = NULL;\n        }\n      });\n      if (!user_key) {\n        return false;\n      }\n    }\n    auto close_user = util::fail_guard([user_key]() {\n      if (user_key) {\n        RegCloseKey(user_key);\n      }\n    });\n\n    auto err = RegOverridePredefKey(HKEY_CLASSES_ROOT, user_classes_root);\n    if (err != ERROR_SUCCESS) {\n      BOOST_LOG(error) << \"Failed to override HKEY_CLASSES_ROOT: \"sv << err;\n      return false;\n    }\n\n    err = RegOverridePredefKey(HKEY_CURRENT_USER, user_key);\n    if (err != ERROR_SUCCESS) {\n      BOOST_LOG(error) << \"Failed to override HKEY_CURRENT_USER: \"sv << err;\n      RegOverridePredefKey(HKEY_CLASSES_ROOT, NULL);\n      return false;\n    }\n\n    return true;\n  }\n\n  /**\n   * @brief Quote/escape an argument according to the Windows parsing convention.\n   * @param argument The raw argument to process.\n   * @return An argument string suitable for use by CreateProcess().\n   */\n  std::wstring\n  escape_argument(const std::wstring &argument) {\n    // If there are no characters requiring quoting/escaping, we're done\n    if (argument.find_first_of(L\" \\t\\n\\v\\\"\") == argument.npos) {\n      return argument;\n    }\n\n    // The algorithm implemented here comes from a MSDN blog post:\n    // https://web.archive.org/web/20120201194949/http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx\n    std::wstring escaped_arg;\n    escaped_arg.push_back(L'\"');\n    for (auto it = argument.begin();; it++) {\n      auto backslash_count = 0U;\n      while (it != argument.end() && *it == L'\\\\') {\n        it++;\n        backslash_count++;\n      }\n\n      if (it == argument.end()) {\n        escaped_arg.append(backslash_count * 2, L'\\\\');\n        break;\n      }\n      else if (*it == L'\"') {\n        escaped_arg.append(backslash_count * 2 + 1, L'\\\\');\n      }\n      else {\n        escaped_arg.append(backslash_count, L'\\\\');\n      }\n\n      escaped_arg.push_back(*it);\n    }\n    escaped_arg.push_back(L'\"');\n    return escaped_arg;\n  }\n\n  /**\n   * @brief Expand environment variables in a command string using the provided environment.\n   * @details This function manually expands %VAR% syntax using variables from the given\n   *          boost::environment, which may contain Sunshine-specific variables like\n   *          SUNSHINE_CLIENT_WIDTH that are not in the current process environment.\n   * @param cmd The command string potentially containing %VAR% syntax.\n   * @param env The environment to use for variable lookup.\n   * @return The command string with environment variables expanded.\n   */\n  std::string\n  expand_env_vars_in_cmd(const std::string &cmd, const bp::environment &env) {\n    // Quick check: if no '%' character, return as-is\n    if (cmd.find('%') == std::string::npos) {\n      return cmd;\n    }\n\n    std::string result;\n    result.reserve(cmd.size() * 2);  // Reserve extra space for potential expansion\n\n    size_t i = 0;\n    while (i < cmd.size()) {\n      if (cmd[i] == '%') {\n        // Look for the closing '%'\n        size_t end = cmd.find('%', i + 1);\n        if (end != std::string::npos && end > i + 1) {\n          // Extract the variable name\n          std::string var_name = cmd.substr(i + 1, end - i - 1);\n\n          // Handle %% escape (becomes single %)\n          if (var_name.empty()) {\n            result += '%';\n            i = end + 1;\n            continue;\n          }\n\n          // Look up the variable in the provided environment (case-insensitive)\n          bool found = false;\n          for (const auto &entry : env) {\n            if (boost::iequals(entry.get_name(), var_name)) {\n              result += entry.to_string();\n              found = true;\n              break;\n            }\n          }\n\n          if (found) {\n            i = end + 1;\n            continue;\n          }\n\n          // If not found in provided env, try the current process environment\n          // Use GetEnvironmentVariableA for Windows (case-insensitive)\n          char sys_val_buf[32768];\n          DWORD sys_len = GetEnvironmentVariableA(var_name.c_str(), sys_val_buf, sizeof(sys_val_buf));\n          if (sys_len > 0 && sys_len < sizeof(sys_val_buf)) {\n            result += sys_val_buf;\n            i = end + 1;\n            continue;\n          }\n\n          // Variable not found, keep the original %VAR% syntax\n          result += cmd.substr(i, end - i + 1);\n          i = end + 1;\n        }\n        else {\n          // No closing '%', keep the character as-is\n          result += cmd[i];\n          i++;\n        }\n      }\n      else {\n        result += cmd[i];\n        i++;\n      }\n    }\n\n    if (result != cmd) {\n      BOOST_LOG(debug) << \"Expanded command: \"sv << cmd << \" -> \"sv << result;\n    }\n\n    return result;\n  }\n\n  /**\n   * @brief Escape an argument according to cmd's parsing convention.\n   * @param argument An argument already escaped by `escape_argument()`.\n   * @return An argument string suitable for use by cmd.exe.\n   */\n  std::wstring\n  escape_argument_for_cmd(const std::wstring &argument) {\n    // Start with the original string and modify from there\n    std::wstring escaped_arg = argument;\n\n    // Look for the next cmd metacharacter\n    size_t match_pos = 0;\n    while ((match_pos = escaped_arg.find_first_of(L\"()%!^\\\"<>&|\", match_pos)) != std::wstring::npos) {\n      // Insert an escape character and skip past the match\n      escaped_arg.insert(match_pos, 1, L'^');\n      match_pos += 2;\n    }\n\n    return escaped_arg;\n  }\n\n  /**\n   * @brief Resolve the given raw command into a proper command string for CreateProcess().\n   * @details This converts URLs and non-executable file paths into a runnable command like ShellExecute().\n   * @param raw_cmd The raw command provided by the user.\n   * @param working_dir The working directory for the new process.\n   * @param token The user token currently being impersonated or `NULL` if running as ourselves.\n   * @param creation_flags The creation flags for CreateProcess(), which may be modified by this function.\n   * @return A command string suitable for use by CreateProcess().\n   */\n  std::wstring\n  resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) {\n    std::wstring raw_cmd_w = from_utf8(raw_cmd);\n\n    // First, convert the given command into parts so we can get the executable/file/URL without parameters\n    auto raw_cmd_parts = boost::program_options::split_winmain(raw_cmd_w);\n    if (raw_cmd_parts.empty()) {\n      // This is highly unexpected, but we'll just return the raw string and hope for the best.\n      BOOST_LOG(warning) << \"Failed to split command string: \"sv << raw_cmd;\n      return from_utf8(raw_cmd);\n    }\n\n    auto raw_target = raw_cmd_parts.at(0);\n    std::wstring lookup_string;\n    HRESULT res;\n\n    if (PathIsURLW(raw_target.c_str())) {\n      std::array<WCHAR, 128> scheme;\n\n      DWORD out_len = scheme.size();\n      res = UrlGetPartW(raw_target.c_str(), scheme.data(), &out_len, URL_PART_SCHEME, 0);\n      if (res != S_OK) {\n        BOOST_LOG(warning) << \"Failed to extract URL scheme from URL: \"sv << raw_target << \" [\"sv << util::hex(res).to_string_view() << ']';\n        return from_utf8(raw_cmd);\n      }\n\n      // If the target is a URL, the class is found using the URL scheme (prior to and not including the ':')\n      lookup_string = scheme.data();\n    }\n    else {\n      // If the target is not a URL, assume it's a regular file path\n      auto extension = PathFindExtensionW(raw_target.c_str());\n      if (extension == nullptr || *extension == 0) {\n        // If the file has no extension, assume it's a command and allow CreateProcess()\n        // to try to find it via PATH\n        return from_utf8(raw_cmd);\n      }\n      else if (boost::iequals(extension, L\".exe\")) {\n        // If the file has an .exe extension, we will bypass the resolution here and\n        // directly pass the unmodified command string to CreateProcess(). The argument\n        // escaping rules are subtly different between CreateProcess() and ShellExecute(),\n        // and we want to preserve backwards compatibility with older configs.\n        return from_utf8(raw_cmd);\n      }\n\n      // For regular files, the class is found using the file extension (including the dot)\n      lookup_string = extension;\n    }\n\n    std::array<WCHAR, MAX_PATH> shell_command_string;\n    bool needs_cmd_escaping = false;\n    {\n      // Overriding these predefined keys affects process-wide state, so serialize all calls\n      // to ensure the handle state is consistent while we perform the command query.\n      static std::mutex per_user_key_mutex;\n      auto lg = std::lock_guard(per_user_key_mutex);\n\n      // Override HKEY_CLASSES_ROOT and HKEY_CURRENT_USER to ensure we query the correct class info\n      if (!override_per_user_predefined_keys(token)) {\n        return from_utf8(raw_cmd);\n      }\n\n      // Find the command string for the specified class\n      DWORD out_len = shell_command_string.size();\n      res = AssocQueryStringW(ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND, lookup_string.c_str(), L\"open\", shell_command_string.data(), &out_len);\n\n      // In some cases (UWP apps), we might not have a command for this target. If that happens,\n      // we'll have to launch via cmd.exe. This prevents proper job tracking, but that was already\n      // broken for UWP apps anyway due to how they are started by Windows. Even 'start /wait'\n      // doesn't work properly for UWP, so really no termination tracking seems to work at all.\n      //\n      // FIXME: Maybe we can improve this in the future.\n      if (res == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) {\n        BOOST_LOG(warning) << \"Using trampoline to handle target: \"sv << raw_cmd;\n        std::wcscpy(shell_command_string.data(), L\"cmd.exe /c start \\\"\\\" /wait \\\"%1\\\" %*\");\n        needs_cmd_escaping = true;\n\n        // We must suppress the console window that would otherwise appear when starting cmd.exe.\n        creation_flags &= ~CREATE_NEW_CONSOLE;\n        creation_flags |= CREATE_NO_WINDOW;\n\n        res = S_OK;\n      }\n\n      // Reset per-user keys back to the original value\n      override_per_user_predefined_keys(NULL);\n    }\n\n    if (res != S_OK) {\n      BOOST_LOG(warning) << \"Failed to query command string for raw command: \"sv << raw_cmd << \" [\"sv << util::hex(res).to_string_view() << ']';\n      return from_utf8(raw_cmd);\n    }\n\n    // Finally, construct the real command string that will be passed into CreateProcess().\n    // We support common substitutions (%*, %1, %2, %L, %W, %V, etc), but there are other\n    // uncommon ones that are unsupported here.\n    //\n    // https://web.archive.org/web/20111002101214/http://msdn.microsoft.com/en-us/library/windows/desktop/cc144101(v=vs.85).aspx\n    std::wstring cmd_string { shell_command_string.data() };\n    size_t match_pos = 0;\n    while ((match_pos = cmd_string.find_first_of(L'%', match_pos)) != std::wstring::npos) {\n      std::wstring match_replacement;\n\n      // If no additional character exists after the match, the dangling '%' is stripped\n      if (match_pos + 1 == cmd_string.size()) {\n        cmd_string.erase(match_pos, 1);\n        break;\n      }\n\n      // Shell command replacements are strictly '%' followed by a single non-'%' character\n      auto next_char = std::tolower(cmd_string.at(match_pos + 1));\n      switch (next_char) {\n        // Escape character\n        case L'%':\n          match_replacement = L'%';\n          break;\n\n        // Argument replacements\n        case L'0':\n        case L'1':\n        case L'2':\n        case L'3':\n        case L'4':\n        case L'5':\n        case L'6':\n        case L'7':\n        case L'8':\n        case L'9': {\n          // Arguments numbers are 1-based, except for %0 which is equivalent to %1\n          int index = next_char - L'0';\n          if (next_char != L'0') {\n            index--;\n          }\n\n          // Replace with the matching argument, or nothing if the index is invalid\n          if (index < raw_cmd_parts.size()) {\n            match_replacement = raw_cmd_parts.at(index);\n          }\n          break;\n        }\n\n        // All arguments following the target\n        case L'*':\n          for (int i = 1; i < raw_cmd_parts.size(); i++) {\n            // Insert a space before arguments after the first one\n            if (i > 1) {\n              match_replacement += L' ';\n            }\n\n            // Argument escaping applies only to %*, not the single substitutions like %2\n            auto escaped_argument = escape_argument(raw_cmd_parts.at(i));\n            if (needs_cmd_escaping) {\n              // If we're using the cmd.exe trampoline, we'll need to add additional escaping\n              escaped_argument = escape_argument_for_cmd(escaped_argument);\n            }\n            match_replacement += escaped_argument;\n          }\n          break;\n\n        // Long file path of target\n        case L'l':\n        case L'd':\n        case L'v': {\n          std::array<WCHAR, MAX_PATH> path;\n          std::array<PCWCHAR, 2> other_dirs { working_dir.c_str(), nullptr };\n\n          // PathFindOnPath() is a little gross because it uses the same\n          // buffer for input and output, so we need to copy our input\n          // into the path array.\n          std::wcsncpy(path.data(), raw_target.c_str(), path.size());\n          if (path[path.size() - 1] != 0) {\n            // The path was so long it was truncated by this copy. We'll\n            // assume it was an absolute path (likely) and use it unmodified.\n            match_replacement = raw_target;\n          }\n          // See if we can find the path on our search path or working directory\n          else if (PathFindOnPathW(path.data(), other_dirs.data())) {\n            match_replacement = std::wstring { path.data() };\n          }\n          else {\n            // We couldn't find the target, so we'll just hope for the best\n            match_replacement = raw_target;\n          }\n          break;\n        }\n\n        // Working directory\n        case L'w':\n          match_replacement = working_dir;\n          break;\n\n        default:\n          BOOST_LOG(warning) << \"Unsupported argument replacement: %%\" << next_char;\n          break;\n      }\n\n      // Replace the % and following character with the match replacement\n      cmd_string.replace(match_pos, 2, match_replacement);\n\n      // Skip beyond the match replacement itself to prevent recursive replacement\n      match_pos += match_replacement.size();\n    }\n\n    BOOST_LOG(info) << \"Resolved user-provided command '\"sv << raw_cmd << \"' to '\"sv << cmd_string << '\\'';\n    return cmd_string;\n  }\n\n  /**\n   * @brief Run a command on the users profile.\n   *\n   * Launches a child process as the user, using the current user's environment and a specific working directory.\n   *\n   * @param elevated Specify whether to elevate the process.\n   * @param interactive Specify whether this will run in a window or hidden.\n   * @param cmd The command to run.\n   * @param working_dir The working directory for the new process.\n   * @param env The environment variables to use for the new process.\n   * @param file A file object to redirect the child process's output to (may be `nullptr`).\n   * @param ec An error code, set to indicate any errors that occur during the launch process.\n   * @param group A pointer to a `bp::group` object to which the new process should belong (may be `nullptr`).\n   * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails.\n   */\n  bp::child\n  run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {\n    std::wstring start_dir = from_utf8(working_dir.string());\n    HANDLE job = group ? group->native_handle() : nullptr;\n    STARTUPINFOEXW startup_info = create_startup_info(file, job ? &job : nullptr, ec);\n    PROCESS_INFORMATION process_info;\n\n    // Clone the environment to create a local copy. Boost.Process (bp) shares the environment with all spawned processes.\n    // Since we're going to modify the 'env' variable by merging user-specific environment variables into it,\n    // we make a clone to prevent side effects to the shared environment.\n    bp::environment cloned_env = env;\n\n    if (ec) {\n      // In the event that startup_info failed, return a blank child process.\n      return bp::child();\n    }\n\n    // Use RAII to ensure the attribute list is freed when we're done with it\n    auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() {\n      free_proc_thread_attr_list(list);\n    });\n\n    DWORD creation_flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_BREAKAWAY_FROM_JOB;\n\n    // Create a new console for interactive processes and use no console for non-interactive processes\n    creation_flags |= interactive ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW;\n\n    // Find the PATH variable in our environment block using a case-insensitive search\n    auto sunshine_wenv = boost::this_process::wenvironment();\n    std::wstring path_var_name { L\"PATH\" };\n    std::wstring old_path_val;\n    auto itr = std::find_if(sunshine_wenv.cbegin(), sunshine_wenv.cend(), [&](const auto &e) { return boost::iequals(e.get_name(), path_var_name); });\n    if (itr != sunshine_wenv.cend()) {\n      // Use the existing variable if it exists, since Boost treats these as case-sensitive.\n      path_var_name = itr->get_name();\n      old_path_val = sunshine_wenv[path_var_name].to_string();\n    }\n\n    // Temporarily prepend the specified working directory to PATH to ensure CreateProcess()\n    // will (preferentially) find binaries that reside in the working directory.\n    sunshine_wenv[path_var_name].assign(start_dir + L\";\" + old_path_val);\n\n    // Restore the old PATH value for our process when we're done here\n    auto restore_path = util::fail_guard([&]() {\n      if (old_path_val.empty()) {\n        sunshine_wenv[path_var_name].clear();\n      }\n      else {\n        sunshine_wenv[path_var_name].assign(old_path_val);\n      }\n    });\n\n    BOOL ret;\n    if (is_running_as_system()) {\n      // Duplicate the current user's token\n      HANDLE user_token = retrieve_users_token(elevated);\n      if (!user_token) {\n        // Fail the launch rather than risking launching with Sunshine's permissions unmodified.\n        ec = std::make_error_code(std::errc::permission_denied);\n        return bp::child();\n      }\n\n      // Use RAII to ensure the shell token is closed when we're done with it\n      auto token_close = util::fail_guard([user_token]() {\n        CloseHandle(user_token);\n      });\n\n      // Populate env with user-specific environment variables\n      if (!merge_user_environment_block(cloned_env, user_token)) {\n        ec = std::make_error_code(std::errc::not_enough_memory);\n        return bp::child();\n      }\n\n      // Open the process as the current user account, elevation is handled in the token itself.\n      ec = impersonate_current_user(user_token, [&]() {\n        std::wstring env_block = create_environment_block(cloned_env);\n        // Expand environment variables in the command using cloned_env (which contains SUNSHINE_* vars)\n        std::string expanded_cmd = expand_env_vars_in_cmd(cmd, cloned_env);\n        std::wstring wcmd = resolve_command_string(expanded_cmd, start_dir, user_token, creation_flags);\n        ret = CreateProcessAsUserW(user_token,\n          NULL,\n          (LPWSTR) wcmd.c_str(),\n          NULL,\n          NULL,\n          !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES),\n          creation_flags,\n          env_block.data(),\n          start_dir.empty() ? NULL : start_dir.c_str(),\n          (LPSTARTUPINFOW) &startup_info,\n          &process_info);\n      });\n    }\n    // Otherwise, launch the process using CreateProcessW()\n    // This will inherit the elevation of whatever the user launched Sunshine with.\n    else {\n      // Open our current token to resolve environment variables\n      HANDLE process_token;\n      if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &process_token)) {\n        ec = std::make_error_code(std::errc::permission_denied);\n        return bp::child();\n      }\n      auto token_close = util::fail_guard([process_token]() {\n        CloseHandle(process_token);\n      });\n\n      // Populate env with user-specific environment variables\n      if (!merge_user_environment_block(cloned_env, process_token)) {\n        ec = std::make_error_code(std::errc::not_enough_memory);\n        return bp::child();\n      }\n\n      std::wstring env_block = create_environment_block(cloned_env);\n      // Expand environment variables in the command using cloned_env (which contains SUNSHINE_* vars)\n      std::string expanded_cmd = expand_env_vars_in_cmd(cmd, cloned_env);\n      std::wstring wcmd = resolve_command_string(expanded_cmd, start_dir, NULL, creation_flags);\n      ret = CreateProcessW(NULL,\n        (LPWSTR) wcmd.c_str(),\n        NULL,\n        NULL,\n        !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES),\n        creation_flags,\n        env_block.data(),\n        start_dir.empty() ? NULL : start_dir.c_str(),\n        (LPSTARTUPINFOW) &startup_info,\n        &process_info);\n    }\n\n    // Use the results of the launch to create a bp::child object\n    return create_boost_child_from_results(ret, cmd, ec, process_info);\n  }\n\n  /**\n   * @brief Open a url in the default web browser.\n   * @param url The url to open.\n   */\n  void\n  open_url(const std::string &url) {\n    boost::process::v1::environment _env = boost::this_process::environment();\n    auto working_dir = boost::filesystem::path();\n    std::error_code ec;\n\n    auto child = run_command(false, true, \"assets/gui/sunshine-gui.exe --url=\" + url, working_dir, _env, nullptr, ec, nullptr);\n    if (ec) {\n      BOOST_LOG(warning) << \"Couldn't open url [\"sv << url << \"]: System: \"sv << ec.message();\n    }\n    else {\n      BOOST_LOG(debug) << \"Opened url [\"sv << url << \"]\"sv;\n      child.detach();\n    }\n  }\n\n  /**\n   * @brief Open a url directly in the system default browser.\n   * @details Uses run_command to let the system handle URL opening directly.\n   * @param url The url to open.\n   */\n  void\n  open_url_in_browser(const std::string &url) {\n    boost::process::v1::environment _env = boost::this_process::environment();\n    auto working_dir = boost::filesystem::path();\n    std::error_code ec;\n\n    auto child = run_command(false, false, url, working_dir, _env, nullptr, ec, nullptr);\n    if (ec) {\n      BOOST_LOG(warning) << \"Couldn't open url [\"sv << url << \"]: System: \"sv << ec.message();\n    }\n    else {\n      BOOST_LOG(info) << \"Opened url [\"sv << url << \"]\"sv;\n      child.detach();\n    }\n  }\n\n  void\n  adjust_thread_priority(thread_priority_e priority) {\n    int win32_priority;\n\n    switch (priority) {\n      case thread_priority_e::low:\n        win32_priority = THREAD_PRIORITY_BELOW_NORMAL;\n        break;\n      case thread_priority_e::normal:\n        win32_priority = THREAD_PRIORITY_NORMAL;\n        break;\n      case thread_priority_e::high:\n        win32_priority = THREAD_PRIORITY_ABOVE_NORMAL;\n        break;\n      case thread_priority_e::critical:\n        win32_priority = THREAD_PRIORITY_HIGHEST;\n        break;\n      default:\n        BOOST_LOG(error) << \"Unknown thread priority: \"sv << (int) priority;\n        return;\n    }\n\n    if (!SetThreadPriority(GetCurrentThread(), win32_priority)) {\n      auto winerr = GetLastError();\n      BOOST_LOG(warning) << \"Unable to set thread priority to \"sv << win32_priority << \": \"sv << winerr;\n    }\n  }\n\n  void\n  streaming_will_start() {\n    static std::once_flag load_wlanapi_once_flag;\n    std::call_once(load_wlanapi_once_flag, []() {\n      // wlanapi.dll is not installed by default on Windows Server, so we load it dynamically\n      HMODULE wlanapi = LoadLibraryExA(\"wlanapi.dll\", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);\n      if (!wlanapi) {\n        BOOST_LOG(debug) << \"wlanapi.dll is not available on this OS\"sv;\n        return;\n      }\n\n      fn_WlanOpenHandle = (decltype(fn_WlanOpenHandle)) GetProcAddress(wlanapi, \"WlanOpenHandle\");\n      fn_WlanCloseHandle = (decltype(fn_WlanCloseHandle)) GetProcAddress(wlanapi, \"WlanCloseHandle\");\n      fn_WlanFreeMemory = (decltype(fn_WlanFreeMemory)) GetProcAddress(wlanapi, \"WlanFreeMemory\");\n      fn_WlanEnumInterfaces = (decltype(fn_WlanEnumInterfaces)) GetProcAddress(wlanapi, \"WlanEnumInterfaces\");\n      fn_WlanSetInterface = (decltype(fn_WlanSetInterface)) GetProcAddress(wlanapi, \"WlanSetInterface\");\n\n      if (!fn_WlanOpenHandle || !fn_WlanCloseHandle || !fn_WlanFreeMemory || !fn_WlanEnumInterfaces || !fn_WlanSetInterface) {\n        BOOST_LOG(error) << \"wlanapi.dll is missing exports?\"sv;\n\n        fn_WlanOpenHandle = nullptr;\n        fn_WlanCloseHandle = nullptr;\n        fn_WlanFreeMemory = nullptr;\n        fn_WlanEnumInterfaces = nullptr;\n        fn_WlanSetInterface = nullptr;\n\n        FreeLibrary(wlanapi);\n        return;\n      }\n    });\n\n    // Enable MMCSS scheduling for DWM\n    DwmEnableMMCSS(true);\n\n    // Reduce timer period to 0.5ms\n    if (nt_set_timer_resolution_max()) {\n      used_nt_set_timer_resolution = true;\n    }\n    else {\n      BOOST_LOG(error) << \"NtSetTimerResolution() failed, falling back to timeBeginPeriod()\";\n      timeBeginPeriod(1);\n      used_nt_set_timer_resolution = false;\n    }\n\n    // Promote ourselves to high priority class\n    SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);\n\n    // Modify NVIDIA control panel settings again, in case they have been changed externally since sunshine launch\n    if (nvprefs_instance.load()) {\n      if (!nvprefs_instance.owning_undo_file()) {\n        nvprefs_instance.restore_from_and_delete_undo_file_if_exists();\n      }\n      nvprefs_instance.modify_application_profile();\n      nvprefs_instance.modify_global_profile();\n      nvprefs_instance.unload();\n    }\n\n    // Enable low latency mode on all connected WLAN NICs if wlanapi.dll is available\n    if (fn_WlanOpenHandle) {\n      DWORD negotiated_version;\n\n      if (fn_WlanOpenHandle(WLAN_API_MAKE_VERSION(2, 0), nullptr, &negotiated_version, &wlan_handle) == ERROR_SUCCESS) {\n        PWLAN_INTERFACE_INFO_LIST wlan_interface_list;\n\n        if (fn_WlanEnumInterfaces(wlan_handle, nullptr, &wlan_interface_list) == ERROR_SUCCESS) {\n          for (DWORD i = 0; i < wlan_interface_list->dwNumberOfItems; i++) {\n            if (wlan_interface_list->InterfaceInfo[i].isState == wlan_interface_state_connected) {\n              // Enable media streaming mode for 802.11 wireless interfaces to reduce latency and\n              // unnecessary background scanning operations that cause packet loss and jitter.\n              //\n              // https://docs.microsoft.com/en-us/windows-hardware/drivers/network/oid-wdi-set-connection-quality\n              // https://docs.microsoft.com/en-us/previous-versions/windows/hardware/wireless/native-802-11-media-streaming\n              BOOL value = TRUE;\n              auto error = fn_WlanSetInterface(wlan_handle, &wlan_interface_list->InterfaceInfo[i].InterfaceGuid,\n                wlan_intf_opcode_media_streaming_mode, sizeof(value), &value, nullptr);\n              if (error == ERROR_SUCCESS) {\n                BOOST_LOG(info) << \"WLAN interface \"sv << i << \" is now in low latency mode\"sv;\n              }\n            }\n          }\n\n          fn_WlanFreeMemory(wlan_interface_list);\n        }\n        else {\n          fn_WlanCloseHandle(wlan_handle, nullptr);\n          wlan_handle = NULL;\n        }\n      }\n    }\n\n    // If there is no mouse connected, enable Mouse Keys to force the cursor to appear\n    if (!GetSystemMetrics(SM_MOUSEPRESENT)) {\n      BOOST_LOG(info) << \"A mouse was not detected. Sunshine will enable Mouse Keys while streaming to force the mouse cursor to appear.\";\n\n      // Get the current state of Mouse Keys so we can restore it when streaming is over\n      previous_mouse_keys_state.cbSize = sizeof(previous_mouse_keys_state);\n      if (SystemParametersInfoW(SPI_GETMOUSEKEYS, 0, &previous_mouse_keys_state, 0)) {\n        MOUSEKEYS new_mouse_keys_state = {};\n\n        // Enable Mouse Keys\n        new_mouse_keys_state.cbSize = sizeof(new_mouse_keys_state);\n        new_mouse_keys_state.dwFlags = MKF_MOUSEKEYSON | MKF_AVAILABLE;\n        new_mouse_keys_state.iMaxSpeed = 10;\n        new_mouse_keys_state.iTimeToMaxSpeed = 1000;\n        if (SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &new_mouse_keys_state, 0)) {\n          // Remember to restore the previous settings when we stop streaming\n          enabled_mouse_keys = true;\n        }\n        else {\n          auto winerr = GetLastError();\n          BOOST_LOG(warning) << \"Unable to enable Mouse Keys: \"sv << winerr;\n        }\n      }\n      else {\n        auto winerr = GetLastError();\n        BOOST_LOG(warning) << \"Unable to get current state of Mouse Keys: \"sv << winerr;\n      }\n    }\n  }\n\n  void\n  streaming_will_stop() {\n    // Demote ourselves back to normal priority class\n    SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS);\n\n    // End our 0.5ms timer request\n    if (used_nt_set_timer_resolution) {\n      used_nt_set_timer_resolution = false;\n      if (!nt_set_timer_resolution_min()) {\n        BOOST_LOG(error) << \"nt_set_timer_resolution_min() failed even though nt_set_timer_resolution_max() succeeded\";\n      }\n    }\n    else {\n      timeEndPeriod(1);\n    }\n\n    // Disable MMCSS scheduling for DWM\n    DwmEnableMMCSS(false);\n\n    // Closing our WLAN client handle will undo our optimizations\n    if (wlan_handle != nullptr) {\n      fn_WlanCloseHandle(wlan_handle, nullptr);\n      wlan_handle = nullptr;\n    }\n\n    // Restore Mouse Keys back to the previous settings if we turned it on\n    if (enabled_mouse_keys) {\n      enabled_mouse_keys = false;\n      if (!SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &previous_mouse_keys_state, 0)) {\n        auto winerr = GetLastError();\n        BOOST_LOG(warning) << \"Unable to restore original state of Mouse Keys: \"sv << winerr;\n      }\n    }\n  }\n\n  void\n  enter_away_mode() {\n    if (away_mode_active.exchange(true)) {\n      BOOST_LOG(debug) << \"Already in Away Mode\"sv;\n      return;\n    }\n\n    BOOST_LOG(info) << \"Entering Away Mode - display off, system stays running\"sv;\n\n    // Set Away Mode: intercept system sleep requests and keep system running.\n    // ES_AWAYMODE_REQUIRED: system enters away mode instead of sleep when idle or sleep is requested\n    // ES_SYSTEM_REQUIRED: prevent system idle timeout from triggering sleep\n    // ES_CONTINUOUS: keep these flags in effect until explicitly cleared\n    SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_AWAYMODE_REQUIRED);\n\n    // Turn off the display to save power\n    // SC_MONITORPOWER with lParam=2 turns off the monitor\n    SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, 2);\n\n    BOOST_LOG(info) << \"Away Mode active - display off, Sunshine continues listening for connections\"sv;\n  }\n\n  void\n  exit_away_mode() {\n    if (!away_mode_active.exchange(false)) {\n      return;  // Not in Away Mode\n    }\n\n    BOOST_LOG(info) << \"Exiting Away Mode - restoring display\"sv;\n\n    // Clear all power flags to return to default system behavior\n    SetThreadExecutionState(ES_CONTINUOUS);\n\n    // Wake up the display by simulating mouse movement\n    // This is more reliable than SC_MONITORPOWER with lParam=-1\n    INPUT input = {};\n    input.type = INPUT_MOUSE;\n    input.mi.dwFlags = MOUSEEVENTF_MOVE;\n    input.mi.dx = 0;\n    input.mi.dy = 0;\n    SendInput(1, &input, sizeof(INPUT));\n\n    // Also force display on via execution state\n    SetThreadExecutionState(ES_DISPLAY_REQUIRED);\n\n    BOOST_LOG(info) << \"Away Mode deactivated - display restored\"sv;\n  }\n\n  bool\n  is_away_mode_active() {\n    return away_mode_active.load();\n  }\n\n  bool\n  system_sleep() {\n    BOOST_LOG(info) << \"Putting system to sleep (S3 suspend) via SetSuspendState API\"sv;\n\n    // Exit Away Mode first if active, otherwise SetSuspendState may be intercepted\n    if (away_mode_active.load()) {\n      exit_away_mode();\n      // Small delay to ensure Away Mode flags are fully cleared\n      std::this_thread::sleep_for(std::chrono::milliseconds(100));\n    }\n\n    // SetSuspendState(bHibernate=FALSE, bForce=TRUE, bWakeupEventsDisabled=FALSE)\n    // bHibernate=FALSE: enter S3 sleep (not S4 hibernate)\n    // bForce=TRUE: force sleep even if apps haven't responded to sleep notification\n    // bWakeupEventsDisabled=FALSE: allow wake events (WOL, keyboard, mouse, etc.)\n    if (!SetSuspendState(FALSE, TRUE, FALSE)) {\n      auto err = GetLastError();\n      BOOST_LOG(error) << \"SetSuspendState(sleep) failed: \"sv << err;\n      return false;\n    }\n    return true;\n  }\n\n  bool\n  system_hibernate() {\n    BOOST_LOG(info) << \"Putting system to hibernate (S4) via SetSuspendState API\"sv;\n\n    if (away_mode_active.load()) {\n      exit_away_mode();\n      std::this_thread::sleep_for(std::chrono::milliseconds(100));\n    }\n\n    // SetSuspendState(bHibernate=TRUE, bForce=TRUE, bWakeupEventsDisabled=FALSE)\n    if (!SetSuspendState(TRUE, TRUE, FALSE)) {\n      auto err = GetLastError();\n      BOOST_LOG(error) << \"SetSuspendState(hibernate) failed: \"sv << err;\n      return false;\n    }\n    return true;\n  }\n\n  void\n  restart_on_exit() {\n    STARTUPINFOEXW startup_info {};\n    startup_info.StartupInfo.cb = sizeof(startup_info);\n\n    WCHAR executable[MAX_PATH];\n    if (GetModuleFileNameW(NULL, executable, ARRAYSIZE(executable)) == 0) {\n      auto winerr = GetLastError();\n      BOOST_LOG(fatal) << \"Failed to get Sunshine path: \"sv << winerr;\n      return;\n    }\n\n    PROCESS_INFORMATION process_info;\n    if (!CreateProcessW(executable,\n          GetCommandLineW(),\n          nullptr,\n          nullptr,\n          false,\n          CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT,\n          nullptr,\n          nullptr,\n          (LPSTARTUPINFOW) &startup_info,\n          &process_info)) {\n      auto winerr = GetLastError();\n      BOOST_LOG(fatal) << \"Unable to restart Sunshine: \"sv << winerr;\n      return;\n    }\n\n    CloseHandle(process_info.hProcess);\n    CloseHandle(process_info.hThread);\n  }\n\n  void\n  restart() {\n    // If we're running standalone, we have to respawn ourselves via CreateProcess().\n    // If we're running from the service, we should just exit and let it respawn us.\n    if (GetConsoleWindow() != NULL) {\n      // Avoid racing with the new process by waiting until we're exiting to start it.\n      atexit(restart_on_exit);\n    }\n\n    // We use an async exit call here because we can't block the HTTP thread or we'll hang shutdown.\n    lifetime::exit_sunshine(0, true);\n  }\n\n  int\n  set_env(const std::string &name, const std::string &value) {\n    return _putenv_s(name.c_str(), value.c_str());\n  }\n\n  int\n  unset_env(const std::string &name) {\n    return _putenv_s(name.c_str(), \"\");\n  }\n\n  struct enum_wnd_context_t {\n    std::set<DWORD> process_ids;\n    bool requested_exit;\n  };\n\n  static BOOL CALLBACK\n  prgrp_enum_windows(HWND hwnd, LPARAM lParam) {\n    auto enum_ctx = (enum_wnd_context_t *) lParam;\n\n    // Find the owner PID of this window\n    DWORD wnd_process_id;\n    if (!GetWindowThreadProcessId(hwnd, &wnd_process_id)) {\n      // Continue enumeration\n      return TRUE;\n    }\n\n    // Check if this window is owned by a process we want to terminate\n    if (enum_ctx->process_ids.find(wnd_process_id) != enum_ctx->process_ids.end()) {\n      // Send an async WM_CLOSE message to this window\n      if (SendNotifyMessageW(hwnd, WM_CLOSE, 0, 0)) {\n        BOOST_LOG(debug) << \"Sent WM_CLOSE to PID: \"sv << wnd_process_id;\n        enum_ctx->requested_exit = true;\n      }\n      else {\n        auto error = GetLastError();\n        BOOST_LOG(warning) << \"Failed to send WM_CLOSE to PID [\"sv << wnd_process_id << \"]: \" << error;\n      }\n    }\n\n    // Continue enumeration\n    return TRUE;\n  }\n\n  bool\n  request_process_group_exit(std::uintptr_t native_handle) {\n    auto job_handle = (HANDLE) native_handle;\n\n    // Get list of all processes in our job object\n    bool success;\n    DWORD required_length = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST);\n    auto process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length);\n    auto fg = util::fail_guard([&process_id_list]() {\n      free(process_id_list);\n    });\n    while (!(success = QueryInformationJobObject(job_handle, JobObjectBasicProcessIdList,\n               process_id_list, required_length, &required_length)) &&\n           GetLastError() == ERROR_MORE_DATA) {\n      free(process_id_list);\n      process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length);\n      if (!process_id_list) {\n        return false;\n      }\n    }\n\n    if (!success) {\n      auto err = GetLastError();\n      BOOST_LOG(warning) << \"Failed to enumerate processes in group: \"sv << err;\n      return false;\n    }\n    else if (process_id_list->NumberOfProcessIdsInList == 0) {\n      // If all processes are already dead, treat it as a success\n      return true;\n    }\n\n    enum_wnd_context_t enum_ctx = {};\n    enum_ctx.requested_exit = false;\n    for (DWORD i = 0; i < process_id_list->NumberOfProcessIdsInList; i++) {\n      enum_ctx.process_ids.emplace(process_id_list->ProcessIdList[i]);\n    }\n\n    // Enumerate all windows belonging to processes in the list\n    EnumWindows(prgrp_enum_windows, (LPARAM) &enum_ctx);\n\n    // Return success if we told at least one window to close\n    return enum_ctx.requested_exit;\n  }\n\n  bool\n  process_group_running(std::uintptr_t native_handle) {\n    JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting_info;\n\n    if (!QueryInformationJobObject((HANDLE) native_handle, JobObjectBasicAccountingInformation, &accounting_info, sizeof(accounting_info), nullptr)) {\n      auto err = GetLastError();\n      BOOST_LOG(error) << \"Failed to get job accounting info: \"sv << err;\n      return false;\n    }\n\n    return accounting_info.ActiveProcesses != 0;\n  }\n\n  SOCKADDR_IN\n  to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) {\n    SOCKADDR_IN saddr_v4 = {};\n\n    saddr_v4.sin_family = AF_INET;\n    saddr_v4.sin_port = htons(port);\n\n    auto addr_bytes = address.to_bytes();\n    memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr));\n\n    return saddr_v4;\n  }\n\n  SOCKADDR_IN6\n  to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) {\n    SOCKADDR_IN6 saddr_v6 = {};\n\n    saddr_v6.sin6_family = AF_INET6;\n    saddr_v6.sin6_port = htons(port);\n    saddr_v6.sin6_scope_id = address.scope_id();\n\n    auto addr_bytes = address.to_bytes();\n    memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr));\n\n    return saddr_v6;\n  }\n\n  // Use UDP segmentation offload if it is supported by the OS. If the NIC is capable, this will use\n  // hardware acceleration to reduce CPU usage. Support for USO was introduced in Windows 10 20H1.\n  bool\n  send_batch(batched_send_info_t &send_info) {\n    WSAMSG msg;\n\n    // Convert the target address into a SOCKADDR\n    SOCKADDR_IN taddr_v4;\n    SOCKADDR_IN6 taddr_v6;\n    if (send_info.target_address.is_v6()) {\n      taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port);\n\n      msg.name = (PSOCKADDR) &taddr_v6;\n      msg.namelen = sizeof(taddr_v6);\n    }\n    else {\n      taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port);\n\n      msg.name = (PSOCKADDR) &taddr_v4;\n      msg.namelen = sizeof(taddr_v4);\n    }\n\n    auto const max_bufs_per_msg = send_info.payload_buffers.size() + (send_info.headers ? 1 : 0);\n\n    WSABUF bufs[(send_info.headers ? send_info.block_count : 1) * max_bufs_per_msg];\n    DWORD bufcount = 0;\n    if (send_info.headers) {\n      // Interleave buffers for headers and payloads\n      for (auto i = 0; i < send_info.block_count; i++) {\n        bufs[bufcount].buf = (char *) &send_info.headers[(send_info.block_offset + i) * send_info.header_size];\n        bufs[bufcount].len = send_info.header_size;\n        bufcount++;\n        auto payload_desc = send_info.buffer_for_payload_offset((send_info.block_offset + i) * send_info.payload_size);\n        bufs[bufcount].buf = (char *) payload_desc.buffer;\n        bufs[bufcount].len = send_info.payload_size;\n        bufcount++;\n      }\n    }\n    else {\n      // Translate buffer descriptors into WSABUFs\n      auto payload_offset = send_info.block_offset * send_info.payload_size;\n      auto payload_length = payload_offset + (send_info.block_count * send_info.payload_size);\n      while (payload_offset < payload_length) {\n        auto payload_desc = send_info.buffer_for_payload_offset(payload_offset);\n        bufs[bufcount].buf = (char *) payload_desc.buffer;\n        bufs[bufcount].len = std::min(payload_desc.size, payload_length - payload_offset);\n        payload_offset += bufs[bufcount].len;\n        bufcount++;\n      }\n    }\n\n    msg.lpBuffers = bufs;\n    msg.dwBufferCount = bufcount;\n    msg.dwFlags = 0;\n\n    // At most, one DWORD option and one PKTINFO option\n    char cmbuf[WSA_CMSG_SPACE(sizeof(DWORD)) +\n               std::max(WSA_CMSG_SPACE(sizeof(IN6_PKTINFO)), WSA_CMSG_SPACE(sizeof(IN_PKTINFO)))] = {};\n    ULONG cmbuflen = 0;\n\n    msg.Control.buf = cmbuf;\n    msg.Control.len = sizeof(cmbuf);\n\n    auto cm = WSA_CMSG_FIRSTHDR(&msg);\n    if (send_info.source_address.is_v6()) {\n      IN6_PKTINFO pktInfo;\n\n      SOCKADDR_IN6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0);\n      pktInfo.ipi6_addr = saddr_v6.sin6_addr;\n      pktInfo.ipi6_ifindex = 0;\n\n      cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo));\n\n      cm->cmsg_level = IPPROTO_IPV6;\n      cm->cmsg_type = IPV6_PKTINFO;\n      cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo));\n      memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo));\n    }\n    else {\n      IN_PKTINFO pktInfo;\n\n      SOCKADDR_IN saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0);\n      pktInfo.ipi_addr = saddr_v4.sin_addr;\n      pktInfo.ipi_ifindex = 0;\n\n      cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo));\n\n      cm->cmsg_level = IPPROTO_IP;\n      cm->cmsg_type = IP_PKTINFO;\n      cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo));\n      memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo));\n    }\n\n    if (send_info.block_count > 1) {\n      cmbuflen += WSA_CMSG_SPACE(sizeof(DWORD));\n\n      cm = WSA_CMSG_NXTHDR(&msg, cm);\n      cm->cmsg_level = IPPROTO_UDP;\n      cm->cmsg_type = UDP_SEND_MSG_SIZE;\n      cm->cmsg_len = WSA_CMSG_LEN(sizeof(DWORD));\n      *((DWORD *) WSA_CMSG_DATA(cm)) = send_info.header_size + send_info.payload_size;\n    }\n\n    msg.Control.len = cmbuflen;\n\n    // If USO is not supported, this will fail and the caller will fall back to unbatched sends.\n    DWORD bytes_sent;\n    return WSASendMsg((SOCKET) send_info.native_socket, &msg, 0, &bytes_sent, nullptr, nullptr) != SOCKET_ERROR;\n  }\n\n  bool\n  send(send_info_t &send_info) {\n    WSAMSG msg;\n\n    // Convert the target address into a SOCKADDR\n    SOCKADDR_IN taddr_v4;\n    SOCKADDR_IN6 taddr_v6;\n    if (send_info.target_address.is_v6()) {\n      taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port);\n\n      msg.name = (PSOCKADDR) &taddr_v6;\n      msg.namelen = sizeof(taddr_v6);\n    }\n    else {\n      taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port);\n\n      msg.name = (PSOCKADDR) &taddr_v4;\n      msg.namelen = sizeof(taddr_v4);\n    }\n\n    WSABUF bufs[2];\n    DWORD bufcount = 0;\n    if (send_info.header) {\n      bufs[bufcount].buf = (char *) send_info.header;\n      bufs[bufcount].len = send_info.header_size;\n      bufcount++;\n    }\n    bufs[bufcount].buf = (char *) send_info.payload;\n    bufs[bufcount].len = send_info.payload_size;\n    bufcount++;\n\n    msg.lpBuffers = bufs;\n    msg.dwBufferCount = bufcount;\n    msg.dwFlags = 0;\n\n    char cmbuf[std::max(WSA_CMSG_SPACE(sizeof(IN6_PKTINFO)), WSA_CMSG_SPACE(sizeof(IN_PKTINFO)))] = {};\n    ULONG cmbuflen = 0;\n\n    msg.Control.buf = cmbuf;\n    msg.Control.len = sizeof(cmbuf);\n\n    auto cm = WSA_CMSG_FIRSTHDR(&msg);\n    if (send_info.source_address.is_v6()) {\n      IN6_PKTINFO pktInfo;\n\n      SOCKADDR_IN6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0);\n      pktInfo.ipi6_addr = saddr_v6.sin6_addr;\n      pktInfo.ipi6_ifindex = 0;\n\n      cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo));\n\n      cm->cmsg_level = IPPROTO_IPV6;\n      cm->cmsg_type = IPV6_PKTINFO;\n      cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo));\n      memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo));\n    }\n    else {\n      IN_PKTINFO pktInfo;\n\n      SOCKADDR_IN saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0);\n      pktInfo.ipi_addr = saddr_v4.sin_addr;\n      pktInfo.ipi_ifindex = 0;\n\n      cmbuflen += WSA_CMSG_SPACE(sizeof(pktInfo));\n\n      cm->cmsg_level = IPPROTO_IP;\n      cm->cmsg_type = IP_PKTINFO;\n      cm->cmsg_len = WSA_CMSG_LEN(sizeof(pktInfo));\n      memcpy(WSA_CMSG_DATA(cm), &pktInfo, sizeof(pktInfo));\n    }\n\n    msg.Control.len = cmbuflen;\n\n    DWORD bytes_sent;\n    if (WSASendMsg((SOCKET) send_info.native_socket, &msg, 0, &bytes_sent, nullptr, nullptr) == SOCKET_ERROR) {\n      auto winerr = WSAGetLastError();\n      BOOST_LOG(warning) << \"WSASendMsg() failed: \"sv << winerr;\n      return false;\n    }\n\n    return true;\n  }\n\n  class qos_t: public deinit_t {\n  public:\n    qos_t(QOS_FLOWID flow_id):\n        flow_id(flow_id) {}\n\n    virtual ~qos_t() {\n      if (!fn_QOSRemoveSocketFromFlow(qos_handle, (SOCKET) NULL, flow_id, 0)) {\n        auto winerr = GetLastError();\n        BOOST_LOG(warning) << \"QOSRemoveSocketFromFlow() failed: \"sv << winerr;\n      }\n    }\n\n  private:\n    QOS_FLOWID flow_id;\n  };\n\n  /**\n   * @brief Enables QoS on the given socket for traffic to the specified destination.\n   * @param native_socket The native socket handle.\n   * @param address The destination address for traffic sent on this socket.\n   * @param port The destination port for traffic sent on this socket.\n   * @param data_type The type of traffic sent on this socket.\n   * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic.\n   */\n  std::unique_ptr<deinit_t>\n  enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) {\n    SOCKADDR_IN saddr_v4;\n    SOCKADDR_IN6 saddr_v6;\n    PSOCKADDR dest_addr;\n    bool using_connect_hack = false;\n\n    // Windows doesn't support any concept of traffic priority without DSCP tagging\n    if (!dscp_tagging) {\n      return nullptr;\n    }\n\n    static std::once_flag load_qwave_once_flag;\n    std::call_once(load_qwave_once_flag, []() {\n      // qWAVE is not installed by default on Windows Server, so we load it dynamically\n      HMODULE qwave = LoadLibraryExA(\"qwave.dll\", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);\n      if (!qwave) {\n        BOOST_LOG(debug) << \"qwave.dll is not available on this OS\"sv;\n        return;\n      }\n\n      fn_QOSCreateHandle = (decltype(fn_QOSCreateHandle)) GetProcAddress(qwave, \"QOSCreateHandle\");\n      fn_QOSAddSocketToFlow = (decltype(fn_QOSAddSocketToFlow)) GetProcAddress(qwave, \"QOSAddSocketToFlow\");\n      fn_QOSRemoveSocketFromFlow = (decltype(fn_QOSRemoveSocketFromFlow)) GetProcAddress(qwave, \"QOSRemoveSocketFromFlow\");\n\n      if (!fn_QOSCreateHandle || !fn_QOSAddSocketToFlow || !fn_QOSRemoveSocketFromFlow) {\n        BOOST_LOG(error) << \"qwave.dll is missing exports?\"sv;\n\n        fn_QOSCreateHandle = nullptr;\n        fn_QOSAddSocketToFlow = nullptr;\n        fn_QOSRemoveSocketFromFlow = nullptr;\n\n        FreeLibrary(qwave);\n        return;\n      }\n\n      QOS_VERSION qos_version { 1, 0 };\n      if (!fn_QOSCreateHandle(&qos_version, &qos_handle)) {\n        auto winerr = GetLastError();\n        BOOST_LOG(warning) << \"QOSCreateHandle() failed: \"sv << winerr;\n        return;\n      }\n    });\n\n    // If qWAVE is unavailable, just return\n    if (!fn_QOSAddSocketToFlow || !qos_handle) {\n      return nullptr;\n    }\n\n    auto disconnect_fg = util::fail_guard([&]() {\n      if (using_connect_hack) {\n        SOCKADDR_IN6 empty = {};\n        empty.sin6_family = AF_INET6;\n        if (connect((SOCKET) native_socket, (PSOCKADDR) &empty, sizeof(empty)) < 0) {\n          auto wsaerr = WSAGetLastError();\n          BOOST_LOG(error) << \"qWAVE dual-stack workaround failed: \"sv << wsaerr;\n        }\n      }\n    });\n\n    if (address.is_v6()) {\n      auto address_v6 = address.to_v6();\n\n      saddr_v6 = to_sockaddr(address_v6, port);\n      dest_addr = (PSOCKADDR) &saddr_v6;\n\n      // qWAVE doesn't properly support IPv4-mapped IPv6 addresses, nor does it\n      // correctly support IPv4 addresses on a dual-stack socket (despite MSDN's\n      // claims to the contrary). To get proper QoS tagging when hosting in dual\n      // stack mode, we will temporarily connect() the socket to allow qWAVE to\n      // successfully initialize a flow, then disconnect it again so WSASendMsg()\n      // works later on.\n      if (address_v6.is_v4_mapped()) {\n        if (connect((SOCKET) native_socket, (PSOCKADDR) &saddr_v6, sizeof(saddr_v6)) < 0) {\n          auto wsaerr = WSAGetLastError();\n          BOOST_LOG(error) << \"qWAVE dual-stack workaround failed: \"sv << wsaerr;\n        }\n        else {\n          BOOST_LOG(debug) << \"Using qWAVE connect() workaround for QoS tagging\"sv;\n          using_connect_hack = true;\n          dest_addr = nullptr;\n        }\n      }\n    }\n    else {\n      saddr_v4 = to_sockaddr(address.to_v4(), port);\n      dest_addr = (PSOCKADDR) &saddr_v4;\n    }\n\n    QOS_TRAFFIC_TYPE traffic_type;\n    switch (data_type) {\n      case qos_data_type_e::audio:\n        traffic_type = QOSTrafficTypeVoice;\n        break;\n      case qos_data_type_e::video:\n        traffic_type = QOSTrafficTypeAudioVideo;\n        break;\n      default:\n        BOOST_LOG(error) << \"Unknown traffic type: \"sv << (int) data_type;\n        return nullptr;\n    }\n\n    QOS_FLOWID flow_id = 0;\n    if (!fn_QOSAddSocketToFlow(qos_handle, (SOCKET) native_socket, dest_addr, traffic_type, QOS_NON_ADAPTIVE_FLOW, &flow_id)) {\n      auto winerr = GetLastError();\n      BOOST_LOG(warning) << \"QOSAddSocketToFlow() failed: \"sv << winerr;\n      return nullptr;\n    }\n\n    return std::make_unique<qos_t>(flow_id);\n  }\n  int64_t\n  qpc_counter() {\n    LARGE_INTEGER performance_counter;\n    if (QueryPerformanceCounter(&performance_counter)) return performance_counter.QuadPart;\n    return 0;\n  }\n\n  std::chrono::nanoseconds\n  qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2) {\n    auto get_frequency = []() {\n      LARGE_INTEGER frequency;\n      frequency.QuadPart = 0;\n      QueryPerformanceFrequency(&frequency);\n      return frequency.QuadPart;\n    };\n    static const double frequency = get_frequency();\n    if (frequency) {\n      return std::chrono::nanoseconds((int64_t) ((performance_counter1 - performance_counter2) * frequency / std::nano::den));\n    }\n    return {};\n  }\n\n  std::wstring\n  from_utf8(const std::string &string) {\n    // No conversion needed if the string is empty\n    if (string.empty()) {\n      return {};\n    }\n\n    // Get the output size required to store the string\n    auto output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), nullptr, 0);\n    if (output_size == 0) {\n      auto winerr = GetLastError();\n      BOOST_LOG(error) << \"Failed to get UTF-16 buffer size: \"sv << winerr;\n      return {};\n    }\n\n    // Perform the conversion\n    std::wstring output(output_size, L'\\0');\n    output_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, string.data(), string.size(), output.data(), output.size());\n    if (output_size == 0) {\n      auto winerr = GetLastError();\n      BOOST_LOG(error) << \"Failed to convert string to UTF-16: \"sv << winerr;\n      return {};\n    }\n\n    return output;\n  }\n\n  std::string\n  to_utf8(const std::wstring &string) {\n    // No conversion needed if the string is empty\n    if (string.empty()) {\n      return {};\n    }\n\n    // Get the output size required to store the string\n    auto output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(),\n      nullptr, 0, nullptr, nullptr);\n    if (output_size == 0) {\n      auto winerr = GetLastError();\n      BOOST_LOG(error) << \"Failed to get UTF-8 buffer size: \"sv << winerr;\n      return {};\n    }\n\n    // Perform the conversion\n    std::string output(output_size, '\\0');\n    output_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, string.data(), string.size(),\n      output.data(), output.size(), nullptr, nullptr);\n    if (output_size == 0) {\n      auto winerr = GetLastError();\n      BOOST_LOG(error) << \"Failed to convert string to UTF-8: \"sv << winerr;\n      return {};\n    }\n\n    return output;\n  }\n\n  std::string\n  get_host_name() {\n    WCHAR hostname[256];\n    if (GetHostNameW(hostname, ARRAYSIZE(hostname)) == SOCKET_ERROR) {\n      BOOST_LOG(error) << \"GetHostNameW() failed: \"sv << WSAGetLastError();\n      return \"Sunshine\"s;\n    }\n    return to_utf8(hostname);\n  }\n\n  class win32_high_precision_timer: public high_precision_timer {\n  public:\n    win32_high_precision_timer() {\n      // Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+)\n      timer = CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);\n      if (!timer) {\n        timer = CreateWaitableTimerEx(nullptr, nullptr, 0, TIMER_ALL_ACCESS);\n        if (!timer) {\n          BOOST_LOG(error) << \"Unable to create high_precision_timer, CreateWaitableTimerEx() failed: \" << GetLastError();\n        }\n      }\n    }\n\n    ~win32_high_precision_timer() {\n      if (timer) CloseHandle(timer);\n    }\n\n    void\n    sleep_for(const std::chrono::nanoseconds &duration) override {\n      if (!timer) {\n        BOOST_LOG(error) << \"Attempting high_precision_timer::sleep_for() with uninitialized timer\";\n        return;\n      }\n      if (duration < 0s) {\n        BOOST_LOG(error) << \"Attempting high_precision_timer::sleep_for() with negative duration\";\n        return;\n      }\n      if (duration > 5s) {\n        BOOST_LOG(error) << \"Attempting high_precision_timer::sleep_for() with unexpectedly large duration (>5s)\";\n        return;\n      }\n\n      LARGE_INTEGER due_time;\n      due_time.QuadPart = duration.count() / -100;\n      SetWaitableTimer(timer, &due_time, 0, nullptr, nullptr, false);\n      WaitForSingleObject(timer, INFINITE);\n    }\n\n    operator bool() override {\n      return timer != NULL;\n    }\n\n  private:\n    HANDLE timer = NULL;\n  };\n\n  std::unique_ptr<high_precision_timer>\n  create_high_precision_timer() {\n    return std::make_unique<win32_high_precision_timer>();\n  }\n\n  bool\n  fuzzy_match(const std::wstring &text, const std::wstring &pattern) {\n    if (pattern.empty()) {\n      return true;\n    }\n    if (text.empty()) {\n      return false;\n    }\n\n    size_t pattern_idx = 0;\n    for (wchar_t c : text) {\n      if (c == pattern[pattern_idx]) {\n        pattern_idx++;\n        if (pattern_idx >= pattern.length()) {\n          return true;  // All characters found in order\n        }\n      }\n    }\n    return false;  // Not all characters found\n  }\n\n  std::vector<std::wstring>\n  split_words(const std::wstring &text) {\n    std::vector<std::wstring> words;\n    if (text.empty()) {\n      return words;\n    }\n\n    // Use boost::algorithm::split with a predicate that checks for word separators\n    boost::algorithm::split(\n      words,\n      text,\n      [](wchar_t c) {\n        return c == L' ' || c == L'-' || c == L'_' || c == L'.' || c == L':';\n      },\n      boost::algorithm::token_compress_on);  // Merge adjacent separators\n\n    // Remove empty strings\n    words.erase(\n      std::remove_if(\n        words.begin(),\n        words.end(),\n        [](const std::wstring &w) { return w.empty(); }),\n      words.end());\n\n    return words;\n  }\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/windows/misc.h",
    "content": "/**\n * @file src/platform/windows/misc.h\n * @brief Miscellaneous declarations for Windows.\n */\n#pragma once\n\n#include <chrono>\n#include <functional>\n#include <string_view>\n#include <system_error>\n#include <windows.h>\n#include <winnt.h>\n\nnamespace platf {\n  void\n  print_status(const std::string_view &prefix, HRESULT status);\n  HDESK\n  syncThreadDesktop();\n\n  int64_t\n  qpc_counter();\n\n  std::chrono::nanoseconds\n  qpc_time_difference(int64_t performance_counter1, int64_t performance_counter2);\n\n  /**\n   * @brief Convert a UTF-8 string into a UTF-16 wide string.\n   * @param string The UTF-8 string.\n   * @return The converted UTF-16 wide string.\n   */\n  std::wstring\n  from_utf8(const std::string &string);\n\n  /**\n   * @brief Convert a UTF-16 wide string into a UTF-8 string.\n   * @param string The UTF-16 wide string.\n   * @return The converted UTF-8 string.\n   */\n  std::string\n  to_utf8(const std::wstring &string);\n\n  /**\n   * @brief Check if the current process is running as SYSTEM.\n   * @return true if running as SYSTEM, false otherwise.\n   */\n  bool\n  is_running_as_system();\n\n  /**\n   * @brief Retrieve the current logged-in user's token.\n   * @param elevated Whether to retrieve an elevated token if available.\n   * @return The user's token handle, or nullptr if not available.\n   */\n  HANDLE\n  retrieve_users_token(bool elevated);\n\n  /**\n   * @brief Impersonate the current user and execute a callback.\n   * @param user_token The user's token to impersonate.\n   * @param callback The callback function to execute while impersonating.\n   * @return Error code, if any.\n   */\n  std::error_code\n  impersonate_current_user(HANDLE user_token, std::function<void()> callback);\n\n  /**\n   * @brief Check if a character sequence appears in order in a string (fuzzy matching).\n   * @param text The text to search in.\n   * @param pattern The pattern to find (characters must appear in order, but can have gaps).\n   * @return true if pattern is found, false otherwise.\n   */\n  bool\n  fuzzy_match(const std::wstring &text, const std::wstring &pattern);\n\n  /**\n   * @brief Split a string into words (by spaces and common separators).\n   * @param text The text to split.\n   * @return Vector of words.\n   */\n  std::vector<std::wstring>\n  split_words(const std::wstring &text);\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/windows/nvprefs/driver_settings.cpp",
    "content": "/**\n * @file src/platform/windows/nvprefs/driver_settings.cpp\n * @brief Definitions for nvidia driver settings.\n */\n// local includes\n#include \"driver_settings.h\"\n#include \"nvprefs_common.h\"\n\nnamespace {\n\n  const auto sunshine_application_profile_name = L\"SunshineStream\";\n  const auto sunshine_application_path = L\"sunshine.exe\";\n\n  void\n  nvapi_error_message(NvAPI_Status status) {\n    NvAPI_ShortString message = {};\n    NvAPI_GetErrorMessage(status, message);\n    nvprefs::error_message(std::string(\"NvAPI error: \") + message);\n  }\n\n  void\n  fill_nvapi_string(NvAPI_UnicodeString &dest, const wchar_t *src) {\n    static_assert(sizeof(NvU16) == sizeof(wchar_t));\n    memcpy_s(dest, NVAPI_UNICODE_STRING_MAX * sizeof(NvU16), src, (wcslen(src) + 1) * sizeof(wchar_t));\n  }\n\n}  // namespace\n\nnamespace nvprefs {\n\n  driver_settings_t::~driver_settings_t() {\n    if (session_handle) {\n      NvAPI_DRS_DestroySession(session_handle);\n    }\n  }\n\n  bool\n  driver_settings_t::init() {\n    if (session_handle) return true;\n\n    NvAPI_Status status;\n\n    status = NvAPI_Initialize();\n    if (status != NVAPI_OK) {\n      info_message(\"NvAPI_Initialize() failed, ignore if you don't have NVIDIA video card\");\n      return false;\n    }\n\n    status = NvAPI_DRS_CreateSession(&session_handle);\n    if (status != NVAPI_OK) {\n      nvapi_error_message(status);\n      error_message(\"NvAPI_DRS_CreateSession() failed\");\n      return false;\n    }\n\n    return load_settings();\n  }\n\n  void\n  driver_settings_t::destroy() {\n    if (session_handle) {\n      NvAPI_DRS_DestroySession(session_handle);\n      session_handle = 0;\n    }\n    NvAPI_Unload();\n  }\n\n  bool\n  driver_settings_t::load_settings() {\n    if (!session_handle) return false;\n\n    NvAPI_Status status = NvAPI_DRS_LoadSettings(session_handle);\n    if (status != NVAPI_OK) {\n      nvapi_error_message(status);\n      error_message(\"NvAPI_DRS_LoadSettings() failed\");\n      destroy();\n      return false;\n    }\n\n    return true;\n  }\n\n  bool\n  driver_settings_t::save_settings() {\n    if (!session_handle) return false;\n\n    NvAPI_Status status = NvAPI_DRS_SaveSettings(session_handle);\n    if (status != NVAPI_OK) {\n      nvapi_error_message(status);\n      error_message(\"NvAPI_DRS_SaveSettings() failed\");\n      return false;\n    }\n\n    return true;\n  }\n\n  bool\n  driver_settings_t::restore_global_profile_to_undo(const undo_data_t &undo_data) {\n    if (!session_handle) return false;\n\n    const auto &swapchain_data = undo_data.get_opengl_swapchain();\n    if (swapchain_data) {\n      NvAPI_Status status;\n\n      NvDRSProfileHandle profile_handle = 0;\n      status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle);\n      if (status != NVAPI_OK) {\n        nvapi_error_message(status);\n        error_message(\"NvAPI_DRS_GetBaseProfile() failed\");\n        return false;\n      }\n\n      NVDRS_SETTING setting = {};\n      setting.version = NVDRS_SETTING_VER;\n      status = NvAPI_DRS_GetSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID, &setting);\n\n      if (status == NVAPI_OK && setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION && setting.u32CurrentValue == swapchain_data->our_value) {\n        if (swapchain_data->undo_value) {\n          setting = {};\n          setting.version = NVDRS_SETTING_VER1;\n          setting.settingId = OGL_CPL_PREFER_DXPRESENT_ID;\n          setting.settingType = NVDRS_DWORD_TYPE;\n          setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION;\n          setting.u32CurrentValue = *swapchain_data->undo_value;\n\n          status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting);\n\n          if (status != NVAPI_OK) {\n            nvapi_error_message(status);\n            error_message(\"NvAPI_DRS_SetSetting() OGL_CPL_PREFER_DXPRESENT failed\");\n            return false;\n          }\n        }\n        else {\n          status = NvAPI_DRS_DeleteProfileSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID);\n\n          if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) {\n            nvapi_error_message(status);\n            error_message(\"NvAPI_DRS_DeleteProfileSetting() OGL_CPL_PREFER_DXPRESENT failed\");\n            return false;\n          }\n        }\n\n        info_message(\"Restored OGL_CPL_PREFER_DXPRESENT for base profile\");\n      }\n      else if (status == NVAPI_OK || status == NVAPI_SETTING_NOT_FOUND) {\n        info_message(\"OGL_CPL_PREFER_DXPRESENT has been changed from our value in base profile, not restoring\");\n      }\n      else {\n        error_message(\"NvAPI_DRS_GetSetting() OGL_CPL_PREFER_DXPRESENT failed\");\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  bool\n  driver_settings_t::check_and_modify_global_profile(std::optional<undo_data_t> &undo_data) {\n    if (!session_handle) return false;\n\n    undo_data.reset();\n    NvAPI_Status status;\n\n    if (!get_nvprefs_options().opengl_vulkan_on_dxgi) {\n      // User requested to leave OpenGL/Vulkan DXGI swapchain setting alone\n      return true;\n    }\n\n    NvDRSProfileHandle profile_handle = 0;\n    status = NvAPI_DRS_GetBaseProfile(session_handle, &profile_handle);\n    if (status != NVAPI_OK) {\n      nvapi_error_message(status);\n      error_message(\"NvAPI_DRS_GetBaseProfile() failed\");\n      return false;\n    }\n\n    NVDRS_SETTING setting = {};\n    setting.version = NVDRS_SETTING_VER;\n    status = NvAPI_DRS_GetSetting(session_handle, profile_handle, OGL_CPL_PREFER_DXPRESENT_ID, &setting);\n\n    // Remember current OpenGL/Vulkan DXGI swapchain setting and change it if needed\n    if (status == NVAPI_SETTING_NOT_FOUND || (status == NVAPI_OK && setting.u32CurrentValue != OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED)) {\n      undo_data = undo_data_t();\n      if (status == NVAPI_OK) {\n        undo_data->set_opengl_swapchain(OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED, setting.u32CurrentValue);\n      }\n      else {\n        undo_data->set_opengl_swapchain(OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED, std::nullopt);\n      }\n\n      setting = {};\n      setting.version = NVDRS_SETTING_VER1;\n      setting.settingId = OGL_CPL_PREFER_DXPRESENT_ID;\n      setting.settingType = NVDRS_DWORD_TYPE;\n      setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION;\n      setting.u32CurrentValue = OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED;\n\n      status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting);\n      if (status != NVAPI_OK) {\n        nvapi_error_message(status);\n        error_message(\"NvAPI_DRS_SetSetting() OGL_CPL_PREFER_DXPRESENT failed\");\n        return false;\n      }\n\n      info_message(\"Changed OGL_CPL_PREFER_DXPRESENT to OGL_CPL_PREFER_DXPRESENT_PREFER_ENABLED for base profile\");\n    }\n    else if (status != NVAPI_OK) {\n      nvapi_error_message(status);\n      error_message(\"NvAPI_DRS_GetSetting() OGL_CPL_PREFER_DXPRESENT failed\");\n      return false;\n    }\n\n    return true;\n  }\n\n  bool\n  driver_settings_t::check_and_modify_application_profile(bool &modified) {\n    if (!session_handle) return false;\n\n    modified = false;\n    NvAPI_Status status;\n\n    NvAPI_UnicodeString profile_name = {};\n    fill_nvapi_string(profile_name, sunshine_application_profile_name);\n\n    NvDRSProfileHandle profile_handle = 0;\n    status = NvAPI_DRS_FindProfileByName(session_handle, profile_name, &profile_handle);\n\n    if (status != NVAPI_OK) {\n      // Create application profile if missing\n      NVDRS_PROFILE profile = {};\n      profile.version = NVDRS_PROFILE_VER1;\n      fill_nvapi_string(profile.profileName, sunshine_application_profile_name);\n      status = NvAPI_DRS_CreateProfile(session_handle, &profile, &profile_handle);\n      if (status != NVAPI_OK) {\n        nvapi_error_message(status);\n        error_message(\"NvAPI_DRS_CreateProfile() failed\");\n        return false;\n      }\n      modified = true;\n    }\n\n    NvAPI_UnicodeString sunshine_path = {};\n    fill_nvapi_string(sunshine_path, sunshine_application_path);\n\n    NVDRS_APPLICATION application = {};\n    application.version = NVDRS_APPLICATION_VER_V1;\n    status = NvAPI_DRS_GetApplicationInfo(session_handle, profile_handle, sunshine_path, &application);\n\n    if (status != NVAPI_OK) {\n      // Add application to application profile if missing\n      application.version = NVDRS_APPLICATION_VER_V1;\n      application.isPredefined = 0;\n      fill_nvapi_string(application.appName, sunshine_application_path);\n      fill_nvapi_string(application.userFriendlyName, sunshine_application_path);\n      fill_nvapi_string(application.launcher, L\"\");\n\n      status = NvAPI_DRS_CreateApplication(session_handle, profile_handle, &application);\n      if (status != NVAPI_OK) {\n        nvapi_error_message(status);\n        error_message(\"NvAPI_DRS_CreateApplication() failed\");\n        return false;\n      }\n      modified = true;\n    }\n\n    NVDRS_SETTING setting = {};\n    setting.version = NVDRS_SETTING_VER1;\n    status = NvAPI_DRS_GetSetting(session_handle, profile_handle, PREFERRED_PSTATE_ID, &setting);\n\n    if (!get_nvprefs_options().sunshine_high_power_mode) {\n      if (status == NVAPI_OK &&\n          setting.settingLocation == NVDRS_CURRENT_PROFILE_LOCATION) {\n        // User requested to not use high power mode for sunshine.exe,\n        // remove the setting from application profile if it's been set previously\n\n        status = NvAPI_DRS_DeleteProfileSetting(session_handle, profile_handle, PREFERRED_PSTATE_ID);\n        if (status != NVAPI_OK && status != NVAPI_SETTING_NOT_FOUND) {\n          nvapi_error_message(status);\n          error_message(\"NvAPI_DRS_DeleteProfileSetting() PREFERRED_PSTATE failed\");\n          return false;\n        }\n        modified = true;\n\n        info_message(std::wstring(L\"Removed PREFERRED_PSTATE for \") + sunshine_application_path);\n      }\n    }\n    else if (status != NVAPI_OK ||\n             setting.settingLocation != NVDRS_CURRENT_PROFILE_LOCATION ||\n             setting.u32CurrentValue != PREFERRED_PSTATE_PREFER_MAX) {\n      // Set power setting if needed\n      setting = {};\n      setting.version = NVDRS_SETTING_VER1;\n      setting.settingId = PREFERRED_PSTATE_ID;\n      setting.settingType = NVDRS_DWORD_TYPE;\n      setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION;\n      setting.u32CurrentValue = PREFERRED_PSTATE_PREFER_MAX;\n\n      status = NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting);\n      if (status != NVAPI_OK) {\n        nvapi_error_message(status);\n        error_message(\"NvAPI_DRS_SetSetting() PREFERRED_PSTATE failed\");\n        return false;\n      }\n      modified = true;\n\n      info_message(std::wstring(L\"Changed PREFERRED_PSTATE to PREFERRED_PSTATE_PREFER_MAX for \") + sunshine_application_path);\n    }\n\n    return true;\n  }\n\n}  // namespace nvprefs\n"
  },
  {
    "path": "src/platform/windows/nvprefs/driver_settings.h",
    "content": "/**\n * @file src/platform/windows/nvprefs/driver_settings.h\n * @brief Declarations for nvidia driver settings.\n */\n#pragma once\n\n// local includes first so standard library headers are pulled in before nvapi's SAL macros\n#include \"undo_data.h\"\n\n// nvapi headers\n// disable clang-format header reordering\n// as <NvApiDriverSettings.h> needs types from <nvapi.h>\n// clang-format off\n\n// With GCC/MinGW, nvapi_lite_salend.h (included transitively via nvapi_lite_d3dext.h)\n// undefines all SAL annotation macros (e.g. __success, __in, __out, __inout) after\n// nvapi_lite_salstart.h had defined them. This leaves NVAPI_INTERFACE and other macros\n// that use SAL annotations broken for the rest of nvapi.h. Defining __NVAPI_EMPTY_SAL\n// makes nvapi_lite_salend.h a no-op, preserving the SAL macro definitions throughout.\n// After nvapi.h, we include nvapi_lite_salend.h explicitly (without __NVAPI_EMPTY_SAL)\n// to clean up the SAL macros and prevent them from polluting subsequent includes.\n#if defined(__GNUC__)\n  #define __NVAPI_EMPTY_SAL\n#endif\n\n#include <nvapi.h>\n#include <NvApiDriverSettings.h>\n\n#if defined(__GNUC__)\n  #undef __NVAPI_EMPTY_SAL\n  // Clean up SAL macros that nvapi_lite_salstart.h defined and salend.h was\n  // prevented from cleaning up (due to __NVAPI_EMPTY_SAL above).\n  #include <nvapi_lite_salend.h>\n#endif\n// clang-format on\n\nnamespace nvprefs {\n\n  class driver_settings_t {\n  public:\n    ~driver_settings_t();\n\n    bool\n    init();\n\n    void\n    destroy();\n\n    bool\n    load_settings();\n\n    bool\n    save_settings();\n\n    bool\n    restore_global_profile_to_undo(const undo_data_t &undo_data);\n\n    bool\n    check_and_modify_global_profile(std::optional<undo_data_t> &undo_data);\n\n    bool\n    check_and_modify_application_profile(bool &modified);\n\n  private:\n    NvDRSSessionHandle session_handle = 0;\n  };\n\n}  // namespace nvprefs\n"
  },
  {
    "path": "src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp",
    "content": "/**\n * @file src/platform/windows/nvprefs/nvapi_opensource_wrapper.cpp\n * @brief Definitions for the NVAPI wrapper.\n */\n// standard library headers\n#include <map>\n\n// local includes\n#include \"driver_settings.h\"\n#include \"nvprefs_common.h\"\n\n// special nvapi header that should be the last include\n#include <nvapi_interface.h>\n\nnamespace {\n\n  std::map<const char *, void *> interfaces;\n  HMODULE dll = NULL;\n\n  template <typename Func, typename... Args>\n  NvAPI_Status\n  call_interface(const char *name, Args... args) {\n    auto func = (Func *) interfaces[name];\n\n    if (!func) {\n      return interfaces.empty() ? NVAPI_API_NOT_INITIALIZED : NVAPI_NOT_SUPPORTED;\n    }\n\n    return func(args...);\n  }\n\n}  // namespace\n\n#undef NVAPI_INTERFACE\n#define NVAPI_INTERFACE NvAPI_Status __cdecl\n\nextern void *__cdecl nvapi_QueryInterface(NvU32 id);\n\nNVAPI_INTERFACE\nNvAPI_Initialize() {\n  if (dll) return NVAPI_OK;\n\n#ifdef _WIN64\n  auto dll_name = \"nvapi64.dll\";\n#else\n  auto dll_name = \"nvapi.dll\";\n#endif\n\n  if ((dll = LoadLibraryEx(dll_name, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32))) {\n    if (auto query_interface = (decltype(nvapi_QueryInterface) *) GetProcAddress(dll, \"nvapi_QueryInterface\")) {\n      for (const auto &item : nvapi_interface_table) {\n        interfaces[item.func] = query_interface(item.id);\n      }\n      return NVAPI_OK;\n    }\n  }\n\n  NvAPI_Unload();\n  return NVAPI_LIBRARY_NOT_FOUND;\n}\n\nNVAPI_INTERFACE\nNvAPI_Unload() {\n  if (dll) {\n    interfaces.clear();\n    FreeLibrary(dll);\n    dll = NULL;\n  }\n  return NVAPI_OK;\n}\n\nNVAPI_INTERFACE\nNvAPI_GetErrorMessage(NvAPI_Status nr, NvAPI_ShortString szDesc) {\n  return call_interface<decltype(NvAPI_GetErrorMessage)>(\"NvAPI_GetErrorMessage\", nr, szDesc);\n}\n\n// This is only a subset of NvAPI_DRS_* functions, more can be added if needed\n\nNVAPI_INTERFACE\nNvAPI_DRS_CreateSession(NvDRSSessionHandle *phSession) {\n  return call_interface<decltype(NvAPI_DRS_CreateSession)>(\"NvAPI_DRS_CreateSession\", phSession);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_DestroySession(NvDRSSessionHandle hSession) {\n  return call_interface<decltype(NvAPI_DRS_DestroySession)>(\"NvAPI_DRS_DestroySession\", hSession);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_LoadSettings(NvDRSSessionHandle hSession) {\n  return call_interface<decltype(NvAPI_DRS_LoadSettings)>(\"NvAPI_DRS_LoadSettings\", hSession);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_SaveSettings(NvDRSSessionHandle hSession) {\n  return call_interface<decltype(NvAPI_DRS_SaveSettings)>(\"NvAPI_DRS_SaveSettings\", hSession);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_CreateProfile(NvDRSSessionHandle hSession, NVDRS_PROFILE *pProfileInfo, NvDRSProfileHandle *phProfile) {\n  return call_interface<decltype(NvAPI_DRS_CreateProfile)>(\"NvAPI_DRS_CreateProfile\", hSession, pProfileInfo, phProfile);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_FindProfileByName(NvDRSSessionHandle hSession, NvAPI_UnicodeString profileName, NvDRSProfileHandle *phProfile) {\n  return call_interface<decltype(NvAPI_DRS_FindProfileByName)>(\"NvAPI_DRS_FindProfileByName\", hSession, profileName, phProfile);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_CreateApplication(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_APPLICATION *pApplication) {\n  return call_interface<decltype(NvAPI_DRS_CreateApplication)>(\"NvAPI_DRS_CreateApplication\", hSession, hProfile, pApplication);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_GetApplicationInfo(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvAPI_UnicodeString appName, NVDRS_APPLICATION *pApplication) {\n  return call_interface<decltype(NvAPI_DRS_GetApplicationInfo)>(\"NvAPI_DRS_GetApplicationInfo\", hSession, hProfile, appName, pApplication);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_SetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NVDRS_SETTING *pSetting) {\n  return call_interface<decltype(NvAPI_DRS_SetSetting)>(\"NvAPI_DRS_SetSetting\", hSession, hProfile, pSetting);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_GetSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId, NVDRS_SETTING *pSetting) {\n  return call_interface<decltype(NvAPI_DRS_GetSetting)>(\"NvAPI_DRS_GetSetting\", hSession, hProfile, settingId, pSetting);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_DeleteProfileSetting(NvDRSSessionHandle hSession, NvDRSProfileHandle hProfile, NvU32 settingId) {\n  return call_interface<decltype(NvAPI_DRS_DeleteProfileSetting)>(\"NvAPI_DRS_DeleteProfileSetting\", hSession, hProfile, settingId);\n}\n\nNVAPI_INTERFACE\nNvAPI_DRS_GetBaseProfile(NvDRSSessionHandle hSession, NvDRSProfileHandle *phProfile) {\n  return call_interface<decltype(NvAPI_DRS_GetBaseProfile)>(\"NvAPI_DRS_GetBaseProfile\", hSession, phProfile);\n}\n"
  },
  {
    "path": "src/platform/windows/nvprefs/nvprefs_common.cpp",
    "content": "/**\n * @file src/platform/windows/nvprefs/nvprefs_common.cpp\n * @brief Definitions for common nvidia preferences.\n */\n// local includes\n#include \"nvprefs_common.h\"\n#include \"src/logging.h\"\n\n// read user override preferences from global sunshine config\n#include \"src/config.h\"\n\nnamespace nvprefs {\n\n  void\n  info_message(const std::wstring &message) {\n    BOOST_LOG(info) << \"nvprefs: \" << message;\n  }\n\n  void\n  info_message(const std::string &message) {\n    BOOST_LOG(info) << \"nvprefs: \" << message;\n  }\n\n  void\n  error_message(const std::wstring &message) {\n    BOOST_LOG(error) << \"nvprefs: \" << message;\n  }\n\n  void\n  error_message(const std::string &message) {\n    BOOST_LOG(error) << \"nvprefs: \" << message;\n  }\n\n  nvprefs_options\n  get_nvprefs_options() {\n    nvprefs_options options;\n    options.opengl_vulkan_on_dxgi = config::video.nv_opengl_vulkan_on_dxgi;\n    options.sunshine_high_power_mode = config::video.nv_sunshine_high_power_mode;\n    return options;\n  }\n\n}  // namespace nvprefs\n"
  },
  {
    "path": "src/platform/windows/nvprefs/nvprefs_common.h",
    "content": "/**\n * @file src/platform/windows/nvprefs/nvprefs_common.h\n * @brief Declarations for common nvidia preferences.\n */\n#pragma once\n\n// sunshine utility header for generic smart pointers\n#include \"src/utility.h\"\n\n// winapi headers\n// disable clang-format header reordering\n// clang-format off\n#include <windows.h>\n#include <aclapi.h>\n// clang-format on\n\nnamespace nvprefs {\n\n  struct safe_handle: public util::safe_ptr_v2<void, BOOL, CloseHandle> {\n    using util::safe_ptr_v2<void, BOOL, CloseHandle>::safe_ptr_v2;\n    explicit\n    operator bool() const {\n      auto handle = get();\n      return handle != NULL && handle != INVALID_HANDLE_VALUE;\n    }\n  };\n\n  struct safe_hlocal_deleter {\n    void\n    operator()(void *p) {\n      LocalFree(p);\n    }\n  };\n\n  template <typename T>\n  using safe_hlocal = util::uniq_ptr<std::remove_pointer_t<T>, safe_hlocal_deleter>;\n\n  using safe_sid = util::safe_ptr_v2<void, PVOID, FreeSid>;\n\n  void\n  info_message(const std::wstring &message);\n\n  void\n  info_message(const std::string &message);\n\n  void\n  error_message(const std::wstring &message);\n\n  void\n  error_message(const std::string &message);\n\n  struct nvprefs_options {\n    bool opengl_vulkan_on_dxgi = true;\n    bool sunshine_high_power_mode = true;\n  };\n\n  nvprefs_options\n  get_nvprefs_options();\n\n}  // namespace nvprefs\n"
  },
  {
    "path": "src/platform/windows/nvprefs/nvprefs_interface.cpp",
    "content": "/**\n * @file src/platform/windows/nvprefs/nvprefs_interface.cpp\n * @brief Definitions for nvidia preferences interface.\n */\n// standard includes\n#include <cassert>\n\n// local includes\n#include \"driver_settings.h\"\n#include \"nvprefs_interface.h\"\n#include \"undo_file.h\"\n\nnamespace {\n\n  const auto sunshine_program_data_folder = \"Sunshine\";\n  const auto nvprefs_undo_file_name = \"nvprefs_undo.json\";\n\n}  // namespace\n\nnamespace nvprefs {\n\n  struct nvprefs_interface::impl {\n    bool loaded = false;\n    driver_settings_t driver_settings;\n    std::filesystem::path undo_folder_path;\n    std::filesystem::path undo_file_path;\n    std::optional<undo_data_t> undo_data;\n    std::optional<undo_file_t> undo_file;\n  };\n\n  nvprefs_interface::nvprefs_interface():\n      pimpl(new impl()) {\n  }\n\n  nvprefs_interface::~nvprefs_interface() {\n    if (owning_undo_file() && load()) {\n      restore_global_profile();\n    }\n    unload();\n  }\n\n  bool\n  nvprefs_interface::load() {\n    if (!pimpl->loaded) {\n      // Check %ProgramData% variable, need it for storing undo file\n      wchar_t program_data_env[MAX_PATH];\n      auto get_env_result = GetEnvironmentVariableW(L\"ProgramData\", program_data_env, MAX_PATH);\n      if (get_env_result == 0 || get_env_result >= MAX_PATH || !std::filesystem::is_directory(program_data_env)) {\n        error_message(\"Missing or malformed %ProgramData% environment variable\");\n        return false;\n      }\n\n      // Prepare undo file path variables\n      pimpl->undo_folder_path = std::filesystem::path(program_data_env) / sunshine_program_data_folder;\n      pimpl->undo_file_path = pimpl->undo_folder_path / nvprefs_undo_file_name;\n\n      // Dynamically load nvapi library and load driver settings\n      pimpl->loaded = pimpl->driver_settings.init();\n    }\n\n    return pimpl->loaded;\n  }\n\n  void\n  nvprefs_interface::unload() {\n    if (pimpl->loaded) {\n      // Unload dynamically loaded nvapi library\n      pimpl->driver_settings.destroy();\n      pimpl->loaded = false;\n    }\n  }\n\n  bool\n  nvprefs_interface::restore_from_and_delete_undo_file_if_exists() {\n    if (!pimpl->loaded) return false;\n\n    // Check for undo file from previous improper termination\n    bool access_denied = false;\n    if (auto undo_file = undo_file_t::open_existing_file(pimpl->undo_file_path, access_denied)) {\n      // Try to restore from the undo file\n      info_message(\"Opened undo file from previous improper termination\");\n      if (auto undo_data = undo_file->read_undo_data()) {\n        if (pimpl->driver_settings.restore_global_profile_to_undo(*undo_data) && pimpl->driver_settings.save_settings()) {\n          info_message(\"Restored global profile settings from undo file - deleting the file\");\n        }\n        else {\n          error_message(\"Failed to restore global profile settings from undo file, deleting the file anyway\");\n        }\n      }\n      else {\n        error_message(\"Coulnd't read undo file, deleting the file anyway\");\n      }\n\n      if (!undo_file->delete_file()) {\n        error_message(\"Couldn't delete undo file\");\n        return false;\n      }\n    }\n    else if (access_denied) {\n      error_message(\"Couldn't open undo file from previous improper termination, or confirm that there's no such file\");\n      return false;\n    }\n\n    return true;\n  }\n\n  bool\n  nvprefs_interface::modify_application_profile() {\n    if (!pimpl->loaded) return false;\n\n    // Modify and save sunshine.exe application profile settings, if needed\n    bool modified = false;\n    if (!pimpl->driver_settings.check_and_modify_application_profile(modified)) {\n      error_message(\"Failed to modify application profile settings\");\n      return false;\n    }\n    else if (modified) {\n      if (pimpl->driver_settings.save_settings()) {\n        info_message(\"Modified application profile settings\");\n      }\n      else {\n        error_message(\"Couldn't save application profile settings\");\n        return false;\n      }\n    }\n    else {\n      info_message(\"No need to modify application profile settings\");\n    }\n\n    return true;\n  }\n\n  bool\n  nvprefs_interface::modify_global_profile() {\n    if (!pimpl->loaded) return false;\n\n    // Modify but not save global profile settings, if needed\n    std::optional<undo_data_t> undo_data;\n    if (!pimpl->driver_settings.check_and_modify_global_profile(undo_data)) {\n      error_message(\"Couldn't modify global profile settings\");\n      return false;\n    }\n    else if (!undo_data) {\n      info_message(\"No need to modify global profile settings\");\n      return true;\n    }\n\n    auto make_undo_and_commit = [&]() -> bool {\n      // Create and lock undo file if it hasn't been done yet\n      if (!pimpl->undo_file) {\n        // Prepare Sunshine folder in ProgramData if it doesn't exist\n        if (!CreateDirectoryW(pimpl->undo_folder_path.c_str(), nullptr) && GetLastError() != ERROR_ALREADY_EXISTS) {\n          error_message(\"Couldn't create undo folder\");\n          return false;\n        }\n\n        // Create undo file to handle improper termination of nvprefs.exe\n        pimpl->undo_file = undo_file_t::create_new_file(pimpl->undo_file_path);\n        if (!pimpl->undo_file) {\n          error_message(\"Couldn't create undo file\");\n          return false;\n        }\n      }\n\n      assert(undo_data);\n      if (pimpl->undo_data) {\n        // Merge undo data if settings has been modified externally since our last modification\n        pimpl->undo_data->merge(*undo_data);\n      }\n      else {\n        pimpl->undo_data = undo_data;\n      }\n\n      // Write undo data to undo file\n      if (!pimpl->undo_file->write_undo_data(*pimpl->undo_data)) {\n        error_message(\"Couldn't write to undo file - deleting the file\");\n        if (!pimpl->undo_file->delete_file()) {\n          error_message(\"Couldn't delete undo file\");\n        }\n        return false;\n      }\n\n      // Save global profile settings\n      if (!pimpl->driver_settings.save_settings()) {\n        error_message(\"Couldn't save global profile settings\");\n        return false;\n      }\n\n      return true;\n    };\n\n    if (!make_undo_and_commit()) {\n      // Revert settings modifications\n      pimpl->driver_settings.load_settings();\n      return false;\n    }\n\n    return true;\n  }\n\n  bool\n  nvprefs_interface::owning_undo_file() {\n    return pimpl->undo_file.has_value();\n  }\n\n  bool\n  nvprefs_interface::restore_global_profile() {\n    if (!pimpl->loaded || !pimpl->undo_data || !pimpl->undo_file) return false;\n\n    // Restore global profile settings with undo data\n    if (pimpl->driver_settings.restore_global_profile_to_undo(*pimpl->undo_data) &&\n        pimpl->driver_settings.save_settings()) {\n      // Global profile settings sucessfully restored, can delete undo file\n      if (!pimpl->undo_file->delete_file()) {\n        error_message(\"Couldn't delete undo file\");\n        return false;\n      }\n      pimpl->undo_data = std::nullopt;\n      pimpl->undo_file = std::nullopt;\n    }\n    else {\n      error_message(\"Couldn't restore global profile settings\");\n      return false;\n    }\n\n    return true;\n  }\n\n}  // namespace nvprefs\n"
  },
  {
    "path": "src/platform/windows/nvprefs/nvprefs_interface.h",
    "content": "/**\n * @file src/platform/windows/nvprefs/nvprefs_interface.h\n * @brief Declarations for nvidia preferences interface.\n */\n#pragma once\n\n// standard library headers\n#include <memory>\n\nnamespace nvprefs {\n\n  class nvprefs_interface {\n  public:\n    nvprefs_interface();\n    ~nvprefs_interface();\n\n    bool\n    load();\n\n    void\n    unload();\n\n    bool\n    restore_from_and_delete_undo_file_if_exists();\n\n    bool\n    modify_application_profile();\n\n    bool\n    modify_global_profile();\n\n    bool\n    owning_undo_file();\n\n    bool\n    restore_global_profile();\n\n  private:\n    struct impl;\n    std::unique_ptr<impl> pimpl;\n  };\n\n}  // namespace nvprefs\n"
  },
  {
    "path": "src/platform/windows/nvprefs/undo_data.cpp",
    "content": "/**\n * @file src/platform/windows/nvprefs/undo_data.cpp\n * @brief Definitions for undoing changes to nvidia preferences.\n */\n// external includes\n#include <nlohmann/json.hpp>\n\n// local includes\n#include \"nvprefs_common.h\"\n#include \"undo_data.h\"\n\nusing json = nlohmann::json;\n\n// Separate namespace for ADL, otherwise we need to define json\n// functions in the same namespace as our types\nnamespace nlohmann {\n  using data_t = nvprefs::undo_data_t::data_t;\n  using opengl_swapchain_t = data_t::opengl_swapchain_t;\n\n  template <typename T>\n  struct adl_serializer<std::optional<T>> {\n    static void\n    to_json(json &j, const std::optional<T> &opt) {\n      if (opt == std::nullopt) {\n        j = nullptr;\n      }\n      else {\n        j = *opt;\n      }\n    }\n\n    static void\n    from_json(const json &j, std::optional<T> &opt) {\n      if (j.is_null()) {\n        opt = std::nullopt;\n      }\n      else {\n        opt = j.template get<T>();\n      }\n    }\n  };\n\n  template <>\n  struct adl_serializer<data_t> {\n    static void\n    to_json(json &j, const data_t &data) {\n      j = json { { \"opengl_swapchain\", data.opengl_swapchain } };\n    }\n\n    static void\n    from_json(const json &j, data_t &data) {\n      j.at(\"opengl_swapchain\").get_to(data.opengl_swapchain);\n    }\n  };\n\n  template <>\n  struct adl_serializer<opengl_swapchain_t> {\n    static void\n    to_json(json &j, const opengl_swapchain_t &opengl_swapchain) {\n      j = json {\n        { \"our_value\", opengl_swapchain.our_value },\n        { \"undo_value\", opengl_swapchain.undo_value }\n      };\n    }\n\n    static void\n    from_json(const json &j, opengl_swapchain_t &opengl_swapchain) {\n      j.at(\"our_value\").get_to(opengl_swapchain.our_value);\n      j.at(\"undo_value\").get_to(opengl_swapchain.undo_value);\n    }\n  };\n}  // namespace nlohmann\n\nnamespace nvprefs {\n\n  void\n  undo_data_t::set_opengl_swapchain(uint32_t our_value, std::optional<uint32_t> undo_value) {\n    data.opengl_swapchain = data_t::opengl_swapchain_t {\n      our_value,\n      undo_value\n    };\n  }\n\n  std::optional<undo_data_t::data_t::opengl_swapchain_t>\n  undo_data_t::get_opengl_swapchain() const {\n    return data.opengl_swapchain;\n  }\n\n  std::string\n  undo_data_t::write() const {\n    try {\n      // Keep this assignment otherwise data will be treated as an array due to\n      // initializer list shenanigangs.\n      const json json_data = data;\n      return json_data.dump();\n    }\n    catch (const std::exception &err) {\n      error_message(std::string { \"failed to serialize json data\" });\n      return {};\n    }\n  }\n\n  void\n  undo_data_t::read(const std::vector<char> &buffer) {\n    try {\n      data = json::parse(std::begin(buffer), std::end(buffer));\n    }\n    catch (const std::exception &err) {\n      error_message(std::string { \"failed to parse json data: \" } + err.what());\n      data = {};\n    }\n  }\n\n  void\n  undo_data_t::merge(const undo_data_t &newer_data) {\n    const auto &swapchain_data = newer_data.get_opengl_swapchain();\n    if (swapchain_data) {\n      set_opengl_swapchain(swapchain_data->our_value, swapchain_data->undo_value);\n    }\n  }\n\n}  // namespace nvprefs\n"
  },
  {
    "path": "src/platform/windows/nvprefs/undo_data.h",
    "content": "/**\n * @file src/platform/windows/nvprefs/undo_data.h\n * @brief Declarations for undoing changes to nvidia preferences.\n */\n#pragma once\n\n// standard library headers\n#include <cstdint>\n#include <optional>\n#include <string>\n#include <vector>\n\nnamespace nvprefs {\n\n  class undo_data_t {\n  public:\n    struct data_t {\n      struct opengl_swapchain_t {\n        uint32_t our_value;\n        std::optional<uint32_t> undo_value;\n      };\n\n      std::optional<opengl_swapchain_t> opengl_swapchain;\n    };\n\n    void\n    set_opengl_swapchain(uint32_t our_value, std::optional<uint32_t> undo_value);\n\n    std::optional<data_t::opengl_swapchain_t>\n    get_opengl_swapchain() const;\n\n    std::string\n    write() const;\n\n    void\n    read(const std::vector<char> &buffer);\n\n    void\n    merge(const undo_data_t &newer_data);\n\n  private:\n    data_t data;\n  };\n\n}  // namespace nvprefs\n"
  },
  {
    "path": "src/platform/windows/nvprefs/undo_file.cpp",
    "content": "/**\n * @file src/platform/windows/nvprefs/undo_file.cpp\n * @brief Definitions for the nvidia undo file.\n */\n// local includes\n#include \"undo_file.h\"\n\nnamespace {\n\n  using namespace nvprefs;\n\n  DWORD\n  relax_permissions(HANDLE file_handle) {\n    PACL old_dacl = nullptr;\n\n    safe_hlocal<PSECURITY_DESCRIPTOR> sd;\n    DWORD status = GetSecurityInfo(file_handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &old_dacl, nullptr, &sd);\n    if (status != ERROR_SUCCESS) return status;\n\n    safe_sid users_sid;\n    SID_IDENTIFIER_AUTHORITY nt_authorithy = SECURITY_NT_AUTHORITY;\n    if (!AllocateAndInitializeSid(&nt_authorithy, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS, 0, 0, 0, 0, 0, 0, &users_sid)) {\n      return GetLastError();\n    }\n\n    EXPLICIT_ACCESS ea = {};\n    ea.grfAccessPermissions = GENERIC_READ | GENERIC_WRITE | DELETE;\n    ea.grfAccessMode = GRANT_ACCESS;\n    ea.grfInheritance = NO_INHERITANCE;\n    ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;\n    ea.Trustee.ptstrName = (LPTSTR) users_sid.get();\n\n    safe_hlocal<PACL> new_dacl;\n    status = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl);\n    if (status != ERROR_SUCCESS) return status;\n\n    status = SetSecurityInfo(file_handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl.get(), nullptr);\n    if (status != ERROR_SUCCESS) return status;\n\n    return 0;\n  }\n\n}  // namespace\n\nnamespace nvprefs {\n\n  std::optional<undo_file_t>\n  undo_file_t::open_existing_file(std::filesystem::path file_path, bool &access_denied) {\n    undo_file_t file;\n    file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_READ | DELETE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL));\n    if (file.file_handle) {\n      access_denied = false;\n      return file;\n    }\n    else {\n      auto last_error = GetLastError();\n      access_denied = (last_error != ERROR_FILE_NOT_FOUND && last_error != ERROR_PATH_NOT_FOUND);\n      return std::nullopt;\n    }\n  }\n\n  std::optional<undo_file_t>\n  undo_file_t::create_new_file(std::filesystem::path file_path) {\n    undo_file_t file;\n    file.file_handle.reset(CreateFileW(file_path.c_str(), GENERIC_WRITE | STANDARD_RIGHTS_ALL, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL));\n\n    if (file.file_handle) {\n      // give GENERIC_READ, GENERIC_WRITE and DELETE permissions to Users group\n      if (relax_permissions(file.file_handle.get()) != 0) {\n        error_message(\"Failed to relax permissions on undo file\");\n      }\n      return file;\n    }\n    else {\n      return std::nullopt;\n    }\n  }\n\n  bool\n  undo_file_t::delete_file() {\n    if (!file_handle) return false;\n\n    FILE_DISPOSITION_INFO delete_file_info = { TRUE };\n    if (SetFileInformationByHandle(file_handle.get(), FileDispositionInfo, &delete_file_info, sizeof(delete_file_info))) {\n      file_handle.reset();\n      return true;\n    }\n    else {\n      return false;\n    }\n  }\n\n  bool\n  undo_file_t::write_undo_data(const undo_data_t &undo_data) {\n    if (!file_handle) return false;\n\n    std::string buffer;\n    try {\n      buffer = undo_data.write();\n    }\n    catch (...) {\n      error_message(\"Couldn't serialize undo data\");\n      return false;\n    }\n\n    if (!SetFilePointerEx(file_handle.get(), {}, nullptr, FILE_BEGIN) || !SetEndOfFile(file_handle.get())) {\n      error_message(\"Couldn't clear undo file\");\n      return false;\n    }\n\n    DWORD bytes_written = 0;\n    if (!WriteFile(file_handle.get(), buffer.data(), buffer.size(), &bytes_written, nullptr) || bytes_written != buffer.size()) {\n      error_message(\"Couldn't write undo file\");\n      return false;\n    }\n\n    if (!FlushFileBuffers(file_handle.get())) {\n      error_message(\"Failed to flush undo file\");\n    }\n\n    return true;\n  }\n\n  std::optional<undo_data_t>\n  undo_file_t::read_undo_data() {\n    if (!file_handle) return std::nullopt;\n\n    LARGE_INTEGER file_size;\n    if (!GetFileSizeEx(file_handle.get(), &file_size)) {\n      error_message(\"Couldn't get undo file size\");\n      return std::nullopt;\n    }\n\n    if ((size_t) file_size.QuadPart > 1024) {\n      error_message(\"Undo file size is unexpectedly large, aborting\");\n      return std::nullopt;\n    }\n\n    std::vector<char> buffer(file_size.QuadPart);\n    DWORD bytes_read = 0;\n    if (!ReadFile(file_handle.get(), buffer.data(), buffer.size(), &bytes_read, nullptr) || bytes_read != buffer.size()) {\n      error_message(\"Couldn't read undo file\");\n      return std::nullopt;\n    }\n\n    undo_data_t undo_data;\n    try {\n      undo_data.read(buffer);\n    }\n    catch (...) {\n      error_message(\"Couldn't parse undo file\");\n      return std::nullopt;\n    }\n    return undo_data;\n  }\n\n}  // namespace nvprefs\n"
  },
  {
    "path": "src/platform/windows/nvprefs/undo_file.h",
    "content": "/**\n * @file src/platform/windows/nvprefs/undo_file.h\n * @brief Declarations for the nvidia undo file.\n */\n#pragma once\n\n// standard library headers\n#include <filesystem>\n\n// local includes\n#include \"nvprefs_common.h\"\n#include \"undo_data.h\"\n\nnamespace nvprefs {\n\n  class undo_file_t {\n  public:\n    static std::optional<undo_file_t>\n    open_existing_file(std::filesystem::path file_path, bool &access_denied);\n\n    static std::optional<undo_file_t>\n    create_new_file(std::filesystem::path file_path);\n\n    bool\n    delete_file();\n\n    bool\n    write_undo_data(const undo_data_t &undo_data);\n\n    std::optional<undo_data_t>\n    read_undo_data();\n\n  private:\n    undo_file_t() = default;\n    safe_handle file_handle;\n  };\n\n}  // namespace nvprefs\n"
  },
  {
    "path": "src/platform/windows/publish.cpp",
    "content": "/**\n * @file src/platform/windows/publish.cpp\n * @brief Definitions for Windows mDNS service registration.\n */\n// platform includes\n// winsock2.h must be included before windows.h\n// clang-format off\n#include <winsock2.h>\n#include <windows.h>\n// clang-format on\n#include <windns.h>\n#include <winerror.h>\n\n// local includes\n#include \"misc.h\"\n#include \"src/config.h\"\n#include \"src/logging.h\"\n#include \"src/network.h\"\n#include \"src/nvhttp.h\"\n#include \"src/platform/common.h\"\n#include \"src/thread_safe.h\"\n\n#define _FN(x, ret, args)    \\\n  typedef ret(*x##_fn) args; \\\n  static x##_fn x\n\nusing namespace std::literals;\n\n#define __SV(quote) L##quote##sv\n#define SV(quote) __SV(quote)\n\nextern \"C\" {\n#ifndef __MINGW32__\nconstexpr auto DNS_REQUEST_PENDING = 9506L;\nconstexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1;\nconstexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1;\n#endif\n\n#define SERVICE_DOMAIN \"local\"\n\nconstexpr auto SERVICE_TYPE_DOMAIN = SV(SERVICE_TYPE \".\" SERVICE_DOMAIN);\n\n#ifndef __MINGW32__\ntypedef struct _DNS_SERVICE_INSTANCE {\n  LPWSTR pszInstanceName;\n  LPWSTR pszHostName;\n\n  IP4_ADDRESS *ip4Address;\n  IP6_ADDRESS *ip6Address;\n\n  WORD wPort;\n  WORD wPriority;\n  WORD wWeight;\n\n  // Property list\n  DWORD dwPropertyCount;\n\n  PWSTR *keys;\n  PWSTR *values;\n\n  DWORD dwInterfaceIndex;\n} DNS_SERVICE_INSTANCE, *PDNS_SERVICE_INSTANCE;\n#endif\n\ntypedef VOID WINAPI\nDNS_SERVICE_REGISTER_COMPLETE(\n  _In_ DWORD Status,\n  _In_ PVOID pQueryContext,\n  _In_ PDNS_SERVICE_INSTANCE pInstance);\n\ntypedef DNS_SERVICE_REGISTER_COMPLETE *PDNS_SERVICE_REGISTER_COMPLETE;\n\n#ifndef __MINGW32__\ntypedef struct _DNS_SERVICE_CANCEL {\n  PVOID reserved;\n} DNS_SERVICE_CANCEL, *PDNS_SERVICE_CANCEL;\n\ntypedef struct _DNS_SERVICE_REGISTER_REQUEST {\n  ULONG Version;\n  ULONG InterfaceIndex;\n  PDNS_SERVICE_INSTANCE pServiceInstance;\n  PDNS_SERVICE_REGISTER_COMPLETE pRegisterCompletionCallback;\n  PVOID pQueryContext;\n  HANDLE hCredentials;\n  BOOL unicastEnabled;\n} DNS_SERVICE_REGISTER_REQUEST, *PDNS_SERVICE_REGISTER_REQUEST;\n#endif\n\n_FN(_DnsServiceFreeInstance, VOID, (_In_ PDNS_SERVICE_INSTANCE pInstance));\n_FN(_DnsServiceDeRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel));\n_FN(_DnsServiceRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _Inout_opt_ PDNS_SERVICE_CANCEL pCancel));\n} /* extern \"C\" */\n\nnamespace platf::publish {\n  VOID WINAPI\n  register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) {\n    auto alarm = (safe::alarm_t<PDNS_SERVICE_INSTANCE>::element_type *) pQueryContext;\n\n    if (status) {\n      print_status(\"register_cb()\"sv, status);\n    }\n\n    alarm->ring(pInstance);\n  }\n\n  static int\n  service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) {\n    auto alarm = safe::make_alarm<PDNS_SERVICE_INSTANCE>();\n\n    std::wstring domain { SERVICE_TYPE_DOMAIN.data(), SERVICE_TYPE_DOMAIN.size() };\n\n    auto hostname = platf::get_host_name();\n    auto name = from_utf8(net::mdns_instance_name(hostname) + '.') + domain;\n    auto host = from_utf8(hostname + \".local\");\n\n    DNS_SERVICE_INSTANCE instance {};\n    instance.pszInstanceName = name.data();\n    instance.wPort = net::map_port(nvhttp::PORT_HTTP);\n    instance.pszHostName = host.data();\n\n    // Setting these values ensures Windows mDNS answers comply with RFC 1035.\n    // If these are unset, Windows will send a TXT record that has zero strings,\n    // which is illegal. Setting them to a single empty value causes Windows to\n    // send a single empty string for the TXT record, which is the correct thing\n    // to do when advertising a service without any TXT strings.\n    //\n    // Most clients aren't strictly checking TXT record compliance with RFC 1035,\n    // but Apple's mDNS resolver does and rejects the entire answer if an invalid\n    // TXT record is present.\n    PWCHAR keys[] = { nullptr };\n    PWCHAR values[] = { nullptr };\n    instance.dwPropertyCount = 1;\n    instance.keys = keys;\n    instance.values = values;\n\n    DNS_SERVICE_REGISTER_REQUEST req {};\n    req.Version = DNS_QUERY_REQUEST_VERSION1;\n    req.pQueryContext = alarm.get();\n    req.pServiceInstance = enable ? &instance : existing_instance;\n    req.pRegisterCompletionCallback = register_cb;\n\n    DNS_STATUS status {};\n\n    if (enable) {\n      status = _DnsServiceRegister(&req, nullptr);\n      if (status != DNS_REQUEST_PENDING) {\n        print_status(\"DnsServiceRegister()\"sv, status);\n        return -1;\n      }\n    }\n    else {\n      status = _DnsServiceDeRegister(&req, nullptr);\n      if (status != DNS_REQUEST_PENDING) {\n        print_status(\"DnsServiceDeRegister()\"sv, status);\n        return -1;\n      }\n    }\n\n    alarm->wait();\n\n    auto registered_instance = alarm->status();\n    if (enable) {\n      // Store this instance for later deregistration\n      existing_instance = registered_instance;\n    }\n    else if (registered_instance) {\n      // Deregistration was successful\n      _DnsServiceFreeInstance(registered_instance);\n      existing_instance = nullptr;\n    }\n\n    return registered_instance ? 0 : -1;\n  }\n\n  class mdns_registration_t: public ::platf::deinit_t {\n  public:\n    mdns_registration_t():\n        existing_instance(nullptr) {\n      if (service(true, existing_instance)) {\n        BOOST_LOG(error) << \"Unable to register Sunshine mDNS service\"sv;\n        return;\n      }\n\n      BOOST_LOG(info) << \"Registered Sunshine mDNS service\"sv;\n    }\n\n    ~mdns_registration_t() override {\n      if (existing_instance) {\n        if (service(false, existing_instance)) {\n          BOOST_LOG(error) << \"Unable to unregister Sunshine mDNS service\"sv;\n          return;\n        }\n\n        BOOST_LOG(info) << \"Unregistered Sunshine mDNS service\"sv;\n      }\n    }\n\n  private:\n    PDNS_SERVICE_INSTANCE existing_instance;\n  };\n\n  int\n  load_funcs(HMODULE handle) {\n    auto fg = util::fail_guard([handle]() {\n      FreeLibrary(handle);\n    });\n\n    _DnsServiceFreeInstance = (_DnsServiceFreeInstance_fn) GetProcAddress(handle, \"DnsServiceFreeInstance\");\n    _DnsServiceDeRegister = (_DnsServiceDeRegister_fn) GetProcAddress(handle, \"DnsServiceDeRegister\");\n    _DnsServiceRegister = (_DnsServiceRegister_fn) GetProcAddress(handle, \"DnsServiceRegister\");\n\n    if (!(_DnsServiceFreeInstance && _DnsServiceDeRegister && _DnsServiceRegister)) {\n      BOOST_LOG(error) << \"mDNS service not available in dnsapi.dll\"sv;\n      return -1;\n    }\n\n    fg.disable();\n    return 0;\n  }\n\n  std::unique_ptr<::platf::deinit_t>\n  start() {\n    HMODULE handle = LoadLibrary(\"dnsapi.dll\");\n\n    if (!handle || load_funcs(handle)) {\n      BOOST_LOG(error) << \"Couldn't load dnsapi.dll, You'll need to add PC manually from Moonlight\"sv;\n      return nullptr;\n    }\n\n    return std::make_unique<mdns_registration_t>();\n  }\n}  // namespace platf::publish"
  },
  {
    "path": "src/platform/windows/virtual_mouse.cpp",
    "content": "/**\n * @file src/platform/windows/virtual_mouse.cpp\n * @brief Zako Virtual Mouse client implementation.\n *\n * Finds and opens the Zako Virtual Mouse HID device by matching\n * VID/PID, then sends mouse data via HID output reports.\n */\n#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include <windows.h>\n\n// HID headers\n#include <hidsdi.h>\n#include <hidpi.h>\n#include <setupapi.h>\n\n#include <atomic>\n#include <chrono>\n#include <mutex>\n#include <thread>\n\n#include \"virtual_mouse.h\"\n#include \"src/logging.h\"\n\n// From vmouse_shared.h - duplicated here to avoid driver header dependency\nstatic constexpr uint16_t VMOUSE_VID = 0x1ACE;\nstatic constexpr uint16_t VMOUSE_PID = 0x0002;\nstatic constexpr uint8_t VMOUSE_OUTPUT_REPORT_ID = 0x02;\nstatic constexpr uint16_t VMOUSE_OUTPUT_REPORT_SIZE = 8;\n\nnamespace {\n#ifdef SUNSHINE_VIRTUAL_MOUSE_STANDALONE_TEST\n  struct null_log_t {\n    template<typename T>\n    null_log_t &\n    operator<<(T &&) {\n      return *this;\n    }\n  };\n\n  null_log_t &\n  null_log() {\n    static null_log_t logger;\n    return logger;\n  }\n\n  #define VMOUSE_LOG(severity) null_log()\n#else\n  #define VMOUSE_LOG(severity) BOOST_LOG(severity)\n#endif\n\n  /**\n   * @brief Convert a wide string to a narrow (UTF-8) string for logging.\n   */\n  std::string\n  wide_to_utf8(const wchar_t *wstr) {\n    if (!wstr) return \"\";\n    int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr);\n    if (len <= 0) return \"\";\n    std::string result(len - 1, '\\0');\n    WideCharToMultiByte(CP_UTF8, 0, wstr, -1, result.data(), len, nullptr, nullptr);\n    return result;\n  }\n\n  bool\n  query_caps(HANDLE device, HIDP_CAPS &caps) {\n    PHIDP_PREPARSED_DATA preparsed = nullptr;\n    if (!HidD_GetPreparsedData(device, &preparsed)) {\n      return false;\n    }\n\n    const auto status = HidP_GetCaps(preparsed, &caps);\n    HidD_FreePreparsedData(preparsed);\n    return status == HIDP_STATUS_SUCCESS;\n  }\n\n  bool\n  is_vmouse_device(const HIDD_ATTRIBUTES &attrs, const HIDP_CAPS &caps) {\n    return attrs.VendorID == VMOUSE_VID &&\n           attrs.ProductID == VMOUSE_PID &&\n           caps.FeatureReportByteLength >= VMOUSE_OUTPUT_REPORT_SIZE;\n  }\n}  // namespace\n\nusing namespace std::literals;\n\nnamespace platf {\n  namespace vmouse {\n    namespace detail {\n      output_report_t\n      build_output_report(uint8_t buttons, int16_t delta_x, int16_t delta_y,\n                          int8_t scroll_v, int8_t scroll_h) {\n        return output_report_t {\n          VMOUSE_OUTPUT_REPORT_ID,\n          buttons,\n          static_cast<uint8_t>(delta_x & 0xFF),\n          static_cast<uint8_t>((delta_x >> 8) & 0xFF),\n          static_cast<uint8_t>(delta_y & 0xFF),\n          static_cast<uint8_t>((delta_y >> 8) & 0xFF),\n          static_cast<uint8_t>(scroll_v),\n          static_cast<uint8_t>(scroll_h),\n        };\n      }\n\n      uint8_t\n      apply_button_transition(uint8_t current_buttons, uint8_t button_mask,\n                              bool release) {\n        return release ? static_cast<uint8_t>(current_buttons & ~button_mask) :\n                         static_cast<uint8_t>(current_buttons | button_mask);\n      }\n\n      bool\n      should_close_on_write_error(unsigned long err) {\n        return err == ERROR_DEVICE_NOT_CONNECTED || err == ERROR_GEN_FAILURE;\n      }\n    }  // namespace detail\n\n    // ========================================================================\n    // Implementation Detail\n    // ========================================================================\n\n    // Flush interval for accumulated mouse movement (4ms ≈ 250Hz).\n    // HidD_SetFeature takes ~3ms, so this is the practical minimum.\n    static constexpr auto FLUSH_INTERVAL = std::chrono::milliseconds(4);\n\n    struct device_t::impl_t {\n      HANDLE hDevice = INVALID_HANDLE_VALUE;\n      uint8_t buttonState = 0;  // Current button state (accumulated)\n\n      // Accumulated mouse deltas (written by callers, read by flush thread)\n      std::mutex accum_mutex;\n      int32_t accum_dx = 0;\n      int32_t accum_dy = 0;\n      bool accum_dirty = false;\n\n      // Flush thread\n      std::thread flush_thread;\n      std::atomic<bool> flush_running { false };\n\n      ~impl_t() {\n        stop_flush_thread();\n        close();\n      }\n\n      void\n      stop_flush_thread() {\n        flush_running.store(false, std::memory_order_release);\n        if (flush_thread.joinable()) {\n          flush_thread.join();\n        }\n      }\n\n      void\n      start_flush_thread() {\n        flush_running.store(true, std::memory_order_release);\n        flush_thread = std::thread([this]() {\n          while (flush_running.load(std::memory_order_acquire)) {\n            flush_accumulated();\n            std::this_thread::sleep_for(FLUSH_INTERVAL);\n          }\n        });\n      }\n\n      void\n      flush_accumulated() {\n        int16_t dx, dy;\n        uint8_t buttons;\n        {\n          std::lock_guard<std::mutex> lk(accum_mutex);\n          if (!accum_dirty) return;\n          dx = static_cast<int16_t>(std::clamp(accum_dx, -32767, 32767));\n          dy = static_cast<int16_t>(std::clamp(accum_dy, -32767, 32767));\n          buttons = buttonState;\n          accum_dx = 0;\n          accum_dy = 0;\n          accum_dirty = false;\n        }\n        sendReportDirect(buttons, dx, dy, 0, 0);\n      }\n\n      void\n      close() {\n        if (hDevice != INVALID_HANDLE_VALUE) {\n          CloseHandle(hDevice);\n          hDevice = INVALID_HANDLE_VALUE;\n        }\n      }\n\n      /**\n       * @brief Find and open the virtual mouse HID device.\n       *\n       * Enumerates all HID devices and finds the one matching our VID/PID.\n       * Opens it for writing (to send output reports).\n       */\n      bool\n      open() {\n        GUID hidGuid;\n        HidD_GetHidGuid(&hidGuid);\n\n        HDEVINFO devInfoSet = SetupDiGetClassDevsW(\n            &hidGuid, NULL, NULL,\n            DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);\n\n        if (devInfoSet == INVALID_HANDLE_VALUE) {\n          VMOUSE_LOG(debug) << \"vmouse: SetupDiGetClassDevs failed\"sv;\n          return false;\n        }\n\n        bool found = false;\n        SP_DEVICE_INTERFACE_DATA devInterfaceData;\n        devInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);\n\n        for (DWORD i = 0;\n             SetupDiEnumDeviceInterfaces(devInfoSet, NULL, &hidGuid, i, &devInterfaceData);\n             i++) {\n          // Get required buffer size\n          DWORD requiredSize = 0;\n          SetupDiGetDeviceInterfaceDetailW(devInfoSet, &devInterfaceData, NULL, 0, &requiredSize, NULL);\n          if (requiredSize == 0) continue;\n\n          // Allocate and get detail\n          auto detailBuf = std::make_unique<BYTE[]>(requiredSize);\n          auto *detail = reinterpret_cast<PSP_DEVICE_INTERFACE_DETAIL_DATA_W>(detailBuf.get());\n          detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);\n\n          if (!SetupDiGetDeviceInterfaceDetailW(\n                  devInfoSet, &devInterfaceData, detail, requiredSize, NULL, NULL)) {\n            continue;\n          }\n\n          // Open with no access first so we can inspect attributes even if\n          // the device only allows write access for output reports.\n          HANDLE h = CreateFileW(\n              detail->DevicePath,\n              0,\n              FILE_SHARE_READ | FILE_SHARE_WRITE,\n              NULL,\n              OPEN_EXISTING,\n              0,\n              NULL);\n\n          if (h == INVALID_HANDLE_VALUE) continue;\n\n          // Check if this is our virtual mouse device.\n          HIDD_ATTRIBUTES attrs;\n          attrs.Size = sizeof(HIDD_ATTRIBUTES);\n          if (HidD_GetAttributes(h, &attrs)) {\n            HIDP_CAPS caps;\n            if (query_caps(h, caps) && is_vmouse_device(attrs, caps)) {\n              hDevice = h;\n              found = true;\n\n              VMOUSE_LOG(info) << \"vmouse: Found virtual mouse device at \"sv\n                               << wide_to_utf8(detail->DevicePath);\n              break;\n            }\n          }\n\n          CloseHandle(h);\n        }\n\n        SetupDiDestroyDeviceInfoList(devInfoSet);\n\n        if (!found) {\n          VMOUSE_LOG(debug) << \"vmouse: Virtual mouse device not found (VID=\"sv\n                            << std::hex << VMOUSE_VID\n                            << \" PID=\"sv << VMOUSE_PID << \")\"sv;\n        }\n\n        if (found) {\n          start_flush_thread();\n        }\n\n        return found;\n      }\n\n      /**\n       * @brief Directly send an output report to the virtual mouse driver.\n       * Called from the flush thread or for non-movement reports.\n       */\n      bool\n      sendReportDirect(uint8_t buttons, int16_t dx, int16_t dy, int8_t sv, int8_t sh) {\n        if (hDevice == INVALID_HANDLE_VALUE) return false;\n\n        auto report = detail::build_output_report(buttons, dx, dy, sv, sh);\n\n        BOOL result = HidD_SetFeature(hDevice, (PVOID) report.data(), static_cast<ULONG>(report.size()));\n\n        if (!result) {\n          DWORD err = GetLastError();\n          if (detail::should_close_on_write_error(err)) {\n            VMOUSE_LOG(warning) << \"vmouse: Device disconnected, closing handle\"sv;\n            flush_running.store(false, std::memory_order_release);\n            close();\n          }\n          return false;\n        }\n\n        return true;\n      }\n    };\n\n    // ========================================================================\n    // device_t Methods\n    // ========================================================================\n\n    device_t::device_t(): impl(std::make_unique<impl_t>()) {}\n    device_t::~device_t() = default;\n    device_t::device_t(device_t &&other) noexcept = default;\n    device_t &device_t::operator=(device_t &&other) noexcept = default;\n\n    bool\n    device_t::is_available() const {\n      return impl && impl->hDevice != INVALID_HANDLE_VALUE;\n    }\n\n    bool\n    device_t::move(int16_t delta_x, int16_t delta_y) {\n      // Accumulate deltas; the flush thread will send them periodically\n      std::lock_guard<std::mutex> lk(impl->accum_mutex);\n      impl->accum_dx += delta_x;\n      impl->accum_dy += delta_y;\n      impl->accum_dirty = true;\n      return true;\n    }\n\n    bool\n    device_t::button(uint8_t button_mask, bool release) {\n      impl->buttonState = detail::apply_button_transition(impl->buttonState, button_mask, release);\n      // Flush any pending movement with the new button state\n      impl->flush_accumulated();\n      return impl->sendReportDirect(impl->buttonState, 0, 0, 0, 0);\n    }\n\n    bool\n    device_t::scroll(int8_t distance) {\n      return impl->sendReportDirect(impl->buttonState, 0, 0, distance, 0);\n    }\n\n    bool\n    device_t::hscroll(int8_t distance) {\n      return impl->sendReportDirect(impl->buttonState, 0, 0, 0, distance);\n    }\n\n    bool\n    device_t::send_report(uint8_t buttons, int16_t delta_x, int16_t delta_y,\n                          int8_t scroll_v, int8_t scroll_h) {\n      impl->buttonState = buttons;\n      return impl->sendReportDirect(buttons, delta_x, delta_y, scroll_v, scroll_h);\n    }\n\n    // ========================================================================\n    // Factory Functions\n    // ========================================================================\n\n    device_t\n    create() {\n      device_t dev;\n      if (!dev.impl->open()) {\n        VMOUSE_LOG(info) << \"vmouse: Virtual mouse driver not available, \"\n                            \"falling back to SendInput\"sv;\n      }\n      return dev;\n    }\n\n    bool\n    is_driver_installed() {\n      GUID hidGuid;\n      HidD_GetHidGuid(&hidGuid);\n\n      HDEVINFO devInfoSet = SetupDiGetClassDevsW(\n          &hidGuid, NULL, NULL,\n          DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);\n\n      if (devInfoSet == INVALID_HANDLE_VALUE) return false;\n\n      bool found = false;\n      SP_DEVICE_INTERFACE_DATA devInterfaceData;\n      devInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);\n\n      for (DWORD i = 0;\n           SetupDiEnumDeviceInterfaces(devInfoSet, NULL, &hidGuid, i, &devInterfaceData);\n           i++) {\n        DWORD requiredSize = 0;\n        SetupDiGetDeviceInterfaceDetailW(devInfoSet, &devInterfaceData, NULL, 0, &requiredSize, NULL);\n        if (requiredSize == 0) continue;\n\n        auto detailBuf = std::make_unique<BYTE[]>(requiredSize);\n        auto *detail = reinterpret_cast<PSP_DEVICE_INTERFACE_DETAIL_DATA_W>(detailBuf.get());\n        detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);\n\n        if (!SetupDiGetDeviceInterfaceDetailW(\n                devInfoSet, &devInterfaceData, detail, requiredSize, NULL, NULL)) {\n          continue;\n        }\n\n        HANDLE h = CreateFileW(\n            detail->DevicePath,\n            0,  // No access needed, just check attributes\n            FILE_SHARE_READ | FILE_SHARE_WRITE,\n            NULL,\n            OPEN_EXISTING,\n            0,\n            NULL);\n\n        if (h == INVALID_HANDLE_VALUE) continue;\n\n        HIDD_ATTRIBUTES attrs;\n        attrs.Size = sizeof(HIDD_ATTRIBUTES);\n        if (HidD_GetAttributes(h, &attrs)) {\n          HIDP_CAPS caps;\n          if (query_caps(h, caps) && is_vmouse_device(attrs, caps)) {\n            found = true;\n            CloseHandle(h);\n            break;\n          }\n        }\n\n        CloseHandle(h);\n      }\n\n      SetupDiDestroyDeviceInfoList(devInfoSet);\n      return found;\n    }\n\n  }  // namespace vmouse\n}  // namespace platf\n\n#undef VMOUSE_LOG\n"
  },
  {
    "path": "src/platform/windows/virtual_mouse.h",
    "content": "/**\n * @file src/platform/windows/virtual_mouse.h\n * @brief Zako Virtual Mouse client interface.\n *\n * Communicates with the UMDF virtual mouse driver via standard HID output reports.\n * When the driver is not available, provides a no-op fallback (caller should\n * use SendInput as fallback).\n */\n#pragma once\n\n#include <array>\n#include <cstdint>\n#include <memory>\n#include <string>\n\nnamespace platf {\n  namespace vmouse {\n\n    // Button flags (same as VMOUSE_BUTTON_* in vmouse_shared.h)\n    // Named BTN_* to avoid collision with Windows BUTTON_LEFT/MIDDLE/RIGHT macros\n    constexpr uint8_t BTN_LEFT = 0x01;\n    constexpr uint8_t BTN_RIGHT = 0x02;\n    constexpr uint8_t BTN_MIDDLE = 0x04;\n    constexpr uint8_t BTN_SIDE = 0x08;   // X1 / Back\n    constexpr uint8_t BTN_EXTRA = 0x10;  // X2 / Forward\n\n    namespace detail {\n      using output_report_t = std::array<uint8_t, 8>;\n\n      output_report_t\n      build_output_report(uint8_t buttons, int16_t delta_x, int16_t delta_y,\n                          int8_t scroll_v, int8_t scroll_h);\n\n      uint8_t\n      apply_button_transition(uint8_t current_buttons, uint8_t button_mask,\n                              bool release);\n\n      bool\n      should_close_on_write_error(unsigned long err);\n    }  // namespace detail\n\n    /**\n     * @brief Virtual mouse device handle.\n     *\n     * Manages the HID device connection and provides methods for sending\n     * mouse input through the UMDF virtual mouse driver.\n     */\n    class device_t {\n    public:\n      device_t();\n      ~device_t();\n\n      // Non-copyable\n      device_t(const device_t &) = delete;\n      device_t &operator=(const device_t &) = delete;\n\n      // Movable\n      device_t(device_t &&other) noexcept;\n      device_t &operator=(device_t &&other) noexcept;\n\n      /**\n       * @brief Check if the virtual mouse driver is connected.\n       * @return true if the HID device is open and ready.\n       */\n      bool\n      is_available() const;\n\n      /**\n       * @brief Send a relative mouse movement.\n       * @param delta_x X movement (-32767 to 32767)\n       * @param delta_y Y movement (-32767 to 32767)\n       * @return true on success.\n       */\n      bool\n      move(int16_t delta_x, int16_t delta_y);\n\n      /**\n       * @brief Press or release a mouse button.\n       * @param button_mask BUTTON_* flags for the button.\n       * @param release true to release, false to press.\n       * @return true on success.\n       */\n      bool\n      button(uint8_t button_mask, bool release);\n\n      /**\n       * @brief Send vertical scroll.\n       * @param distance Scroll distance (-127 to 127, positive = up).\n       * @return true on success.\n       */\n      bool\n      scroll(int8_t distance);\n\n      /**\n       * @brief Send horizontal scroll.\n       * @param distance Scroll distance (-127 to 127, positive = right).\n       * @return true on success.\n       */\n      bool\n      hscroll(int8_t distance);\n\n      /**\n       * @brief Send a combined mouse report (movement + buttons + scroll).\n       *\n       * More efficient than calling move/button/scroll separately when\n       * multiple changes happen simultaneously.\n       *\n       * @param buttons Current button state (BUTTON_* flags OR'd together)\n       * @param delta_x X movement\n       * @param delta_y Y movement\n       * @param scroll_v Vertical scroll\n       * @param scroll_h Horizontal scroll\n       * @return true on success.\n       */\n      bool\n      send_report(uint8_t buttons, int16_t delta_x, int16_t delta_y,\n                  int8_t scroll_v, int8_t scroll_h);\n\n    private:\n      struct impl_t;\n      std::unique_ptr<impl_t> impl;\n\n      friend device_t create();\n    };\n\n    /**\n     * @brief Try to create and connect to the virtual mouse driver.\n     * @return A device_t instance. Check is_available() to see if connection succeeded.\n     */\n    device_t\n    create();\n\n    /**\n     * @brief Check if the virtual mouse driver is installed on the system.\n     * @return true if the driver device is found.\n     */\n    bool\n    is_driver_installed();\n\n  }  // namespace vmouse\n}  // namespace platf\n"
  },
  {
    "path": "src/platform/windows/win_dark_mode.cpp",
    "content": "/**\n * @file src/platform/windows/win_dark_mode.cpp\n * @brief Implementation of Windows dark mode support\n */\n\n#ifdef _WIN32\n\n#include \"win_dark_mode.h\"\n\n#include <dwmapi.h>\n#include <mutex>\n\nnamespace win_dark_mode {\n\n  // Undocumented PreferredAppMode enum from uxtheme.dll\n  enum class PreferredAppMode {\n    Default = 0,     // Use system default (usually light)\n    AllowDark = 1,   // Allow dark mode (follow system setting)\n    ForceDark = 2,   // Force dark mode regardless of system setting\n    ForceLight = 3,  // Force light mode regardless of system setting\n    Max = 4,\n  };\n\n  // Function pointer types for undocumented uxtheme.dll APIs\n  // Windows 10 1903+ uses SetPreferredAppMode (ordinal 135)\n  // Windows 10 1809-1903 uses AllowDarkModeForApp (ordinal 135) - same ordinal, different signature\n  // We use SetPreferredAppMode as it works on all modern Windows versions\n  using SetPreferredAppModeFn = PreferredAppMode(WINAPI *)(PreferredAppMode);\n\n  // Global function pointer (initialized once)\n  static SetPreferredAppModeFn g_SetPreferredAppMode = nullptr;\n  static std::once_flag g_init_flag;\n\n  // Static holder for uxtheme.dll handle\n  // Design decision: We intentionally keep this handle loaded for the lifetime of the process\n  // because the function pointers (g_SetPreferredAppMode) must remain valid. The library is\n  // only loaded once via std::call_once, so there's no risk of multiple loads. The OS will\n  // automatically clean up when the process exits.\n  static HMODULE g_hUxTheme = nullptr;\n\n  /**\n   * @brief Initialize the dark mode API function pointers\n   *\n   * This function loads uxtheme.dll and retrieves the undocumented function pointers.\n   * It only runs once and caches the results using std::call_once for thread safety.\n   */\n  static void\n  init_dark_mode_apis() {\n    // Load uxtheme.dll\n    g_hUxTheme = LoadLibraryW(L\"uxtheme.dll\");\n    if (!g_hUxTheme) {\n      return;\n    }\n\n    // Get SetPreferredAppMode (ordinal 135)\n    // This works on Windows 10 1809+ (build 17763+)\n    // On older versions, the function will exist but may not have the expected effect\n    g_SetPreferredAppMode =\n      reinterpret_cast<SetPreferredAppModeFn>(\n        GetProcAddress(g_hUxTheme, MAKEINTRESOURCEA(135)));\n  }\n\n  void\n  enable_process_dark_mode() {\n    // Thread-safe one-time initialization\n    std::call_once(g_init_flag, init_dark_mode_apis);\n\n    // Call the function if available\n    if (g_SetPreferredAppMode) {\n      // Use AllowDark to follow the system's dark/light mode preference\n      g_SetPreferredAppMode(PreferredAppMode::AllowDark);\n    }\n    // If API is not available, dark mode is not supported on this Windows version\n    // (Windows 10 < 1809 or earlier). We silently do nothing in this case.\n  }\n\n}  // namespace win_dark_mode\n\n#endif  // _WIN32\n"
  },
  {
    "path": "src/platform/windows/win_dark_mode.h",
    "content": "/**\n * @file src/platform/windows/win_dark_mode.h\n * @brief Windows dark mode support for the entire process\n *\n * This module provides process-wide dark mode support for Windows 10 1809+ and Windows 11.\n * It handles the undocumented Windows APIs for enabling dark mode for menus, dialogs, and windows.\n *\n * Note: This should be called early in program initialization, before creating any windows or menus.\n */\n\n#pragma once\n\n#ifdef _WIN32\n\n#include <windows.h>\n\nnamespace win_dark_mode {\n\n  /**\n   * @brief Enable dark mode support for the entire process\n   *\n   * This function enables dark mode support by calling SetPreferredAppMode (or AllowDarkModeForApp\n   * on older Windows versions). It should be called once during application initialization,\n   * before creating any windows or system tray icons.\n   *\n   * The function will:\n   * - Load the necessary APIs from uxtheme.dll\n   * - Set the process to allow dark mode (follows system setting)\n   * - This affects all menus, dialogs, and windows created after this call\n   */\n  void enable_process_dark_mode();\n\n}  // namespace win_dark_mode\n\n#endif  // _WIN32\n"
  },
  {
    "path": "src/platform/windows/windows.rc.in",
    "content": "/**\n * @file src/platform/windows/windows.rc.in\n * @brief Windows resource file template.\n * @note The final `windows.rc` is generated from this file during the CMake build.\n * @todo Use CMake definitions directly, instead of configuring this file.\n */\n#include \"winver.h\"\nVS_VERSION_INFO VERSIONINFO\nFILEVERSION     @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0\nPRODUCTVERSION  @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0\nFILEOS          VOS__WINDOWS32\nFILETYPE        VFT_APP\nFILESUBTYPE     VFT2_UNKNOWN\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904E4\"\n        BEGIN\n            VALUE \"CompanyName\",      \"LizardByte\\0\"\n            VALUE \"FileDescription\",  \"Sunshine\\0\"\n            VALUE \"FileVersion\",      \"@PROJECT_VERSION@\\0\"\n            VALUE \"InternalName\",     \"Sunshine\\0\"\n            VALUE \"LegalCopyright\",   \"https://raw.githubusercontent.com/LizardByte/Sunshine/master/LICENSE\\0\"\n            VALUE \"ProductName\",      \"Sunshine\\0\"\n            VALUE \"ProductVersion\",   \"@PROJECT_VERSION@\\0\"\n        END\n    END\n\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        /* The following line should only be modified for localized versions.     */\n        /* It consists of any number of WORD,WORD pairs, with each pair           */\n        /* describing a language,codepage combination supported by the file.      */\n        /*                                                                        */\n        /* For example, a file might have values \"0x409,1252\" indicating that it  */\n        /* supports English language (0x409) in the Windows ANSI codepage (1252). */\n\n        VALUE \"Translation\", 0x409, 1252\n\n    END\nEND\nSuperDuperAmazing   ICON    DISCARDABLE    \"@SUNSHINE_ICON_PATH@\"\n"
  },
  {
    "path": "src/process.cpp",
    "content": "/**\n * @file src/process.cpp\n * @brief Definitions for the startup and shutdown of the apps started by a streaming Session.\n */\n#define BOOST_BIND_GLOBAL_PLACEHOLDERS\n\n#include \"process.h\"\n\n#include <algorithm>\n#include <filesystem>\n#include <string>\n#include <thread>\n#include <vector>\n\n#include <boost/algorithm/string.hpp>\n#include <boost/crc.hpp>\n#include <boost/filesystem.hpp>\n#include <boost/program_options/parsers.hpp>\n#include <boost/property_tree/json_parser.hpp>\n#include <boost/property_tree/ptree.hpp>\n#include <boost/token_functions.hpp>\n#include <openssl/evp.h>\n#include <openssl/sha.h>\n\n#include \"config.h\"\n#include \"crypto.h\"\n#include \"display_device/session.h\"\n#include \"httpcommon.h\"\n#include \"logging.h\"\n#include \"platform/common.h\"\n#include \"platform/run_command.h\"\n#include \"system_tray.h\"\n#include \"utility.h\"\n\n#ifdef _WIN32\n  // from_utf8() string conversion function\n  #include \"platform/windows/misc.h\"\n\n  // _SH constants for _wfsopen()\n  #include <share.h>\n\n  #define STB_IMAGE_IMPLEMENTATION\n  #include \"stb_image.h\"\n  #define STB_IMAGE_WRITE_IMPLEMENTATION\n  #include \"stb_image_write.h\"\n#endif\n\n#define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR \"/box.png\"\n\nnamespace proc {\n  using namespace std::literals;\n  namespace pt = boost::property_tree;\n\n  uint32_t calculate_crc32(const std::string &input);\n\n  proc_t proc;\n\n  class deinit_t: public platf::deinit_t {\n  public:\n    ~deinit_t() {\n      proc.terminate();\n    }\n  };\n\n  std::unique_ptr<platf::deinit_t>\n  init() {\n    return std::make_unique<deinit_t>();\n  }\n\n  void\n  terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout) {\n    if (group.valid() && platf::process_group_running((std::uintptr_t) group.native_handle())) {\n      if (exit_timeout.count() > 0) {\n        // Request processes in the group to exit gracefully\n        if (platf::request_process_group_exit((std::uintptr_t) group.native_handle())) {\n          // If the request was successful, wait for a little while for them to exit.\n          BOOST_LOG(info) << \"Successfully requested the app to exit. Waiting up to \"sv << exit_timeout.count() << \" seconds for it to close.\"sv;\n\n          // group::wait_for() and similar functions are broken and deprecated, so we use a simple polling loop\n          while (platf::process_group_running((std::uintptr_t) group.native_handle()) && (--exit_timeout).count() >= 0) {\n            std::this_thread::sleep_for(1s);\n          }\n\n          if (exit_timeout.count() < 0) {\n            BOOST_LOG(warning) << \"App did not fully exit within the timeout. Terminating the app's remaining processes.\"sv;\n          }\n          else {\n            BOOST_LOG(info) << \"All app processes have successfully exited.\"sv;\n          }\n        }\n        else {\n          BOOST_LOG(info) << \"App did not respond to a graceful termination request. Forcefully terminating the app's processes.\"sv;\n        }\n      }\n      else {\n        BOOST_LOG(info) << \"No graceful exit timeout was specified for this app. Forcefully terminating the app's processes.\"sv;\n      }\n\n      // We always call terminate() even if we waited successfully for all processes above.\n      // This ensures the process group state is consistent with the OS in boost.\n      std::error_code ec;\n      group.terminate(ec);\n      group.detach();\n    }\n\n    if (proc.valid()) {\n      // avoid zombie process\n      proc.detach();\n    }\n  }\n\n  boost::filesystem::path\n  find_working_directory(const std::string &cmd, boost::process::v1::environment &env) {\n    // Parse the raw command string into parts to get the actual command portion\n    std::vector<std::string> parts;\n    try {\n#ifdef _WIN32\n      parts = boost::program_options::split_winmain(cmd);\n#else\n      parts = boost::program_options::split_unix(cmd);\n#endif\n    } catch (boost::escaped_list_error &err) {\n      BOOST_LOG(error) << \"Boost failed to parse command [\"sv << cmd << \"] because \" << err.what();\n      return boost::filesystem::path();\n    }\n    if (parts.empty()) {\n      BOOST_LOG(error) << \"Unable to parse command: \"sv << cmd;\n      return boost::filesystem::path();\n    }\n\n    BOOST_LOG(debug) << \"Parsed target [\"sv << parts.at(0) << \"] from command [\"sv << cmd << ']';\n\n    // If the target is a URL, don't parse any further here\n    if (parts.at(0).find(\"://\") != std::string::npos) {\n      return boost::filesystem::path();\n    }\n\n    // If the cmd path is not an absolute path, resolve it using our PATH variable\n    boost::filesystem::path cmd_path(parts.at(0));\n    if (!cmd_path.is_absolute()) {\n      cmd_path = boost::process::v1::search_path(parts.at(0));\n      if (cmd_path.empty()) {\n        BOOST_LOG(error) << \"Unable to find executable [\"sv << parts.at(0) << \"]. Is it in your PATH?\"sv;\n        return boost::filesystem::path();\n      }\n    }\n\n    BOOST_LOG(debug) << \"Resolved target [\"sv << parts.at(0) << \"] to path [\"sv << cmd_path << ']';\n\n    // Now that we have a complete path, we can just use parent_path()\n    return cmd_path.parent_path();\n  }\n\n  int\n  proc_t::execute(int app_id, std::shared_ptr<rtsp_stream::launch_session_t> launch_session) {\n    // Ensure starting from a clean slate\n    terminate();\n\n    auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) {\n      return app.id == std::to_string(app_id);\n    });\n\n    if (iter == _apps.end()) {\n      BOOST_LOG(error) << \"Couldn't find app with ID [\"sv << app_id << ']';\n      return 404;\n    }\n\n    _app_id = app_id;\n    _app = *iter;\n    _app_prep_begin = std::begin(_app.prep_cmds);\n    _app_prep_it = _app_prep_begin;\n\n    // Apply per-app mouse mode\n    platf::set_mouse_mode(_app.mouse_mode);\n\n    // Add Stream-specific environment variables\n    // These variables are dynamically set for each streaming session and will be passed\n    // to the launched application. They should be preserved during refresh() if an app is running.\n    // Note: Some variables like SUNSHINE_CLIENT_ID, SUNSHINE_CLIENT_UNIQUE_ID, SUNSHINE_CLIENT_USE_VDD,\n    // and SUNSHINE_CLIENT_CERT_UUID are set in launch_session->env (in nvhttp.cpp) but not here,\n    // as they are used for different purposes (e.g., display device session management).\n    _env[\"SUNSHINE_APP_ID\"] = std::to_string(_app_id);\n    _env[\"SUNSHINE_APP_NAME\"] = _app.name;\n    _env[\"SUNSHINE_CLIENT_NAME\"] = launch_session->client_name;\n    _env[\"SUNSHINE_CLIENT_WIDTH\"] = std::to_string(launch_session->width);\n    _env[\"SUNSHINE_CLIENT_HEIGHT\"] = std::to_string(launch_session->height);\n    _env[\"SUNSHINE_CLIENT_FPS\"] = std::to_string(launch_session->fps);\n    _env[\"SUNSHINE_CLIENT_HDR\"] = launch_session->enable_hdr ? \"true\" : \"false\";\n    _env[\"SUNSHINE_CLIENT_GCMAP\"] = std::to_string(launch_session->gcmap);\n    _env[\"SUNSHINE_CLIENT_HOST_AUDIO\"] = launch_session->host_audio ? \"true\" : \"false\";\n    _env[\"SUNSHINE_CLIENT_ENABLE_SOPS\"] = launch_session->enable_sops ? \"true\" : \"false\";\n    _env[\"SUNSHINE_CLIENT_ENABLE_MIC\"] = launch_session->enable_mic ? \"true\" : \"false\";\n    _env[\"SUNSHINE_CLIENT_CUSTOM_SCREEN_MODE\"] = std::to_string(launch_session->custom_screen_mode);\n    int channelCount = launch_session->surround_info & (65535);\n    switch (channelCount) {\n      case 2:\n        _env[\"SUNSHINE_CLIENT_AUDIO_CONFIGURATION\"] = \"2.0\";\n        break;\n      case 6:\n        _env[\"SUNSHINE_CLIENT_AUDIO_CONFIGURATION\"] = \"5.1\";\n        break;\n      case 8:\n        _env[\"SUNSHINE_CLIENT_AUDIO_CONFIGURATION\"] = \"7.1\";\n        break;\n      case 12:\n        _env[\"SUNSHINE_CLIENT_AUDIO_CONFIGURATION\"] = \"7.1.4\";\n        break;\n    }\n    _env[\"SUNSHINE_CLIENT_AUDIO_SURROUND_PARAMS\"] = launch_session->surround_params;\n\n    if (!_app.output.empty() && _app.output != \"null\"sv) {\n#ifdef _WIN32\n      // fopen() interprets the filename as an ANSI string on Windows, so we must convert it\n      // to UTF-16 and use the wchar_t variants for proper Unicode log file path support.\n      auto woutput = platf::from_utf8(_app.output);\n\n      // Use _SH_DENYNO to allow us to open this log file again for writing even if it is\n      // still open from a previous execution. This is required to handle the case of a\n      // detached process executing again while the previous process is still running.\n      _pipe.reset(_wfsopen(woutput.c_str(), L\"a\", _SH_DENYNO));\n#else\n      _pipe.reset(fopen(_app.output.c_str(), \"a\"));\n#endif\n    }\n\n    std::error_code ec;\n    // Executed when returning from function\n    auto fg = util::fail_guard([&]() {\n      terminate();\n    });\n\n    for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) {\n      auto &cmd = *_app_prep_it;\n\n      // Skip empty commands\n      if (cmd.do_cmd.empty()) {\n        continue;\n      }\n\n      boost::filesystem::path working_dir = _app.working_dir.empty() ?\n                                              find_working_directory(cmd.do_cmd, _env) :\n                                              boost::filesystem::path(_app.working_dir);\n      BOOST_LOG(info) << \"Executing Do Cmd: [\"sv << cmd.do_cmd << ']';\n      auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, _env, _pipe.get(), ec, nullptr);\n\n      if (ec) {\n        BOOST_LOG(error) << \"Couldn't run [\"sv << cmd.do_cmd << \"]: System: \"sv << ec.message();\n        // We don't want any prep commands failing launch of the desktop.\n        // This is to prevent the issue where users reboot their PC and need to log in with Sunshine.\n        // permission_denied is typically returned when the user impersonation fails, which can happen when user is not signed in yet.\n        if (!(_app.cmd.empty() && ec == std::errc::permission_denied)) {\n          return -1;\n        }\n      }\n\n      child.wait(ec);\n      if (ec) {\n        BOOST_LOG(error) << '[' << cmd.do_cmd << \"] wait failed with error code [\"sv << ec << ']';\n        return -1;\n      }\n      auto ret = child.exit_code();\n      if (ret != 0) {\n        BOOST_LOG(error) << '[' << cmd.do_cmd << \"] exited with code [\"sv << ret << ']';\n        return -1;\n      }\n    }\n\n    for (auto &cmd : _app.detached) {\n      boost::filesystem::path working_dir = _app.working_dir.empty() ?\n                                              find_working_directory(cmd, _env) :\n                                              boost::filesystem::path(_app.working_dir);\n      BOOST_LOG(info) << \"Spawning [\"sv << cmd << \"] in [\"sv << working_dir << ']';\n      auto child = platf::run_command(_app.elevated, true, cmd, working_dir, _env, _pipe.get(), ec, nullptr);\n      if (ec) {\n        BOOST_LOG(warning) << \"Couldn't spawn [\"sv << cmd << \"]: System: \"sv << ec.message();\n      }\n      else {\n        child.detach();\n      }\n    }\n\n    if (_app.cmd.empty()) {\n      BOOST_LOG(info) << \"Executing [Desktop]\"sv;\n      placebo = true;\n    }\n    else {\n      boost::filesystem::path working_dir = _app.working_dir.empty() ?\n                                              find_working_directory(_app.cmd, _env) :\n                                              boost::filesystem::path(_app.working_dir);\n      BOOST_LOG(info) << \"Executing: [\"sv << _app.cmd << \"] in [\"sv << working_dir << ']';\n      _process = platf::run_command(_app.elevated, true, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_group);\n      if (ec) {\n        BOOST_LOG(warning) << \"Couldn't run [\"sv << _app.cmd << \"]: System: \"sv << ec.message();\n        return -1;\n      }\n    }\n\n    _app_launch_time = std::chrono::steady_clock::now();\n\n    fg.disable();\n\n    return 0;\n  }\n\n  int\n  proc_t::running() {\n#ifndef _WIN32\n    // On POSIX OSes, we must periodically wait for our children to avoid\n    // them becoming zombies. This must be synchronized carefully with\n    // calls to bp::wait() and platf::process_group_running() which both\n    // invoke waitpid() under the hood.\n    auto reaper = util::fail_guard([]() {\n      while (waitpid(-1, nullptr, WNOHANG) > 0);\n    });\n#endif\n\n    if (placebo) {\n      return _app_id;\n    }\n    else if (_app.wait_all && _process_group && platf::process_group_running((std::uintptr_t) _process_group.native_handle())) {\n      // The app is still running if any process in the group is still running\n      return _app_id;\n    }\n    else if (_process.running()) {\n      // The app is still running only if the initial process launched is still running\n      return _app_id;\n    }\n    else if (_app.auto_detach && _process.native_exit_code() == 0 &&\n             std::chrono::steady_clock::now() - _app_launch_time < 5s) {\n      BOOST_LOG(info) << \"App exited gracefully within 5 seconds of launch. Treating the app as a detached command.\"sv;\n      BOOST_LOG(info) << \"Adjust this behavior in the Applications tab or apps.json if this is not what you want.\"sv;\n      placebo = true;\n      return _app_id;\n    }\n\n    // Perform cleanup actions now if needed\n    if (_process) {\n      BOOST_LOG(info) << \"App exited with code [\"sv << _process.native_exit_code() << ']';\n      terminate();\n    }\n\n    return 0;\n  }\n\n  void\n  proc_t::terminate() {\n    std::error_code ec;\n    placebo = false;\n    terminate_process_group(_process, _process_group, _app.exit_timeout);\n    _process = boost::process::v1::child();\n    _process_group = boost::process::v1::group();\n\n    for (; _app_prep_it != _app_prep_begin; --_app_prep_it) {\n      auto &cmd = *(_app_prep_it - 1);\n\n      if (cmd.undo_cmd.empty()) {\n        continue;\n      }\n\n      boost::filesystem::path working_dir = _app.working_dir.empty() ?\n                                              find_working_directory(cmd.undo_cmd, _env) :\n                                              boost::filesystem::path(_app.working_dir);\n      BOOST_LOG(info) << \"Executing Undo Cmd: [\"sv << cmd.undo_cmd << ']';\n      auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, _env, _pipe.get(), ec, nullptr);\n\n      if (ec) {\n        BOOST_LOG(warning) << \"System: \"sv << ec.message();\n      }\n\n      child.wait();\n      auto ret = child.exit_code();\n\n      if (ret != 0) {\n        BOOST_LOG(warning) << \"Return code [\"sv << ret << ']';\n      }\n    }\n\n    _pipe.reset();\n    bool has_run = _app_id > 0;\n    // Only show the Stopped notification if we actually have an app to stop\n    // Since terminate() is always run when a new app has started\n    if (proc::proc.get_last_run_app_name().length() > 0 && has_run) {\n#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1\n      system_tray::update_tray_stopped(proc::proc.get_last_run_app_name());\n#endif\n\n      // Same applies when restoring display state\n      // display_device::session_t::get().restore_state();\n    }\n\n    _app_id = -1;\n\n    // Reset mouse mode to auto when app terminates\n    platf::set_mouse_mode(0);\n  }\n\n  const std::vector<ctx_t> &\n  proc_t::get_apps() const {\n    return _apps;\n  }\n  std::vector<ctx_t> &\n  proc_t::get_apps() {\n    return _apps;\n  }\n  void\n  proc_t::set_apps(std::vector<ctx_t> apps) {\n    // Calculate uniqueEtag for the app list before moving apps\n    std::string combined_info;\n    for (const auto &app : apps) {\n      combined_info += app.id + app.name;\n    }\n    \n    // Use CRC32 for the tag, same as used elsewhere\n    auto crc = calculate_crc32(combined_info);\n    _apps_etag = std::to_string(crc);\n\n    _apps = std::move(apps);\n  }\n\n  std::string\n  proc_t::get_apps_etag() const {\n    return _apps_etag;\n  }\n\n  const boost::process::v1::environment &\n  proc_t::get_env() const {\n    return _env;\n  }\n  boost::process::v1::environment &\n  proc_t::get_env() {\n    return _env;\n  }\n  void\n  proc_t::set_env(boost::process::v1::environment env) {\n    _env = std::move(env);\n  }\n\n  // Gets application image from application list.\n  // Returns image from assets directory if found there.\n  // Returns default image if image configuration is not set.\n  // Returns http content-type header compatible image type.\n  std::string\n  proc_t::get_app_image(int app_id) {\n    auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) {\n      return app.id == std::to_string(app_id);\n    });\n    auto app_image_path = iter == _apps.end() ? std::string() : iter->image_path;\n\n    return validate_app_image_path(app_image_path);\n  }\n\n  std::string\n  proc_t::get_app_name(int app_id) {\n    auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) {\n      return app.id == std::to_string(app_id);\n    });\n    return iter == _apps.end() ? std::string() : iter->name;\n  }\n\n  std::string\n  proc_t::get_app_cmd(int app_id) {\n    auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) {\n      return app.id == std::to_string(app_id);\n    });\n    return iter == _apps.end() ? std::string() : iter->cmd;\n  }\n\n  std::string\n  proc_t::get_last_run_app_name() {\n    return _app.name;\n  }\n\n  void\n  proc_t::run_menu_cmd(std::string cmd_id) {\n    auto iter = std::find_if(_app.menu_cmds.begin(), _app.menu_cmds.end(), [&cmd_id](const auto menu_cmd) {\n      return menu_cmd.id == cmd_id;\n    });\n\n    if (iter != _app.menu_cmds.end()) {\n      auto cmd = iter->do_cmd;\n      std::error_code ec;\n\n      boost::filesystem::path working_dir = _app.working_dir.empty() ?\n                                              find_working_directory(cmd, _env) :\n                                              boost::filesystem::path(_app.working_dir);\n      auto child = platf::run_command(iter->elevated, true, cmd, working_dir, _env, nullptr, ec, nullptr);\n      if (ec) {\n        BOOST_LOG(warning) << \"Couldn't run cmd [\"sv << cmd << \"]: System: \"sv << ec.message();\n      }\n      else {\n        BOOST_LOG(info) << \"Executing cmd [\"sv << cmd << \"]\"sv;\n        child.detach();\n      }\n    } else {\n      BOOST_LOG(warning) << \"Couldn't find cmd [\"sv << cmd_id << \"]\"sv;\n      for (auto &cmd : _app.menu_cmds) {\n        BOOST_LOG(warning) << \"Menu cmd: [\"sv << cmd.id << \"]\"sv;\n      }\n    }\n  }\n\n  proc_t::~proc_t() {\n    // It's not safe to call terminate() here because our proc_t is a static variable\n    // that may be destroyed after the Boost loggers have been destroyed. Instead,\n    // we return a deinit_t to main() to handle termination when we're exiting.\n    // Once we reach this point here, termination must have already happened.\n    assert(!placebo);\n    assert(!_process.running());\n  }\n\n  std::string_view::iterator\n  find_match(std::string_view::iterator begin, std::string_view::iterator end) {\n    int stack = 0;\n\n    --begin;\n    do {\n      ++begin;\n      switch (*begin) {\n        case '(':\n          ++stack;\n          break;\n        case ')':\n          --stack;\n      }\n    } while (begin != end && stack != 0);\n\n    if (begin == end) {\n      throw std::out_of_range(\"Missing closing bracket \\')\\'\");\n    }\n    return begin;\n  }\n\n  std::string\n  parse_env_val(boost::process::v1::native_environment &env, const std::string_view &val_raw) {\n    auto pos = std::begin(val_raw);\n    auto dollar = std::find(pos, std::end(val_raw), '$');\n\n    std::stringstream ss;\n\n    while (dollar != std::end(val_raw)) {\n      auto next = dollar + 1;\n      if (next != std::end(val_raw)) {\n        switch (*next) {\n          case '(': {\n            ss.write(pos, (dollar - pos));\n            auto var_begin = next + 1;\n            auto var_end = find_match(next, std::end(val_raw));\n            auto var_name = std::string { var_begin, var_end };\n\n#ifdef _WIN32\n            // Windows treats environment variable names in a case-insensitive manner,\n            // so we look for a case-insensitive match here. This is critical for\n            // correctly appending to PATH on Windows.\n            auto itr = std::find_if(env.cbegin(), env.cend(),\n              [&](const auto &e) { return boost::iequals(e.get_name(), var_name); });\n            if (itr != env.cend()) {\n              // Use an existing case-insensitive match\n              var_name = itr->get_name();\n            }\n#endif\n\n            ss << env[var_name].to_string();\n\n            pos = var_end + 1;\n            next = var_end;\n\n            break;\n          }\n          case '$':\n            ss.write(pos, (next - pos));\n            pos = next + 1;\n            ++next;\n            break;\n        }\n\n        dollar = std::find(next, std::end(val_raw), '$');\n      }\n      else {\n        dollar = next;\n      }\n    }\n\n    ss.write(pos, (dollar - pos));\n\n    return ss.str();\n  }\n\n  std::string\n  validate_app_image_path(std::string app_image_path) {\n    if (app_image_path.empty()) {\n      return DEFAULT_APP_IMAGE_PATH;\n    }\n\n    // 处理网络图片下载\n    if (app_image_path.find(\"http://\") == 0 || app_image_path.find(\"https://\") == 0) {\n      try {\n        std::string original_url = app_image_path;\n        \n        // 移除查询参数\n        size_t query_start = app_image_path.find('?');\n        if (query_start != std::string::npos) {\n          app_image_path = app_image_path.substr(0, query_start);\n        }\n\n        // 从URL提取文件名\n        auto hash = std::hash<std::string>{}(original_url);\n        auto ext = std::filesystem::path(app_image_path).extension().string();\n        \n        // 安全检查：验证文件扩展名\n        std::string ext_lower = ext;\n        std::transform(ext_lower.begin(), ext_lower.end(), ext_lower.begin(), ::tolower);\n        if (ext_lower != \".png\" && ext_lower != \".jpg\" && ext_lower != \".jpeg\" && \n            ext_lower != \".bmp\" && ext_lower != \".webp\" && ext_lower != \".ico\") {\n          BOOST_LOG(warning) << \"Blocked download of non-image extension: \" << ext;\n          return DEFAULT_APP_IMAGE_PATH; \n        }\n\n        std::string filename = \"url_\" + std::to_string(hash) + (ext.empty() ? \".png\" : ext);\n        \n        // 保存到本地 covers 目录 (User requested to save to covers instead of assets)\n        auto local_path = std::filesystem::path(platf::appdata().string()) / \"covers\" / filename;\n        \n        // 如果文件不存在则下载\n        if (!std::filesystem::exists(local_path)) {\n          BOOST_LOG(info) << \"Downloading image from URL: \" << original_url;\n          // 使用流式校验下载，如果Magic Byte不匹配会直接中断下载\n          if (!http::download_image_with_magic_check(original_url, local_path.string())) {\n            BOOST_LOG(warning) << \"Failed to download image (or rejected by magic check) from URL: \" << original_url;\n            return DEFAULT_APP_IMAGE_PATH;\n          }\n        }\n        \n        app_image_path = local_path.string();\n      } catch (const std::exception& e) {\n        BOOST_LOG(warning) << \"Error processing image URL: \" << e.what();\n        return DEFAULT_APP_IMAGE_PATH;\n      }\n    }\n\n    // 特殊处理：桌面壁纸\n    if (app_image_path == \"desktop\") {\n#ifdef _WIN32\n      wchar_t wallpaperPathW[MAX_PATH];\n      SystemParametersInfoW(SPI_GETDESKWALLPAPER, MAX_PATH, wallpaperPathW, 0);\n      auto wallpaperPath = platf::to_utf8(std::wstring(wallpaperPathW));\n      BOOST_LOG(info) << \"Use desktop image [\"sv << wallpaperPath << ']';\n      return wallpaperPath;\n#else\n      BOOST_LOG(warning) << \"Desktop wallpaper path not supported on this platform\";\n      return DEFAULT_APP_IMAGE_PATH;\n#endif\n    }\n\n    // 检查图像扩展名是否支持\n    auto image_extension = std::filesystem::path(app_image_path).extension().string();\n    boost::to_lower(image_extension);\n    if (image_extension != \".png\" && image_extension != \".jpg\" && image_extension != \".jpeg\") {\n      BOOST_LOG(warning) << \"Unsupported image extension: \" << image_extension;\n      return DEFAULT_APP_IMAGE_PATH;\n    }\n\n    // 检查各种可能的图像路径\n    std::vector<std::string> paths_to_check = {\n      // 1. 检查assets目录中的相对路径\n      (std::filesystem::path(SUNSHINE_ASSETS_DIR) / app_image_path).string(),\n      // 2. 检查covers目录中的相对路径\n      (std::filesystem::path(platf::appdata().string()) / \"covers\" / app_image_path).string(),\n      // 2. 处理旧的steam默认图像定义\n      app_image_path == \"./assets/steam.png\" ? SUNSHINE_ASSETS_DIR \"/steam.png\" : \"\",\n      // 3. 检查绝对路径\n      app_image_path\n    };\n    \n    for (const auto& path : paths_to_check) {\n      if (!path.empty() && std::filesystem::exists(path)) {\n        return path;\n      }\n    }\n    \n    // 如果所有路径都不存在，返回默认图像\n    BOOST_LOG(warning) << \"Couldn't find app image at path [\"sv << app_image_path << ']';\n    return DEFAULT_APP_IMAGE_PATH;\n  }\n\n  std::optional<std::string>\n  calculate_sha256(const std::string &filename) {\n    crypto::md_ctx_t ctx { EVP_MD_CTX_create() };\n    if (!ctx) {\n      return std::nullopt;\n    }\n\n    if (!EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr)) {\n      return std::nullopt;\n    }\n\n    // Read file and update calculated SHA\n    char buf[1024 * 16];\n    std::ifstream file(filename, std::ifstream::binary);\n    while (file.good()) {\n      file.read(buf, sizeof(buf));\n      if (!EVP_DigestUpdate(ctx.get(), buf, file.gcount())) {\n        return std::nullopt;\n      }\n    }\n    file.close();\n\n    unsigned char result[SHA256_DIGEST_LENGTH];\n    if (!EVP_DigestFinal_ex(ctx.get(), result, nullptr)) {\n      return std::nullopt;\n    }\n\n    // Transform byte-array to string\n    std::stringstream ss;\n    ss << std::hex << std::setfill('0');\n    for (const auto &byte : result) {\n      ss << std::setw(2) << (int) byte;\n    }\n    return ss.str();\n  }\n\n  uint32_t\n  calculate_crc32(const std::string &input) {\n    boost::crc_32_type result;\n    result.process_bytes(input.data(), input.length());\n    return result.checksum();\n  }\n\n  std::tuple<std::string, std::string>\n  calculate_app_id(const std::string &app_name, std::string app_image_path, int index) {\n    // Generate id by hashing name with image data if present\n    std::vector<std::string> to_hash;\n    to_hash.push_back(app_name);\n\n    // Fix for unstable AppID when wallpaper changes:\n    // If the image path is \"desktop\", use the literal string \"desktop\" for hashing \n    // instead of the resolved wallpaper path/content. This ensures the AppID \n    // remains constant even if the user changes their wallpaper.\n    if (app_image_path == \"desktop\") {\n      to_hash.push_back(\"desktop\");\n    }\n    else {\n      auto file_path = validate_app_image_path(app_image_path);\n      if (file_path != DEFAULT_APP_IMAGE_PATH) {\n        auto file_hash = calculate_sha256(file_path);\n        if (file_hash) {\n          to_hash.push_back(file_hash.value());\n        }\n        else {\n          // Fallback to just hashing image path\n          to_hash.push_back(file_path);\n        }\n      }\n    }\n\n    // Create combined strings for hash\n    std::stringstream ss;\n    for_each(to_hash.begin(), to_hash.end(), [&ss](const std::string &s) { ss << s; });\n    auto input_no_index = ss.str();\n    ss << index;\n    auto input_with_index = ss.str();\n\n    // CRC32 then truncate to signed 32-bit range due to client limitations\n    auto id_no_index = std::to_string(abs((int32_t) calculate_crc32(input_no_index)));\n    auto id_with_index = std::to_string(abs((int32_t) calculate_crc32(input_with_index)));\n\n    return std::make_tuple(id_no_index, id_with_index);\n  }\n\n  std::optional<proc::proc_t>\n  parse(const std::string &file_name) {\n    pt::ptree tree;\n\n    try {\n      pt::read_json(file_name, tree);\n\n      auto &apps_node = tree.get_child(\"apps\"s);\n      auto &env_vars = tree.get_child(\"env\"s);\n\n      auto this_env = boost::this_process::environment();\n\n      for (auto &[name, val] : env_vars) {\n        this_env[name] = parse_env_val(this_env, val.get_value<std::string>());\n      }\n\n      std::set<std::string> ids;\n      std::vector<proc::ctx_t> apps;\n      int i = 0;\n      for (auto &[_, app_node] : apps_node) {\n        proc::ctx_t ctx;\n\n        auto prep_nodes_opt = app_node.get_child_optional(\"prep-cmd\"s);\n        auto menu_nodes_opt = app_node.get_child_optional(\"menu-cmd\"s);\n        auto detached_nodes_opt = app_node.get_child_optional(\"detached\"s);\n        auto exclude_global_prep = app_node.get_optional<bool>(\"exclude-global-prep-cmd\"s);\n        auto output = app_node.get_optional<std::string>(\"output\"s);\n        auto name = parse_env_val(this_env, app_node.get<std::string>(\"name\"s));\n        auto cmd = app_node.get_optional<std::string>(\"cmd\"s);\n        auto image_path = app_node.get_optional<std::string>(\"image-path\"s);\n        auto working_dir = app_node.get_optional<std::string>(\"working-dir\"s);\n        auto elevated = app_node.get_optional<bool>(\"elevated\"s);\n        auto auto_detach = app_node.get_optional<bool>(\"auto-detach\"s);\n        auto wait_all = app_node.get_optional<bool>(\"wait-all\"s);\n        auto exit_timeout = app_node.get_optional<int>(\"exit-timeout\"s);\n        auto mouse_mode = app_node.get_optional<int>(\"mouse-mode\"s);\n\n        std::vector<proc::cmd_t> prep_cmds;\n        if (!exclude_global_prep.value_or(false)) {\n          prep_cmds.reserve(config::sunshine.prep_cmds.size());\n          for (auto &prep_cmd : config::sunshine.prep_cmds) {\n            auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd);\n            auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd);\n\n            prep_cmds.emplace_back(\n              std::move(do_cmd),\n              std::move(undo_cmd),\n              std::move(prep_cmd.elevated));\n          }\n        }\n\n        if (prep_nodes_opt) {\n          auto &prep_nodes = *prep_nodes_opt;\n\n          prep_cmds.reserve(prep_cmds.size() + prep_nodes.size());\n          for (auto &[_, prep_node] : prep_nodes) {\n            auto do_cmd = prep_node.get_optional<std::string>(\"do\"s);\n            auto undo_cmd = prep_node.get_optional<std::string>(\"undo\"s);\n            auto elevated = prep_node.get_optional<bool>(\"elevated\");\n\n            prep_cmds.emplace_back(\n              parse_env_val(this_env, do_cmd.value_or(\"\")),\n              parse_env_val(this_env, undo_cmd.value_or(\"\")),\n              std::move(elevated.value_or(false)));\n          }\n        }\n\n        std::vector<proc::scmd_t> menu_cmds;\n        if (menu_nodes_opt) {\n          auto &menu_nodes = *menu_nodes_opt;\n\n          menu_cmds.reserve(menu_nodes.size());\n          for (auto &[_, menu_node] : menu_nodes) {\n            auto id = menu_node.get<std::string>(\"id\"s);\n            auto name = menu_node.get<std::string>(\"name\"s);\n            auto do_cmd = parse_env_val(this_env, menu_node.get<std::string>(\"cmd\"s));\n            auto elevated = menu_node.get_optional<bool>(\"elevated\");\n            menu_cmds.emplace_back(std::move(id), std::move(name), std::move(do_cmd), std::move(elevated.value_or(false)));\n          }\n        }\n\n        std::vector<std::string> detached;\n        if (detached_nodes_opt) {\n          auto &detached_nodes = *detached_nodes_opt;\n\n          detached.reserve(detached_nodes.size());\n          for (auto &[_, detached_val] : detached_nodes) {\n            detached.emplace_back(parse_env_val(this_env, detached_val.get_value<std::string>()));\n          }\n        }\n\n        if (output) {\n          ctx.output = parse_env_val(this_env, *output);\n        }\n\n        if (cmd) {\n          ctx.cmd = parse_env_val(this_env, *cmd);\n        }\n\n        if (working_dir) {\n          ctx.working_dir = parse_env_val(this_env, *working_dir);\n#ifdef _WIN32\n          // The working directory, unlike the command itself, should not be quoted\n          // when it contains spaces. Unlike POSIX, Windows forbids quotes in paths,\n          // so we can safely strip them all out here to avoid confusing the user.\n          boost::erase_all(ctx.working_dir, \"\\\"\");\n#endif\n        }\n\n        if (image_path) {\n          ctx.image_path = parse_env_val(this_env, *image_path);\n        }\n\n        ctx.elevated = elevated.value_or(false);\n        ctx.auto_detach = auto_detach.value_or(true);\n        ctx.wait_all = wait_all.value_or(true);\n        ctx.mouse_mode = mouse_mode.value_or(0);\n        ctx.exit_timeout = std::chrono::seconds { exit_timeout.value_or(5) };\n\n        auto possible_ids = calculate_app_id(name, ctx.image_path, i++);\n        if (ids.count(std::get<0>(possible_ids)) == 0) {\n          // Avoid using index to generate id if possible\n          ctx.id = std::get<0>(possible_ids);\n        }\n        else {\n          // Fallback to include index on collision\n          ctx.id = std::get<1>(possible_ids);\n        }\n        ids.insert(ctx.id);\n\n        ctx.name = std::move(name);\n        ctx.prep_cmds = std::move(prep_cmds);\n        ctx.menu_cmds = std::move(menu_cmds);\n        ctx.detached = std::move(detached);\n\n        apps.emplace_back(std::move(ctx));\n      }\n\n      return proc::proc_t {\n        std::move(this_env), std::move(apps)\n      };\n    }\n    catch (std::exception &e) {\n      BOOST_LOG(error) << e.what();\n    }\n\n    return std::nullopt;\n  }\n\n  void\n  refresh(const std::string &file_name) {\n    auto proc_opt = proc::parse(file_name);\n\n    if (proc_opt) {\n      // 如果当前有应用正在运行，需要保留动态环境变量（SUNSHINE_*）\n      // 这些变量是在 execute() 中动态添加的，不应该被配置文件中的环境变量覆盖\n      // \n      // 环境变量构成说明：\n      // 1. 系统环境变量：从 boost::this_process::environment() 获取（PATH、HOME 等）\n      // 2. 配置文件环境变量：从 apps.json 的 env 节点读取（用户自定义）\n      // 3. SUNSHINE_* 动态变量：在 execute() 时设置（串流会话相关）\n      // \n      // refresh() 时的行为：\n      // - 系统环境变量和配置文件环境变量会被重新读取（反映最新状态）\n      // - SUNSHINE_* 变量会被保留（确保正在运行的进程正常工作）\n      if (proc.running()) {\n        // 保存当前环境变量中的 SUNSHINE_* 动态变量\n        const boost::process::v1::environment &current_env = proc.get_env();\n        boost::process::v1::environment new_env = proc_opt->get_env();\n        \n        // 将当前环境变量中的 SUNSHINE_* 变量复制到新环境变量中\n        for (const auto &entry : current_env) {\n          const std::string &var_name = entry.get_name();\n          if (var_name.find(\"SUNSHINE_\") == 0) {\n            new_env[var_name] = entry.to_string();\n          }\n        }\n        \n        proc.set_env(std::move(new_env));\n      }\n      else {\n        // 没有应用运行时，可以安全地替换环境变量\n        proc.set_env(proc_opt->get_env());\n      }\n      proc.set_apps(proc_opt->get_apps());\n    }\n  }\n}  // namespace proc\n"
  },
  {
    "path": "src/process.h",
    "content": "/**\n * @file src/process.h\n * @brief Declarations for the startup and shutdown of the apps started by a streaming Session.\n */\n#pragma once\n\n#ifndef __kernel_entry\n  #define __kernel_entry\n#endif\n\n#include <optional>\n#include <unordered_map>\n\n#include <boost/process/v1.hpp>\n\n#include \"config.h\"\n#include \"platform/common.h\"\n#include \"rtsp.h\"\n#include \"utility.h\"\n\nnamespace proc {\n  using file_t = util::safe_ptr_v2<FILE, int, fclose>;\n\n  typedef config::prep_cmd_t cmd_t;\n  struct scmd_t {\n    scmd_t(std::string &&id, std::string &&name, std::string &&do_cmd, bool &&elevated):\n        id(std::move(id)), name(std::move(name)), do_cmd(std::move(do_cmd)), elevated(std::move(elevated)) {}\n    std::string id;\n    std::string name;\n    std::string do_cmd;\n    bool elevated;\n  };\n  /**\n   * pre_cmds -- guaranteed to be executed unless any of the commands fail.\n   * detached -- commands detached from Sunshine\n   * cmd -- Runs indefinitely until:\n   *    No session is running and a different set of commands it to be executed\n   *    Command exits\n   * working_dir -- the process working directory. This is required for some games to run properly.\n   * cmd_output --\n   *    empty    -- The output of the commands are appended to the output of sunshine\n   *    \"null\"   -- The output of the commands are discarded\n   *    filename -- The output of the commands are appended to filename\n   */\n  struct ctx_t {\n    std::vector<cmd_t> prep_cmds;\n    std::vector<scmd_t> menu_cmds;\n\n    /**\n     * Some applications, such as Steam, either exit quickly, or keep running indefinitely.\n     *\n     * Apps that launch normal child processes and terminate will be handled by the process\n     * grouping logic (wait_all). However, apps that launch child processes indirectly or\n     * into another process group (such as UWP apps) can only be handled by the auto-detach\n     * heuristic which catches processes that exit 0 very quickly, but we won't have proper\n     * process tracking for those.\n     *\n     * For cases where users just want to kick off a background process and never manage the\n     * lifetime of that process, they can use detached commands for that.\n     */\n    std::vector<std::string> detached;\n\n    std::string name;\n    std::string cmd;\n    std::string working_dir;\n    std::string output;\n    std::string image_path;\n    std::string id;\n    bool elevated;\n    bool auto_detach;\n    bool wait_all;\n    int mouse_mode;  ///< 0=auto (use global config), 1=force virtual mouse, 2=force SendInput\n    std::chrono::seconds exit_timeout;\n  };\n\n  class proc_t {\n  public:\n    KITTY_DEFAULT_CONSTR_MOVE_THROW(proc_t)\n\n    proc_t(\n      boost::process::v1::environment &&env,\n      std::vector<ctx_t> &&apps):\n        _app_id(0),\n        _env(std::move(env)),\n        _apps(std::move(apps)) {}\n\n    int\n    execute(int app_id, std::shared_ptr<rtsp_stream::launch_session_t> launch_session);\n\n    /**\n     * @return `_app_id` if a process is running, otherwise returns `0`\n     */\n    int\n    running();\n\n    ~proc_t();\n\n    const std::vector<ctx_t> &\n    get_apps() const;\n    std::vector<ctx_t> &\n    get_apps();\n    void\n    set_apps(std::vector<ctx_t> apps);\n    std::string\n    get_app_image(int app_id);\n    std::string\n    get_app_name(int app_id);\n    std::string\n    get_app_cmd(int app_id);\n    std::string\n    get_last_run_app_name();\n    const boost::process::v1::environment &\n    get_env() const;\n    boost::process::v1::environment &\n    get_env();\n    void\n    set_env(boost::process::v1::environment env);\n    void\n    run_menu_cmd(std::string cmd_id);\n    void\n    terminate();\n    std::string\n    get_apps_etag() const;\n\n  private:\n    int _app_id;\n\n    std::string _apps_etag;\n\n    boost::process::v1::environment _env;\n    std::vector<ctx_t> _apps;\n    ctx_t _app;\n    std::chrono::steady_clock::time_point _app_launch_time;\n\n    // If no command associated with _app_id, yet it's still running\n    bool placebo {};\n\n    boost::process::v1::child _process;\n    boost::process::v1::group _process_group;\n\n    file_t _pipe;\n    std::vector<cmd_t>::const_iterator _app_prep_it;\n    std::vector<cmd_t>::const_iterator _app_prep_begin;\n  };\n\n  /**\n   * @brief Calculate a stable id based on name and image data\n   * @return Tuple of id calculated without index (for use if no collision) and one with.\n   */\n  std::tuple<std::string, std::string>\n  calculate_app_id(const std::string &app_name, std::string app_image_path, int index);\n\n  std::string\n  validate_app_image_path(std::string app_image_path);\n  void\n  refresh(const std::string &file_name);\n  std::optional<proc::proc_t>\n  parse(const std::string &file_name);\n\n  /**\n   * @brief Initialize proc functions\n   * @return Unique pointer to `deinit_t` to manage cleanup\n   */\n  std::unique_ptr<platf::deinit_t>\n  init();\n\n  /**\n   * @brief Terminates all child processes in a process group.\n   * @param proc The child process itself.\n   * @param group The group of all children in the process tree.\n   * @param exit_timeout The timeout to wait for the process group to gracefully exit.\n   */\n  void\n  terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout);\n\n  extern proc_t proc;\n}  // namespace proc\n"
  },
  {
    "path": "src/round_robin.h",
    "content": "/**\n * @file src/round_robin.h\n * @brief Declarations for a round-robin iterator.\n */\n#pragma once\n\n#include <iterator>\n\n/**\n * @brief A round-robin iterator utility.\n * @tparam V The value type.\n * @tparam T The iterator type.\n */\nnamespace round_robin_util {\n  template <class V, class T>\n  class it_wrap_t {\n  public:\n    using iterator_category = std::random_access_iterator_tag;\n    using value_type = V;\n    using difference_type = V;\n    using pointer = V *;\n    using const_pointer = V const *;\n    using reference = V &;\n    using const_reference = V const &;\n\n    typedef T iterator;\n    typedef std::ptrdiff_t diff_t;\n\n    iterator\n    operator+=(diff_t step) {\n      while (step-- > 0) {\n        ++_this();\n      }\n\n      return _this();\n    }\n\n    iterator\n    operator-=(diff_t step) {\n      while (step-- > 0) {\n        --_this();\n      }\n\n      return _this();\n    }\n\n    iterator\n    operator+(diff_t step) {\n      iterator new_ = _this();\n\n      return new_ += step;\n    }\n\n    iterator\n    operator-(diff_t step) {\n      iterator new_ = _this();\n\n      return new_ -= step;\n    }\n\n    diff_t\n    operator-(iterator first) {\n      diff_t step = 0;\n      while (first != _this()) {\n        ++step;\n        ++first;\n      }\n\n      return step;\n    }\n\n    iterator\n    operator++() {\n      _this().inc();\n      return _this();\n    }\n    iterator\n    operator--() {\n      _this().dec();\n      return _this();\n    }\n\n    iterator\n    operator++(int) {\n      iterator new_ = _this();\n\n      ++_this();\n\n      return new_;\n    }\n\n    iterator\n    operator--(int) {\n      iterator new_ = _this();\n\n      --_this();\n\n      return new_;\n    }\n\n    reference\n    operator*() { return *_this().get(); }\n    const_reference\n    operator*() const { return *_this().get(); }\n\n    pointer\n    operator->() { return &*_this(); }\n    const_pointer\n    operator->() const { return &*_this(); }\n\n    bool\n    operator!=(const iterator &other) const {\n      return !(_this() == other);\n    }\n\n    bool\n    operator<(const iterator &other) const {\n      return !(_this() >= other);\n    }\n\n    bool\n    operator>=(const iterator &other) const {\n      return _this() == other || _this() > other;\n    }\n\n    bool\n    operator<=(const iterator &other) const {\n      return _this() == other || _this() < other;\n    }\n\n    bool\n    operator==(const iterator &other) const { return _this().eq(other); };\n    bool\n    operator>(const iterator &other) const { return _this().gt(other); }\n\n  private:\n    iterator &\n    _this() { return *static_cast<iterator *>(this); }\n    const iterator &\n    _this() const { return *static_cast<const iterator *>(this); }\n  };\n\n  template <class V, class It>\n  class round_robin_t: public it_wrap_t<V, round_robin_t<V, It>> {\n  public:\n    using iterator = It;\n    using pointer = V *;\n\n    round_robin_t(iterator begin, iterator end):\n        _begin(begin), _end(end), _pos(begin) {}\n\n    void\n    inc() {\n      ++_pos;\n\n      if (_pos == _end) {\n        _pos = _begin;\n      }\n    }\n\n    void\n    dec() {\n      if (_pos == _begin) {\n        _pos = _end;\n      }\n\n      --_pos;\n    }\n\n    bool\n    eq(const round_robin_t &other) const {\n      return *_pos == *other._pos;\n    }\n\n    pointer\n    get() const {\n      return &*_pos;\n    }\n\n  private:\n    It _begin;\n    It _end;\n\n    It _pos;\n  };\n\n  template <class V, class It>\n  round_robin_t<V, It>\n  make_round_robin(It begin, It end) {\n    return round_robin_t<V, It>(begin, end);\n  }\n}  // namespace round_robin_util\n"
  },
  {
    "path": "src/rswrapper.c",
    "content": "/**\n * @file src/rswrapper.c\n * @brief Wrappers for nanors vectorization with different ISA options\n */\n\n// _FORTIY_SOURCE can cause some versions of GCC to try to inline\n// memset() with incompatible target options when compiling rs.c\n#ifdef _FORTIFY_SOURCE\n  #undef _FORTIFY_SOURCE\n#endif\n\n// The assert() function is decorated with __cold on macOS which\n// is incompatible with Clang's target multiversioning feature\n#ifndef NDEBUG\n  #define NDEBUG\n#endif\n\n#define DECORATE_FUNC_I(a, b) a##b\n#define DECORATE_FUNC(a, b) DECORATE_FUNC_I(a, b)\n\n// Append an ISA suffix to the public RS API\n#define reed_solomon_init DECORATE_FUNC(reed_solomon_init, ISA_SUFFIX)\n#define reed_solomon_new DECORATE_FUNC(reed_solomon_new, ISA_SUFFIX)\n#define reed_solomon_new_static DECORATE_FUNC(reed_solomon_new_static, ISA_SUFFIX)\n#define reed_solomon_release DECORATE_FUNC(reed_solomon_release, ISA_SUFFIX)\n#define reed_solomon_decode DECORATE_FUNC(reed_solomon_decode, ISA_SUFFIX)\n#define reed_solomon_encode DECORATE_FUNC(reed_solomon_encode, ISA_SUFFIX)\n\n// Append an ISA suffix to internal functions to prevent multiple definition errors\n#define obl_axpy_ref DECORATE_FUNC(obl_axpy_ref, ISA_SUFFIX)\n#define obl_scal_ref DECORATE_FUNC(obl_scal_ref, ISA_SUFFIX)\n#define obl_axpyb32_ref DECORATE_FUNC(obl_axpyb32_ref, ISA_SUFFIX)\n#define obl_axpy DECORATE_FUNC(obl_axpy, ISA_SUFFIX)\n#define obl_scal DECORATE_FUNC(obl_scal, ISA_SUFFIX)\n#define obl_swap DECORATE_FUNC(obl_swap, ISA_SUFFIX)\n#define obl_axpyb32 DECORATE_FUNC(obl_axpyb32, ISA_SUFFIX)\n#define axpy DECORATE_FUNC(axpy, ISA_SUFFIX)\n#define scal DECORATE_FUNC(scal, ISA_SUFFIX)\n#define gemm DECORATE_FUNC(gemm, ISA_SUFFIX)\n#define invert_mat DECORATE_FUNC(invert_mat, ISA_SUFFIX)\n\n#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(__amd64__) || defined(_M_AMD64)\n\n  // Compile a variant for SSSE3\n  #if defined(__clang__)\n    #pragma clang attribute push(__attribute__((target(\"ssse3\"))), apply_to = function)\n  #else\n    #pragma GCC push_options\n    #pragma GCC target(\"ssse3\")\n  #endif\n  #define ISA_SUFFIX _ssse3\n  #define OBLAS_SSE3\n  #include \"../third-party/nanors/rs.c\"\n  #undef OBLAS_SSE3\n  #undef ISA_SUFFIX\n  #if defined(__clang__)\n    #pragma clang attribute pop\n  #else\n    #pragma GCC pop_options\n  #endif\n\n  // Compile a variant for AVX2\n  #if defined(__clang__)\n    #pragma clang attribute push(__attribute__((target(\"avx2\"))), apply_to = function)\n  #else\n    #pragma GCC push_options\n    #pragma GCC target(\"avx2\")\n  #endif\n  #define ISA_SUFFIX _avx2\n  #define OBLAS_AVX2\n  #include \"../third-party/nanors/rs.c\"\n  #undef OBLAS_AVX2\n  #undef ISA_SUFFIX\n  #if defined(__clang__)\n    #pragma clang attribute pop\n  #else\n    #pragma GCC pop_options\n  #endif\n\n  // Compile a variant for AVX512BW\n  #if defined(__clang__)\n    #pragma clang attribute push(__attribute__((target(\"avx512f,avx512bw\"))), apply_to = function)\n  #else\n    #pragma GCC push_options\n    #pragma GCC target(\"avx512f,avx512bw\")\n  #endif\n  #define ISA_SUFFIX _avx512\n  #define OBLAS_AVX512\n  #include \"../third-party/nanors/rs.c\"\n  #undef OBLAS_AVX512\n  #undef ISA_SUFFIX\n  #if defined(__clang__)\n    #pragma clang attribute pop\n  #else\n    #pragma GCC pop_options\n  #endif\n\n#endif\n\n// Compile a default variant\n#define ISA_SUFFIX _def\n#include \"../third-party/nanors/deps/obl/autoshim.h\"\n#include \"../third-party/nanors/rs.c\"\n#undef ISA_SUFFIX\n\n#undef reed_solomon_init\n#undef reed_solomon_new\n#undef reed_solomon_new_static\n#undef reed_solomon_release\n#undef reed_solomon_decode\n#undef reed_solomon_encode\n\n#include \"rswrapper.h\"\n\nreed_solomon_new_t reed_solomon_new_fn;\nreed_solomon_release_t reed_solomon_release_fn;\nreed_solomon_encode_t reed_solomon_encode_fn;\nreed_solomon_decode_t reed_solomon_decode_fn;\n\n/**\n * @brief This initializes the RS function pointers to the best vectorized version available.\n * @details The streaming code will directly invoke these function pointers during encoding.\n */\nvoid\nreed_solomon_init(void) {\n#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(__amd64__) || defined(_M_AMD64)\n  if (__builtin_cpu_supports(\"avx512f\") && __builtin_cpu_supports(\"avx512bw\")) {\n    reed_solomon_new_fn = reed_solomon_new_avx512;\n    reed_solomon_release_fn = reed_solomon_release_avx512;\n    reed_solomon_encode_fn = reed_solomon_encode_avx512;\n    reed_solomon_decode_fn = reed_solomon_decode_avx512;\n    reed_solomon_init_avx512();\n  }\n  else if (__builtin_cpu_supports(\"avx2\")) {\n    reed_solomon_new_fn = reed_solomon_new_avx2;\n    reed_solomon_release_fn = reed_solomon_release_avx2;\n    reed_solomon_encode_fn = reed_solomon_encode_avx2;\n    reed_solomon_decode_fn = reed_solomon_decode_avx2;\n    reed_solomon_init_avx2();\n  }\n  else if (__builtin_cpu_supports(\"ssse3\")) {\n    reed_solomon_new_fn = reed_solomon_new_ssse3;\n    reed_solomon_release_fn = reed_solomon_release_ssse3;\n    reed_solomon_encode_fn = reed_solomon_encode_ssse3;\n    reed_solomon_decode_fn = reed_solomon_decode_ssse3;\n    reed_solomon_init_ssse3();\n  }\n  else\n#endif\n  {\n    reed_solomon_new_fn = reed_solomon_new_def;\n    reed_solomon_release_fn = reed_solomon_release_def;\n    reed_solomon_encode_fn = reed_solomon_encode_def;\n    reed_solomon_decode_fn = reed_solomon_decode_def;\n    reed_solomon_init_def();\n  }\n}\n"
  },
  {
    "path": "src/rswrapper.h",
    "content": "/**\n * @file src/rswrapper.h\n * @brief Wrappers for nanors vectorization\n * @details This is a drop-in replacement for nanors rs.h\n */\n#pragma once\n\n#include <stdint.h>\n\ntypedef struct _reed_solomon reed_solomon;\n\ntypedef reed_solomon *(*reed_solomon_new_t)(int data_shards, int parity_shards);\ntypedef void (*reed_solomon_release_t)(reed_solomon *rs);\ntypedef int (*reed_solomon_encode_t)(reed_solomon *rs, uint8_t **shards, int nr_shards, int bs);\ntypedef int (*reed_solomon_decode_t)(reed_solomon *rs, uint8_t **shards, uint8_t *marks, int nr_shards, int bs);\n\nextern reed_solomon_new_t reed_solomon_new_fn;\nextern reed_solomon_release_t reed_solomon_release_fn;\nextern reed_solomon_encode_t reed_solomon_encode_fn;\nextern reed_solomon_decode_t reed_solomon_decode_fn;\n\n#define reed_solomon_new reed_solomon_new_fn\n#define reed_solomon_release reed_solomon_release_fn\n#define reed_solomon_encode reed_solomon_encode_fn\n#define reed_solomon_decode reed_solomon_decode_fn\n\n/**\n * @brief This initializes the RS function pointers to the best vectorized version available.\n * @details The streaming code will directly invoke these function pointers during encoding.\n */\nvoid\nreed_solomon_init(void);\n"
  },
  {
    "path": "src/rtsp.cpp",
    "content": "/**\n * @file src/rtsp.cpp\n * @brief Definitions for RTSP streaming.\n */\n#define BOOST_BIND_GLOBAL_PLACEHOLDERS\n\nextern \"C\" {\n#include <moonlight-common-c/src/Limelight-internal.h>\n#include <moonlight-common-c/src/Rtsp.h>\n}\n\n// standard includes\n#include <algorithm>\n#include <array>\n#include <cctype>\n#include <set>\n#include <unordered_map>\n#include <utility>\n#include <vector>\n\n// lib includes\n#include <boost/asio.hpp>\n#include <boost/bind.hpp>\n\n// local includes\n#include \"config.h\"\n#include \"globals.h\"\n#include \"input.h\"\n#include \"logging.h\"\n#include \"network.h\"\n#include \"rtsp.h\"\n#include \"stream.h\"\n#include \"sync.h\"\n#include \"video.h\"\n\nnamespace asio = boost::asio;\n\nusing asio::ip::tcp;\nusing asio::ip::udp;\n\nusing namespace std::literals;\n\nnamespace rtsp_stream {\n  namespace {\n    bool\n    parse_legacy_surround_params(std::string_view params, int requested_channels, audio::stream_params_t &result) {\n      if (params.length() <= 3 || !std::all_of(params.begin(), params.end(), [](char c) { return std::isdigit((unsigned char) c); })) {\n        return false;\n      }\n\n      int channel_count = params[0] - '0';\n      int streams = params[1] - '0';\n      int coupled_streams = params[2] - '0';\n      if (channel_count != requested_channels || channel_count < 2 || channel_count > platf::speaker::MAX_SPEAKERS ||\n          streams + coupled_streams != channel_count || params.length() != (size_t) channel_count + 3) {\n        return false;\n      }\n\n      for (int i = 0; i < channel_count; ++i) {\n        auto map_value = params[i + 3] - '0';\n        if (map_value < 0 || map_value >= channel_count) {\n          return false;\n        }\n\n        result.mapping[i] = (std::uint8_t) map_value;\n      }\n\n      result.channelCount = channel_count;\n      result.streams = streams;\n      result.coupledStreams = coupled_streams;\n      return true;\n    }\n\n    bool\n    parse_delimited_surround_params(std::string_view params, int requested_channels, audio::stream_params_t &result) {\n      std::vector<int> values;\n      values.reserve(3 + platf::speaker::MAX_SPEAKERS);\n\n      int current = 0;\n      bool has_digit = false;\n      for (char ch : params) {\n        if (std::isdigit((unsigned char) ch)) {\n          current = current * 10 + (ch - '0');\n          has_digit = true;\n          continue;\n        }\n\n        if (ch == ',' || ch == ';' || ch == ':' || ch == '|' || std::isspace((unsigned char) ch)) {\n          if (has_digit) {\n            values.push_back(current);\n            current = 0;\n            has_digit = false;\n          }\n          continue;\n        }\n\n        return false;\n      }\n\n      if (has_digit) {\n        values.push_back(current);\n      }\n\n      if (values.size() < 3) {\n        return false;\n      }\n\n      int channel_count = values[0];\n      int streams = values[1];\n      int coupled_streams = values[2];\n\n      if (channel_count != requested_channels || channel_count < 2 || channel_count > platf::speaker::MAX_SPEAKERS ||\n          streams + coupled_streams != channel_count || values.size() != (size_t) channel_count + 3) {\n        return false;\n      }\n\n      for (int i = 0; i < channel_count; ++i) {\n        auto map_value = values[i + 3];\n        if (map_value < 0 || map_value >= channel_count) {\n          return false;\n        }\n\n        result.mapping[i] = (std::uint8_t) map_value;\n      }\n\n      result.channelCount = channel_count;\n      result.streams = streams;\n      result.coupledStreams = coupled_streams;\n      return true;\n    }\n\n    bool\n    parse_surround_params(std::string_view params, int requested_channels, audio::stream_params_t &result) {\n      return parse_legacy_surround_params(params, requested_channels, result) ||\n             parse_delimited_surround_params(params, requested_channels, result);\n    }\n  }  // namespace\n\n  void\n  free_msg(PRTSP_MESSAGE msg) {\n    freeMessage(msg);\n\n    delete msg;\n  }\n\n#pragma pack(push, 1)\n\n  struct encrypted_rtsp_header_t {\n    // We set the MSB in encrypted RTSP messages to allow format-agnostic\n    // parsing code to be able to tell encrypted from plaintext messages.\n    static constexpr std::uint32_t ENCRYPTED_MESSAGE_TYPE_BIT = 0x80000000;\n\n    uint8_t *\n    payload() {\n      return (uint8_t *) (this + 1);\n    }\n\n    std::uint32_t\n    payload_length() {\n      return util::endian::big<std::uint32_t>(typeAndLength) & ~ENCRYPTED_MESSAGE_TYPE_BIT;\n    }\n\n    bool\n    is_encrypted() {\n      return !!(util::endian::big<std::uint32_t>(typeAndLength) & ENCRYPTED_MESSAGE_TYPE_BIT);\n    }\n\n    // This field is the length of the payload + ENCRYPTED_MESSAGE_TYPE_BIT in big-endian\n    std::uint32_t typeAndLength;\n\n    // This field is the number used to initialize the bottom 4 bytes of the AES IV in big-endian\n    std::uint32_t sequenceNumber;\n\n    // This field is the AES GCM authentication tag\n    std::uint8_t tag[16];\n  };\n\n#pragma pack(pop)\n\n  class rtsp_server_t;\n\n  using msg_t = util::safe_ptr<RTSP_MESSAGE, free_msg>;\n  using cmd_func_t = std::function<void(rtsp_server_t *server, tcp::socket &, launch_session_t &, msg_t &&)>;\n\n  void\n  print_msg(PRTSP_MESSAGE msg);\n  void\n  cmd_not_found(tcp::socket &sock, launch_session_t &, msg_t &&req);\n  void\n  respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload);\n\n  class socket_t: public std::enable_shared_from_this<socket_t> {\n  public:\n    socket_t(boost::asio::io_context &io_context, std::function<void(tcp::socket &sock, launch_session_t &, msg_t &&)> &&handle_data_fn):\n        handle_data_fn { std::move(handle_data_fn) },\n        sock { io_context } {\n    }\n\n    /**\n     * @brief Queue an asynchronous read to begin the next message.\n     */\n    void\n    read() {\n      if (begin == std::end(msg_buf) || (session->rtsp_cipher && begin + sizeof(encrypted_rtsp_header_t) >= std::end(msg_buf))) {\n        BOOST_LOG(error) << \"RTSP: read(): Exceeded maximum rtsp packet size: \"sv << msg_buf.size();\n\n        respond(sock, *session, nullptr, 400, \"BAD REQUEST\", 0, {});\n\n        boost::system::error_code ec;\n        sock.close(ec);\n        if (ec) {\n          BOOST_LOG(debug) << \"Error closing socket: \"sv << ec.message();\n        }\n\n        return;\n      }\n\n      if (session->rtsp_cipher) {\n        // For encrypted RTSP, we will read the the entire header first\n        boost::asio::async_read(sock, boost::asio::buffer(begin, sizeof(encrypted_rtsp_header_t)), boost::bind(&socket_t::handle_read_encrypted_header, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));\n      }\n      else {\n        sock.async_read_some(\n          boost::asio::buffer(begin, (std::size_t) (std::end(msg_buf) - begin)),\n          boost::bind(\n            &socket_t::handle_read_plaintext,\n            shared_from_this(),\n            boost::asio::placeholders::error,\n            boost::asio::placeholders::bytes_transferred));\n      }\n    }\n\n    /**\n     * @brief Handle the initial read of the header of an encrypted message.\n     * @param socket The socket the message was received on.\n     * @param ec The error code of the read operation.\n     * @param bytes The number of bytes read.\n     */\n    static void\n    handle_read_encrypted_header(std::shared_ptr<socket_t> &socket, const boost::system::error_code &ec, std::size_t bytes) {\n      BOOST_LOG(debug) << \"handle_read_encrypted_header(): Handle read of size: \"sv << bytes << \" bytes\"sv;\n\n      auto sock_close = util::fail_guard([&socket]() {\n        boost::system::error_code ec;\n        socket->sock.close(ec);\n\n        if (ec) {\n          BOOST_LOG(error) << \"RTSP: handle_read_encrypted_header(): Couldn't close tcp socket: \"sv << ec.message();\n        }\n      });\n\n      if (ec || bytes < sizeof(encrypted_rtsp_header_t)) {\n        BOOST_LOG(error) << \"RTSP: handle_read_encrypted_header(): Couldn't read from tcp socket: \"sv << ec.message();\n\n        respond(socket->sock, *socket->session, nullptr, 400, \"BAD REQUEST\", 0, {});\n        return;\n      }\n\n      auto header = (encrypted_rtsp_header_t *) socket->begin;\n      if (!header->is_encrypted()) {\n        BOOST_LOG(error) << \"RTSP: handle_read_encrypted_header(): Rejecting unencrypted RTSP message\"sv;\n\n        respond(socket->sock, *socket->session, nullptr, 400, \"BAD REQUEST\", 0, {});\n        return;\n      }\n\n      auto payload_length = header->payload_length();\n\n      // Check if we have enough space to read this message\n      if (socket->begin + sizeof(*header) + payload_length >= std::end(socket->msg_buf)) {\n        BOOST_LOG(error) << \"RTSP: handle_read_encrypted_header(): Exceeded maximum rtsp packet size: \"sv << socket->msg_buf.size();\n\n        respond(socket->sock, *socket->session, nullptr, 400, \"BAD REQUEST\", 0, {});\n        return;\n      }\n\n      sock_close.disable();\n\n      // Read the remainder of the header and full encrypted payload\n      boost::asio::async_read(socket->sock, boost::asio::buffer(socket->begin + bytes, payload_length), boost::bind(&socket_t::handle_read_encrypted_message, socket->shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));\n    }\n\n    /**\n     * @brief Handle the final read of the content of an encrypted message.\n     * @param socket The socket the message was received on.\n     * @param ec The error code of the read operation.\n     * @param bytes The number of bytes read.\n     */\n    static void\n    handle_read_encrypted_message(std::shared_ptr<socket_t> &socket, const boost::system::error_code &ec, std::size_t bytes) {\n      BOOST_LOG(debug) << \"handle_read_encrypted(): Handle read of size: \"sv << bytes << \" bytes\"sv;\n\n      auto sock_close = util::fail_guard([&socket]() {\n        boost::system::error_code ec;\n        socket->sock.close(ec);\n\n        if (ec) {\n          BOOST_LOG(error) << \"RTSP: handle_read_encrypted_message(): Couldn't close tcp socket: \"sv << ec.message();\n        }\n      });\n\n      auto header = (encrypted_rtsp_header_t *) socket->begin;\n      auto payload_length = header->payload_length();\n      auto seq = util::endian::big<std::uint32_t>(header->sequenceNumber);\n\n      if (ec || bytes < payload_length) {\n        BOOST_LOG(error) << \"RTSP: handle_read_encrypted(): Couldn't read from tcp socket: \"sv << ec.message();\n\n        respond(socket->sock, *socket->session, nullptr, 400, \"BAD REQUEST\", 0, {});\n        return;\n      }\n\n      // We use the deterministic IV construction algorithm specified in NIST SP 800-38D\n      // Section 8.2.1. The sequence number is our \"invocation\" field and the 'RC' in the\n      // high bytes is the \"fixed\" field. Because each client provides their own unique\n      // key, our values in the fixed field need only uniquely identify each independent\n      // use of the client's key with AES-GCM in our code.\n      //\n      // The sequence number is 32 bits long which allows for 2^32 RTSP messages to be\n      // received from each client before the IV repeats.\n      crypto::aes_t iv(12);\n      std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv));\n      iv[10] = 'C';  // Client originated\n      iv[11] = 'R';  // RTSP\n\n      std::vector<uint8_t> plaintext;\n      if (socket->session->rtsp_cipher->decrypt(std::string_view { (const char *) header->tag, sizeof(header->tag) + bytes }, plaintext, &iv)) {\n        BOOST_LOG(error) << \"Failed to verify RTSP message tag\"sv;\n\n        respond(socket->sock, *socket->session, nullptr, 400, \"BAD REQUEST\", 0, {});\n        return;\n      }\n\n      msg_t req { new msg_t::element_type {} };\n      if (auto status = parseRtspMessage(req.get(), (char *) plaintext.data(), plaintext.size())) {\n        BOOST_LOG(error) << \"Malformed RTSP message: [\"sv << status << ']';\n\n        respond(socket->sock, *socket->session, nullptr, 400, \"BAD REQUEST\", 0, {});\n        return;\n      }\n\n      sock_close.disable();\n\n      print_msg(req.get());\n\n      socket->handle_data(std::move(req));\n    }\n\n    /**\n     * @brief Queue an asynchronous read of the payload portion of a plaintext message.\n     */\n    void\n    read_plaintext_payload() {\n      if (begin == std::end(msg_buf)) {\n        BOOST_LOG(error) << \"RTSP: read_plaintext_payload(): Exceeded maximum rtsp packet size: \"sv << msg_buf.size();\n\n        respond(sock, *session, nullptr, 400, \"BAD REQUEST\", 0, {});\n\n        boost::system::error_code ec;\n        sock.close(ec);\n        if (ec) {\n          BOOST_LOG(debug) << \"Error closing socket: \"sv << ec.message();\n        }\n\n        return;\n      }\n\n      sock.async_read_some(\n        boost::asio::buffer(begin, (std::size_t) (std::end(msg_buf) - begin)),\n        boost::bind(\n          &socket_t::handle_plaintext_payload,\n          shared_from_this(),\n          boost::asio::placeholders::error,\n          boost::asio::placeholders::bytes_transferred));\n    }\n\n    /**\n     * @brief Handle the read of the payload portion of a plaintext message.\n     * @param socket The socket the message was received on.\n     * @param ec The error code of the read operation.\n     * @param bytes The number of bytes read.\n     */\n    static void\n    handle_plaintext_payload(std::shared_ptr<socket_t> &socket, const boost::system::error_code &ec, std::size_t bytes) {\n      BOOST_LOG(debug) << \"handle_plaintext_payload(): Handle read of size: \"sv << bytes << \" bytes\"sv;\n\n      auto sock_close = util::fail_guard([&socket]() {\n        boost::system::error_code ec;\n        socket->sock.close(ec);\n\n        if (ec) {\n          BOOST_LOG(error) << \"RTSP: handle_plaintext_payload(): Couldn't close tcp socket: \"sv << ec.message();\n        }\n      });\n\n      if (ec) {\n        BOOST_LOG(error) << \"RTSP: handle_plaintext_payload(): Couldn't read from tcp socket: \"sv << ec.message();\n\n        return;\n      }\n\n      auto end = socket->begin + bytes;\n      msg_t req { new msg_t::element_type {} };\n      if (auto status = parseRtspMessage(req.get(), socket->msg_buf.data(), (std::size_t) (end - socket->msg_buf.data()))) {\n        BOOST_LOG(error) << \"Malformed RTSP message: [\"sv << status << ']';\n\n        respond(socket->sock, *socket->session, nullptr, 400, \"BAD REQUEST\", 0, {});\n        return;\n      }\n\n      sock_close.disable();\n\n      auto fg = util::fail_guard([&socket]() {\n        socket->read_plaintext_payload();\n      });\n\n      auto content_length = 0;\n      for (auto option = req->options; option != nullptr; option = option->next) {\n        if (\"Content-length\"sv == option->option) {\n          BOOST_LOG(debug) << \"Found Content-Length: \"sv << option->content << \" bytes\"sv;\n\n          // If content_length > bytes read, then we need to store current data read,\n          // to be appended by the next read.\n          std::string_view content { option->content };\n          auto begin = std::find_if(std::begin(content), std::end(content), [](auto ch) {\n            return (bool) std::isdigit(ch);\n          });\n\n          content_length = util::from_chars(begin, std::end(content));\n          break;\n        }\n      }\n\n      if (end - socket->crlf >= content_length) {\n        if (end - socket->crlf > content_length) {\n          BOOST_LOG(warning) << \"(end - socket->crlf) > content_length -- \"sv << (std::size_t) (end - socket->crlf) << \" > \"sv << content_length;\n        }\n\n        fg.disable();\n        print_msg(req.get());\n\n        socket->handle_data(std::move(req));\n      }\n\n      socket->begin = end;\n    }\n\n    /**\n     * @brief Handle the read of the header portion of a plaintext message.\n     * @param socket The socket the message was received on.\n     * @param ec The error code of the read operation.\n     * @param bytes The number of bytes read.\n     */\n    static void\n    handle_read_plaintext(std::shared_ptr<socket_t> &socket, const boost::system::error_code &ec, std::size_t bytes) {\n      BOOST_LOG(debug) << \"handle_read_plaintext(): Handle read of size: \"sv << bytes << \" bytes\"sv;\n\n      if (ec) {\n        BOOST_LOG(error) << \"RTSP: handle_read_plaintext(): Couldn't read from tcp socket: \"sv << ec.message();\n\n        boost::system::error_code ec;\n        socket->sock.close(ec);\n\n        if (ec) {\n          BOOST_LOG(error) << \"RTSP: handle_read_plaintext(): Couldn't close tcp socket: \"sv << ec.message();\n        }\n\n        return;\n      }\n\n      auto fg = util::fail_guard([&socket]() {\n        socket->read();\n      });\n\n      auto begin = std::max(socket->begin - 4, socket->begin);\n      auto buf_size = bytes + (begin - socket->begin);\n      auto end = begin + buf_size;\n\n      constexpr auto needle = \"\\r\\n\\r\\n\"sv;\n\n      auto it = std::search(begin, begin + buf_size, std::begin(needle), std::end(needle));\n      if (it == end) {\n        socket->begin = end;\n\n        return;\n      }\n\n      // Emulate read completion for payload data\n      socket->begin = it + needle.size();\n      socket->crlf = socket->begin;\n      buf_size = end - socket->begin;\n\n      fg.disable();\n      handle_plaintext_payload(socket, ec, buf_size);\n    }\n\n    void\n    handle_data(msg_t &&req) {\n      handle_data_fn(sock, *session, std::move(req));\n    }\n\n    std::function<void(tcp::socket &sock, launch_session_t &, msg_t &&)> handle_data_fn;\n\n    tcp::socket sock;\n\n    std::array<char, 2048> msg_buf;\n\n    char *crlf;\n    char *begin = msg_buf.data();\n\n    std::shared_ptr<launch_session_t> session;\n  };\n\n  class rtsp_server_t {\n  public:\n    ~rtsp_server_t() {\n      clear();\n    }\n\n    int\n    bind(net::af_e af, std::uint16_t port, boost::system::error_code &ec) {\n      acceptor.open(af == net::IPV4 ? tcp::v4() : tcp::v6(), ec);\n      if (ec) {\n        return -1;\n      }\n\n      acceptor.set_option(boost::asio::socket_base::reuse_address { true });\n\n      auto bind_addr_str = net::get_bind_address(af);\n      const auto bind_addr = boost::asio::ip::make_address(bind_addr_str, ec);\n      if (ec) {\n        BOOST_LOG(error) << \"Invalid bind address: \"sv << bind_addr_str << \" - \" << ec.message();\n        return -1;\n      }\n\n      acceptor.bind(tcp::endpoint(bind_addr, port), ec);\n      if (ec) {\n        return -1;\n      }\n\n      acceptor.listen(4096, ec);\n      if (ec) {\n        return -1;\n      }\n\n      next_socket = std::make_shared<socket_t>(io_context, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) {\n        handle_msg(sock, session, std::move(msg));\n      });\n\n      acceptor.async_accept(next_socket->sock, [this](const auto &ec) {\n        handle_accept(ec);\n      });\n\n      return 0;\n    }\n\n    void\n    handle_msg(tcp::socket &sock, launch_session_t &session, msg_t &&req) {\n      auto func = _map_cmd_cb.find(req->message.request.command);\n      if (func != std::end(_map_cmd_cb)) {\n        func->second(this, sock, session, std::move(req));\n      }\n      else {\n        cmd_not_found(sock, session, std::move(req));\n      }\n\n      boost::system::error_code ec;\n      sock.shutdown(boost::asio::socket_base::shutdown_type::shutdown_both, ec);\n      if (ec) {\n        BOOST_LOG(debug) << \"Error shutting down socket: \"sv << ec.message();\n      }\n    }\n\n    void\n    handle_accept(const boost::system::error_code &ec) {\n      if (ec) {\n        BOOST_LOG(error) << \"Couldn't accept incoming connections: \"sv << ec.message();\n\n        // Stop server\n        clear();\n        return;\n      }\n\n      auto socket = std::move(next_socket);\n\n      auto launch_session { launch_event.view(0s) };\n      if (launch_session) {\n        // Associate the current RTSP session with this socket and start reading\n        socket->session = launch_session;\n        socket->read();\n      }\n      else {\n        // This can happen due to normal things like port scanning, so let's not make these visible by default\n        BOOST_LOG(debug) << \"No pending session for incoming RTSP connection\"sv;\n\n        // If there is no session pending, close the connection immediately\n        boost::system::error_code ec;\n        socket->sock.close(ec);\n        if (ec) {\n          BOOST_LOG(debug) << \"Error closing socket: \"sv << ec.message();\n        }\n      }\n\n      // Queue another asynchronous accept for the next incoming connection\n      next_socket = std::make_shared<socket_t>(io_context, [this](tcp::socket &sock, launch_session_t &session, msg_t &&msg) {\n        handle_msg(sock, session, std::move(msg));\n      });\n      acceptor.async_accept(next_socket->sock, [this](const auto &ec) {\n        handle_accept(ec);\n      });\n    }\n\n    void\n    map(const std::string_view &type, cmd_func_t cb) {\n      _map_cmd_cb.emplace(type, std::move(cb));\n    }\n\n    /**\n     * @brief Launch a new streaming session.\n     * @note If the client does not begin streaming within the ping_timeout,\n     *       the session will be discarded.\n     * @param launch_session Streaming session information.\n     */\n    void\n    session_raise(std::shared_ptr<launch_session_t> launch_session) {\n      // If a launch event is still pending, don't overwrite it.\n      if (launch_event.view(0s)) {\n        return;\n      }\n\n      // Raise the new launch session to prepare for the RTSP handshake\n      launch_event.raise(std::move(launch_session));\n\n      // Arm the timer to expire this launch session if the client times out\n      raised_timer.expires_after(config::stream.ping_timeout);\n      raised_timer.async_wait([this](const boost::system::error_code &ec) {\n        if (!ec) {\n          auto discarded = launch_event.pop(0s);\n          if (discarded) {\n            BOOST_LOG(debug) << \"Event timeout: \"sv << discarded->unique_id;\n          }\n        } else {\n          BOOST_LOG(debug) << \"Timer error: \"sv << ec.message();\n        }\n      });\n    }\n\n    /**\n     * @brief Clear state for the oldest launch session.\n     * @param launch_session_id The ID of the session to clear.\n     */\n    void\n    session_clear(uint32_t launch_session_id) {\n      // We currently only support a single pending RTSP session,\n      // so the ID should always match the one for that session.\n      auto launch_session = launch_event.view(0s);\n      if (launch_session) {\n        if (launch_session->id != launch_session_id) {\n          BOOST_LOG(error) << \"Attempted to clear unexpected session: \"sv << launch_session_id << \" vs \"sv << launch_session->id;\n        }\n        else {\n          raised_timer.cancel();\n          launch_event.pop();\n        }\n      }\n    }\n\n    /**\n     * @brief Get the number of active sessions.\n     * @return Count of active sessions.\n     */\n    int\n    session_count() {\n      auto lg = _session_slots.lock();\n      return _session_slots->size();\n    }\n\n    safe::event_t<std::shared_ptr<launch_session_t>> launch_event;\n\n    /**\n     * @brief Clear launch sessions.\n     * @param all If true, clear all sessions. Otherwise, only clear timed out and stopped sessions.\n     * @examples\n     * clear(false);\n     * @examples_end\n     */\n    void\n    clear(bool all = true) {\n      auto lg = _session_slots.lock();\n\n      for (auto i = _session_slots->begin(); i != _session_slots->end();) {\n        auto &slot = *(*i);\n        if (all || stream::session::state(slot) == stream::session::state_e::STOPPING) {\n          stream::session::stop(slot);\n          stream::session::join(slot);\n\n          i = _session_slots->erase(i);\n        }\n        else {\n          i++;\n        }\n      }\n    }\n\n    /**\n     * @brief Removes the provided session from the set of sessions.\n     * @param session The session to remove.\n     */\n    void\n    remove(const std::shared_ptr<stream::session_t> &session) {\n      auto lg = _session_slots.lock();\n      _session_slots->erase(session);\n    }\n\n    /**\n     * @brief Inserts the provided session into the set of sessions.\n     * @param session The session to insert.\n     */\n    void\n    insert(const std::shared_ptr<stream::session_t> &session) {\n      auto lg = _session_slots.lock();\n      _session_slots->emplace(session);\n      BOOST_LOG(info) << \"New streaming session started [active sessions: \"sv << _session_slots->size() << ']';\n    }\n\n    /**\n     * @brief Runs an iteration of the RTSP server loop\n     */\n    void\n    iterate() {\n      // If we have a session, we will return to the server loop every\n      // 500ms to allow session cleanup to happen.\n      if (session_count() > 0) {\n        io_context.run_one_for(500ms);\n      }\n      else {\n        io_context.run_one();\n      }\n    }\n\n    /**\n     * @brief Stop the RTSP server.\n     */\n    void\n    stop() {\n      acceptor.close();\n      io_context.stop();\n      clear();\n    }\n\n  private:\n    std::unordered_map<std::string_view, cmd_func_t> _map_cmd_cb;\n\n    sync_util::sync_t<std::set<std::shared_ptr<stream::session_t>>> _session_slots;\n\n    boost::asio::io_context io_context;\n    tcp::acceptor acceptor { io_context };\n    boost::asio::steady_timer raised_timer { io_context };\n\n    std::shared_ptr<socket_t> next_socket;\n  };\n\n  rtsp_server_t server {};\n\n  void\n  launch_session_raise(std::shared_ptr<launch_session_t> launch_session) {\n    server.session_raise(std::move(launch_session));\n  }\n\n  void\n  launch_session_clear(uint32_t launch_session_id) {\n    server.session_clear(launch_session_id);\n  }\n\n  int\n  session_count() {\n    // Ensure session_count is up-to-date\n    server.clear(false);\n\n    return server.session_count();\n  }\n\n  void\n  terminate_sessions() {\n    server.clear(true);\n  }\n\n  int\n  send(tcp::socket &sock, const std::string_view &sv) {\n    std::size_t bytes_send = 0;\n\n    while (bytes_send != sv.size()) {\n      boost::system::error_code ec;\n      bytes_send += sock.send(boost::asio::buffer(sv.substr(bytes_send)), 0, ec);\n\n      if (ec) {\n        BOOST_LOG(error) << \"RTSP: Couldn't send data over tcp socket: \"sv << ec.message();\n        return -1;\n      }\n    }\n\n    return 0;\n  }\n\n  void\n  respond(tcp::socket &sock, launch_session_t &session, msg_t &resp) {\n    auto payload = std::make_pair(resp->payload, resp->payloadLength);\n\n    // Restore response message for proper destruction\n    auto lg = util::fail_guard([&]() {\n      resp->payload = payload.first;\n      resp->payloadLength = payload.second;\n    });\n\n    resp->payload = nullptr;\n    resp->payloadLength = 0;\n\n    int serialized_len;\n    util::c_ptr<char> raw_resp { serializeRtspMessage(resp.get(), &serialized_len) };\n    BOOST_LOG(debug)\n      << \"---Begin Response---\"sv << std::endl\n      << std::string_view { raw_resp.get(), (std::size_t) serialized_len } << std::endl\n      << std::string_view { payload.first, (std::size_t) payload.second } << std::endl\n      << \"---End Response---\"sv << std::endl;\n\n    // Encrypt the RTSP message if encryption is enabled\n    if (session.rtsp_cipher) {\n      // We use the deterministic IV construction algorithm specified in NIST SP 800-38D\n      // Section 8.2.1. The sequence number is our \"invocation\" field and the 'RH' in the\n      // high bytes is the \"fixed\" field. Because each client provides their own unique\n      // key, our values in the fixed field need only uniquely identify each independent\n      // use of the client's key with AES-GCM in our code.\n      //\n      // The sequence number is 32 bits long which allows for 2^32 RTSP messages to be\n      // sent to each client before the IV repeats.\n      crypto::aes_t iv(12);\n      session.rtsp_iv_counter++;\n      std::copy_n((uint8_t *) &session.rtsp_iv_counter, sizeof(session.rtsp_iv_counter), std::begin(iv));\n      iv[10] = 'H';  // Host originated\n      iv[11] = 'R';  // RTSP\n\n      // Allocate the message with an empty header and reserved space for the payload\n      auto payload_length = serialized_len + payload.second;\n      std::vector<uint8_t> message(sizeof(encrypted_rtsp_header_t));\n      message.reserve(message.size() + payload_length);\n\n      // Copy the complete plaintext into the message\n      std::copy_n(raw_resp.get(), serialized_len, std::back_inserter(message));\n      std::copy_n(payload.first, payload.second, std::back_inserter(message));\n\n      // Initialize the message header\n      auto header = (encrypted_rtsp_header_t *) message.data();\n      header->typeAndLength = util::endian::big<std::uint32_t>(encrypted_rtsp_header_t::ENCRYPTED_MESSAGE_TYPE_BIT + payload_length);\n      header->sequenceNumber = util::endian::big<std::uint32_t>(session.rtsp_iv_counter);\n\n      // Encrypt the RTSP message in place\n      session.rtsp_cipher->encrypt(std::string_view { (const char *) header->payload(), (std::size_t) payload_length }, header->tag, &iv);\n\n      // Send the full encrypted message\n      send(sock, std::string_view { (char *) message.data(), message.size() });\n    }\n    else {\n      std::string_view tmp_resp { raw_resp.get(), (size_t) serialized_len };\n\n      // Send the plaintext RTSP message header\n      if (send(sock, tmp_resp)) {\n        return;\n      }\n\n      // Send the plaintext RTSP message payload (if present)\n      send(sock, std::string_view { payload.first, (std::size_t) payload.second });\n    }\n  }\n\n  void\n  respond(tcp::socket &sock, launch_session_t &session, POPTION_ITEM options, int statuscode, const char *status_msg, int seqn, const std::string_view &payload) {\n    msg_t resp { new msg_t::element_type };\n    createRtspResponse(resp.get(), nullptr, 0, const_cast<char *>(\"RTSP/1.0\"), statuscode, const_cast<char *>(status_msg), seqn, options, const_cast<char *>(payload.data()), (int) payload.size());\n\n    respond(sock, session, resp);\n  }\n\n  void\n  cmd_not_found(tcp::socket &sock, launch_session_t &session, msg_t &&req) {\n    respond(sock, session, nullptr, 404, \"NOT FOUND\", req->sequenceNumber, {});\n  }\n\n  void\n  cmd_option(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) {\n    OPTION_ITEM option {};\n\n    // I know these string literals will not be modified\n    option.option = const_cast<char *>(\"CSeq\");\n\n    auto seqn_str = std::to_string(req->sequenceNumber);\n    option.content = const_cast<char *>(seqn_str.c_str());\n\n    respond(sock, session, &option, 200, \"OK\", req->sequenceNumber, {});\n  }\n\n  void\n  cmd_describe(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) {\n    OPTION_ITEM option {};\n\n    // I know these string literals will not be modified\n    option.option = const_cast<char *>(\"CSeq\");\n\n    auto seqn_str = std::to_string(req->sequenceNumber);\n    option.content = const_cast<char *>(seqn_str.c_str());\n\n    std::stringstream ss;\n\n    // Tell the client about our supported features\n    ss << \"a=x-ss-general.featureFlags:\" << (uint32_t) platf::get_capabilities() << std::endl;\n\n    // Always request new control stream encryption if the client supports it\n    uint32_t encryption_flags_supported = SS_ENC_CONTROL_V2 | SS_ENC_AUDIO | SS_ENC_MIC;\n    uint32_t encryption_flags_requested = SS_ENC_CONTROL_V2;\n\n    // Determine the encryption desired for this remote endpoint\n    auto encryption_mode = net::encryption_mode_for_address(sock.remote_endpoint().address());\n    if (encryption_mode != config::ENCRYPTION_MODE_NEVER) {\n      // Advertise support for video encryption if it's not disabled\n      encryption_flags_supported |= SS_ENC_VIDEO;\n\n      // If it's mandatory, also request it to enable use if the client\n      // didn't explicitly opt in, but it otherwise has support.\n      if (encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {\n        encryption_flags_requested |= SS_ENC_VIDEO | SS_ENC_AUDIO | SS_ENC_MIC;\n      } else {\n        // Even if not mandatory, request audio and mic encryption if encryption is enabled\n        // This ensures clients that check encryptionRequested will enable audio and MIC encryption\n        encryption_flags_requested |= SS_ENC_AUDIO | SS_ENC_MIC;\n      }\n    }\n\n    // Report supported and required encryption flags\n    ss << \"a=x-ss-general.encryptionSupported:\" << encryption_flags_supported << std::endl;\n    ss << \"a=x-ss-general.encryptionRequested:\" << encryption_flags_requested << std::endl;\n    \n    // 记录加密请求状态用于调试\n    BOOST_LOG(info) << \"RTSP DESCRIBE encryption flags: supported=0x\" << std::hex << encryption_flags_supported << std::dec\n                    << \", requested=0x\" << std::hex << encryption_flags_requested << std::dec\n                    << \" (CONTROL_V2=\" << ((encryption_flags_requested & SS_ENC_CONTROL_V2) ? \"1\" : \"0\")\n                    << \", VIDEO=\" << ((encryption_flags_requested & SS_ENC_VIDEO) ? \"1\" : \"0\")\n                    << \", AUDIO=\" << ((encryption_flags_requested & SS_ENC_AUDIO) ? \"1\" : \"0\")\n                    << \", MIC=\" << ((encryption_flags_requested & SS_ENC_MIC) ? \"1\" : \"0\") << \")\";\n\n    if (video::last_encoder_probe_supported_ref_frames_invalidation) {\n      ss << \"a=x-nv-video[0].refPicInvalidation:1\"sv << std::endl;\n    }\n\n    if (video::active_hevc_mode != 1) {\n      ss << \"sprop-parameter-sets=AAAAAU\"sv << std::endl;\n    }\n\n    if (video::active_av1_mode != 1) {\n      ss << \"a=rtpmap:98 AV1/90000\"sv << std::endl;\n    }\n\n    if (!session.surround_params.empty()) {\n      // If we have our own surround parameters, advertise them twice first\n      ss << \"a=fmtp:97 surround-params=\"sv << session.surround_params << std::endl;\n      ss << \"a=fmtp:97 surround-params=\"sv << session.surround_params << std::endl;\n    }\n\n    // 添加麦克风流支持（仅在启用时）\n    if (config::audio.stream_mic) {\n      ss << \"m=audio \" << net::map_port(stream::MIC_STREAM_PORT) << \" RTP/AVP 96\" << std::endl;\n      ss << \"a=rtpmap:96 opus/48000/2\" << std::endl;\n      ss << \"a=fmtp:96 minptime=10;useinbandfec=1\" << std::endl;\n    }\n\n    for (int x = 0; x < audio::MAX_STREAM_CONFIG; ++x) {\n      auto &stream_config = audio::stream_configs[x];\n      std::uint8_t mapping[platf::speaker::MAX_SPEAKERS];\n\n      auto mapping_p = stream_config.mapping;\n\n      /**\n       * GFE advertises incorrect mapping for normal quality configurations,\n       * as a result, Moonlight rotates all channels from index '3' to the right\n       * To work around this, rotate channels to the left from index '3'\n       */\n      if (x == audio::SURROUND51 || x == audio::SURROUND71) {\n        std::copy_n(mapping_p, stream_config.channelCount, mapping);\n        std::rotate(mapping + 3, mapping + 4, mapping + stream_config.channelCount);\n\n        mapping_p = mapping;\n      }\n\n      // For channel counts > 8 (e.g., 7.1.4 with 12 channels), use comma-delimited format\n      // because mapping values can exceed single digits (e.g., 10, 11)\n      if (stream_config.channelCount > 8) {\n        ss << \"a=fmtp:97 surround-params=\"sv << stream_config.channelCount;\n\n        // Use comma-delimited format: channelCount,streams,coupledStreams,m0,m1,...\n        ss << ',' << (int) stream_config.streams << ',' << (int) stream_config.coupledStreams;\n\n        std::for_each_n(mapping_p, stream_config.channelCount, [&ss](std::uint8_t val) {\n          ss << ',' << (int) val;\n        });\n      }\n      else {\n        ss << \"a=fmtp:97 surround-params=\"sv << stream_config.channelCount << stream_config.streams << stream_config.coupledStreams;\n\n        std::for_each_n(mapping_p, stream_config.channelCount, [&ss](std::uint8_t digit) {\n          ss << (char) (digit + '0');\n        });\n      }\n\n      ss << std::endl;\n    }\n\n    respond(sock, session, &option, 200, \"OK\", req->sequenceNumber, ss.str());\n  }\n\n  void\n  cmd_setup(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) {\n    OPTION_ITEM options[4] {};\n\n    auto &seqn = options[0];\n    auto &session_option = options[1];\n    auto &port_option = options[2];\n    auto &payload_option = options[3];\n\n    seqn.option = const_cast<char *>(\"CSeq\");\n\n    auto seqn_str = std::to_string(req->sequenceNumber);\n    seqn.content = const_cast<char *>(seqn_str.c_str());\n\n    std::string_view target { req->message.request.target };\n    auto begin = std::find(std::begin(target), std::end(target), '=') + 1;\n    auto end = std::find(begin, std::end(target), '/');\n    std::string_view type { begin, (size_t) std::distance(begin, end) };\n\n    std::uint16_t port;\n    if (type == \"audio\"sv) {\n      session.setup_audio = true;\n      port = net::map_port(stream::AUDIO_STREAM_PORT);\n    }\n    else if (type == \"video\"sv) {\n      session.setup_video = true;\n      port = net::map_port(stream::VIDEO_STREAM_PORT);\n    }\n    else if (type == \"control\"sv) {\n      session.setup_control = true;\n      port = net::map_port(stream::CONTROL_PORT);\n    }\n    else if (type == \"mic\"sv) {\n      session.enable_mic = true;\n      session.setup_mic = true;\n      port = net::map_port(stream::MIC_STREAM_PORT);\n    }\n    else {\n      cmd_not_found(sock, session, std::move(req));\n\n      return;\n    }\n\n    seqn.next = &session_option;\n\n    session_option.option = const_cast<char *>(\"Session\");\n    session_option.content = const_cast<char *>(\"DEADBEEFCAFE;timeout = 90\");\n\n    session_option.next = &port_option;\n\n    // Moonlight merely requires 'server_port=<port>'\n    auto port_value = \"server_port=\" + std::to_string(port);\n\n    port_option.option = const_cast<char *>(\"Transport\");\n    port_option.content = port_value.data();\n\n    // Send identifiers that will be echoed in the other connections\n    auto connect_data = std::to_string(session.control_connect_data);\n    if (type == \"control\"sv) {\n      payload_option.option = const_cast<char *>(\"X-SS-Connect-Data\");\n      payload_option.content = connect_data.data();\n    }\n    else {\n      payload_option.option = const_cast<char *>(\"X-SS-Ping-Payload\");\n      payload_option.content = session.av_ping_payload.data();\n    }\n\n    port_option.next = &payload_option;\n\n    respond(sock, session, &seqn, 200, \"OK\", req->sequenceNumber, {});\n  }\n\n  void\n  cmd_announce(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) {\n    OPTION_ITEM option {};\n\n    // I know these string literals will not be modified\n    option.option = const_cast<char *>(\"CSeq\");\n\n    auto seqn_str = std::to_string(req->sequenceNumber);\n    option.content = const_cast<char *>(seqn_str.c_str());\n\n    std::string_view payload { req->payload, (size_t) req->payloadLength };\n\n    std::vector<std::string_view> lines;\n\n    auto whitespace = [](char ch) {\n      return ch == '\\n' || ch == '\\r';\n    };\n\n    {\n      auto pos = std::begin(payload);\n      auto begin = pos;\n      while (pos != std::end(payload)) {\n        if (whitespace(*pos++)) {\n          lines.emplace_back(begin, pos - begin - 1);\n\n          while (pos != std::end(payload) && whitespace(*pos)) {\n            ++pos;\n          }\n          begin = pos;\n        }\n      }\n    }\n\n    std::string_view client;\n    std::unordered_map<std::string_view, std::string_view> args;\n\n    for (auto line : lines) {\n      auto type = line.substr(0, 2);\n      if (type == \"s=\"sv) {\n        client = line.substr(2);\n      }\n      else if (type == \"a=\") {\n        auto pos = line.find(':');\n\n        auto name = line.substr(2, pos - 2);\n        auto val = line.substr(pos + 1);\n\n        if (val[val.size() - 1] == ' ') {\n          val = val.substr(0, val.size() - 1);\n        }\n        args.emplace(name, val);\n      }\n    }\n\n    // Initialize any omitted parameters to defaults\n    args.try_emplace(\"x-nv-video[0].encoderCscMode\"sv, \"0\"sv);\n    args.try_emplace(\"x-nv-vqos[0].bitStreamFormat\"sv, \"0\"sv);\n    args.try_emplace(\"x-nv-video[0].dynamicRangeMode\"sv, \"0\"sv);\n    args.try_emplace(\"x-nv-aqos.packetDuration\"sv, \"5\"sv);\n    args.try_emplace(\"x-nv-general.useReliableUdp\"sv, \"1\"sv);\n    args.try_emplace(\"x-nv-vqos[0].fec.minRequiredFecPackets\"sv, \"0\"sv);\n    args.try_emplace(\"x-nv-general.featureFlags\"sv, \"135\"sv);\n    args.try_emplace(\"x-ml-general.featureFlags\"sv, \"0\"sv);\n    args.try_emplace(\"x-nv-vqos[0].qosTrafficType\"sv, \"5\"sv);\n    args.try_emplace(\"x-nv-aqos.qosTrafficType\"sv, \"4\"sv);\n    args.try_emplace(\"x-ml-video.configuredBitrateKbps\"sv, \"0\"sv);\n    args.try_emplace(\"x-ss-general.encryptionEnabled\"sv, \"0\"sv);\n    args.try_emplace(\"x-ss-video[0].chromaSamplingType\"sv, \"0\"sv);\n    args.try_emplace(\"x-ss-video[0].intraRefresh\"sv, \"0\"sv);\n    args.try_emplace(\"x-nv-video[0].clientRefreshRateX100\"sv, \"0\"sv);  // NTSC framerate support (e.g., 5994 = 59.94fps)\n\n    stream::config_t config;\n\n    std::int64_t configuredBitrateKbps;\n    config.audio.flags[audio::config_t::HOST_AUDIO] = session.host_audio;\n    auto getArg = [&args](std::string_view key) {\n      return util::from_view(args.at(key));\n    };\n\n    try {\n      config.audio.channels = getArg(\"x-nv-audio.surround.numChannels\"sv);\n      config.audio.mask = getArg(\"x-nv-audio.surround.channelMask\"sv);\n      config.audio.packetDuration = getArg(\"x-nv-aqos.packetDuration\"sv);\n      config.audio.flags[audio::config_t::HIGH_QUALITY] = getArg(\"x-nv-audio.surround.AudioQuality\"sv);\n\n      config.controlProtocolType = getArg(\"x-nv-general.useReliableUdp\"sv);\n      config.packetsize = getArg(\"x-nv-video[0].packetSize\"sv);\n      config.minRequiredFecPackets = getArg(\"x-nv-vqos[0].fec.minRequiredFecPackets\"sv);\n      config.mlFeatureFlags = getArg(\"x-ml-general.featureFlags\"sv);\n      config.audioQosType = getArg(\"x-nv-aqos.qosTrafficType\"sv);\n      config.videoQosType = getArg(\"x-nv-vqos[0].qosTrafficType\"sv);\n      config.encryptionFlagsEnabled = getArg(\"x-ss-general.encryptionEnabled\"sv);\n\n      // Legacy clients use nvFeatureFlags to indicate support for audio encryption\n      if (getArg(\"x-nv-general.featureFlags\"sv) & 0x20) {\n        config.encryptionFlagsEnabled |= SS_ENC_AUDIO;\n      }\n\n      auto &monitor = config.monitor;\n      monitor.height = getArg(\"x-nv-video[0].clientViewportHt\"sv);\n      monitor.width = getArg(\"x-nv-video[0].clientViewportWd\"sv);\n      BOOST_LOG(info) << \"Client requested stream resolution (clientViewport): \" << monitor.width << \"x\" << monitor.height;\n      monitor.framerate = getArg(\"x-nv-video[0].maxFPS\"sv);\n      monitor.bitrate = getArg(\"x-nv-vqos[0].bw.maximumBitrateKbps\"sv);\n      monitor.slicesPerFrame = getArg(\"x-nv-video[0].videoEncoderSlicesPerFrame\"sv);\n      monitor.numRefFrames = getArg(\"x-nv-video[0].maxNumReferenceFrames\"sv);\n      monitor.encoderCscMode = getArg(\"x-nv-video[0].encoderCscMode\"sv);\n      monitor.videoFormat = getArg(\"x-nv-vqos[0].bitStreamFormat\"sv);\n      monitor.dynamicRange = getArg(\"x-nv-video[0].dynamicRangeMode\"sv);\n      monitor.chromaSamplingType = getArg(\"x-ss-video[0].chromaSamplingType\"sv);\n      monitor.enableIntraRefresh = getArg(\"x-ss-video[0].intraRefresh\"sv);\n\n      int clientRefreshRateX100 = getArg(\"x-nv-video[0].clientRefreshRateX100\"sv);\n\n      // Only use clientRefreshRateX100 if it's within 2% of maxFPS\n      bool useClientRefreshRate = false;\n      if (clientRefreshRateX100 > 0 && monitor.framerate > 0) {\n        double ratio = (clientRefreshRateX100 / 100.0) / monitor.framerate;\n        useClientRefreshRate = (ratio > 0.98 && ratio < 1.02);\n      }\n\n      if (useClientRefreshRate) {\n        int remainder = clientRefreshRateX100 % 100;\n        monitor.frameRateNum = (remainder == 0) ? clientRefreshRateX100 / 100 : clientRefreshRateX100;\n        monitor.frameRateDen = (remainder == 0) ? 1 : 100;\n\n        BOOST_LOG(info) << \"Client framerate: \" << clientRefreshRateX100 / 100.0 << \" fps (\"\n                        << monitor.frameRateNum << \"/\" << monitor.frameRateDen << \")\";\n      }\n      else {\n        monitor.frameRateNum = monitor.framerate;\n        monitor.frameRateDen = 1;\n      }\n\n      configuredBitrateKbps = getArg(\"x-ml-video.configuredBitrateKbps\"sv);\n\n      // Set display_name from session environment or use global configuration\n      if (auto it = session.env.find(\"SUNSHINE_CLIENT_DISPLAY_NAME\"); it != session.env.end()) {\n        monitor.display_name = it->to_string();\n        BOOST_LOG(info) << \"Session using specified display: \" << monitor.display_name;\n      }\n      else {\n        monitor.display_name = config::video.output_name;\n      }\n    }\n    catch (std::out_of_range &) {\n      respond(sock, session, &option, 400, \"BAD REQUEST\", req->sequenceNumber, {});\n      return;\n    }\n\n    // When using stereo audio, the audio quality is (strangely) indicated by whether the Host field\n    // in the RTSP message matches a local interface's IP address. Fortunately, Moonlight always sends\n    // 0.0.0.0 when it wants low quality, so it is easy to check without enumerating interfaces.\n    if (config.audio.channels == 2) {\n      for (auto option = req->options; option != nullptr; option = option->next) {\n        if (\"Host\"sv == option->option) {\n          std::string_view content { option->content };\n          BOOST_LOG(debug) << \"Found Host: \"sv << content;\n          config.audio.flags[audio::config_t::HIGH_QUALITY] = (content.find(\"0.0.0.0\"sv) == std::string::npos);\n        }\n      }\n    }\n    else if (!session.surround_params.empty()) {\n      config.audio.flags[audio::config_t::CUSTOM_SURROUND_PARAMS] =\n        parse_surround_params(session.surround_params, config.audio.channels, config.audio.customStreamParams);\n    }\n\n    if (config.audio.channels == 12 && !config.audio.flags[audio::config_t::CUSTOM_SURROUND_PARAMS]) {\n      config.audio.customStreamParams.channelCount = 12;\n      config.audio.customStreamParams.streams = 8;\n      config.audio.customStreamParams.coupledStreams = 4;\n      std::copy_n(std::begin(platf::speaker::map_surround714), 12, std::begin(config.audio.customStreamParams.mapping));\n      config.audio.flags[audio::config_t::CUSTOM_SURROUND_PARAMS] = true;\n    }\n\n    // If the client sent a configured bitrate, we will choose the actual bitrate ourselves\n    // by using FEC percentage and audio quality settings. If the calculated bitrate ends up\n    // too low, we'll allow it to exceed the limits rather than reducing the encoding bitrate\n    // down to nearly nothing.\n    if (configuredBitrateKbps) {\n      BOOST_LOG(debug) << \"Client configured bitrate is \"sv << configuredBitrateKbps << \" Kbps\"sv;\n\n      // If the FEC percentage isn't too high, adjust the configured bitrate to ensure video\n      // traffic doesn't exceed the user's selected bitrate when the FEC shards are included.\n      if (config::stream.fec_percentage <= 80) {\n        configuredBitrateKbps /= 100.f / (100 - config::stream.fec_percentage);\n      }\n\n      // Adjust the bitrate to account for audio traffic bandwidth usage (capped at 20% reduction).\n      // The bitrate per channel is 256 Kbps for high quality mode and 96 Kbps for normal quality.\n      auto audioBitrateAdjustment = (config.audio.flags[audio::config_t::HIGH_QUALITY] ? 256 : 96) * config.audio.channels;\n      configuredBitrateKbps -= std::min((std::int64_t) audioBitrateAdjustment, configuredBitrateKbps / 5);\n\n      // Reduce it by another 500Kbps to account for A/V packet overhead and control data\n      // traffic (capped at 10% reduction).\n      configuredBitrateKbps -= std::min((std::int64_t) 500, configuredBitrateKbps / 10);\n\n      BOOST_LOG(debug) << \"Final adjusted video encoding bitrate is \"sv << configuredBitrateKbps << \" Kbps\"sv;\n      config.monitor.bitrate = configuredBitrateKbps;\n    }\n\n    if (config.monitor.videoFormat == 1 && video::active_hevc_mode == 1) {\n      BOOST_LOG(warning) << \"HEVC is disabled, yet the client requested HEVC\"sv;\n\n      respond(sock, session, &option, 400, \"BAD REQUEST\", req->sequenceNumber, {});\n      return;\n    }\n\n    if (config.monitor.videoFormat == 2 && video::active_av1_mode == 1) {\n      BOOST_LOG(warning) << \"AV1 is disabled, yet the client requested AV1\"sv;\n\n      respond(sock, session, &option, 400, \"BAD REQUEST\", req->sequenceNumber, {});\n      return;\n    }\n\n    // 检测是否仅控制流会话（只有 control 流被设置，没有 video 和 audio）\n    session.control_only = session.setup_control && !session.setup_video && !session.setup_audio;\n    if (session.control_only) {\n      BOOST_LOG(info) << \"Control-only session detected: client [\"sv << session.client_name << \"] will only provide input control\"sv;\n    }\n\n    // Check that any required encryption is enabled\n    // 对于仅控制流会话，跳过视频/音频加密检查\n    if (!session.control_only) {\n      auto encryption_mode = net::encryption_mode_for_address(sock.remote_endpoint().address());\n      if (encryption_mode == config::ENCRYPTION_MODE_MANDATORY &&\n          (config.encryptionFlagsEnabled & (SS_ENC_VIDEO | SS_ENC_AUDIO)) != (SS_ENC_VIDEO | SS_ENC_AUDIO)) {\n        BOOST_LOG(error) << \"Rejecting client that cannot comply with mandatory encryption requirement\"sv;\n\n        respond(sock, session, &option, 403, \"Forbidden\", req->sequenceNumber, {});\n        return;\n      }\n    }\n\n    auto stream_session = stream::session::alloc(config, session);\n    server->insert(stream_session);\n\n    if (stream::session::start(*stream_session, sock.remote_endpoint().address().to_string())) {\n      BOOST_LOG(error) << \"Failed to start a streaming session\"sv;\n\n      server->remove(stream_session);\n      respond(sock, session, &option, 500, \"Internal Server Error\", req->sequenceNumber, {});\n      return;\n    }\n\n    respond(sock, session, &option, 200, \"OK\", req->sequenceNumber, {});\n  }\n\n  void\n  cmd_play(rtsp_server_t *server, tcp::socket &sock, launch_session_t &session, msg_t &&req) {\n    OPTION_ITEM option {};\n\n    // I know these string literals will not be modified\n    option.option = const_cast<char *>(\"CSeq\");\n\n    auto seqn_str = std::to_string(req->sequenceNumber);\n    option.content = const_cast<char *>(seqn_str.c_str());\n\n    respond(sock, session, &option, 200, \"OK\", req->sequenceNumber, {});\n  }\n\n  void\n  start() {\n    auto shutdown_event = mail::man->event<bool>(mail::shutdown);\n\n    server.map(\"OPTIONS\"sv, &cmd_option);\n    server.map(\"DESCRIBE\"sv, &cmd_describe);\n    server.map(\"SETUP\"sv, &cmd_setup);\n    server.map(\"ANNOUNCE\"sv, &cmd_announce);\n    server.map(\"PLAY\"sv, &cmd_play);\n\n    boost::system::error_code ec;\n    if (server.bind(net::af_from_enum_string(config::sunshine.address_family), net::map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) {\n      BOOST_LOG(fatal) << \"Couldn't bind RTSP server to port [\"sv << net::map_port(rtsp_stream::RTSP_SETUP_PORT) << \"], \" << ec.message();\n      shutdown_event->raise(true);\n\n      return;\n    }\n\n    std::thread rtsp_thread { [&shutdown_event] {\n      auto broadcast_shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);\n\n      while (!shutdown_event->peek()) {\n        server.iterate();\n\n        if (broadcast_shutdown_event->peek()) {\n          server.clear();\n        }\n        else {\n          // cleanup all stopped sessions\n          server.clear(false);\n        }\n      }\n\n      server.clear();\n    } };\n\n    // Wait for shutdown\n    shutdown_event->view();\n\n    // Stop the server and join the server thread\n    server.stop();\n    rtsp_thread.join();\n  }\n\n  void\n  print_msg(PRTSP_MESSAGE msg) {\n    std::string_view type = msg->type == TYPE_RESPONSE ? \"RESPONSE\"sv : \"REQUEST\"sv;\n\n    std::string_view payload { msg->payload, (size_t) msg->payloadLength };\n    std::string_view protocol { msg->protocol };\n    auto seqnm = msg->sequenceNumber;\n    std::string_view messageBuffer { msg->messageBuffer };\n\n    std::ostringstream log_stream;\n    log_stream << \"type [\"sv << type << \"], sequence number [\"sv << seqnm << \"], protocol :: \"sv << protocol << \", payload :: \"sv << payload;\n\n    if (msg->type == TYPE_RESPONSE) {\n      auto &resp = msg->message.response;\n\n      auto statuscode = resp.statusCode;\n      std::string_view status { resp.statusString };\n\n      log_stream << \"statuscode :: \"sv << statuscode << \", status :: \"sv << status;\n    }\n    else {\n      auto &req = msg->message.request;\n\n      std::string_view command { req.command };\n      std::string_view target { req.target };\n\n      log_stream << \"command :: \"sv << command << \", target :: \"sv << target;\n    }\n\n    for (auto option = msg->options; option != nullptr; option = option->next) {\n      std::string_view content { option->content };\n      std::string_view name { option->option };\n\n      log_stream << name << \" :: \"sv << content;\n    }\n\n    log_stream << std::endl\n               << \"---Begin MessageBuffer---\"sv << std::endl\n               << messageBuffer << std::endl\n               << \"---End MessageBuffer---\"sv;\n    BOOST_LOG(debug) << log_stream.str();\n  }\n}  // namespace rtsp_stream\n"
  },
  {
    "path": "src/rtsp.h",
    "content": "/**\n * @file src/rtsp.h\n * @brief Declarations for RTSP streaming.\n */\n#pragma once\n\n#include <atomic>\n\n#include <boost/process/v1.hpp>\n\n#include \"crypto.h\"\n#include \"thread_safe.h\"\n\nnamespace rtsp_stream {\n  constexpr auto RTSP_SETUP_PORT = 21;\n\n  struct launch_session_t {\n    uint32_t id;\n\n    crypto::aes_t gcm_key;\n    crypto::aes_t iv;\n\n    std::string av_ping_payload;\n    uint32_t control_connect_data;\n\n    boost::process::v1::environment env;\n\n    bool host_audio;\n    std::string unique_id;\n    std::string client_name;\n    int width;\n    int height;\n    int fps;\n    int gcmap;\n    int appid;\n    int surround_info;\n    std::string surround_params;\n    bool enable_hdr;\n    bool enable_sops;\n    bool enable_mic;\n    bool use_vdd;\n    int custom_screen_mode;\n    float max_nits;\n    float min_nits;\n    float max_full_nits;\n\n    std::optional<crypto::cipher::gcm_t> rtsp_cipher;\n    std::string rtsp_url_scheme;\n    uint32_t rtsp_iv_counter;\n\n    // 跟踪已设置的流类型\n    bool setup_video { false };\n    bool setup_audio { false };\n    bool setup_control { false };\n    bool setup_mic { false };\n    bool control_only { false };\n  };\n\n  void\n  launch_session_raise(std::shared_ptr<launch_session_t> launch_session);\n\n  /**\n   * @brief Clear state for the specified launch session.\n   * @param launch_session_id The ID of the session to clear.\n   */\n  void\n  launch_session_clear(uint32_t launch_session_id);\n\n  /**\n   * @brief Get the number of active sessions.\n   * @return Count of active sessions.\n   */\n  int\n  session_count();\n\n  /**\n   * @brief Terminates all running streaming sessions.\n   */\n  void\n  terminate_sessions();\n\n  /**\n   * @brief Runs the RTSP server loop.\n   */\n  void start();\n}  // namespace rtsp_stream\n"
  },
  {
    "path": "src/stat_trackers.cpp",
    "content": "/**\r\n * @file src/stat_trackers.cpp\r\n * @brief Definitions for streaming statistic tracking.\r\n */\r\n#include \"stat_trackers.h\"\r\n\r\nnamespace stat_trackers {\r\n\r\n  boost::format\r\n  one_digit_after_decimal() {\r\n    return boost::format(\"%1$.1f\");\r\n  }\r\n\r\n  boost::format\r\n  two_digits_after_decimal() {\r\n    return boost::format(\"%1$.2f\");\r\n  }\r\n\r\n}  // namespace stat_trackers\r\n"
  },
  {
    "path": "src/stat_trackers.h",
    "content": "/**\r\n * @file src/stat_trackers.h\r\n * @brief Declarations for streaming statistic tracking.\r\n */\r\n#pragma once\r\n\r\n#include <chrono>\r\n#include <functional>\r\n#include <limits>\r\n\r\n#include <boost/format.hpp>\r\n\r\nnamespace stat_trackers {\r\n\r\n  boost::format\r\n  one_digit_after_decimal();\r\n\r\n  boost::format\r\n  two_digits_after_decimal();\r\n\r\n  template <typename T>\r\n  class min_max_avg_tracker {\r\n  public:\r\n    using callback_function = std::function<void(T stat_min, T stat_max, double stat_avg)>;\r\n\r\n    void\r\n    collect_and_callback_on_interval(T stat, const callback_function &callback, std::chrono::seconds interval_in_seconds) {\r\n      if (data.calls == 0) {\r\n        data.last_callback_time = std::chrono::steady_clock::now();\r\n      }\r\n      else if (std::chrono::steady_clock::now() > data.last_callback_time + interval_in_seconds) {\r\n        callback(data.stat_min, data.stat_max, data.stat_total / data.calls);\r\n        data = {};\r\n      }\r\n      data.stat_min = std::min(data.stat_min, stat);\r\n      data.stat_max = std::max(data.stat_max, stat);\r\n      data.stat_total += stat;\r\n      data.calls += 1;\r\n    }\r\n\r\n    void\r\n    reset() {\r\n      data = {};\r\n    }\r\n\r\n  private:\r\n    struct {\r\n      std::chrono::steady_clock::time_point last_callback_time = std::chrono::steady_clock::now();\r\n      T stat_min = std::numeric_limits<T>::max();\r\n      T stat_max = std::numeric_limits<T>::min();\r\n      double stat_total = 0;\r\n      uint32_t calls = 0;\r\n    } data;\r\n  };\r\n\r\n}  // namespace stat_trackers\r\n"
  },
  {
    "path": "src/stb_image.h",
    "content": "/* stb_image - v2.30 - public domain image loader - http://nothings.org/stb\n                                  no warranty implied; use at your own risk\n\n   Do this:\n      #define STB_IMAGE_IMPLEMENTATION\n   before you include this file in *one* C or C++ file to create the implementation.\n\n   // i.e. it should look like this:\n   #include ...\n   #include ...\n   #include ...\n   #define STB_IMAGE_IMPLEMENTATION\n   #include \"stb_image.h\"\n\n   You can #define STBI_ASSERT(x) before the #include to avoid using assert.h.\n   And #define STBI_MALLOC, STBI_REALLOC, and STBI_FREE to avoid using malloc,realloc,free\n\n\n   QUICK NOTES:\n      Primarily of interest to game developers and other people who can\n          avoid problematic images and only need the trivial interface\n\n      JPEG baseline & progressive (12 bpc/arithmetic not supported, same as stock IJG lib)\n      PNG 1/2/4/8/16-bit-per-channel\n\n      TGA (not sure what subset, if a subset)\n      BMP non-1bpp, non-RLE\n      PSD (composited view only, no extra channels, 8/16 bit-per-channel)\n\n      GIF (*comp always reports as 4-channel)\n      HDR (radiance rgbE format)\n      PIC (Softimage PIC)\n      PNM (PPM and PGM binary only)\n\n      Animated GIF still needs a proper API, but here's one way to do it:\n          http://gist.github.com/urraka/685d9a6340b26b830d49\n\n      - decode from memory or through FILE (define STBI_NO_STDIO to remove code)\n      - decode from arbitrary I/O callbacks\n      - SIMD acceleration on x86/x64 (SSE2) and ARM (NEON)\n\n   Full documentation under \"DOCUMENTATION\" below.\n\n\nLICENSE\n\n  See end of file for license information.\n\nRECENT REVISION HISTORY:\n\n      2.30  (2024-05-31) avoid erroneous gcc warning\n      2.29  (2023-05-xx) optimizations\n      2.28  (2023-01-29) many error fixes, security errors, just tons of stuff\n      2.27  (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes\n      2.26  (2020-07-13) many minor fixes\n      2.25  (2020-02-02) fix warnings\n      2.24  (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically\n      2.23  (2019-08-11) fix clang static analysis warning\n      2.22  (2019-03-04) gif fixes, fix warnings\n      2.21  (2019-02-25) fix typo in comment\n      2.20  (2019-02-07) support utf8 filenames in Windows; fix warnings and platform ifdefs\n      2.19  (2018-02-11) fix warning\n      2.18  (2018-01-30) fix warnings\n      2.17  (2018-01-29) bugfix, 1-bit BMP, 16-bitness query, fix warnings\n      2.16  (2017-07-23) all functions have 16-bit variants; optimizations; bugfixes\n      2.15  (2017-03-18) fix png-1,2,4; all Imagenet JPGs; no runtime SSE detection on GCC\n      2.14  (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet JPGs\n      2.13  (2016-12-04) experimental 16-bit API, only for PNG so far; fixes\n      2.12  (2016-04-02) fix typo in 2.11 PSD fix that caused crashes\n      2.11  (2016-04-02) 16-bit PNGS; enable SSE2 in non-gcc x64\n                         RGB-format JPEG; remove white matting in PSD;\n                         allocate large structures on the stack;\n                         correct channel count for PNG & BMP\n      2.10  (2016-01-22) avoid warning introduced in 2.09\n      2.09  (2016-01-16) 16-bit TGA; comments in PNM files; STBI_REALLOC_SIZED\n\n   See end of file for full revision history.\n\n\n ============================    Contributors    =========================\n\n Image formats                          Extensions, features\n    Sean Barrett (jpeg, png, bmp)          Jetro Lauha (stbi_info)\n    Nicolas Schulz (hdr, psd)              Martin \"SpartanJ\" Golini (stbi_info)\n    Jonathan Dummer (tga)                  James \"moose2000\" Brown (iPhone PNG)\n    Jean-Marc Lienher (gif)                Ben \"Disch\" Wenger (io callbacks)\n    Tom Seddon (pic)                       Omar Cornut (1/2/4-bit PNG)\n    Thatcher Ulrich (psd)                  Nicolas Guillemot (vertical flip)\n    Ken Miller (pgm, ppm)                  Richard Mitton (16-bit PSD)\n    github:urraka (animated gif)           Junggon Kim (PNM comments)\n    Christopher Forseth (animated gif)     Daniel Gibson (16-bit TGA)\n                                           socks-the-fox (16-bit PNG)\n                                           Jeremy Sawicki (handle all ImageNet JPGs)\n Optimizations & bugfixes                  Mikhail Morozov (1-bit BMP)\n    Fabian \"ryg\" Giesen                    Anael Seghezzi (is-16-bit query)\n    Arseny Kapoulkine                      Simon Breuss (16-bit PNM)\n    John-Mark Allen\n    Carmelo J Fdez-Aguera\n\n Bug & warning fixes\n    Marc LeBlanc            David Woo          Guillaume George     Martins Mozeiko\n    Christpher Lloyd        Jerry Jansson      Joseph Thomson       Blazej Dariusz Roszkowski\n    Phil Jordan                                Dave Moore           Roy Eltham\n    Hayaki Saito            Nathan Reed        Won Chun\n    Luke Graham             Johan Duparc       Nick Verigakis       the Horde3D community\n    Thomas Ruf              Ronny Chevalier                         github:rlyeh\n    Janez Zemva             John Bartholomew   Michal Cichon        github:romigrou\n    Jonathan Blow           Ken Hamada         Tero Hanninen        github:svdijk\n    Eugene Golushkov        Laurent Gomila     Cort Stratton        github:snagar\n    Aruelien Pocheville     Sergio Gonzalez    Thibault Reuille     github:Zelex\n    Cass Everitt            Ryamond Barbiero                        github:grim210\n    Paul Du Bois            Engin Manap        Aldo Culquicondor    github:sammyhw\n    Philipp Wiesemann       Dale Weiler        Oriol Ferrer Mesia   github:phprus\n    Josh Tobin              Neil Bickford      Matthew Gregan       github:poppolopoppo\n    Julian Raschke          Gregory Mullen     Christian Floisand   github:darealshinji\n    Baldur Karlsson         Kevin Schmidt      JR Smith             github:Michaelangel007\n                            Brad Weinberger    Matvey Cherevko      github:mosra\n    Luca Sas                Alexander Veselov  Zack Middleton       [reserved]\n    Ryan C. Gordon          [reserved]                              [reserved]\n                     DO NOT ADD YOUR NAME HERE\n\n                     Jacko Dirks\n\n  To add your name to the credits, pick a random blank space in the middle and fill it.\n  80% of merge conflicts on stb PRs are due to people adding their name at the end\n  of the credits.\n*/\n\n#ifndef STBI_INCLUDE_STB_IMAGE_H\n#define STBI_INCLUDE_STB_IMAGE_H\n\n// DOCUMENTATION\n//\n// Limitations:\n//    - no 12-bit-per-channel JPEG\n//    - no JPEGs with arithmetic coding\n//    - GIF always returns *comp=4\n//\n// Basic usage (see HDR discussion below for HDR usage):\n//    int x,y,n;\n//    unsigned char *data = stbi_load(filename, &x, &y, &n, 0);\n//    // ... process data if not NULL ...\n//    // ... x = width, y = height, n = # 8-bit components per pixel ...\n//    // ... replace '0' with '1'..'4' to force that many components per pixel\n//    // ... but 'n' will always be the number that it would have been if you said 0\n//    stbi_image_free(data);\n//\n// Standard parameters:\n//    int *x                 -- outputs image width in pixels\n//    int *y                 -- outputs image height in pixels\n//    int *channels_in_file  -- outputs # of image components in image file\n//    int desired_channels   -- if non-zero, # of image components requested in result\n//\n// The return value from an image loader is an 'unsigned char *' which points\n// to the pixel data, or NULL on an allocation failure or if the image is\n// corrupt or invalid. The pixel data consists of *y scanlines of *x pixels,\n// with each pixel consisting of N interleaved 8-bit components; the first\n// pixel pointed to is top-left-most in the image. There is no padding between\n// image scanlines or between pixels, regardless of format. The number of\n// components N is 'desired_channels' if desired_channels is non-zero, or\n// *channels_in_file otherwise. If desired_channels is non-zero,\n// *channels_in_file has the number of components that _would_ have been\n// output otherwise. E.g. if you set desired_channels to 4, you will always\n// get RGBA output, but you can check *channels_in_file to see if it's trivially\n// opaque because e.g. there were only 3 channels in the source image.\n//\n// An output image with N components has the following components interleaved\n// in this order in each pixel:\n//\n//     N=#comp     components\n//       1           grey\n//       2           grey, alpha\n//       3           red, green, blue\n//       4           red, green, blue, alpha\n//\n// If image loading fails for any reason, the return value will be NULL,\n// and *x, *y, *channels_in_file will be unchanged. The function\n// stbi_failure_reason() can be queried for an extremely brief, end-user\n// unfriendly explanation of why the load failed. Define STBI_NO_FAILURE_STRINGS\n// to avoid compiling these strings at all, and STBI_FAILURE_USERMSG to get slightly\n// more user-friendly ones.\n//\n// Paletted PNG, BMP, GIF, and PIC images are automatically depalettized.\n//\n// To query the width, height and component count of an image without having to\n// decode the full file, you can use the stbi_info family of functions:\n//\n//   int x,y,n,ok;\n//   ok = stbi_info(filename, &x, &y, &n);\n//   // returns ok=1 and sets x, y, n if image is a supported format,\n//   // 0 otherwise.\n//\n// Note that stb_image pervasively uses ints in its public API for sizes,\n// including sizes of memory buffers. This is now part of the API and thus\n// hard to change without causing breakage. As a result, the various image\n// loaders all have certain limits on image size; these differ somewhat\n// by format but generally boil down to either just under 2GB or just under\n// 1GB. When the decoded image would be larger than this, stb_image decoding\n// will fail.\n//\n// Additionally, stb_image will reject image files that have any of their\n// dimensions set to a larger value than the configurable STBI_MAX_DIMENSIONS,\n// which defaults to 2**24 = 16777216 pixels. Due to the above memory limit,\n// the only way to have an image with such dimensions load correctly\n// is for it to have a rather extreme aspect ratio. Either way, the\n// assumption here is that such larger images are likely to be malformed\n// or malicious. If you do need to load an image with individual dimensions\n// larger than that, and it still fits in the overall size limit, you can\n// #define STBI_MAX_DIMENSIONS on your own to be something larger.\n//\n// ===========================================================================\n//\n// UNICODE:\n//\n//   If compiling for Windows and you wish to use Unicode filenames, compile\n//   with\n//       #define STBI_WINDOWS_UTF8\n//   and pass utf8-encoded filenames. Call stbi_convert_wchar_to_utf8 to convert\n//   Windows wchar_t filenames to utf8.\n//\n// ===========================================================================\n//\n// Philosophy\n//\n// stb libraries are designed with the following priorities:\n//\n//    1. easy to use\n//    2. easy to maintain\n//    3. good performance\n//\n// Sometimes I let \"good performance\" creep up in priority over \"easy to maintain\",\n// and for best performance I may provide less-easy-to-use APIs that give higher\n// performance, in addition to the easy-to-use ones. Nevertheless, it's important\n// to keep in mind that from the standpoint of you, a client of this library,\n// all you care about is #1 and #3, and stb libraries DO NOT emphasize #3 above all.\n//\n// Some secondary priorities arise directly from the first two, some of which\n// provide more explicit reasons why performance can't be emphasized.\n//\n//    - Portable (\"ease of use\")\n//    - Small source code footprint (\"easy to maintain\")\n//    - No dependencies (\"ease of use\")\n//\n// ===========================================================================\n//\n// I/O callbacks\n//\n// I/O callbacks allow you to read from arbitrary sources, like packaged\n// files or some other source. Data read from callbacks are processed\n// through a small internal buffer (currently 128 bytes) to try to reduce\n// overhead.\n//\n// The three functions you must define are \"read\" (reads some bytes of data),\n// \"skip\" (skips some bytes of data), \"eof\" (reports if the stream is at the end).\n//\n// ===========================================================================\n//\n// SIMD support\n//\n// The JPEG decoder will try to automatically use SIMD kernels on x86 when\n// supported by the compiler. For ARM Neon support, you must explicitly\n// request it.\n//\n// (The old do-it-yourself SIMD API is no longer supported in the current\n// code.)\n//\n// On x86, SSE2 will automatically be used when available based on a run-time\n// test; if not, the generic C versions are used as a fall-back. On ARM targets,\n// the typical path is to have separate builds for NEON and non-NEON devices\n// (at least this is true for iOS and Android). Therefore, the NEON support is\n// toggled by a build flag: define STBI_NEON to get NEON loops.\n//\n// If for some reason you do not want to use any of SIMD code, or if\n// you have issues compiling it, you can disable it entirely by\n// defining STBI_NO_SIMD.\n//\n// ===========================================================================\n//\n// HDR image support   (disable by defining STBI_NO_HDR)\n//\n// stb_image supports loading HDR images in general, and currently the Radiance\n// .HDR file format specifically. You can still load any file through the existing\n// interface; if you attempt to load an HDR file, it will be automatically remapped\n// to LDR, assuming gamma 2.2 and an arbitrary scale factor defaulting to 1;\n// both of these constants can be reconfigured through this interface:\n//\n//     stbi_hdr_to_ldr_gamma(2.2f);\n//     stbi_hdr_to_ldr_scale(1.0f);\n//\n// (note, do not use _inverse_ constants; stbi_image will invert them\n// appropriately).\n//\n// Additionally, there is a new, parallel interface for loading files as\n// (linear) floats to preserve the full dynamic range:\n//\n//    float *data = stbi_loadf(filename, &x, &y, &n, 0);\n//\n// If you load LDR images through this interface, those images will\n// be promoted to floating point values, run through the inverse of\n// constants corresponding to the above:\n//\n//     stbi_ldr_to_hdr_scale(1.0f);\n//     stbi_ldr_to_hdr_gamma(2.2f);\n//\n// Finally, given a filename (or an open file or memory block--see header\n// file for details) containing image data, you can query for the \"most\n// appropriate\" interface to use (that is, whether the image is HDR or\n// not), using:\n//\n//     stbi_is_hdr(char *filename);\n//\n// ===========================================================================\n//\n// iPhone PNG support:\n//\n// We optionally support converting iPhone-formatted PNGs (which store\n// premultiplied BGRA) back to RGB, even though they're internally encoded\n// differently. To enable this conversion, call\n// stbi_convert_iphone_png_to_rgb(1).\n//\n// Call stbi_set_unpremultiply_on_load(1) as well to force a divide per\n// pixel to remove any premultiplied alpha *only* if the image file explicitly\n// says there's premultiplied data (currently only happens in iPhone images,\n// and only if iPhone convert-to-rgb processing is on).\n//\n// ===========================================================================\n//\n// ADDITIONAL CONFIGURATION\n//\n//  - You can suppress implementation of any of the decoders to reduce\n//    your code footprint by #defining one or more of the following\n//    symbols before creating the implementation.\n//\n//        STBI_NO_JPEG\n//        STBI_NO_PNG\n//        STBI_NO_BMP\n//        STBI_NO_PSD\n//        STBI_NO_TGA\n//        STBI_NO_GIF\n//        STBI_NO_HDR\n//        STBI_NO_PIC\n//        STBI_NO_PNM   (.ppm and .pgm)\n//\n//  - You can request *only* certain decoders and suppress all other ones\n//    (this will be more forward-compatible, as addition of new decoders\n//    doesn't require you to disable them explicitly):\n//\n//        STBI_ONLY_JPEG\n//        STBI_ONLY_PNG\n//        STBI_ONLY_BMP\n//        STBI_ONLY_PSD\n//        STBI_ONLY_TGA\n//        STBI_ONLY_GIF\n//        STBI_ONLY_HDR\n//        STBI_ONLY_PIC\n//        STBI_ONLY_PNM   (.ppm and .pgm)\n//\n//   - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still\n//     want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB\n//\n//  - If you define STBI_MAX_DIMENSIONS, stb_image will reject images greater\n//    than that size (in either width or height) without further processing.\n//    This is to let programs in the wild set an upper bound to prevent\n//    denial-of-service attacks on untrusted data, as one could generate a\n//    valid image of gigantic dimensions and force stb_image to allocate a\n//    huge block of memory and spend disproportionate time decoding it. By\n//    default this is set to (1 << 24), which is 16777216, but that's still\n//    very big.\n\n#ifndef STBI_NO_STDIO\n#include <stdio.h>\n#endif // STBI_NO_STDIO\n\n#define STBI_VERSION 1\n\nenum\n{\n   STBI_default = 0, // only used for desired_channels\n\n   STBI_grey       = 1,\n   STBI_grey_alpha = 2,\n   STBI_rgb        = 3,\n   STBI_rgb_alpha  = 4\n};\n\n#include <stdlib.h>\ntypedef unsigned char stbi_uc;\ntypedef unsigned short stbi_us;\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n#ifndef STBIDEF\n#ifdef STB_IMAGE_STATIC\n#define STBIDEF static\n#else\n#define STBIDEF extern\n#endif\n#endif\n\n//////////////////////////////////////////////////////////////////////////////\n//\n// PRIMARY API - works on images of any type\n//\n\n//\n// load image by filename, open file, or memory buffer\n//\n\ntypedef struct\n{\n   int      (*read)  (void *user,char *data,int size);   // fill 'data' with 'size' bytes.  return number of bytes actually read\n   void     (*skip)  (void *user,int n);                 // skip the next 'n' bytes, or 'unget' the last -n bytes if negative\n   int      (*eof)   (void *user);                       // returns nonzero if we are at end of file/data\n} stbi_io_callbacks;\n\n////////////////////////////////////\n//\n// 8-bits-per-channel interface\n//\n\nSTBIDEF stbi_uc *stbi_load_from_memory   (stbi_uc           const *buffer, int len   , int *x, int *y, int *channels_in_file, int desired_channels);\nSTBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk  , void *user, int *x, int *y, int *channels_in_file, int desired_channels);\n\n#ifndef STBI_NO_STDIO\nSTBIDEF stbi_uc *stbi_load            (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels);\nSTBIDEF stbi_uc *stbi_load_from_file  (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels);\n// for stbi_load_from_file, file pointer is left pointing immediately after image\n#endif\n\n#ifndef STBI_NO_GIF\nSTBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp);\n#endif\n\n#ifdef STBI_WINDOWS_UTF8\nSTBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input);\n#endif\n\n////////////////////////////////////\n//\n// 16-bits-per-channel interface\n//\n\nSTBIDEF stbi_us *stbi_load_16_from_memory   (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels);\nSTBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels);\n\n#ifndef STBI_NO_STDIO\nSTBIDEF stbi_us *stbi_load_16          (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels);\nSTBIDEF stbi_us *stbi_load_from_file_16(FILE *f, int *x, int *y, int *channels_in_file, int desired_channels);\n#endif\n\n////////////////////////////////////\n//\n// float-per-channel interface\n//\n#ifndef STBI_NO_LINEAR\n   STBIDEF float *stbi_loadf_from_memory     (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels);\n   STBIDEF float *stbi_loadf_from_callbacks  (stbi_io_callbacks const *clbk, void *user, int *x, int *y,  int *channels_in_file, int desired_channels);\n\n   #ifndef STBI_NO_STDIO\n   STBIDEF float *stbi_loadf            (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels);\n   STBIDEF float *stbi_loadf_from_file  (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels);\n   #endif\n#endif\n\n#ifndef STBI_NO_HDR\n   STBIDEF void   stbi_hdr_to_ldr_gamma(float gamma);\n   STBIDEF void   stbi_hdr_to_ldr_scale(float scale);\n#endif // STBI_NO_HDR\n\n#ifndef STBI_NO_LINEAR\n   STBIDEF void   stbi_ldr_to_hdr_gamma(float gamma);\n   STBIDEF void   stbi_ldr_to_hdr_scale(float scale);\n#endif // STBI_NO_LINEAR\n\n// stbi_is_hdr is always defined, but always returns false if STBI_NO_HDR\nSTBIDEF int    stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user);\nSTBIDEF int    stbi_is_hdr_from_memory(stbi_uc const *buffer, int len);\n#ifndef STBI_NO_STDIO\nSTBIDEF int      stbi_is_hdr          (char const *filename);\nSTBIDEF int      stbi_is_hdr_from_file(FILE *f);\n#endif // STBI_NO_STDIO\n\n\n// get a VERY brief reason for failure\n// on most compilers (and ALL modern mainstream compilers) this is threadsafe\nSTBIDEF const char *stbi_failure_reason  (void);\n\n// free the loaded image -- this is just free()\nSTBIDEF void     stbi_image_free      (void *retval_from_stbi_load);\n\n// get image dimensions & components without fully decoding\nSTBIDEF int      stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp);\nSTBIDEF int      stbi_info_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp);\nSTBIDEF int      stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len);\nSTBIDEF int      stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *clbk, void *user);\n\n#ifndef STBI_NO_STDIO\nSTBIDEF int      stbi_info               (char const *filename,     int *x, int *y, int *comp);\nSTBIDEF int      stbi_info_from_file     (FILE *f,                  int *x, int *y, int *comp);\nSTBIDEF int      stbi_is_16_bit          (char const *filename);\nSTBIDEF int      stbi_is_16_bit_from_file(FILE *f);\n#endif\n\n\n\n// for image formats that explicitly notate that they have premultiplied alpha,\n// we just return the colors as stored in the file. set this flag to force\n// unpremultiplication. results are undefined if the unpremultiply overflow.\nSTBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply);\n\n// indicate whether we should process iphone images back to canonical format,\n// or just pass them through \"as-is\"\nSTBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert);\n\n// flip the image vertically, so the first pixel in the output array is the bottom left\nSTBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip);\n\n// as above, but only applies to images loaded on the thread that calls the function\n// this function is only available if your compiler supports thread-local variables;\n// calling it will fail to link if your compiler doesn't\nSTBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply);\nSTBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert);\nSTBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip);\n\n// ZLIB client - used by PNG, available for other purposes\n\nSTBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen);\nSTBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header);\nSTBIDEF char *stbi_zlib_decode_malloc(const char *buffer, int len, int *outlen);\nSTBIDEF int   stbi_zlib_decode_buffer(char *obuffer, int olen, const char *ibuffer, int ilen);\n\nSTBIDEF char *stbi_zlib_decode_noheader_malloc(const char *buffer, int len, int *outlen);\nSTBIDEF int   stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen);\n\n\n#ifdef __cplusplus\n}\n#endif\n\n//\n//\n////   end header file   /////////////////////////////////////////////////////\n#endif // STBI_INCLUDE_STB_IMAGE_H\n\n#ifdef STB_IMAGE_IMPLEMENTATION\n\n#if defined(STBI_ONLY_JPEG) || defined(STBI_ONLY_PNG) || defined(STBI_ONLY_BMP) \\\n  || defined(STBI_ONLY_TGA) || defined(STBI_ONLY_GIF) || defined(STBI_ONLY_PSD) \\\n  || defined(STBI_ONLY_HDR) || defined(STBI_ONLY_PIC) || defined(STBI_ONLY_PNM) \\\n  || defined(STBI_ONLY_ZLIB)\n   #ifndef STBI_ONLY_JPEG\n   #define STBI_NO_JPEG\n   #endif\n   #ifndef STBI_ONLY_PNG\n   #define STBI_NO_PNG\n   #endif\n   #ifndef STBI_ONLY_BMP\n   #define STBI_NO_BMP\n   #endif\n   #ifndef STBI_ONLY_PSD\n   #define STBI_NO_PSD\n   #endif\n   #ifndef STBI_ONLY_TGA\n   #define STBI_NO_TGA\n   #endif\n   #ifndef STBI_ONLY_GIF\n   #define STBI_NO_GIF\n   #endif\n   #ifndef STBI_ONLY_HDR\n   #define STBI_NO_HDR\n   #endif\n   #ifndef STBI_ONLY_PIC\n   #define STBI_NO_PIC\n   #endif\n   #ifndef STBI_ONLY_PNM\n   #define STBI_NO_PNM\n   #endif\n#endif\n\n#if defined(STBI_NO_PNG) && !defined(STBI_SUPPORT_ZLIB) && !defined(STBI_NO_ZLIB)\n#define STBI_NO_ZLIB\n#endif\n\n\n#include <stdarg.h>\n#include <stddef.h> // ptrdiff_t on osx\n#include <stdlib.h>\n#include <string.h>\n#include <limits.h>\n\n#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR)\n#include <math.h>  // ldexp, pow\n#endif\n\n#ifndef STBI_NO_STDIO\n#include <stdio.h>\n#endif\n\n#ifndef STBI_ASSERT\n#include <assert.h>\n#define STBI_ASSERT(x) assert(x)\n#endif\n\n#ifdef __cplusplus\n#define STBI_EXTERN extern \"C\"\n#else\n#define STBI_EXTERN extern\n#endif\n\n\n#ifndef _MSC_VER\n   #ifdef __cplusplus\n   #define stbi_inline inline\n   #else\n   #define stbi_inline\n   #endif\n#else\n   #define stbi_inline __forceinline\n#endif\n\n#ifndef STBI_NO_THREAD_LOCALS\n   #if defined(__cplusplus) &&  __cplusplus >= 201103L\n      #define STBI_THREAD_LOCAL       thread_local\n   #elif defined(__GNUC__) && __GNUC__ < 5\n      #define STBI_THREAD_LOCAL       __thread\n   #elif defined(_MSC_VER)\n      #define STBI_THREAD_LOCAL       __declspec(thread)\n   #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__)\n      #define STBI_THREAD_LOCAL       _Thread_local\n   #endif\n\n   #ifndef STBI_THREAD_LOCAL\n      #if defined(__GNUC__)\n        #define STBI_THREAD_LOCAL       __thread\n      #endif\n   #endif\n#endif\n\n#if defined(_MSC_VER) || defined(__SYMBIAN32__)\ntypedef unsigned short stbi__uint16;\ntypedef   signed short stbi__int16;\ntypedef unsigned int   stbi__uint32;\ntypedef   signed int   stbi__int32;\n#else\n#include <stdint.h>\ntypedef uint16_t stbi__uint16;\ntypedef int16_t  stbi__int16;\ntypedef uint32_t stbi__uint32;\ntypedef int32_t  stbi__int32;\n#endif\n\n// should produce compiler error if size is wrong\ntypedef unsigned char validate_uint32[sizeof(stbi__uint32)==4 ? 1 : -1];\n\n#ifdef _MSC_VER\n#define STBI_NOTUSED(v)  (void)(v)\n#else\n#define STBI_NOTUSED(v)  (void)sizeof(v)\n#endif\n\n#ifdef _MSC_VER\n#define STBI_HAS_LROTL\n#endif\n\n#ifdef STBI_HAS_LROTL\n   #define stbi_lrot(x,y)  _lrotl(x,y)\n#else\n   #define stbi_lrot(x,y)  (((x) << (y)) | ((x) >> (-(y) & 31)))\n#endif\n\n#if defined(STBI_MALLOC) && defined(STBI_FREE) && (defined(STBI_REALLOC) || defined(STBI_REALLOC_SIZED))\n// ok\n#elif !defined(STBI_MALLOC) && !defined(STBI_FREE) && !defined(STBI_REALLOC) && !defined(STBI_REALLOC_SIZED)\n// ok\n#else\n#error \"Must define all or none of STBI_MALLOC, STBI_FREE, and STBI_REALLOC (or STBI_REALLOC_SIZED).\"\n#endif\n\n#ifndef STBI_MALLOC\n#define STBI_MALLOC(sz)           malloc(sz)\n#define STBI_REALLOC(p,newsz)     realloc(p,newsz)\n#define STBI_FREE(p)              free(p)\n#endif\n\n#ifndef STBI_REALLOC_SIZED\n#define STBI_REALLOC_SIZED(p,oldsz,newsz) STBI_REALLOC(p,newsz)\n#endif\n\n// x86/x64 detection\n#if defined(__x86_64__) || defined(_M_X64)\n#define STBI__X64_TARGET\n#elif defined(__i386) || defined(_M_IX86)\n#define STBI__X86_TARGET\n#endif\n\n#if defined(__GNUC__) && defined(STBI__X86_TARGET) && !defined(__SSE2__) && !defined(STBI_NO_SIMD)\n// gcc doesn't support sse2 intrinsics unless you compile with -msse2,\n// which in turn means it gets to use SSE2 everywhere. This is unfortunate,\n// but previous attempts to provide the SSE2 functions with runtime\n// detection caused numerous issues. The way architecture extensions are\n// exposed in GCC/Clang is, sadly, not really suited for one-file libs.\n// New behavior: if compiled with -msse2, we use SSE2 without any\n// detection; if not, we don't use it at all.\n#define STBI_NO_SIMD\n#endif\n\n#if defined(__MINGW32__) && defined(STBI__X86_TARGET) && !defined(STBI_MINGW_ENABLE_SSE2) && !defined(STBI_NO_SIMD)\n// Note that __MINGW32__ doesn't actually mean 32-bit, so we have to avoid STBI__X64_TARGET\n//\n// 32-bit MinGW wants ESP to be 16-byte aligned, but this is not in the\n// Windows ABI and VC++ as well as Windows DLLs don't maintain that invariant.\n// As a result, enabling SSE2 on 32-bit MinGW is dangerous when not\n// simultaneously enabling \"-mstackrealign\".\n//\n// See https://github.com/nothings/stb/issues/81 for more information.\n//\n// So default to no SSE2 on 32-bit MinGW. If you've read this far and added\n// -mstackrealign to your build settings, feel free to #define STBI_MINGW_ENABLE_SSE2.\n#define STBI_NO_SIMD\n#endif\n\n#if !defined(STBI_NO_SIMD) && (defined(STBI__X86_TARGET) || defined(STBI__X64_TARGET))\n#define STBI_SSE2\n#include <emmintrin.h>\n\n#ifdef _MSC_VER\n\n#if _MSC_VER >= 1400  // not VC6\n#include <intrin.h> // __cpuid\nstatic int stbi__cpuid3(void)\n{\n   int info[4];\n   __cpuid(info,1);\n   return info[3];\n}\n#else\nstatic int stbi__cpuid3(void)\n{\n   int res;\n   __asm {\n      mov  eax,1\n      cpuid\n      mov  res,edx\n   }\n   return res;\n}\n#endif\n\n#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name\n\n#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2)\nstatic int stbi__sse2_available(void)\n{\n   int info3 = stbi__cpuid3();\n   return ((info3 >> 26) & 1) != 0;\n}\n#endif\n\n#else // assume GCC-style if not VC++\n#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16)))\n\n#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2)\nstatic int stbi__sse2_available(void)\n{\n   // If we're even attempting to compile this on GCC/Clang, that means\n   // -msse2 is on, which means the compiler is allowed to use SSE2\n   // instructions at will, and so are we.\n   return 1;\n}\n#endif\n\n#endif\n#endif\n\n// ARM NEON\n#if defined(STBI_NO_SIMD) && defined(STBI_NEON)\n#undef STBI_NEON\n#endif\n\n#ifdef STBI_NEON\n#include <arm_neon.h>\n#ifdef _MSC_VER\n#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name\n#else\n#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16)))\n#endif\n#endif\n\n#ifndef STBI_SIMD_ALIGN\n#define STBI_SIMD_ALIGN(type, name) type name\n#endif\n\n#ifndef STBI_MAX_DIMENSIONS\n#define STBI_MAX_DIMENSIONS (1 << 24)\n#endif\n\n///////////////////////////////////////////////\n//\n//  stbi__context struct and start_xxx functions\n\n// stbi__context structure is our basic context used by all images, so it\n// contains all the IO context, plus some basic image information\ntypedef struct\n{\n   stbi__uint32 img_x, img_y;\n   int img_n, img_out_n;\n\n   stbi_io_callbacks io;\n   void *io_user_data;\n\n   int read_from_callbacks;\n   int buflen;\n   stbi_uc buffer_start[128];\n   int callback_already_read;\n\n   stbi_uc *img_buffer, *img_buffer_end;\n   stbi_uc *img_buffer_original, *img_buffer_original_end;\n} stbi__context;\n\n\nstatic void stbi__refill_buffer(stbi__context *s);\n\n// initialize a memory-decode context\nstatic void stbi__start_mem(stbi__context *s, stbi_uc const *buffer, int len)\n{\n   s->io.read = NULL;\n   s->read_from_callbacks = 0;\n   s->callback_already_read = 0;\n   s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer;\n   s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len;\n}\n\n// initialize a callback-based context\nstatic void stbi__start_callbacks(stbi__context *s, stbi_io_callbacks *c, void *user)\n{\n   s->io = *c;\n   s->io_user_data = user;\n   s->buflen = sizeof(s->buffer_start);\n   s->read_from_callbacks = 1;\n   s->callback_already_read = 0;\n   s->img_buffer = s->img_buffer_original = s->buffer_start;\n   stbi__refill_buffer(s);\n   s->img_buffer_original_end = s->img_buffer_end;\n}\n\n#ifndef STBI_NO_STDIO\n\nstatic int stbi__stdio_read(void *user, char *data, int size)\n{\n   return (int) fread(data,1,size,(FILE*) user);\n}\n\nstatic void stbi__stdio_skip(void *user, int n)\n{\n   int ch;\n   fseek((FILE*) user, n, SEEK_CUR);\n   ch = fgetc((FILE*) user);  /* have to read a byte to reset feof()'s flag */\n   if (ch != EOF) {\n      ungetc(ch, (FILE *) user);  /* push byte back onto stream if valid. */\n   }\n}\n\nstatic int stbi__stdio_eof(void *user)\n{\n   return feof((FILE*) user) || ferror((FILE *) user);\n}\n\nstatic stbi_io_callbacks stbi__stdio_callbacks =\n{\n   stbi__stdio_read,\n   stbi__stdio_skip,\n   stbi__stdio_eof,\n};\n\nstatic void stbi__start_file(stbi__context *s, FILE *f)\n{\n   stbi__start_callbacks(s, &stbi__stdio_callbacks, (void *) f);\n}\n\n//static void stop_file(stbi__context *s) { }\n\n#endif // !STBI_NO_STDIO\n\nstatic void stbi__rewind(stbi__context *s)\n{\n   // conceptually rewind SHOULD rewind to the beginning of the stream,\n   // but we just rewind to the beginning of the initial buffer, because\n   // we only use it after doing 'test', which only ever looks at at most 92 bytes\n   s->img_buffer = s->img_buffer_original;\n   s->img_buffer_end = s->img_buffer_original_end;\n}\n\nenum\n{\n   STBI_ORDER_RGB,\n   STBI_ORDER_BGR\n};\n\ntypedef struct\n{\n   int bits_per_channel;\n   int num_channels;\n   int channel_order;\n} stbi__result_info;\n\n#ifndef STBI_NO_JPEG\nstatic int      stbi__jpeg_test(stbi__context *s);\nstatic void    *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);\nstatic int      stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp);\n#endif\n\n#ifndef STBI_NO_PNG\nstatic int      stbi__png_test(stbi__context *s);\nstatic void    *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);\nstatic int      stbi__png_info(stbi__context *s, int *x, int *y, int *comp);\nstatic int      stbi__png_is16(stbi__context *s);\n#endif\n\n#ifndef STBI_NO_BMP\nstatic int      stbi__bmp_test(stbi__context *s);\nstatic void    *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);\nstatic int      stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp);\n#endif\n\n#ifndef STBI_NO_TGA\nstatic int      stbi__tga_test(stbi__context *s);\nstatic void    *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);\nstatic int      stbi__tga_info(stbi__context *s, int *x, int *y, int *comp);\n#endif\n\n#ifndef STBI_NO_PSD\nstatic int      stbi__psd_test(stbi__context *s);\nstatic void    *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc);\nstatic int      stbi__psd_info(stbi__context *s, int *x, int *y, int *comp);\nstatic int      stbi__psd_is16(stbi__context *s);\n#endif\n\n#ifndef STBI_NO_HDR\nstatic int      stbi__hdr_test(stbi__context *s);\nstatic float   *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);\nstatic int      stbi__hdr_info(stbi__context *s, int *x, int *y, int *comp);\n#endif\n\n#ifndef STBI_NO_PIC\nstatic int      stbi__pic_test(stbi__context *s);\nstatic void    *stbi__pic_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);\nstatic int      stbi__pic_info(stbi__context *s, int *x, int *y, int *comp);\n#endif\n\n#ifndef STBI_NO_GIF\nstatic int      stbi__gif_test(stbi__context *s);\nstatic void    *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);\nstatic void    *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp);\nstatic int      stbi__gif_info(stbi__context *s, int *x, int *y, int *comp);\n#endif\n\n#ifndef STBI_NO_PNM\nstatic int      stbi__pnm_test(stbi__context *s);\nstatic void    *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);\nstatic int      stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp);\nstatic int      stbi__pnm_is16(stbi__context *s);\n#endif\n\nstatic\n#ifdef STBI_THREAD_LOCAL\nSTBI_THREAD_LOCAL\n#endif\nconst char *stbi__g_failure_reason;\n\nSTBIDEF const char *stbi_failure_reason(void)\n{\n   return stbi__g_failure_reason;\n}\n\n#ifndef STBI_NO_FAILURE_STRINGS\nstatic int stbi__err(const char *str)\n{\n   stbi__g_failure_reason = str;\n   return 0;\n}\n#endif\n\nstatic void *stbi__malloc(size_t size)\n{\n    return STBI_MALLOC(size);\n}\n\n// stb_image uses ints pervasively, including for offset calculations.\n// therefore the largest decoded image size we can support with the\n// current code, even on 64-bit targets, is INT_MAX. this is not a\n// significant limitation for the intended use case.\n//\n// we do, however, need to make sure our size calculations don't\n// overflow. hence a few helper functions for size calculations that\n// multiply integers together, making sure that they're non-negative\n// and no overflow occurs.\n\n// return 1 if the sum is valid, 0 on overflow.\n// negative terms are considered invalid.\nstatic int stbi__addsizes_valid(int a, int b)\n{\n   if (b < 0) return 0;\n   // now 0 <= b <= INT_MAX, hence also\n   // 0 <= INT_MAX - b <= INTMAX.\n   // And \"a + b <= INT_MAX\" (which might overflow) is the\n   // same as a <= INT_MAX - b (no overflow)\n   return a <= INT_MAX - b;\n}\n\n// returns 1 if the product is valid, 0 on overflow.\n// negative factors are considered invalid.\nstatic int stbi__mul2sizes_valid(int a, int b)\n{\n   if (a < 0 || b < 0) return 0;\n   if (b == 0) return 1; // mul-by-0 is always safe\n   // portable way to check for no overflows in a*b\n   return a <= INT_MAX/b;\n}\n\n#if !defined(STBI_NO_JPEG) || !defined(STBI_NO_PNG) || !defined(STBI_NO_TGA) || !defined(STBI_NO_HDR)\n// returns 1 if \"a*b + add\" has no negative terms/factors and doesn't overflow\nstatic int stbi__mad2sizes_valid(int a, int b, int add)\n{\n   return stbi__mul2sizes_valid(a, b) && stbi__addsizes_valid(a*b, add);\n}\n#endif\n\n// returns 1 if \"a*b*c + add\" has no negative terms/factors and doesn't overflow\nstatic int stbi__mad3sizes_valid(int a, int b, int c, int add)\n{\n   return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) &&\n      stbi__addsizes_valid(a*b*c, add);\n}\n\n// returns 1 if \"a*b*c*d + add\" has no negative terms/factors and doesn't overflow\n#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM)\nstatic int stbi__mad4sizes_valid(int a, int b, int c, int d, int add)\n{\n   return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) &&\n      stbi__mul2sizes_valid(a*b*c, d) && stbi__addsizes_valid(a*b*c*d, add);\n}\n#endif\n\n#if !defined(STBI_NO_JPEG) || !defined(STBI_NO_PNG) || !defined(STBI_NO_TGA) || !defined(STBI_NO_HDR)\n// mallocs with size overflow checking\nstatic void *stbi__malloc_mad2(int a, int b, int add)\n{\n   if (!stbi__mad2sizes_valid(a, b, add)) return NULL;\n   return stbi__malloc(a*b + add);\n}\n#endif\n\nstatic void *stbi__malloc_mad3(int a, int b, int c, int add)\n{\n   if (!stbi__mad3sizes_valid(a, b, c, add)) return NULL;\n   return stbi__malloc(a*b*c + add);\n}\n\n#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM)\nstatic void *stbi__malloc_mad4(int a, int b, int c, int d, int add)\n{\n   if (!stbi__mad4sizes_valid(a, b, c, d, add)) return NULL;\n   return stbi__malloc(a*b*c*d + add);\n}\n#endif\n\n// returns 1 if the sum of two signed ints is valid (between -2^31 and 2^31-1 inclusive), 0 on overflow.\nstatic int stbi__addints_valid(int a, int b)\n{\n   if ((a >= 0) != (b >= 0)) return 1; // a and b have different signs, so no overflow\n   if (a < 0 && b < 0) return a >= INT_MIN - b; // same as a + b >= INT_MIN; INT_MIN - b cannot overflow since b < 0.\n   return a <= INT_MAX - b;\n}\n\n// returns 1 if the product of two ints fits in a signed short, 0 on overflow.\nstatic int stbi__mul2shorts_valid(int a, int b)\n{\n   if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow\n   if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid\n   if (b < 0) return a <= SHRT_MIN / b; // same as a * b >= SHRT_MIN\n   return a >= SHRT_MIN / b;\n}\n\n// stbi__err - error\n// stbi__errpf - error returning pointer to float\n// stbi__errpuc - error returning pointer to unsigned char\n\n#ifdef STBI_NO_FAILURE_STRINGS\n   #define stbi__err(x,y)  0\n#elif defined(STBI_FAILURE_USERMSG)\n   #define stbi__err(x,y)  stbi__err(y)\n#else\n   #define stbi__err(x,y)  stbi__err(x)\n#endif\n\n#define stbi__errpf(x,y)   ((float *)(size_t) (stbi__err(x,y)?NULL:NULL))\n#define stbi__errpuc(x,y)  ((unsigned char *)(size_t) (stbi__err(x,y)?NULL:NULL))\n\nSTBIDEF void stbi_image_free(void *retval_from_stbi_load)\n{\n   STBI_FREE(retval_from_stbi_load);\n}\n\n#ifndef STBI_NO_LINEAR\nstatic float   *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp);\n#endif\n\n#ifndef STBI_NO_HDR\nstatic stbi_uc *stbi__hdr_to_ldr(float   *data, int x, int y, int comp);\n#endif\n\nstatic int stbi__vertically_flip_on_load_global = 0;\n\nSTBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip)\n{\n   stbi__vertically_flip_on_load_global = flag_true_if_should_flip;\n}\n\n#ifndef STBI_THREAD_LOCAL\n#define stbi__vertically_flip_on_load  stbi__vertically_flip_on_load_global\n#else\nstatic STBI_THREAD_LOCAL int stbi__vertically_flip_on_load_local, stbi__vertically_flip_on_load_set;\n\nSTBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip)\n{\n   stbi__vertically_flip_on_load_local = flag_true_if_should_flip;\n   stbi__vertically_flip_on_load_set = 1;\n}\n\n#define stbi__vertically_flip_on_load  (stbi__vertically_flip_on_load_set       \\\n                                         ? stbi__vertically_flip_on_load_local  \\\n                                         : stbi__vertically_flip_on_load_global)\n#endif // STBI_THREAD_LOCAL\n\nstatic void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc)\n{\n   memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields\n   ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed\n   ri->channel_order = STBI_ORDER_RGB; // all current input & output are this, but this is here so we can add BGR order\n   ri->num_channels = 0;\n\n   // test the formats with a very explicit header first (at least a FOURCC\n   // or distinctive magic number first)\n   #ifndef STBI_NO_PNG\n   if (stbi__png_test(s))  return stbi__png_load(s,x,y,comp,req_comp, ri);\n   #endif\n   #ifndef STBI_NO_BMP\n   if (stbi__bmp_test(s))  return stbi__bmp_load(s,x,y,comp,req_comp, ri);\n   #endif\n   #ifndef STBI_NO_GIF\n   if (stbi__gif_test(s))  return stbi__gif_load(s,x,y,comp,req_comp, ri);\n   #endif\n   #ifndef STBI_NO_PSD\n   if (stbi__psd_test(s))  return stbi__psd_load(s,x,y,comp,req_comp, ri, bpc);\n   #else\n   STBI_NOTUSED(bpc);\n   #endif\n   #ifndef STBI_NO_PIC\n   if (stbi__pic_test(s))  return stbi__pic_load(s,x,y,comp,req_comp, ri);\n   #endif\n\n   // then the formats that can end up attempting to load with just 1 or 2\n   // bytes matching expectations; these are prone to false positives, so\n   // try them later\n   #ifndef STBI_NO_JPEG\n   if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri);\n   #endif\n   #ifndef STBI_NO_PNM\n   if (stbi__pnm_test(s))  return stbi__pnm_load(s,x,y,comp,req_comp, ri);\n   #endif\n\n   #ifndef STBI_NO_HDR\n   if (stbi__hdr_test(s)) {\n      float *hdr = stbi__hdr_load(s, x,y,comp,req_comp, ri);\n      return stbi__hdr_to_ldr(hdr, *x, *y, req_comp ? req_comp : *comp);\n   }\n   #endif\n\n   #ifndef STBI_NO_TGA\n   // test tga last because it's a crappy test!\n   if (stbi__tga_test(s))\n      return stbi__tga_load(s,x,y,comp,req_comp, ri);\n   #endif\n\n   return stbi__errpuc(\"unknown image type\", \"Image not of any known type, or corrupt\");\n}\n\nstatic stbi_uc *stbi__convert_16_to_8(stbi__uint16 *orig, int w, int h, int channels)\n{\n   int i;\n   int img_len = w * h * channels;\n   stbi_uc *reduced;\n\n   reduced = (stbi_uc *) stbi__malloc(img_len);\n   if (reduced == NULL) return stbi__errpuc(\"outofmem\", \"Out of memory\");\n\n   for (i = 0; i < img_len; ++i)\n      reduced[i] = (stbi_uc)((orig[i] >> 8) & 0xFF); // top half of each byte is sufficient approx of 16->8 bit scaling\n\n   STBI_FREE(orig);\n   return reduced;\n}\n\nstatic stbi__uint16 *stbi__convert_8_to_16(stbi_uc *orig, int w, int h, int channels)\n{\n   int i;\n   int img_len = w * h * channels;\n   stbi__uint16 *enlarged;\n\n   enlarged = (stbi__uint16 *) stbi__malloc(img_len*2);\n   if (enlarged == NULL) return (stbi__uint16 *) stbi__errpuc(\"outofmem\", \"Out of memory\");\n\n   for (i = 0; i < img_len; ++i)\n      enlarged[i] = (stbi__uint16)((orig[i] << 8) + orig[i]); // replicate to high and low byte, maps 0->0, 255->0xffff\n\n   STBI_FREE(orig);\n   return enlarged;\n}\n\nstatic void stbi__vertical_flip(void *image, int w, int h, int bytes_per_pixel)\n{\n   int row;\n   size_t bytes_per_row = (size_t)w * bytes_per_pixel;\n   stbi_uc temp[2048];\n   stbi_uc *bytes = (stbi_uc *)image;\n\n   for (row = 0; row < (h>>1); row++) {\n      stbi_uc *row0 = bytes + row*bytes_per_row;\n      stbi_uc *row1 = bytes + (h - row - 1)*bytes_per_row;\n      // swap row0 with row1\n      size_t bytes_left = bytes_per_row;\n      while (bytes_left) {\n         size_t bytes_copy = (bytes_left < sizeof(temp)) ? bytes_left : sizeof(temp);\n         memcpy(temp, row0, bytes_copy);\n         memcpy(row0, row1, bytes_copy);\n         memcpy(row1, temp, bytes_copy);\n         row0 += bytes_copy;\n         row1 += bytes_copy;\n         bytes_left -= bytes_copy;\n      }\n   }\n}\n\n#ifndef STBI_NO_GIF\nstatic void stbi__vertical_flip_slices(void *image, int w, int h, int z, int bytes_per_pixel)\n{\n   int slice;\n   int slice_size = w * h * bytes_per_pixel;\n\n   stbi_uc *bytes = (stbi_uc *)image;\n   for (slice = 0; slice < z; ++slice) {\n      stbi__vertical_flip(bytes, w, h, bytes_per_pixel);\n      bytes += slice_size;\n   }\n}\n#endif\n\nstatic unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)\n{\n   stbi__result_info ri;\n   void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8);\n\n   if (result == NULL)\n      return NULL;\n\n   // it is the responsibility of the loaders to make sure we get either 8 or 16 bit.\n   STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16);\n\n   if (ri.bits_per_channel != 8) {\n      result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp);\n      ri.bits_per_channel = 8;\n   }\n\n   // @TODO: move stbi__convert_format to here\n\n   if (stbi__vertically_flip_on_load) {\n      int channels = req_comp ? req_comp : *comp;\n      stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi_uc));\n   }\n\n   return (unsigned char *) result;\n}\n\nstatic stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)\n{\n   stbi__result_info ri;\n   void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16);\n\n   if (result == NULL)\n      return NULL;\n\n   // it is the responsibility of the loaders to make sure we get either 8 or 16 bit.\n   STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16);\n\n   if (ri.bits_per_channel != 16) {\n      result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp);\n      ri.bits_per_channel = 16;\n   }\n\n   // @TODO: move stbi__convert_format16 to here\n   // @TODO: special case RGB-to-Y (and RGBA-to-YA) for 8-bit-to-16-bit case to keep more precision\n\n   if (stbi__vertically_flip_on_load) {\n      int channels = req_comp ? req_comp : *comp;\n      stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi__uint16));\n   }\n\n   return (stbi__uint16 *) result;\n}\n\n#if !defined(STBI_NO_HDR) && !defined(STBI_NO_LINEAR)\nstatic void stbi__float_postprocess(float *result, int *x, int *y, int *comp, int req_comp)\n{\n   if (stbi__vertically_flip_on_load && result != NULL) {\n      int channels = req_comp ? req_comp : *comp;\n      stbi__vertical_flip(result, *x, *y, channels * sizeof(float));\n   }\n}\n#endif\n\n#ifndef STBI_NO_STDIO\n\n#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8)\nSTBI_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide);\nSTBI_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default);\n#endif\n\n#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8)\nSTBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input)\n{\n\treturn WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL);\n}\n#endif\n\nstatic FILE *stbi__fopen(char const *filename, char const *mode)\n{\n   FILE *f;\n#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8)\n   wchar_t wMode[64];\n   wchar_t wFilename[1024];\n\tif (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename)))\n      return 0;\n\n\tif (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode)))\n      return 0;\n\n#if defined(_MSC_VER) && _MSC_VER >= 1400\n\tif (0 != _wfopen_s(&f, wFilename, wMode))\n\t\tf = 0;\n#else\n   f = _wfopen(wFilename, wMode);\n#endif\n\n#elif defined(_MSC_VER) && _MSC_VER >= 1400\n   if (0 != fopen_s(&f, filename, mode))\n      f=0;\n#else\n   f = fopen(filename, mode);\n#endif\n   return f;\n}\n\n\nSTBIDEF stbi_uc *stbi_load(char const *filename, int *x, int *y, int *comp, int req_comp)\n{\n   FILE *f = stbi__fopen(filename, \"rb\");\n   unsigned char *result;\n   if (!f) return stbi__errpuc(\"can't fopen\", \"Unable to open file\");\n   result = stbi_load_from_file(f,x,y,comp,req_comp);\n   fclose(f);\n   return result;\n}\n\nSTBIDEF stbi_uc *stbi_load_from_file(FILE *f, int *x, int *y, int *comp, int req_comp)\n{\n   unsigned char *result;\n   stbi__context s;\n   stbi__start_file(&s,f);\n   result = stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp);\n   if (result) {\n      // need to 'unget' all the characters in the IO buffer\n      fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR);\n   }\n   return result;\n}\n\nSTBIDEF stbi__uint16 *stbi_load_from_file_16(FILE *f, int *x, int *y, int *comp, int req_comp)\n{\n   stbi__uint16 *result;\n   stbi__context s;\n   stbi__start_file(&s,f);\n   result = stbi__load_and_postprocess_16bit(&s,x,y,comp,req_comp);\n   if (result) {\n      // need to 'unget' all the characters in the IO buffer\n      fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR);\n   }\n   return result;\n}\n\nSTBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, int req_comp)\n{\n   FILE *f = stbi__fopen(filename, \"rb\");\n   stbi__uint16 *result;\n   if (!f) return (stbi_us *) stbi__errpuc(\"can't fopen\", \"Unable to open file\");\n   result = stbi_load_from_file_16(f,x,y,comp,req_comp);\n   fclose(f);\n   return result;\n}\n\n\n#endif //!STBI_NO_STDIO\n\nSTBIDEF stbi_us *stbi_load_16_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels)\n{\n   stbi__context s;\n   stbi__start_mem(&s,buffer,len);\n   return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels);\n}\n\nSTBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels)\n{\n   stbi__context s;\n   stbi__start_callbacks(&s, (stbi_io_callbacks *)clbk, user);\n   return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels);\n}\n\nSTBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp)\n{\n   stbi__context s;\n   stbi__start_mem(&s,buffer,len);\n   return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp);\n}\n\nSTBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp)\n{\n   stbi__context s;\n   stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user);\n   return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp);\n}\n\n#ifndef STBI_NO_GIF\nSTBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp)\n{\n   unsigned char *result;\n   stbi__context s;\n   stbi__start_mem(&s,buffer,len);\n\n   result = (unsigned char*) stbi__load_gif_main(&s, delays, x, y, z, comp, req_comp);\n   if (stbi__vertically_flip_on_load) {\n      stbi__vertical_flip_slices( result, *x, *y, *z, *comp );\n   }\n\n   return result;\n}\n#endif\n\n#ifndef STBI_NO_LINEAR\nstatic float *stbi__loadf_main(stbi__context *s, int *x, int *y, int *comp, int req_comp)\n{\n   unsigned char *data;\n   #ifndef STBI_NO_HDR\n   if (stbi__hdr_test(s)) {\n      stbi__result_info ri;\n      float *hdr_data = stbi__hdr_load(s,x,y,comp,req_comp, &ri);\n      if (hdr_data)\n         stbi__float_postprocess(hdr_data,x,y,comp,req_comp);\n      return hdr_data;\n   }\n   #endif\n   data = stbi__load_and_postprocess_8bit(s, x, y, comp, req_comp);\n   if (data)\n      return stbi__ldr_to_hdr(data, *x, *y, req_comp ? req_comp : *comp);\n   return stbi__errpf(\"unknown image type\", \"Image not of any known type, or corrupt\");\n}\n\nSTBIDEF float *stbi_loadf_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp)\n{\n   stbi__context s;\n   stbi__start_mem(&s,buffer,len);\n   return stbi__loadf_main(&s,x,y,comp,req_comp);\n}\n\nSTBIDEF float *stbi_loadf_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp)\n{\n   stbi__context s;\n   stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user);\n   return stbi__loadf_main(&s,x,y,comp,req_comp);\n}\n\n#ifndef STBI_NO_STDIO\nSTBIDEF float *stbi_loadf(char const *filename, int *x, int *y, int *comp, int req_comp)\n{\n   float *result;\n   FILE *f = stbi__fopen(filename, \"rb\");\n   if (!f) return stbi__errpf(\"can't fopen\", \"Unable to open file\");\n   result = stbi_loadf_from_file(f,x,y,comp,req_comp);\n   fclose(f);\n   return result;\n}\n\nSTBIDEF float *stbi_loadf_from_file(FILE *f, int *x, int *y, int *comp, int req_comp)\n{\n   stbi__context s;\n   stbi__start_file(&s,f);\n   return stbi__loadf_main(&s,x,y,comp,req_comp);\n}\n#endif // !STBI_NO_STDIO\n\n#endif // !STBI_NO_LINEAR\n\n// these is-hdr-or-not is defined independent of whether STBI_NO_LINEAR is\n// defined, for API simplicity; if STBI_NO_LINEAR is defined, it always\n// reports false!\n\nSTBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len)\n{\n   #ifndef STBI_NO_HDR\n   stbi__context s;\n   stbi__start_mem(&s,buffer,len);\n   return stbi__hdr_test(&s);\n   #else\n   STBI_NOTUSED(buffer);\n   STBI_NOTUSED(len);\n   return 0;\n   #endif\n}\n\n#ifndef STBI_NO_STDIO\nSTBIDEF int      stbi_is_hdr          (char const *filename)\n{\n   FILE *f = stbi__fopen(filename, \"rb\");\n   int result=0;\n   if (f) {\n      result = stbi_is_hdr_from_file(f);\n      fclose(f);\n   }\n   return result;\n}\n\nSTBIDEF int stbi_is_hdr_from_file(FILE *f)\n{\n   #ifndef STBI_NO_HDR\n   long pos = ftell(f);\n   int res;\n   stbi__context s;\n   stbi__start_file(&s,f);\n   res = stbi__hdr_test(&s);\n   fseek(f, pos, SEEK_SET);\n   return res;\n   #else\n   STBI_NOTUSED(f);\n   return 0;\n   #endif\n}\n#endif // !STBI_NO_STDIO\n\nSTBIDEF int      stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user)\n{\n   #ifndef STBI_NO_HDR\n   stbi__context s;\n   stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user);\n   return stbi__hdr_test(&s);\n   #else\n   STBI_NOTUSED(clbk);\n   STBI_NOTUSED(user);\n   return 0;\n   #endif\n}\n\n#ifndef STBI_NO_LINEAR\nstatic float stbi__l2h_gamma=2.2f, stbi__l2h_scale=1.0f;\n\nSTBIDEF void   stbi_ldr_to_hdr_gamma(float gamma) { stbi__l2h_gamma = gamma; }\nSTBIDEF void   stbi_ldr_to_hdr_scale(float scale) { stbi__l2h_scale = scale; }\n#endif\n\nstatic float stbi__h2l_gamma_i=1.0f/2.2f, stbi__h2l_scale_i=1.0f;\n\nSTBIDEF void   stbi_hdr_to_ldr_gamma(float gamma) { stbi__h2l_gamma_i = 1/gamma; }\nSTBIDEF void   stbi_hdr_to_ldr_scale(float scale) { stbi__h2l_scale_i = 1/scale; }\n\n\n//////////////////////////////////////////////////////////////////////////////\n//\n// Common code used by all image loaders\n//\n\nenum\n{\n   STBI__SCAN_load=0,\n   STBI__SCAN_type,\n   STBI__SCAN_header\n};\n\nstatic void stbi__refill_buffer(stbi__context *s)\n{\n   int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen);\n   s->callback_already_read += (int) (s->img_buffer - s->img_buffer_original);\n   if (n == 0) {\n      // at end of file, treat same as if from memory, but need to handle case\n      // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file\n      s->read_from_callbacks = 0;\n      s->img_buffer = s->buffer_start;\n      s->img_buffer_end = s->buffer_start+1;\n      *s->img_buffer = 0;\n   } else {\n      s->img_buffer = s->buffer_start;\n      s->img_buffer_end = s->buffer_start + n;\n   }\n}\n\nstbi_inline static stbi_uc stbi__get8(stbi__context *s)\n{\n   if (s->img_buffer < s->img_buffer_end)\n      return *s->img_buffer++;\n   if (s->read_from_callbacks) {\n      stbi__refill_buffer(s);\n      return *s->img_buffer++;\n   }\n   return 0;\n}\n\n#if defined(STBI_NO_JPEG) && defined(STBI_NO_HDR) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM)\n// nothing\n#else\nstbi_inline static int stbi__at_eof(stbi__context *s)\n{\n   if (s->io.read) {\n      if (!(s->io.eof)(s->io_user_data)) return 0;\n      // if feof() is true, check if buffer = end\n      // special case: we've only got the special 0 character at the end\n      if (s->read_from_callbacks == 0) return 1;\n   }\n\n   return s->img_buffer >= s->img_buffer_end;\n}\n#endif\n\n#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC)\n// nothing\n#else\nstatic void stbi__skip(stbi__context *s, int n)\n{\n   if (n == 0) return;  // already there!\n   if (n < 0) {\n      s->img_buffer = s->img_buffer_end;\n      return;\n   }\n   if (s->io.read) {\n      int blen = (int) (s->img_buffer_end - s->img_buffer);\n      if (blen < n) {\n         s->img_buffer = s->img_buffer_end;\n         (s->io.skip)(s->io_user_data, n - blen);\n         return;\n      }\n   }\n   s->img_buffer += n;\n}\n#endif\n\n#if defined(STBI_NO_PNG) && defined(STBI_NO_TGA) && defined(STBI_NO_HDR) && defined(STBI_NO_PNM)\n// nothing\n#else\nstatic int stbi__getn(stbi__context *s, stbi_uc *buffer, int n)\n{\n   if (s->io.read) {\n      int blen = (int) (s->img_buffer_end - s->img_buffer);\n      if (blen < n) {\n         int res, count;\n\n         memcpy(buffer, s->img_buffer, blen);\n\n         count = (s->io.read)(s->io_user_data, (char*) buffer + blen, n - blen);\n         res = (count == (n-blen));\n         s->img_buffer = s->img_buffer_end;\n         return res;\n      }\n   }\n\n   if (s->img_buffer+n <= s->img_buffer_end) {\n      memcpy(buffer, s->img_buffer, n);\n      s->img_buffer += n;\n      return 1;\n   } else\n      return 0;\n}\n#endif\n\n#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_PSD) && defined(STBI_NO_PIC)\n// nothing\n#else\nstatic int stbi__get16be(stbi__context *s)\n{\n   int z = stbi__get8(s);\n   return (z << 8) + stbi__get8(s);\n}\n#endif\n\n#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) && defined(STBI_NO_PIC)\n// nothing\n#else\nstatic stbi__uint32 stbi__get32be(stbi__context *s)\n{\n   stbi__uint32 z = stbi__get16be(s);\n   return (z << 16) + stbi__get16be(s);\n}\n#endif\n\n#if defined(STBI_NO_BMP) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF)\n// nothing\n#else\nstatic int stbi__get16le(stbi__context *s)\n{\n   int z = stbi__get8(s);\n   return z + (stbi__get8(s) << 8);\n}\n#endif\n\n#ifndef STBI_NO_BMP\nstatic stbi__uint32 stbi__get32le(stbi__context *s)\n{\n   stbi__uint32 z = stbi__get16le(s);\n   z += (stbi__uint32)stbi__get16le(s) << 16;\n   return z;\n}\n#endif\n\n#define STBI__BYTECAST(x)  ((stbi_uc) ((x) & 255))  // truncate int to byte without warnings\n\n#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM)\n// nothing\n#else\n//////////////////////////////////////////////////////////////////////////////\n//\n//  generic converter from built-in img_n to req_comp\n//    individual types do this automatically as much as possible (e.g. jpeg\n//    does all cases internally since it needs to colorspace convert anyway,\n//    and it never has alpha, so very few cases ). png can automatically\n//    interleave an alpha=255 channel, but falls back to this for other cases\n//\n//  assume data buffer is malloced, so malloc a new one and free that one\n//  only failure mode is malloc failing\n\nstatic stbi_uc stbi__compute_y(int r, int g, int b)\n{\n   return (stbi_uc) (((r*77) + (g*150) +  (29*b)) >> 8);\n}\n#endif\n\n#if defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM)\n// nothing\n#else\nstatic unsigned char *stbi__convert_format(unsigned char *data, int img_n, int req_comp, unsigned int x, unsigned int y)\n{\n   int i,j;\n   unsigned char *good;\n\n   if (req_comp == img_n) return data;\n   STBI_ASSERT(req_comp >= 1 && req_comp <= 4);\n\n   good = (unsigned char *) stbi__malloc_mad3(req_comp, x, y, 0);\n   if (good == NULL) {\n      STBI_FREE(data);\n      return stbi__errpuc(\"outofmem\", \"Out of memory\");\n   }\n\n   for (j=0; j < (int) y; ++j) {\n      unsigned char *src  = data + j * x * img_n   ;\n      unsigned char *dest = good + j * x * req_comp;\n\n      #define STBI__COMBO(a,b)  ((a)*8+(b))\n      #define STBI__CASE(a,b)   case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b)\n      // convert source image with img_n components to one with req_comp components;\n      // avoid switch per pixel, so use switch per scanline and massive macros\n      switch (STBI__COMBO(img_n, req_comp)) {\n         STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=255;                                     } break;\n         STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0];                                  } break;\n         STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=255;                     } break;\n         STBI__CASE(2,1) { dest[0]=src[0];                                                  } break;\n         STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0];                                  } break;\n         STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1];                  } break;\n         STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=255;        } break;\n         STBI__CASE(3,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]);                   } break;\n         STBI__CASE(3,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = 255;    } break;\n         STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]);                   } break;\n         STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break;\n         STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];                    } break;\n         default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return stbi__errpuc(\"unsupported\", \"Unsupported format conversion\");\n      }\n      #undef STBI__CASE\n   }\n\n   STBI_FREE(data);\n   return good;\n}\n#endif\n\n#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD)\n// nothing\n#else\nstatic stbi__uint16 stbi__compute_y_16(int r, int g, int b)\n{\n   return (stbi__uint16) (((r*77) + (g*150) +  (29*b)) >> 8);\n}\n#endif\n\n#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD)\n// nothing\n#else\nstatic stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int req_comp, unsigned int x, unsigned int y)\n{\n   int i,j;\n   stbi__uint16 *good;\n\n   if (req_comp == img_n) return data;\n   STBI_ASSERT(req_comp >= 1 && req_comp <= 4);\n\n   good = (stbi__uint16 *) stbi__malloc(req_comp * x * y * 2);\n   if (good == NULL) {\n      STBI_FREE(data);\n      return (stbi__uint16 *) stbi__errpuc(\"outofmem\", \"Out of memory\");\n   }\n\n   for (j=0; j < (int) y; ++j) {\n      stbi__uint16 *src  = data + j * x * img_n   ;\n      stbi__uint16 *dest = good + j * x * req_comp;\n\n      #define STBI__COMBO(a,b)  ((a)*8+(b))\n      #define STBI__CASE(a,b)   case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b)\n      // convert source image with img_n components to one with req_comp components;\n      // avoid switch per pixel, so use switch per scanline and massive macros\n      switch (STBI__COMBO(img_n, req_comp)) {\n         STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=0xffff;                                     } break;\n         STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0];                                     } break;\n         STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=0xffff;                     } break;\n         STBI__CASE(2,1) { dest[0]=src[0];                                                     } break;\n         STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0];                                     } break;\n         STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1];                     } break;\n         STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=0xffff;        } break;\n         STBI__CASE(3,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]);                   } break;\n         STBI__CASE(3,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = 0xffff; } break;\n         STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]);                   } break;\n         STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break;\n         STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];                       } break;\n         default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return (stbi__uint16*) stbi__errpuc(\"unsupported\", \"Unsupported format conversion\");\n      }\n      #undef STBI__CASE\n   }\n\n   STBI_FREE(data);\n   return good;\n}\n#endif\n\n#ifndef STBI_NO_LINEAR\nstatic float   *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp)\n{\n   int i,k,n;\n   float *output;\n   if (!data) return NULL;\n   output = (float *) stbi__malloc_mad4(x, y, comp, sizeof(float), 0);\n   if (output == NULL) { STBI_FREE(data); return stbi__errpf(\"outofmem\", \"Out of memory\"); }\n   // compute number of non-alpha components\n   if (comp & 1) n = comp; else n = comp-1;\n   for (i=0; i < x*y; ++i) {\n      for (k=0; k < n; ++k) {\n         output[i*comp + k] = (float) (pow(data[i*comp+k]/255.0f, stbi__l2h_gamma) * stbi__l2h_scale);\n      }\n   }\n   if (n < comp) {\n      for (i=0; i < x*y; ++i) {\n         output[i*comp + n] = data[i*comp + n]/255.0f;\n      }\n   }\n   STBI_FREE(data);\n   return output;\n}\n#endif\n\n#ifndef STBI_NO_HDR\n#define stbi__float2int(x)   ((int) (x))\nstatic stbi_uc *stbi__hdr_to_ldr(float   *data, int x, int y, int comp)\n{\n   int i,k,n;\n   stbi_uc *output;\n   if (!data) return NULL;\n   output = (stbi_uc *) stbi__malloc_mad3(x, y, comp, 0);\n   if (output == NULL) { STBI_FREE(data); return stbi__errpuc(\"outofmem\", \"Out of memory\"); }\n   // compute number of non-alpha components\n   if (comp & 1) n = comp; else n = comp-1;\n   for (i=0; i < x*y; ++i) {\n      for (k=0; k < n; ++k) {\n         float z = (float) pow(data[i*comp+k]*stbi__h2l_scale_i, stbi__h2l_gamma_i) * 255 + 0.5f;\n         if (z < 0) z = 0;\n         if (z > 255) z = 255;\n         output[i*comp + k] = (stbi_uc) stbi__float2int(z);\n      }\n      if (k < comp) {\n         float z = data[i*comp+k] * 255 + 0.5f;\n         if (z < 0) z = 0;\n         if (z > 255) z = 255;\n         output[i*comp + k] = (stbi_uc) stbi__float2int(z);\n      }\n   }\n   STBI_FREE(data);\n   return output;\n}\n#endif\n\n//////////////////////////////////////////////////////////////////////////////\n//\n//  \"baseline\" JPEG/JFIF decoder\n//\n//    simple implementation\n//      - doesn't support delayed output of y-dimension\n//      - simple interface (only one output format: 8-bit interleaved RGB)\n//      - doesn't try to recover corrupt jpegs\n//      - doesn't allow partial loading, loading multiple at once\n//      - still fast on x86 (copying globals into locals doesn't help x86)\n//      - allocates lots of intermediate memory (full size of all components)\n//        - non-interleaved case requires this anyway\n//        - allows good upsampling (see next)\n//    high-quality\n//      - upsampled channels are bilinearly interpolated, even across blocks\n//      - quality integer IDCT derived from IJG's 'slow'\n//    performance\n//      - fast huffman; reasonable integer IDCT\n//      - some SIMD kernels for common paths on targets with SSE2/NEON\n//      - uses a lot of intermediate memory, could cache poorly\n\n#ifndef STBI_NO_JPEG\n\n// huffman decoding acceleration\n#define FAST_BITS   9  // larger handles more cases; smaller stomps less cache\n\ntypedef struct\n{\n   stbi_uc  fast[1 << FAST_BITS];\n   // weirdly, repacking this into AoS is a 10% speed loss, instead of a win\n   stbi__uint16 code[256];\n   stbi_uc  values[256];\n   stbi_uc  size[257];\n   unsigned int maxcode[18];\n   int    delta[17];   // old 'firstsymbol' - old 'firstcode'\n} stbi__huffman;\n\ntypedef struct\n{\n   stbi__context *s;\n   stbi__huffman huff_dc[4];\n   stbi__huffman huff_ac[4];\n   stbi__uint16 dequant[4][64];\n   stbi__int16 fast_ac[4][1 << FAST_BITS];\n\n// sizes for components, interleaved MCUs\n   int img_h_max, img_v_max;\n   int img_mcu_x, img_mcu_y;\n   int img_mcu_w, img_mcu_h;\n\n// definition of jpeg image component\n   struct\n   {\n      int id;\n      int h,v;\n      int tq;\n      int hd,ha;\n      int dc_pred;\n\n      int x,y,w2,h2;\n      stbi_uc *data;\n      void *raw_data, *raw_coeff;\n      stbi_uc *linebuf;\n      short   *coeff;   // progressive only\n      int      coeff_w, coeff_h; // number of 8x8 coefficient blocks\n   } img_comp[4];\n\n   stbi__uint32   code_buffer; // jpeg entropy-coded buffer\n   int            code_bits;   // number of valid bits\n   unsigned char  marker;      // marker seen while filling entropy buffer\n   int            nomore;      // flag if we saw a marker so must stop\n\n   int            progressive;\n   int            spec_start;\n   int            spec_end;\n   int            succ_high;\n   int            succ_low;\n   int            eob_run;\n   int            jfif;\n   int            app14_color_transform; // Adobe APP14 tag\n   int            rgb;\n\n   int scan_n, order[4];\n   int restart_interval, todo;\n\n// kernels\n   void (*idct_block_kernel)(stbi_uc *out, int out_stride, short data[64]);\n   void (*YCbCr_to_RGB_kernel)(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step);\n   stbi_uc *(*resample_row_hv_2_kernel)(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs);\n} stbi__jpeg;\n\nstatic int stbi__build_huffman(stbi__huffman *h, int *count)\n{\n   int i,j,k=0;\n   unsigned int code;\n   // build size list for each symbol (from JPEG spec)\n   for (i=0; i < 16; ++i) {\n      for (j=0; j < count[i]; ++j) {\n         h->size[k++] = (stbi_uc) (i+1);\n         if(k >= 257) return stbi__err(\"bad size list\",\"Corrupt JPEG\");\n      }\n   }\n   h->size[k] = 0;\n\n   // compute actual symbols (from jpeg spec)\n   code = 0;\n   k = 0;\n   for(j=1; j <= 16; ++j) {\n      // compute delta to add to code to compute symbol id\n      h->delta[j] = k - code;\n      if (h->size[k] == j) {\n         while (h->size[k] == j)\n            h->code[k++] = (stbi__uint16) (code++);\n         if (code-1 >= (1u << j)) return stbi__err(\"bad code lengths\",\"Corrupt JPEG\");\n      }\n      // compute largest code + 1 for this size, preshifted as needed later\n      h->maxcode[j] = code << (16-j);\n      code <<= 1;\n   }\n   h->maxcode[j] = 0xffffffff;\n\n   // build non-spec acceleration table; 255 is flag for not-accelerated\n   memset(h->fast, 255, 1 << FAST_BITS);\n   for (i=0; i < k; ++i) {\n      int s = h->size[i];\n      if (s <= FAST_BITS) {\n         int c = h->code[i] << (FAST_BITS-s);\n         int m = 1 << (FAST_BITS-s);\n         for (j=0; j < m; ++j) {\n            h->fast[c+j] = (stbi_uc) i;\n         }\n      }\n   }\n   return 1;\n}\n\n// build a table that decodes both magnitude and value of small ACs in\n// one go.\nstatic void stbi__build_fast_ac(stbi__int16 *fast_ac, stbi__huffman *h)\n{\n   int i;\n   for (i=0; i < (1 << FAST_BITS); ++i) {\n      stbi_uc fast = h->fast[i];\n      fast_ac[i] = 0;\n      if (fast < 255) {\n         int rs = h->values[fast];\n         int run = (rs >> 4) & 15;\n         int magbits = rs & 15;\n         int len = h->size[fast];\n\n         if (magbits && len + magbits <= FAST_BITS) {\n            // magnitude code followed by receive_extend code\n            int k = ((i << len) & ((1 << FAST_BITS) - 1)) >> (FAST_BITS - magbits);\n            int m = 1 << (magbits - 1);\n            if (k < m) k += (~0U << magbits) + 1;\n            // if the result is small enough, we can fit it in fast_ac table\n            if (k >= -128 && k <= 127)\n               fast_ac[i] = (stbi__int16) ((k * 256) + (run * 16) + (len + magbits));\n         }\n      }\n   }\n}\n\nstatic void stbi__grow_buffer_unsafe(stbi__jpeg *j)\n{\n   do {\n      unsigned int b = j->nomore ? 0 : stbi__get8(j->s);\n      if (b == 0xff) {\n         int c = stbi__get8(j->s);\n         while (c == 0xff) c = stbi__get8(j->s); // consume fill bytes\n         if (c != 0) {\n            j->marker = (unsigned char) c;\n            j->nomore = 1;\n            return;\n         }\n      }\n      j->code_buffer |= b << (24 - j->code_bits);\n      j->code_bits += 8;\n   } while (j->code_bits <= 24);\n}\n\n// (1 << n) - 1\nstatic const stbi__uint32 stbi__bmask[17]={0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535};\n\n// decode a jpeg huffman value from the bitstream\nstbi_inline static int stbi__jpeg_huff_decode(stbi__jpeg *j, stbi__huffman *h)\n{\n   unsigned int temp;\n   int c,k;\n\n   if (j->code_bits < 16) stbi__grow_buffer_unsafe(j);\n\n   // look at the top FAST_BITS and determine what symbol ID it is,\n   // if the code is <= FAST_BITS\n   c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1);\n   k = h->fast[c];\n   if (k < 255) {\n      int s = h->size[k];\n      if (s > j->code_bits)\n         return -1;\n      j->code_buffer <<= s;\n      j->code_bits -= s;\n      return h->values[k];\n   }\n\n   // naive test is to shift the code_buffer down so k bits are\n   // valid, then test against maxcode. To speed this up, we've\n   // preshifted maxcode left so that it has (16-k) 0s at the\n   // end; in other words, regardless of the number of bits, it\n   // wants to be compared against something shifted to have 16;\n   // that way we don't need to shift inside the loop.\n   temp = j->code_buffer >> 16;\n   for (k=FAST_BITS+1 ; ; ++k)\n      if (temp < h->maxcode[k])\n         break;\n   if (k == 17) {\n      // error! code not found\n      j->code_bits -= 16;\n      return -1;\n   }\n\n   if (k > j->code_bits)\n      return -1;\n\n   // convert the huffman code to the symbol id\n   c = ((j->code_buffer >> (32 - k)) & stbi__bmask[k]) + h->delta[k];\n   if(c < 0 || c >= 256) // symbol id out of bounds!\n       return -1;\n   STBI_ASSERT((((j->code_buffer) >> (32 - h->size[c])) & stbi__bmask[h->size[c]]) == h->code[c]);\n\n   // convert the id to a symbol\n   j->code_bits -= k;\n   j->code_buffer <<= k;\n   return h->values[c];\n}\n\n// bias[n] = (-1<<n) + 1\nstatic const int stbi__jbias[16] = {0,-1,-3,-7,-15,-31,-63,-127,-255,-511,-1023,-2047,-4095,-8191,-16383,-32767};\n\n// combined JPEG 'receive' and JPEG 'extend', since baseline\n// always extends everything it receives.\nstbi_inline static int stbi__extend_receive(stbi__jpeg *j, int n)\n{\n   unsigned int k;\n   int sgn;\n   if (j->code_bits < n) stbi__grow_buffer_unsafe(j);\n   if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing\n\n   sgn = j->code_buffer >> 31; // sign bit always in MSB; 0 if MSB clear (positive), 1 if MSB set (negative)\n   k = stbi_lrot(j->code_buffer, n);\n   j->code_buffer = k & ~stbi__bmask[n];\n   k &= stbi__bmask[n];\n   j->code_bits -= n;\n   return k + (stbi__jbias[n] & (sgn - 1));\n}\n\n// get some unsigned bits\nstbi_inline static int stbi__jpeg_get_bits(stbi__jpeg *j, int n)\n{\n   unsigned int k;\n   if (j->code_bits < n) stbi__grow_buffer_unsafe(j);\n   if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing\n   k = stbi_lrot(j->code_buffer, n);\n   j->code_buffer = k & ~stbi__bmask[n];\n   k &= stbi__bmask[n];\n   j->code_bits -= n;\n   return k;\n}\n\nstbi_inline static int stbi__jpeg_get_bit(stbi__jpeg *j)\n{\n   unsigned int k;\n   if (j->code_bits < 1) stbi__grow_buffer_unsafe(j);\n   if (j->code_bits < 1) return 0; // ran out of bits from stream, return 0s intead of continuing\n   k = j->code_buffer;\n   j->code_buffer <<= 1;\n   --j->code_bits;\n   return k & 0x80000000;\n}\n\n// given a value that's at position X in the zigzag stream,\n// where does it appear in the 8x8 matrix coded as row-major?\nstatic const stbi_uc stbi__jpeg_dezigzag[64+15] =\n{\n    0,  1,  8, 16,  9,  2,  3, 10,\n   17, 24, 32, 25, 18, 11,  4,  5,\n   12, 19, 26, 33, 40, 48, 41, 34,\n   27, 20, 13,  6,  7, 14, 21, 28,\n   35, 42, 49, 56, 57, 50, 43, 36,\n   29, 22, 15, 23, 30, 37, 44, 51,\n   58, 59, 52, 45, 38, 31, 39, 46,\n   53, 60, 61, 54, 47, 55, 62, 63,\n   // let corrupt input sample past end\n   63, 63, 63, 63, 63, 63, 63, 63,\n   63, 63, 63, 63, 63, 63, 63\n};\n\n// decode one 64-entry block--\nstatic int stbi__jpeg_decode_block(stbi__jpeg *j, short data[64], stbi__huffman *hdc, stbi__huffman *hac, stbi__int16 *fac, int b, stbi__uint16 *dequant)\n{\n   int diff,dc,k;\n   int t;\n\n   if (j->code_bits < 16) stbi__grow_buffer_unsafe(j);\n   t = stbi__jpeg_huff_decode(j, hdc);\n   if (t < 0 || t > 15) return stbi__err(\"bad huffman code\",\"Corrupt JPEG\");\n\n   // 0 all the ac values now so we can do it 32-bits at a time\n   memset(data,0,64*sizeof(data[0]));\n\n   diff = t ? stbi__extend_receive(j, t) : 0;\n   if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err(\"bad delta\",\"Corrupt JPEG\");\n   dc = j->img_comp[b].dc_pred + diff;\n   j->img_comp[b].dc_pred = dc;\n   if (!stbi__mul2shorts_valid(dc, dequant[0])) return stbi__err(\"can't merge dc and ac\", \"Corrupt JPEG\");\n   data[0] = (short) (dc * dequant[0]);\n\n   // decode AC components, see JPEG spec\n   k = 1;\n   do {\n      unsigned int zig;\n      int c,r,s;\n      if (j->code_bits < 16) stbi__grow_buffer_unsafe(j);\n      c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1);\n      r = fac[c];\n      if (r) { // fast-AC path\n         k += (r >> 4) & 15; // run\n         s = r & 15; // combined length\n         if (s > j->code_bits) return stbi__err(\"bad huffman code\", \"Combined length longer than code bits available\");\n         j->code_buffer <<= s;\n         j->code_bits -= s;\n         // decode into unzigzag'd location\n         zig = stbi__jpeg_dezigzag[k++];\n         data[zig] = (short) ((r >> 8) * dequant[zig]);\n      } else {\n         int rs = stbi__jpeg_huff_decode(j, hac);\n         if (rs < 0) return stbi__err(\"bad huffman code\",\"Corrupt JPEG\");\n         s = rs & 15;\n         r = rs >> 4;\n         if (s == 0) {\n            if (rs != 0xf0) break; // end block\n            k += 16;\n         } else {\n            k += r;\n            // decode into unzigzag'd location\n            zig = stbi__jpeg_dezigzag[k++];\n            data[zig] = (short) (stbi__extend_receive(j,s) * dequant[zig]);\n         }\n      }\n   } while (k < 64);\n   return 1;\n}\n\nstatic int stbi__jpeg_decode_block_prog_dc(stbi__jpeg *j, short data[64], stbi__huffman *hdc, int b)\n{\n   int diff,dc;\n   int t;\n   if (j->spec_end != 0) return stbi__err(\"can't merge dc and ac\", \"Corrupt JPEG\");\n\n   if (j->code_bits < 16) stbi__grow_buffer_unsafe(j);\n\n   if (j->succ_high == 0) {\n      // first scan for DC coefficient, must be first\n      memset(data,0,64*sizeof(data[0])); // 0 all the ac values now\n      t = stbi__jpeg_huff_decode(j, hdc);\n      if (t < 0 || t > 15) return stbi__err(\"can't merge dc and ac\", \"Corrupt JPEG\");\n      diff = t ? stbi__extend_receive(j, t) : 0;\n\n      if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err(\"bad delta\", \"Corrupt JPEG\");\n      dc = j->img_comp[b].dc_pred + diff;\n      j->img_comp[b].dc_pred = dc;\n      if (!stbi__mul2shorts_valid(dc, 1 << j->succ_low)) return stbi__err(\"can't merge dc and ac\", \"Corrupt JPEG\");\n      data[0] = (short) (dc * (1 << j->succ_low));\n   } else {\n      // refinement scan for DC coefficient\n      if (stbi__jpeg_get_bit(j))\n         data[0] += (short) (1 << j->succ_low);\n   }\n   return 1;\n}\n\n// @OPTIMIZE: store non-zigzagged during the decode passes,\n// and only de-zigzag when dequantizing\nstatic int stbi__jpeg_decode_block_prog_ac(stbi__jpeg *j, short data[64], stbi__huffman *hac, stbi__int16 *fac)\n{\n   int k;\n   if (j->spec_start == 0) return stbi__err(\"can't merge dc and ac\", \"Corrupt JPEG\");\n\n   if (j->succ_high == 0) {\n      int shift = j->succ_low;\n\n      if (j->eob_run) {\n         --j->eob_run;\n         return 1;\n      }\n\n      k = j->spec_start;\n      do {\n         unsigned int zig;\n         int c,r,s;\n         if (j->code_bits < 16) stbi__grow_buffer_unsafe(j);\n         c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1);\n         r = fac[c];\n         if (r) { // fast-AC path\n            k += (r >> 4) & 15; // run\n            s = r & 15; // combined length\n            if (s > j->code_bits) return stbi__err(\"bad huffman code\", \"Combined length longer than code bits available\");\n            j->code_buffer <<= s;\n            j->code_bits -= s;\n            zig = stbi__jpeg_dezigzag[k++];\n            data[zig] = (short) ((r >> 8) * (1 << shift));\n         } else {\n            int rs = stbi__jpeg_huff_decode(j, hac);\n            if (rs < 0) return stbi__err(\"bad huffman code\",\"Corrupt JPEG\");\n            s = rs & 15;\n            r = rs >> 4;\n            if (s == 0) {\n               if (r < 15) {\n                  j->eob_run = (1 << r);\n                  if (r)\n                     j->eob_run += stbi__jpeg_get_bits(j, r);\n                  --j->eob_run;\n                  break;\n               }\n               k += 16;\n            } else {\n               k += r;\n               zig = stbi__jpeg_dezigzag[k++];\n               data[zig] = (short) (stbi__extend_receive(j,s) * (1 << shift));\n            }\n         }\n      } while (k <= j->spec_end);\n   } else {\n      // refinement scan for these AC coefficients\n\n      short bit = (short) (1 << j->succ_low);\n\n      if (j->eob_run) {\n         --j->eob_run;\n         for (k = j->spec_start; k <= j->spec_end; ++k) {\n            short *p = &data[stbi__jpeg_dezigzag[k]];\n            if (*p != 0)\n               if (stbi__jpeg_get_bit(j))\n                  if ((*p & bit)==0) {\n                     if (*p > 0)\n                        *p += bit;\n                     else\n                        *p -= bit;\n                  }\n         }\n      } else {\n         k = j->spec_start;\n         do {\n            int r,s;\n            int rs = stbi__jpeg_huff_decode(j, hac); // @OPTIMIZE see if we can use the fast path here, advance-by-r is so slow, eh\n            if (rs < 0) return stbi__err(\"bad huffman code\",\"Corrupt JPEG\");\n            s = rs & 15;\n            r = rs >> 4;\n            if (s == 0) {\n               if (r < 15) {\n                  j->eob_run = (1 << r) - 1;\n                  if (r)\n                     j->eob_run += stbi__jpeg_get_bits(j, r);\n                  r = 64; // force end of block\n               } else {\n                  // r=15 s=0 should write 16 0s, so we just do\n                  // a run of 15 0s and then write s (which is 0),\n                  // so we don't have to do anything special here\n               }\n            } else {\n               if (s != 1) return stbi__err(\"bad huffman code\", \"Corrupt JPEG\");\n               // sign bit\n               if (stbi__jpeg_get_bit(j))\n                  s = bit;\n               else\n                  s = -bit;\n            }\n\n            // advance by r\n            while (k <= j->spec_end) {\n               short *p = &data[stbi__jpeg_dezigzag[k++]];\n               if (*p != 0) {\n                  if (stbi__jpeg_get_bit(j))\n                     if ((*p & bit)==0) {\n                        if (*p > 0)\n                           *p += bit;\n                        else\n                           *p -= bit;\n                     }\n               } else {\n                  if (r == 0) {\n                     *p = (short) s;\n                     break;\n                  }\n                  --r;\n               }\n            }\n         } while (k <= j->spec_end);\n      }\n   }\n   return 1;\n}\n\n// take a -128..127 value and stbi__clamp it and convert to 0..255\nstbi_inline static stbi_uc stbi__clamp(int x)\n{\n   // trick to use a single test to catch both cases\n   if ((unsigned int) x > 255) {\n      if (x < 0) return 0;\n      if (x > 255) return 255;\n   }\n   return (stbi_uc) x;\n}\n\n#define stbi__f2f(x)  ((int) (((x) * 4096 + 0.5)))\n#define stbi__fsh(x)  ((x) * 4096)\n\n// derived from jidctint -- DCT_ISLOW\n#define STBI__IDCT_1D(s0,s1,s2,s3,s4,s5,s6,s7) \\\n   int t0,t1,t2,t3,p1,p2,p3,p4,p5,x0,x1,x2,x3; \\\n   p2 = s2;                                    \\\n   p3 = s6;                                    \\\n   p1 = (p2+p3) * stbi__f2f(0.5411961f);       \\\n   t2 = p1 + p3*stbi__f2f(-1.847759065f);      \\\n   t3 = p1 + p2*stbi__f2f( 0.765366865f);      \\\n   p2 = s0;                                    \\\n   p3 = s4;                                    \\\n   t0 = stbi__fsh(p2+p3);                      \\\n   t1 = stbi__fsh(p2-p3);                      \\\n   x0 = t0+t3;                                 \\\n   x3 = t0-t3;                                 \\\n   x1 = t1+t2;                                 \\\n   x2 = t1-t2;                                 \\\n   t0 = s7;                                    \\\n   t1 = s5;                                    \\\n   t2 = s3;                                    \\\n   t3 = s1;                                    \\\n   p3 = t0+t2;                                 \\\n   p4 = t1+t3;                                 \\\n   p1 = t0+t3;                                 \\\n   p2 = t1+t2;                                 \\\n   p5 = (p3+p4)*stbi__f2f( 1.175875602f);      \\\n   t0 = t0*stbi__f2f( 0.298631336f);           \\\n   t1 = t1*stbi__f2f( 2.053119869f);           \\\n   t2 = t2*stbi__f2f( 3.072711026f);           \\\n   t3 = t3*stbi__f2f( 1.501321110f);           \\\n   p1 = p5 + p1*stbi__f2f(-0.899976223f);      \\\n   p2 = p5 + p2*stbi__f2f(-2.562915447f);      \\\n   p3 = p3*stbi__f2f(-1.961570560f);           \\\n   p4 = p4*stbi__f2f(-0.390180644f);           \\\n   t3 += p1+p4;                                \\\n   t2 += p2+p3;                                \\\n   t1 += p2+p4;                                \\\n   t0 += p1+p3;\n\nstatic void stbi__idct_block(stbi_uc *out, int out_stride, short data[64])\n{\n   int i,val[64],*v=val;\n   stbi_uc *o;\n   short *d = data;\n\n   // columns\n   for (i=0; i < 8; ++i,++d, ++v) {\n      // if all zeroes, shortcut -- this avoids dequantizing 0s and IDCTing\n      if (d[ 8]==0 && d[16]==0 && d[24]==0 && d[32]==0\n           && d[40]==0 && d[48]==0 && d[56]==0) {\n         //    no shortcut                 0     seconds\n         //    (1|2|3|4|5|6|7)==0          0     seconds\n         //    all separate               -0.047 seconds\n         //    1 && 2|3 && 4|5 && 6|7:    -0.047 seconds\n         int dcterm = d[0]*4;\n         v[0] = v[8] = v[16] = v[24] = v[32] = v[40] = v[48] = v[56] = dcterm;\n      } else {\n         STBI__IDCT_1D(d[ 0],d[ 8],d[16],d[24],d[32],d[40],d[48],d[56])\n         // constants scaled things up by 1<<12; let's bring them back\n         // down, but keep 2 extra bits of precision\n         x0 += 512; x1 += 512; x2 += 512; x3 += 512;\n         v[ 0] = (x0+t3) >> 10;\n         v[56] = (x0-t3) >> 10;\n         v[ 8] = (x1+t2) >> 10;\n         v[48] = (x1-t2) >> 10;\n         v[16] = (x2+t1) >> 10;\n         v[40] = (x2-t1) >> 10;\n         v[24] = (x3+t0) >> 10;\n         v[32] = (x3-t0) >> 10;\n      }\n   }\n\n   for (i=0, v=val, o=out; i < 8; ++i,v+=8,o+=out_stride) {\n      // no fast case since the first 1D IDCT spread components out\n      STBI__IDCT_1D(v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7])\n      // constants scaled things up by 1<<12, plus we had 1<<2 from first\n      // loop, plus horizontal and vertical each scale by sqrt(8) so together\n      // we've got an extra 1<<3, so 1<<17 total we need to remove.\n      // so we want to round that, which means adding 0.5 * 1<<17,\n      // aka 65536. Also, we'll end up with -128 to 127 that we want\n      // to encode as 0..255 by adding 128, so we'll add that before the shift\n      x0 += 65536 + (128<<17);\n      x1 += 65536 + (128<<17);\n      x2 += 65536 + (128<<17);\n      x3 += 65536 + (128<<17);\n      // tried computing the shifts into temps, or'ing the temps to see\n      // if any were out of range, but that was slower\n      o[0] = stbi__clamp((x0+t3) >> 17);\n      o[7] = stbi__clamp((x0-t3) >> 17);\n      o[1] = stbi__clamp((x1+t2) >> 17);\n      o[6] = stbi__clamp((x1-t2) >> 17);\n      o[2] = stbi__clamp((x2+t1) >> 17);\n      o[5] = stbi__clamp((x2-t1) >> 17);\n      o[3] = stbi__clamp((x3+t0) >> 17);\n      o[4] = stbi__clamp((x3-t0) >> 17);\n   }\n}\n\n#ifdef STBI_SSE2\n// sse2 integer IDCT. not the fastest possible implementation but it\n// produces bit-identical results to the generic C version so it's\n// fully \"transparent\".\nstatic void stbi__idct_simd(stbi_uc *out, int out_stride, short data[64])\n{\n   // This is constructed to match our regular (generic) integer IDCT exactly.\n   __m128i row0, row1, row2, row3, row4, row5, row6, row7;\n   __m128i tmp;\n\n   // dot product constant: even elems=x, odd elems=y\n   #define dct_const(x,y)  _mm_setr_epi16((x),(y),(x),(y),(x),(y),(x),(y))\n\n   // out(0) = c0[even]*x + c0[odd]*y   (c0, x, y 16-bit, out 32-bit)\n   // out(1) = c1[even]*x + c1[odd]*y\n   #define dct_rot(out0,out1, x,y,c0,c1) \\\n      __m128i c0##lo = _mm_unpacklo_epi16((x),(y)); \\\n      __m128i c0##hi = _mm_unpackhi_epi16((x),(y)); \\\n      __m128i out0##_l = _mm_madd_epi16(c0##lo, c0); \\\n      __m128i out0##_h = _mm_madd_epi16(c0##hi, c0); \\\n      __m128i out1##_l = _mm_madd_epi16(c0##lo, c1); \\\n      __m128i out1##_h = _mm_madd_epi16(c0##hi, c1)\n\n   // out = in << 12  (in 16-bit, out 32-bit)\n   #define dct_widen(out, in) \\\n      __m128i out##_l = _mm_srai_epi32(_mm_unpacklo_epi16(_mm_setzero_si128(), (in)), 4); \\\n      __m128i out##_h = _mm_srai_epi32(_mm_unpackhi_epi16(_mm_setzero_si128(), (in)), 4)\n\n   // wide add\n   #define dct_wadd(out, a, b) \\\n      __m128i out##_l = _mm_add_epi32(a##_l, b##_l); \\\n      __m128i out##_h = _mm_add_epi32(a##_h, b##_h)\n\n   // wide sub\n   #define dct_wsub(out, a, b) \\\n      __m128i out##_l = _mm_sub_epi32(a##_l, b##_l); \\\n      __m128i out##_h = _mm_sub_epi32(a##_h, b##_h)\n\n   // butterfly a/b, add bias, then shift by \"s\" and pack\n   #define dct_bfly32o(out0, out1, a,b,bias,s) \\\n      { \\\n         __m128i abiased_l = _mm_add_epi32(a##_l, bias); \\\n         __m128i abiased_h = _mm_add_epi32(a##_h, bias); \\\n         dct_wadd(sum, abiased, b); \\\n         dct_wsub(dif, abiased, b); \\\n         out0 = _mm_packs_epi32(_mm_srai_epi32(sum_l, s), _mm_srai_epi32(sum_h, s)); \\\n         out1 = _mm_packs_epi32(_mm_srai_epi32(dif_l, s), _mm_srai_epi32(dif_h, s)); \\\n      }\n\n   // 8-bit interleave step (for transposes)\n   #define dct_interleave8(a, b) \\\n      tmp = a; \\\n      a = _mm_unpacklo_epi8(a, b); \\\n      b = _mm_unpackhi_epi8(tmp, b)\n\n   // 16-bit interleave step (for transposes)\n   #define dct_interleave16(a, b) \\\n      tmp = a; \\\n      a = _mm_unpacklo_epi16(a, b); \\\n      b = _mm_unpackhi_epi16(tmp, b)\n\n   #define dct_pass(bias,shift) \\\n      { \\\n         /* even part */ \\\n         dct_rot(t2e,t3e, row2,row6, rot0_0,rot0_1); \\\n         __m128i sum04 = _mm_add_epi16(row0, row4); \\\n         __m128i dif04 = _mm_sub_epi16(row0, row4); \\\n         dct_widen(t0e, sum04); \\\n         dct_widen(t1e, dif04); \\\n         dct_wadd(x0, t0e, t3e); \\\n         dct_wsub(x3, t0e, t3e); \\\n         dct_wadd(x1, t1e, t2e); \\\n         dct_wsub(x2, t1e, t2e); \\\n         /* odd part */ \\\n         dct_rot(y0o,y2o, row7,row3, rot2_0,rot2_1); \\\n         dct_rot(y1o,y3o, row5,row1, rot3_0,rot3_1); \\\n         __m128i sum17 = _mm_add_epi16(row1, row7); \\\n         __m128i sum35 = _mm_add_epi16(row3, row5); \\\n         dct_rot(y4o,y5o, sum17,sum35, rot1_0,rot1_1); \\\n         dct_wadd(x4, y0o, y4o); \\\n         dct_wadd(x5, y1o, y5o); \\\n         dct_wadd(x6, y2o, y5o); \\\n         dct_wadd(x7, y3o, y4o); \\\n         dct_bfly32o(row0,row7, x0,x7,bias,shift); \\\n         dct_bfly32o(row1,row6, x1,x6,bias,shift); \\\n         dct_bfly32o(row2,row5, x2,x5,bias,shift); \\\n         dct_bfly32o(row3,row4, x3,x4,bias,shift); \\\n      }\n\n   __m128i rot0_0 = dct_const(stbi__f2f(0.5411961f), stbi__f2f(0.5411961f) + stbi__f2f(-1.847759065f));\n   __m128i rot0_1 = dct_const(stbi__f2f(0.5411961f) + stbi__f2f( 0.765366865f), stbi__f2f(0.5411961f));\n   __m128i rot1_0 = dct_const(stbi__f2f(1.175875602f) + stbi__f2f(-0.899976223f), stbi__f2f(1.175875602f));\n   __m128i rot1_1 = dct_const(stbi__f2f(1.175875602f), stbi__f2f(1.175875602f) + stbi__f2f(-2.562915447f));\n   __m128i rot2_0 = dct_const(stbi__f2f(-1.961570560f) + stbi__f2f( 0.298631336f), stbi__f2f(-1.961570560f));\n   __m128i rot2_1 = dct_const(stbi__f2f(-1.961570560f), stbi__f2f(-1.961570560f) + stbi__f2f( 3.072711026f));\n   __m128i rot3_0 = dct_const(stbi__f2f(-0.390180644f) + stbi__f2f( 2.053119869f), stbi__f2f(-0.390180644f));\n   __m128i rot3_1 = dct_const(stbi__f2f(-0.390180644f), stbi__f2f(-0.390180644f) + stbi__f2f( 1.501321110f));\n\n   // rounding biases in column/row passes, see stbi__idct_block for explanation.\n   __m128i bias_0 = _mm_set1_epi32(512);\n   __m128i bias_1 = _mm_set1_epi32(65536 + (128<<17));\n\n   // load\n   row0 = _mm_load_si128((const __m128i *) (data + 0*8));\n   row1 = _mm_load_si128((const __m128i *) (data + 1*8));\n   row2 = _mm_load_si128((const __m128i *) (data + 2*8));\n   row3 = _mm_load_si128((const __m128i *) (data + 3*8));\n   row4 = _mm_load_si128((const __m128i *) (data + 4*8));\n   row5 = _mm_load_si128((const __m128i *) (data + 5*8));\n   row6 = _mm_load_si128((const __m128i *) (data + 6*8));\n   row7 = _mm_load_si128((const __m128i *) (data + 7*8));\n\n   // column pass\n   dct_pass(bias_0, 10);\n\n   {\n      // 16bit 8x8 transpose pass 1\n      dct_interleave16(row0, row4);\n      dct_interleave16(row1, row5);\n      dct_interleave16(row2, row6);\n      dct_interleave16(row3, row7);\n\n      // transpose pass 2\n      dct_interleave16(row0, row2);\n      dct_interleave16(row1, row3);\n      dct_interleave16(row4, row6);\n      dct_interleave16(row5, row7);\n\n      // transpose pass 3\n      dct_interleave16(row0, row1);\n      dct_interleave16(row2, row3);\n      dct_interleave16(row4, row5);\n      dct_interleave16(row6, row7);\n   }\n\n   // row pass\n   dct_pass(bias_1, 17);\n\n   {\n      // pack\n      __m128i p0 = _mm_packus_epi16(row0, row1); // a0a1a2a3...a7b0b1b2b3...b7\n      __m128i p1 = _mm_packus_epi16(row2, row3);\n      __m128i p2 = _mm_packus_epi16(row4, row5);\n      __m128i p3 = _mm_packus_epi16(row6, row7);\n\n      // 8bit 8x8 transpose pass 1\n      dct_interleave8(p0, p2); // a0e0a1e1...\n      dct_interleave8(p1, p3); // c0g0c1g1...\n\n      // transpose pass 2\n      dct_interleave8(p0, p1); // a0c0e0g0...\n      dct_interleave8(p2, p3); // b0d0f0h0...\n\n      // transpose pass 3\n      dct_interleave8(p0, p2); // a0b0c0d0...\n      dct_interleave8(p1, p3); // a4b4c4d4...\n\n      // store\n      _mm_storel_epi64((__m128i *) out, p0); out += out_stride;\n      _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p0, 0x4e)); out += out_stride;\n      _mm_storel_epi64((__m128i *) out, p2); out += out_stride;\n      _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p2, 0x4e)); out += out_stride;\n      _mm_storel_epi64((__m128i *) out, p1); out += out_stride;\n      _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p1, 0x4e)); out += out_stride;\n      _mm_storel_epi64((__m128i *) out, p3); out += out_stride;\n      _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p3, 0x4e));\n   }\n\n#undef dct_const\n#undef dct_rot\n#undef dct_widen\n#undef dct_wadd\n#undef dct_wsub\n#undef dct_bfly32o\n#undef dct_interleave8\n#undef dct_interleave16\n#undef dct_pass\n}\n\n#endif // STBI_SSE2\n\n#ifdef STBI_NEON\n\n// NEON integer IDCT. should produce bit-identical\n// results to the generic C version.\nstatic void stbi__idct_simd(stbi_uc *out, int out_stride, short data[64])\n{\n   int16x8_t row0, row1, row2, row3, row4, row5, row6, row7;\n\n   int16x4_t rot0_0 = vdup_n_s16(stbi__f2f(0.5411961f));\n   int16x4_t rot0_1 = vdup_n_s16(stbi__f2f(-1.847759065f));\n   int16x4_t rot0_2 = vdup_n_s16(stbi__f2f( 0.765366865f));\n   int16x4_t rot1_0 = vdup_n_s16(stbi__f2f( 1.175875602f));\n   int16x4_t rot1_1 = vdup_n_s16(stbi__f2f(-0.899976223f));\n   int16x4_t rot1_2 = vdup_n_s16(stbi__f2f(-2.562915447f));\n   int16x4_t rot2_0 = vdup_n_s16(stbi__f2f(-1.961570560f));\n   int16x4_t rot2_1 = vdup_n_s16(stbi__f2f(-0.390180644f));\n   int16x4_t rot3_0 = vdup_n_s16(stbi__f2f( 0.298631336f));\n   int16x4_t rot3_1 = vdup_n_s16(stbi__f2f( 2.053119869f));\n   int16x4_t rot3_2 = vdup_n_s16(stbi__f2f( 3.072711026f));\n   int16x4_t rot3_3 = vdup_n_s16(stbi__f2f( 1.501321110f));\n\n#define dct_long_mul(out, inq, coeff) \\\n   int32x4_t out##_l = vmull_s16(vget_low_s16(inq), coeff); \\\n   int32x4_t out##_h = vmull_s16(vget_high_s16(inq), coeff)\n\n#define dct_long_mac(out, acc, inq, coeff) \\\n   int32x4_t out##_l = vmlal_s16(acc##_l, vget_low_s16(inq), coeff); \\\n   int32x4_t out##_h = vmlal_s16(acc##_h, vget_high_s16(inq), coeff)\n\n#define dct_widen(out, inq) \\\n   int32x4_t out##_l = vshll_n_s16(vget_low_s16(inq), 12); \\\n   int32x4_t out##_h = vshll_n_s16(vget_high_s16(inq), 12)\n\n// wide add\n#define dct_wadd(out, a, b) \\\n   int32x4_t out##_l = vaddq_s32(a##_l, b##_l); \\\n   int32x4_t out##_h = vaddq_s32(a##_h, b##_h)\n\n// wide sub\n#define dct_wsub(out, a, b) \\\n   int32x4_t out##_l = vsubq_s32(a##_l, b##_l); \\\n   int32x4_t out##_h = vsubq_s32(a##_h, b##_h)\n\n// butterfly a/b, then shift using \"shiftop\" by \"s\" and pack\n#define dct_bfly32o(out0,out1, a,b,shiftop,s) \\\n   { \\\n      dct_wadd(sum, a, b); \\\n      dct_wsub(dif, a, b); \\\n      out0 = vcombine_s16(shiftop(sum_l, s), shiftop(sum_h, s)); \\\n      out1 = vcombine_s16(shiftop(dif_l, s), shiftop(dif_h, s)); \\\n   }\n\n#define dct_pass(shiftop, shift) \\\n   { \\\n      /* even part */ \\\n      int16x8_t sum26 = vaddq_s16(row2, row6); \\\n      dct_long_mul(p1e, sum26, rot0_0); \\\n      dct_long_mac(t2e, p1e, row6, rot0_1); \\\n      dct_long_mac(t3e, p1e, row2, rot0_2); \\\n      int16x8_t sum04 = vaddq_s16(row0, row4); \\\n      int16x8_t dif04 = vsubq_s16(row0, row4); \\\n      dct_widen(t0e, sum04); \\\n      dct_widen(t1e, dif04); \\\n      dct_wadd(x0, t0e, t3e); \\\n      dct_wsub(x3, t0e, t3e); \\\n      dct_wadd(x1, t1e, t2e); \\\n      dct_wsub(x2, t1e, t2e); \\\n      /* odd part */ \\\n      int16x8_t sum15 = vaddq_s16(row1, row5); \\\n      int16x8_t sum17 = vaddq_s16(row1, row7); \\\n      int16x8_t sum35 = vaddq_s16(row3, row5); \\\n      int16x8_t sum37 = vaddq_s16(row3, row7); \\\n      int16x8_t sumodd = vaddq_s16(sum17, sum35); \\\n      dct_long_mul(p5o, sumodd, rot1_0); \\\n      dct_long_mac(p1o, p5o, sum17, rot1_1); \\\n      dct_long_mac(p2o, p5o, sum35, rot1_2); \\\n      dct_long_mul(p3o, sum37, rot2_0); \\\n      dct_long_mul(p4o, sum15, rot2_1); \\\n      dct_wadd(sump13o, p1o, p3o); \\\n      dct_wadd(sump24o, p2o, p4o); \\\n      dct_wadd(sump23o, p2o, p3o); \\\n      dct_wadd(sump14o, p1o, p4o); \\\n      dct_long_mac(x4, sump13o, row7, rot3_0); \\\n      dct_long_mac(x5, sump24o, row5, rot3_1); \\\n      dct_long_mac(x6, sump23o, row3, rot3_2); \\\n      dct_long_mac(x7, sump14o, row1, rot3_3); \\\n      dct_bfly32o(row0,row7, x0,x7,shiftop,shift); \\\n      dct_bfly32o(row1,row6, x1,x6,shiftop,shift); \\\n      dct_bfly32o(row2,row5, x2,x5,shiftop,shift); \\\n      dct_bfly32o(row3,row4, x3,x4,shiftop,shift); \\\n   }\n\n   // load\n   row0 = vld1q_s16(data + 0*8);\n   row1 = vld1q_s16(data + 1*8);\n   row2 = vld1q_s16(data + 2*8);\n   row3 = vld1q_s16(data + 3*8);\n   row4 = vld1q_s16(data + 4*8);\n   row5 = vld1q_s16(data + 5*8);\n   row6 = vld1q_s16(data + 6*8);\n   row7 = vld1q_s16(data + 7*8);\n\n   // add DC bias\n   row0 = vaddq_s16(row0, vsetq_lane_s16(1024, vdupq_n_s16(0), 0));\n\n   // column pass\n   dct_pass(vrshrn_n_s32, 10);\n\n   // 16bit 8x8 transpose\n   {\n// these three map to a single VTRN.16, VTRN.32, and VSWP, respectively.\n// whether compilers actually get this is another story, sadly.\n#define dct_trn16(x, y) { int16x8x2_t t = vtrnq_s16(x, y); x = t.val[0]; y = t.val[1]; }\n#define dct_trn32(x, y) { int32x4x2_t t = vtrnq_s32(vreinterpretq_s32_s16(x), vreinterpretq_s32_s16(y)); x = vreinterpretq_s16_s32(t.val[0]); y = vreinterpretq_s16_s32(t.val[1]); }\n#define dct_trn64(x, y) { int16x8_t x0 = x; int16x8_t y0 = y; x = vcombine_s16(vget_low_s16(x0), vget_low_s16(y0)); y = vcombine_s16(vget_high_s16(x0), vget_high_s16(y0)); }\n\n      // pass 1\n      dct_trn16(row0, row1); // a0b0a2b2a4b4a6b6\n      dct_trn16(row2, row3);\n      dct_trn16(row4, row5);\n      dct_trn16(row6, row7);\n\n      // pass 2\n      dct_trn32(row0, row2); // a0b0c0d0a4b4c4d4\n      dct_trn32(row1, row3);\n      dct_trn32(row4, row6);\n      dct_trn32(row5, row7);\n\n      // pass 3\n      dct_trn64(row0, row4); // a0b0c0d0e0f0g0h0\n      dct_trn64(row1, row5);\n      dct_trn64(row2, row6);\n      dct_trn64(row3, row7);\n\n#undef dct_trn16\n#undef dct_trn32\n#undef dct_trn64\n   }\n\n   // row pass\n   // vrshrn_n_s32 only supports shifts up to 16, we need\n   // 17. so do a non-rounding shift of 16 first then follow\n   // up with a rounding shift by 1.\n   dct_pass(vshrn_n_s32, 16);\n\n   {\n      // pack and round\n      uint8x8_t p0 = vqrshrun_n_s16(row0, 1);\n      uint8x8_t p1 = vqrshrun_n_s16(row1, 1);\n      uint8x8_t p2 = vqrshrun_n_s16(row2, 1);\n      uint8x8_t p3 = vqrshrun_n_s16(row3, 1);\n      uint8x8_t p4 = vqrshrun_n_s16(row4, 1);\n      uint8x8_t p5 = vqrshrun_n_s16(row5, 1);\n      uint8x8_t p6 = vqrshrun_n_s16(row6, 1);\n      uint8x8_t p7 = vqrshrun_n_s16(row7, 1);\n\n      // again, these can translate into one instruction, but often don't.\n#define dct_trn8_8(x, y) { uint8x8x2_t t = vtrn_u8(x, y); x = t.val[0]; y = t.val[1]; }\n#define dct_trn8_16(x, y) { uint16x4x2_t t = vtrn_u16(vreinterpret_u16_u8(x), vreinterpret_u16_u8(y)); x = vreinterpret_u8_u16(t.val[0]); y = vreinterpret_u8_u16(t.val[1]); }\n#define dct_trn8_32(x, y) { uint32x2x2_t t = vtrn_u32(vreinterpret_u32_u8(x), vreinterpret_u32_u8(y)); x = vreinterpret_u8_u32(t.val[0]); y = vreinterpret_u8_u32(t.val[1]); }\n\n      // sadly can't use interleaved stores here since we only write\n      // 8 bytes to each scan line!\n\n      // 8x8 8-bit transpose pass 1\n      dct_trn8_8(p0, p1);\n      dct_trn8_8(p2, p3);\n      dct_trn8_8(p4, p5);\n      dct_trn8_8(p6, p7);\n\n      // pass 2\n      dct_trn8_16(p0, p2);\n      dct_trn8_16(p1, p3);\n      dct_trn8_16(p4, p6);\n      dct_trn8_16(p5, p7);\n\n      // pass 3\n      dct_trn8_32(p0, p4);\n      dct_trn8_32(p1, p5);\n      dct_trn8_32(p2, p6);\n      dct_trn8_32(p3, p7);\n\n      // store\n      vst1_u8(out, p0); out += out_stride;\n      vst1_u8(out, p1); out += out_stride;\n      vst1_u8(out, p2); out += out_stride;\n      vst1_u8(out, p3); out += out_stride;\n      vst1_u8(out, p4); out += out_stride;\n      vst1_u8(out, p5); out += out_stride;\n      vst1_u8(out, p6); out += out_stride;\n      vst1_u8(out, p7);\n\n#undef dct_trn8_8\n#undef dct_trn8_16\n#undef dct_trn8_32\n   }\n\n#undef dct_long_mul\n#undef dct_long_mac\n#undef dct_widen\n#undef dct_wadd\n#undef dct_wsub\n#undef dct_bfly32o\n#undef dct_pass\n}\n\n#endif // STBI_NEON\n\n#define STBI__MARKER_none  0xff\n// if there's a pending marker from the entropy stream, return that\n// otherwise, fetch from the stream and get a marker. if there's no\n// marker, return 0xff, which is never a valid marker value\nstatic stbi_uc stbi__get_marker(stbi__jpeg *j)\n{\n   stbi_uc x;\n   if (j->marker != STBI__MARKER_none) { x = j->marker; j->marker = STBI__MARKER_none; return x; }\n   x = stbi__get8(j->s);\n   if (x != 0xff) return STBI__MARKER_none;\n   while (x == 0xff)\n      x = stbi__get8(j->s); // consume repeated 0xff fill bytes\n   return x;\n}\n\n// in each scan, we'll have scan_n components, and the order\n// of the components is specified by order[]\n#define STBI__RESTART(x)     ((x) >= 0xd0 && (x) <= 0xd7)\n\n// after a restart interval, stbi__jpeg_reset the entropy decoder and\n// the dc prediction\nstatic void stbi__jpeg_reset(stbi__jpeg *j)\n{\n   j->code_bits = 0;\n   j->code_buffer = 0;\n   j->nomore = 0;\n   j->img_comp[0].dc_pred = j->img_comp[1].dc_pred = j->img_comp[2].dc_pred = j->img_comp[3].dc_pred = 0;\n   j->marker = STBI__MARKER_none;\n   j->todo = j->restart_interval ? j->restart_interval : 0x7fffffff;\n   j->eob_run = 0;\n   // no more than 1<<31 MCUs if no restart_interal? that's plenty safe,\n   // since we don't even allow 1<<30 pixels\n}\n\nstatic int stbi__parse_entropy_coded_data(stbi__jpeg *z)\n{\n   stbi__jpeg_reset(z);\n   if (!z->progressive) {\n      if (z->scan_n == 1) {\n         int i,j;\n         STBI_SIMD_ALIGN(short, data[64]);\n         int n = z->order[0];\n         // non-interleaved data, we just need to process one block at a time,\n         // in trivial scanline order\n         // number of blocks to do just depends on how many actual \"pixels\" this\n         // component has, independent of interleaved MCU blocking and such\n         int w = (z->img_comp[n].x+7) >> 3;\n         int h = (z->img_comp[n].y+7) >> 3;\n         for (j=0; j < h; ++j) {\n            for (i=0; i < w; ++i) {\n               int ha = z->img_comp[n].ha;\n               if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0;\n               z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data);\n               // every data block is an MCU, so countdown the restart interval\n               if (--z->todo <= 0) {\n                  if (z->code_bits < 24) stbi__grow_buffer_unsafe(z);\n                  // if it's NOT a restart, then just bail, so we get corrupt data\n                  // rather than no data\n                  if (!STBI__RESTART(z->marker)) return 1;\n                  stbi__jpeg_reset(z);\n               }\n            }\n         }\n         return 1;\n      } else { // interleaved\n         int i,j,k,x,y;\n         STBI_SIMD_ALIGN(short, data[64]);\n         for (j=0; j < z->img_mcu_y; ++j) {\n            for (i=0; i < z->img_mcu_x; ++i) {\n               // scan an interleaved mcu... process scan_n components in order\n               for (k=0; k < z->scan_n; ++k) {\n                  int n = z->order[k];\n                  // scan out an mcu's worth of this component; that's just determined\n                  // by the basic H and V specified for the component\n                  for (y=0; y < z->img_comp[n].v; ++y) {\n                     for (x=0; x < z->img_comp[n].h; ++x) {\n                        int x2 = (i*z->img_comp[n].h + x)*8;\n                        int y2 = (j*z->img_comp[n].v + y)*8;\n                        int ha = z->img_comp[n].ha;\n                        if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0;\n                        z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*y2+x2, z->img_comp[n].w2, data);\n                     }\n                  }\n               }\n               // after all interleaved components, that's an interleaved MCU,\n               // so now count down the restart interval\n               if (--z->todo <= 0) {\n                  if (z->code_bits < 24) stbi__grow_buffer_unsafe(z);\n                  if (!STBI__RESTART(z->marker)) return 1;\n                  stbi__jpeg_reset(z);\n               }\n            }\n         }\n         return 1;\n      }\n   } else {\n      if (z->scan_n == 1) {\n         int i,j;\n         int n = z->order[0];\n         // non-interleaved data, we just need to process one block at a time,\n         // in trivial scanline order\n         // number of blocks to do just depends on how many actual \"pixels\" this\n         // component has, independent of interleaved MCU blocking and such\n         int w = (z->img_comp[n].x+7) >> 3;\n         int h = (z->img_comp[n].y+7) >> 3;\n         for (j=0; j < h; ++j) {\n            for (i=0; i < w; ++i) {\n               short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w);\n               if (z->spec_start == 0) {\n                  if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n))\n                     return 0;\n               } else {\n                  int ha = z->img_comp[n].ha;\n                  if (!stbi__jpeg_decode_block_prog_ac(z, data, &z->huff_ac[ha], z->fast_ac[ha]))\n                     return 0;\n               }\n               // every data block is an MCU, so countdown the restart interval\n               if (--z->todo <= 0) {\n                  if (z->code_bits < 24) stbi__grow_buffer_unsafe(z);\n                  if (!STBI__RESTART(z->marker)) return 1;\n                  stbi__jpeg_reset(z);\n               }\n            }\n         }\n         return 1;\n      } else { // interleaved\n         int i,j,k,x,y;\n         for (j=0; j < z->img_mcu_y; ++j) {\n            for (i=0; i < z->img_mcu_x; ++i) {\n               // scan an interleaved mcu... process scan_n components in order\n               for (k=0; k < z->scan_n; ++k) {\n                  int n = z->order[k];\n                  // scan out an mcu's worth of this component; that's just determined\n                  // by the basic H and V specified for the component\n                  for (y=0; y < z->img_comp[n].v; ++y) {\n                     for (x=0; x < z->img_comp[n].h; ++x) {\n                        int x2 = (i*z->img_comp[n].h + x);\n                        int y2 = (j*z->img_comp[n].v + y);\n                        short *data = z->img_comp[n].coeff + 64 * (x2 + y2 * z->img_comp[n].coeff_w);\n                        if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n))\n                           return 0;\n                     }\n                  }\n               }\n               // after all interleaved components, that's an interleaved MCU,\n               // so now count down the restart interval\n               if (--z->todo <= 0) {\n                  if (z->code_bits < 24) stbi__grow_buffer_unsafe(z);\n                  if (!STBI__RESTART(z->marker)) return 1;\n                  stbi__jpeg_reset(z);\n               }\n            }\n         }\n         return 1;\n      }\n   }\n}\n\nstatic void stbi__jpeg_dequantize(short *data, stbi__uint16 *dequant)\n{\n   int i;\n   for (i=0; i < 64; ++i)\n      data[i] *= dequant[i];\n}\n\nstatic void stbi__jpeg_finish(stbi__jpeg *z)\n{\n   if (z->progressive) {\n      // dequantize and idct the data\n      int i,j,n;\n      for (n=0; n < z->s->img_n; ++n) {\n         int w = (z->img_comp[n].x+7) >> 3;\n         int h = (z->img_comp[n].y+7) >> 3;\n         for (j=0; j < h; ++j) {\n            for (i=0; i < w; ++i) {\n               short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w);\n               stbi__jpeg_dequantize(data, z->dequant[z->img_comp[n].tq]);\n               z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data);\n            }\n         }\n      }\n   }\n}\n\nstatic int stbi__process_marker(stbi__jpeg *z, int m)\n{\n   int L;\n   switch (m) {\n      case STBI__MARKER_none: // no marker found\n         return stbi__err(\"expected marker\",\"Corrupt JPEG\");\n\n      case 0xDD: // DRI - specify restart interval\n         if (stbi__get16be(z->s) != 4) return stbi__err(\"bad DRI len\",\"Corrupt JPEG\");\n         z->restart_interval = stbi__get16be(z->s);\n         return 1;\n\n      case 0xDB: // DQT - define quantization table\n         L = stbi__get16be(z->s)-2;\n         while (L > 0) {\n            int q = stbi__get8(z->s);\n            int p = q >> 4, sixteen = (p != 0);\n            int t = q & 15,i;\n            if (p != 0 && p != 1) return stbi__err(\"bad DQT type\",\"Corrupt JPEG\");\n            if (t > 3) return stbi__err(\"bad DQT table\",\"Corrupt JPEG\");\n\n            for (i=0; i < 64; ++i)\n               z->dequant[t][stbi__jpeg_dezigzag[i]] = (stbi__uint16)(sixteen ? stbi__get16be(z->s) : stbi__get8(z->s));\n            L -= (sixteen ? 129 : 65);\n         }\n         return L==0;\n\n      case 0xC4: // DHT - define huffman table\n         L = stbi__get16be(z->s)-2;\n         while (L > 0) {\n            stbi_uc *v;\n            int sizes[16],i,n=0;\n            int q = stbi__get8(z->s);\n            int tc = q >> 4;\n            int th = q & 15;\n            if (tc > 1 || th > 3) return stbi__err(\"bad DHT header\",\"Corrupt JPEG\");\n            for (i=0; i < 16; ++i) {\n               sizes[i] = stbi__get8(z->s);\n               n += sizes[i];\n            }\n            if(n > 256) return stbi__err(\"bad DHT header\",\"Corrupt JPEG\"); // Loop over i < n would write past end of values!\n            L -= 17;\n            if (tc == 0) {\n               if (!stbi__build_huffman(z->huff_dc+th, sizes)) return 0;\n               v = z->huff_dc[th].values;\n            } else {\n               if (!stbi__build_huffman(z->huff_ac+th, sizes)) return 0;\n               v = z->huff_ac[th].values;\n            }\n            for (i=0; i < n; ++i)\n               v[i] = stbi__get8(z->s);\n            if (tc != 0)\n               stbi__build_fast_ac(z->fast_ac[th], z->huff_ac + th);\n            L -= n;\n         }\n         return L==0;\n   }\n\n   // check for comment block or APP blocks\n   if ((m >= 0xE0 && m <= 0xEF) || m == 0xFE) {\n      L = stbi__get16be(z->s);\n      if (L < 2) {\n         if (m == 0xFE)\n            return stbi__err(\"bad COM len\",\"Corrupt JPEG\");\n         else\n            return stbi__err(\"bad APP len\",\"Corrupt JPEG\");\n      }\n      L -= 2;\n\n      if (m == 0xE0 && L >= 5) { // JFIF APP0 segment\n         static const unsigned char tag[5] = {'J','F','I','F','\\0'};\n         int ok = 1;\n         int i;\n         for (i=0; i < 5; ++i)\n            if (stbi__get8(z->s) != tag[i])\n               ok = 0;\n         L -= 5;\n         if (ok)\n            z->jfif = 1;\n      } else if (m == 0xEE && L >= 12) { // Adobe APP14 segment\n         static const unsigned char tag[6] = {'A','d','o','b','e','\\0'};\n         int ok = 1;\n         int i;\n         for (i=0; i < 6; ++i)\n            if (stbi__get8(z->s) != tag[i])\n               ok = 0;\n         L -= 6;\n         if (ok) {\n            stbi__get8(z->s); // version\n            stbi__get16be(z->s); // flags0\n            stbi__get16be(z->s); // flags1\n            z->app14_color_transform = stbi__get8(z->s); // color transform\n            L -= 6;\n         }\n      }\n\n      stbi__skip(z->s, L);\n      return 1;\n   }\n\n   return stbi__err(\"unknown marker\",\"Corrupt JPEG\");\n}\n\n// after we see SOS\nstatic int stbi__process_scan_header(stbi__jpeg *z)\n{\n   int i;\n   int Ls = stbi__get16be(z->s);\n   z->scan_n = stbi__get8(z->s);\n   if (z->scan_n < 1 || z->scan_n > 4 || z->scan_n > (int) z->s->img_n) return stbi__err(\"bad SOS component count\",\"Corrupt JPEG\");\n   if (Ls != 6+2*z->scan_n) return stbi__err(\"bad SOS len\",\"Corrupt JPEG\");\n   for (i=0; i < z->scan_n; ++i) {\n      int id = stbi__get8(z->s), which;\n      int q = stbi__get8(z->s);\n      for (which = 0; which < z->s->img_n; ++which)\n         if (z->img_comp[which].id == id)\n            break;\n      if (which == z->s->img_n) return 0; // no match\n      z->img_comp[which].hd = q >> 4;   if (z->img_comp[which].hd > 3) return stbi__err(\"bad DC huff\",\"Corrupt JPEG\");\n      z->img_comp[which].ha = q & 15;   if (z->img_comp[which].ha > 3) return stbi__err(\"bad AC huff\",\"Corrupt JPEG\");\n      z->order[i] = which;\n   }\n\n   {\n      int aa;\n      z->spec_start = stbi__get8(z->s);\n      z->spec_end   = stbi__get8(z->s); // should be 63, but might be 0\n      aa = stbi__get8(z->s);\n      z->succ_high = (aa >> 4);\n      z->succ_low  = (aa & 15);\n      if (z->progressive) {\n         if (z->spec_start > 63 || z->spec_end > 63  || z->spec_start > z->spec_end || z->succ_high > 13 || z->succ_low > 13)\n            return stbi__err(\"bad SOS\", \"Corrupt JPEG\");\n      } else {\n         if (z->spec_start != 0) return stbi__err(\"bad SOS\",\"Corrupt JPEG\");\n         if (z->succ_high != 0 || z->succ_low != 0) return stbi__err(\"bad SOS\",\"Corrupt JPEG\");\n         z->spec_end = 63;\n      }\n   }\n\n   return 1;\n}\n\nstatic int stbi__free_jpeg_components(stbi__jpeg *z, int ncomp, int why)\n{\n   int i;\n   for (i=0; i < ncomp; ++i) {\n      if (z->img_comp[i].raw_data) {\n         STBI_FREE(z->img_comp[i].raw_data);\n         z->img_comp[i].raw_data = NULL;\n         z->img_comp[i].data = NULL;\n      }\n      if (z->img_comp[i].raw_coeff) {\n         STBI_FREE(z->img_comp[i].raw_coeff);\n         z->img_comp[i].raw_coeff = 0;\n         z->img_comp[i].coeff = 0;\n      }\n      if (z->img_comp[i].linebuf) {\n         STBI_FREE(z->img_comp[i].linebuf);\n         z->img_comp[i].linebuf = NULL;\n      }\n   }\n   return why;\n}\n\nstatic int stbi__process_frame_header(stbi__jpeg *z, int scan)\n{\n   stbi__context *s = z->s;\n   int Lf,p,i,q, h_max=1,v_max=1,c;\n   Lf = stbi__get16be(s);         if (Lf < 11) return stbi__err(\"bad SOF len\",\"Corrupt JPEG\"); // JPEG\n   p  = stbi__get8(s);            if (p != 8) return stbi__err(\"only 8-bit\",\"JPEG format not supported: 8-bit only\"); // JPEG baseline\n   s->img_y = stbi__get16be(s);   if (s->img_y == 0) return stbi__err(\"no header height\", \"JPEG format not supported: delayed height\"); // Legal, but we don't handle it--but neither does IJG\n   s->img_x = stbi__get16be(s);   if (s->img_x == 0) return stbi__err(\"0 width\",\"Corrupt JPEG\"); // JPEG requires\n   if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err(\"too large\",\"Very large image (corrupt?)\");\n   if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err(\"too large\",\"Very large image (corrupt?)\");\n   c = stbi__get8(s);\n   if (c != 3 && c != 1 && c != 4) return stbi__err(\"bad component count\",\"Corrupt JPEG\");\n   s->img_n = c;\n   for (i=0; i < c; ++i) {\n      z->img_comp[i].data = NULL;\n      z->img_comp[i].linebuf = NULL;\n   }\n\n   if (Lf != 8+3*s->img_n) return stbi__err(\"bad SOF len\",\"Corrupt JPEG\");\n\n   z->rgb = 0;\n   for (i=0; i < s->img_n; ++i) {\n      static const unsigned char rgb[3] = { 'R', 'G', 'B' };\n      z->img_comp[i].id = stbi__get8(s);\n      if (s->img_n == 3 && z->img_comp[i].id == rgb[i])\n         ++z->rgb;\n      q = stbi__get8(s);\n      z->img_comp[i].h = (q >> 4);  if (!z->img_comp[i].h || z->img_comp[i].h > 4) return stbi__err(\"bad H\",\"Corrupt JPEG\");\n      z->img_comp[i].v = q & 15;    if (!z->img_comp[i].v || z->img_comp[i].v > 4) return stbi__err(\"bad V\",\"Corrupt JPEG\");\n      z->img_comp[i].tq = stbi__get8(s);  if (z->img_comp[i].tq > 3) return stbi__err(\"bad TQ\",\"Corrupt JPEG\");\n   }\n\n   if (scan != STBI__SCAN_load) return 1;\n\n   if (!stbi__mad3sizes_valid(s->img_x, s->img_y, s->img_n, 0)) return stbi__err(\"too large\", \"Image too large to decode\");\n\n   for (i=0; i < s->img_n; ++i) {\n      if (z->img_comp[i].h > h_max) h_max = z->img_comp[i].h;\n      if (z->img_comp[i].v > v_max) v_max = z->img_comp[i].v;\n   }\n\n   // check that plane subsampling factors are integer ratios; our resamplers can't deal with fractional ratios\n   // and I've never seen a non-corrupted JPEG file actually use them\n   for (i=0; i < s->img_n; ++i) {\n      if (h_max % z->img_comp[i].h != 0) return stbi__err(\"bad H\",\"Corrupt JPEG\");\n      if (v_max % z->img_comp[i].v != 0) return stbi__err(\"bad V\",\"Corrupt JPEG\");\n   }\n\n   // compute interleaved mcu info\n   z->img_h_max = h_max;\n   z->img_v_max = v_max;\n   z->img_mcu_w = h_max * 8;\n   z->img_mcu_h = v_max * 8;\n   // these sizes can't be more than 17 bits\n   z->img_mcu_x = (s->img_x + z->img_mcu_w-1) / z->img_mcu_w;\n   z->img_mcu_y = (s->img_y + z->img_mcu_h-1) / z->img_mcu_h;\n\n   for (i=0; i < s->img_n; ++i) {\n      // number of effective pixels (e.g. for non-interleaved MCU)\n      z->img_comp[i].x = (s->img_x * z->img_comp[i].h + h_max-1) / h_max;\n      z->img_comp[i].y = (s->img_y * z->img_comp[i].v + v_max-1) / v_max;\n      // to simplify generation, we'll allocate enough memory to decode\n      // the bogus oversized data from using interleaved MCUs and their\n      // big blocks (e.g. a 16x16 iMCU on an image of width 33); we won't\n      // discard the extra data until colorspace conversion\n      //\n      // img_mcu_x, img_mcu_y: <=17 bits; comp[i].h and .v are <=4 (checked earlier)\n      // so these muls can't overflow with 32-bit ints (which we require)\n      z->img_comp[i].w2 = z->img_mcu_x * z->img_comp[i].h * 8;\n      z->img_comp[i].h2 = z->img_mcu_y * z->img_comp[i].v * 8;\n      z->img_comp[i].coeff = 0;\n      z->img_comp[i].raw_coeff = 0;\n      z->img_comp[i].linebuf = NULL;\n      z->img_comp[i].raw_data = stbi__malloc_mad2(z->img_comp[i].w2, z->img_comp[i].h2, 15);\n      if (z->img_comp[i].raw_data == NULL)\n         return stbi__free_jpeg_components(z, i+1, stbi__err(\"outofmem\", \"Out of memory\"));\n      // align blocks for idct using mmx/sse\n      z->img_comp[i].data = (stbi_uc*) (((size_t) z->img_comp[i].raw_data + 15) & ~15);\n      if (z->progressive) {\n         // w2, h2 are multiples of 8 (see above)\n         z->img_comp[i].coeff_w = z->img_comp[i].w2 / 8;\n         z->img_comp[i].coeff_h = z->img_comp[i].h2 / 8;\n         z->img_comp[i].raw_coeff = stbi__malloc_mad3(z->img_comp[i].w2, z->img_comp[i].h2, sizeof(short), 15);\n         if (z->img_comp[i].raw_coeff == NULL)\n            return stbi__free_jpeg_components(z, i+1, stbi__err(\"outofmem\", \"Out of memory\"));\n         z->img_comp[i].coeff = (short*) (((size_t) z->img_comp[i].raw_coeff + 15) & ~15);\n      }\n   }\n\n   return 1;\n}\n\n// use comparisons since in some cases we handle more than one case (e.g. SOF)\n#define stbi__DNL(x)         ((x) == 0xdc)\n#define stbi__SOI(x)         ((x) == 0xd8)\n#define stbi__EOI(x)         ((x) == 0xd9)\n#define stbi__SOF(x)         ((x) == 0xc0 || (x) == 0xc1 || (x) == 0xc2)\n#define stbi__SOS(x)         ((x) == 0xda)\n\n#define stbi__SOF_progressive(x)   ((x) == 0xc2)\n\nstatic int stbi__decode_jpeg_header(stbi__jpeg *z, int scan)\n{\n   int m;\n   z->jfif = 0;\n   z->app14_color_transform = -1; // valid values are 0,1,2\n   z->marker = STBI__MARKER_none; // initialize cached marker to empty\n   m = stbi__get_marker(z);\n   if (!stbi__SOI(m)) return stbi__err(\"no SOI\",\"Corrupt JPEG\");\n   if (scan == STBI__SCAN_type) return 1;\n   m = stbi__get_marker(z);\n   while (!stbi__SOF(m)) {\n      if (!stbi__process_marker(z,m)) return 0;\n      m = stbi__get_marker(z);\n      while (m == STBI__MARKER_none) {\n         // some files have extra padding after their blocks, so ok, we'll scan\n         if (stbi__at_eof(z->s)) return stbi__err(\"no SOF\", \"Corrupt JPEG\");\n         m = stbi__get_marker(z);\n      }\n   }\n   z->progressive = stbi__SOF_progressive(m);\n   if (!stbi__process_frame_header(z, scan)) return 0;\n   return 1;\n}\n\nstatic stbi_uc stbi__skip_jpeg_junk_at_end(stbi__jpeg *j)\n{\n   // some JPEGs have junk at end, skip over it but if we find what looks\n   // like a valid marker, resume there\n   while (!stbi__at_eof(j->s)) {\n      stbi_uc x = stbi__get8(j->s);\n      while (x == 0xff) { // might be a marker\n         if (stbi__at_eof(j->s)) return STBI__MARKER_none;\n         x = stbi__get8(j->s);\n         if (x != 0x00 && x != 0xff) {\n            // not a stuffed zero or lead-in to another marker, looks\n            // like an actual marker, return it\n            return x;\n         }\n         // stuffed zero has x=0 now which ends the loop, meaning we go\n         // back to regular scan loop.\n         // repeated 0xff keeps trying to read the next byte of the marker.\n      }\n   }\n   return STBI__MARKER_none;\n}\n\n// decode image to YCbCr format\nstatic int stbi__decode_jpeg_image(stbi__jpeg *j)\n{\n   int m;\n   for (m = 0; m < 4; m++) {\n      j->img_comp[m].raw_data = NULL;\n      j->img_comp[m].raw_coeff = NULL;\n   }\n   j->restart_interval = 0;\n   if (!stbi__decode_jpeg_header(j, STBI__SCAN_load)) return 0;\n   m = stbi__get_marker(j);\n   while (!stbi__EOI(m)) {\n      if (stbi__SOS(m)) {\n         if (!stbi__process_scan_header(j)) return 0;\n         if (!stbi__parse_entropy_coded_data(j)) return 0;\n         if (j->marker == STBI__MARKER_none ) {\n         j->marker = stbi__skip_jpeg_junk_at_end(j);\n            // if we reach eof without hitting a marker, stbi__get_marker() below will fail and we'll eventually return 0\n         }\n         m = stbi__get_marker(j);\n         if (STBI__RESTART(m))\n            m = stbi__get_marker(j);\n      } else if (stbi__DNL(m)) {\n         int Ld = stbi__get16be(j->s);\n         stbi__uint32 NL = stbi__get16be(j->s);\n         if (Ld != 4) return stbi__err(\"bad DNL len\", \"Corrupt JPEG\");\n         if (NL != j->s->img_y) return stbi__err(\"bad DNL height\", \"Corrupt JPEG\");\n         m = stbi__get_marker(j);\n      } else {\n         if (!stbi__process_marker(j, m)) return 1;\n         m = stbi__get_marker(j);\n      }\n   }\n   if (j->progressive)\n      stbi__jpeg_finish(j);\n   return 1;\n}\n\n// static jfif-centered resampling (across block boundaries)\n\ntypedef stbi_uc *(*resample_row_func)(stbi_uc *out, stbi_uc *in0, stbi_uc *in1,\n                                    int w, int hs);\n\n#define stbi__div4(x) ((stbi_uc) ((x) >> 2))\n\nstatic stbi_uc *resample_row_1(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs)\n{\n   STBI_NOTUSED(out);\n   STBI_NOTUSED(in_far);\n   STBI_NOTUSED(w);\n   STBI_NOTUSED(hs);\n   return in_near;\n}\n\nstatic stbi_uc* stbi__resample_row_v_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs)\n{\n   // need to generate two samples vertically for every one in input\n   int i;\n   STBI_NOTUSED(hs);\n   for (i=0; i < w; ++i)\n      out[i] = stbi__div4(3*in_near[i] + in_far[i] + 2);\n   return out;\n}\n\nstatic stbi_uc*  stbi__resample_row_h_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs)\n{\n   // need to generate two samples horizontally for every one in input\n   int i;\n   stbi_uc *input = in_near;\n\n   if (w == 1) {\n      // if only one sample, can't do any interpolation\n      out[0] = out[1] = input[0];\n      return out;\n   }\n\n   out[0] = input[0];\n   out[1] = stbi__div4(input[0]*3 + input[1] + 2);\n   for (i=1; i < w-1; ++i) {\n      int n = 3*input[i]+2;\n      out[i*2+0] = stbi__div4(n+input[i-1]);\n      out[i*2+1] = stbi__div4(n+input[i+1]);\n   }\n   out[i*2+0] = stbi__div4(input[w-2]*3 + input[w-1] + 2);\n   out[i*2+1] = input[w-1];\n\n   STBI_NOTUSED(in_far);\n   STBI_NOTUSED(hs);\n\n   return out;\n}\n\n#define stbi__div16(x) ((stbi_uc) ((x) >> 4))\n\nstatic stbi_uc *stbi__resample_row_hv_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs)\n{\n   // need to generate 2x2 samples for every one in input\n   int i,t0,t1;\n   if (w == 1) {\n      out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2);\n      return out;\n   }\n\n   t1 = 3*in_near[0] + in_far[0];\n   out[0] = stbi__div4(t1+2);\n   for (i=1; i < w; ++i) {\n      t0 = t1;\n      t1 = 3*in_near[i]+in_far[i];\n      out[i*2-1] = stbi__div16(3*t0 + t1 + 8);\n      out[i*2  ] = stbi__div16(3*t1 + t0 + 8);\n   }\n   out[w*2-1] = stbi__div4(t1+2);\n\n   STBI_NOTUSED(hs);\n\n   return out;\n}\n\n#if defined(STBI_SSE2) || defined(STBI_NEON)\nstatic stbi_uc *stbi__resample_row_hv_2_simd(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs)\n{\n   // need to generate 2x2 samples for every one in input\n   int i=0,t0,t1;\n\n   if (w == 1) {\n      out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2);\n      return out;\n   }\n\n   t1 = 3*in_near[0] + in_far[0];\n   // process groups of 8 pixels for as long as we can.\n   // note we can't handle the last pixel in a row in this loop\n   // because we need to handle the filter boundary conditions.\n   for (; i < ((w-1) & ~7); i += 8) {\n#if defined(STBI_SSE2)\n      // load and perform the vertical filtering pass\n      // this uses 3*x + y = 4*x + (y - x)\n      __m128i zero  = _mm_setzero_si128();\n      __m128i farb  = _mm_loadl_epi64((__m128i *) (in_far + i));\n      __m128i nearb = _mm_loadl_epi64((__m128i *) (in_near + i));\n      __m128i farw  = _mm_unpacklo_epi8(farb, zero);\n      __m128i nearw = _mm_unpacklo_epi8(nearb, zero);\n      __m128i diff  = _mm_sub_epi16(farw, nearw);\n      __m128i nears = _mm_slli_epi16(nearw, 2);\n      __m128i curr  = _mm_add_epi16(nears, diff); // current row\n\n      // horizontal filter works the same based on shifted vers of current\n      // row. \"prev\" is current row shifted right by 1 pixel; we need to\n      // insert the previous pixel value (from t1).\n      // \"next\" is current row shifted left by 1 pixel, with first pixel\n      // of next block of 8 pixels added in.\n      __m128i prv0 = _mm_slli_si128(curr, 2);\n      __m128i nxt0 = _mm_srli_si128(curr, 2);\n      __m128i prev = _mm_insert_epi16(prv0, t1, 0);\n      __m128i next = _mm_insert_epi16(nxt0, 3*in_near[i+8] + in_far[i+8], 7);\n\n      // horizontal filter, polyphase implementation since it's convenient:\n      // even pixels = 3*cur + prev = cur*4 + (prev - cur)\n      // odd  pixels = 3*cur + next = cur*4 + (next - cur)\n      // note the shared term.\n      __m128i bias  = _mm_set1_epi16(8);\n      __m128i curs = _mm_slli_epi16(curr, 2);\n      __m128i prvd = _mm_sub_epi16(prev, curr);\n      __m128i nxtd = _mm_sub_epi16(next, curr);\n      __m128i curb = _mm_add_epi16(curs, bias);\n      __m128i even = _mm_add_epi16(prvd, curb);\n      __m128i odd  = _mm_add_epi16(nxtd, curb);\n\n      // interleave even and odd pixels, then undo scaling.\n      __m128i int0 = _mm_unpacklo_epi16(even, odd);\n      __m128i int1 = _mm_unpackhi_epi16(even, odd);\n      __m128i de0  = _mm_srli_epi16(int0, 4);\n      __m128i de1  = _mm_srli_epi16(int1, 4);\n\n      // pack and write output\n      __m128i outv = _mm_packus_epi16(de0, de1);\n      _mm_storeu_si128((__m128i *) (out + i*2), outv);\n#elif defined(STBI_NEON)\n      // load and perform the vertical filtering pass\n      // this uses 3*x + y = 4*x + (y - x)\n      uint8x8_t farb  = vld1_u8(in_far + i);\n      uint8x8_t nearb = vld1_u8(in_near + i);\n      int16x8_t diff  = vreinterpretq_s16_u16(vsubl_u8(farb, nearb));\n      int16x8_t nears = vreinterpretq_s16_u16(vshll_n_u8(nearb, 2));\n      int16x8_t curr  = vaddq_s16(nears, diff); // current row\n\n      // horizontal filter works the same based on shifted vers of current\n      // row. \"prev\" is current row shifted right by 1 pixel; we need to\n      // insert the previous pixel value (from t1).\n      // \"next\" is current row shifted left by 1 pixel, with first pixel\n      // of next block of 8 pixels added in.\n      int16x8_t prv0 = vextq_s16(curr, curr, 7);\n      int16x8_t nxt0 = vextq_s16(curr, curr, 1);\n      int16x8_t prev = vsetq_lane_s16(t1, prv0, 0);\n      int16x8_t next = vsetq_lane_s16(3*in_near[i+8] + in_far[i+8], nxt0, 7);\n\n      // horizontal filter, polyphase implementation since it's convenient:\n      // even pixels = 3*cur + prev = cur*4 + (prev - cur)\n      // odd  pixels = 3*cur + next = cur*4 + (next - cur)\n      // note the shared term.\n      int16x8_t curs = vshlq_n_s16(curr, 2);\n      int16x8_t prvd = vsubq_s16(prev, curr);\n      int16x8_t nxtd = vsubq_s16(next, curr);\n      int16x8_t even = vaddq_s16(curs, prvd);\n      int16x8_t odd  = vaddq_s16(curs, nxtd);\n\n      // undo scaling and round, then store with even/odd phases interleaved\n      uint8x8x2_t o;\n      o.val[0] = vqrshrun_n_s16(even, 4);\n      o.val[1] = vqrshrun_n_s16(odd,  4);\n      vst2_u8(out + i*2, o);\n#endif\n\n      // \"previous\" value for next iter\n      t1 = 3*in_near[i+7] + in_far[i+7];\n   }\n\n   t0 = t1;\n   t1 = 3*in_near[i] + in_far[i];\n   out[i*2] = stbi__div16(3*t1 + t0 + 8);\n\n   for (++i; i < w; ++i) {\n      t0 = t1;\n      t1 = 3*in_near[i]+in_far[i];\n      out[i*2-1] = stbi__div16(3*t0 + t1 + 8);\n      out[i*2  ] = stbi__div16(3*t1 + t0 + 8);\n   }\n   out[w*2-1] = stbi__div4(t1+2);\n\n   STBI_NOTUSED(hs);\n\n   return out;\n}\n#endif\n\nstatic stbi_uc *stbi__resample_row_generic(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs)\n{\n   // resample with nearest-neighbor\n   int i,j;\n   STBI_NOTUSED(in_far);\n   for (i=0; i < w; ++i)\n      for (j=0; j < hs; ++j)\n         out[i*hs+j] = in_near[i];\n   return out;\n}\n\n// this is a reduced-precision calculation of YCbCr-to-RGB introduced\n// to make sure the code produces the same results in both SIMD and scalar\n#define stbi__float2fixed(x)  (((int) ((x) * 4096.0f + 0.5f)) << 8)\nstatic void stbi__YCbCr_to_RGB_row(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step)\n{\n   int i;\n   for (i=0; i < count; ++i) {\n      int y_fixed = (y[i] << 20) + (1<<19); // rounding\n      int r,g,b;\n      int cr = pcr[i] - 128;\n      int cb = pcb[i] - 128;\n      r = y_fixed +  cr* stbi__float2fixed(1.40200f);\n      g = y_fixed + (cr*-stbi__float2fixed(0.71414f)) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000);\n      b = y_fixed                                     +   cb* stbi__float2fixed(1.77200f);\n      r >>= 20;\n      g >>= 20;\n      b >>= 20;\n      if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; }\n      if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; }\n      if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; }\n      out[0] = (stbi_uc)r;\n      out[1] = (stbi_uc)g;\n      out[2] = (stbi_uc)b;\n      out[3] = 255;\n      out += step;\n   }\n}\n\n#if defined(STBI_SSE2) || defined(STBI_NEON)\nstatic void stbi__YCbCr_to_RGB_simd(stbi_uc *out, stbi_uc const *y, stbi_uc const *pcb, stbi_uc const *pcr, int count, int step)\n{\n   int i = 0;\n\n#ifdef STBI_SSE2\n   // step == 3 is pretty ugly on the final interleave, and i'm not convinced\n   // it's useful in practice (you wouldn't use it for textures, for example).\n   // so just accelerate step == 4 case.\n   if (step == 4) {\n      // this is a fairly straightforward implementation and not super-optimized.\n      __m128i signflip  = _mm_set1_epi8(-0x80);\n      __m128i cr_const0 = _mm_set1_epi16(   (short) ( 1.40200f*4096.0f+0.5f));\n      __m128i cr_const1 = _mm_set1_epi16( - (short) ( 0.71414f*4096.0f+0.5f));\n      __m128i cb_const0 = _mm_set1_epi16( - (short) ( 0.34414f*4096.0f+0.5f));\n      __m128i cb_const1 = _mm_set1_epi16(   (short) ( 1.77200f*4096.0f+0.5f));\n      __m128i y_bias = _mm_set1_epi8((char) (unsigned char) 128);\n      __m128i xw = _mm_set1_epi16(255); // alpha channel\n\n      for (; i+7 < count; i += 8) {\n         // load\n         __m128i y_bytes = _mm_loadl_epi64((__m128i *) (y+i));\n         __m128i cr_bytes = _mm_loadl_epi64((__m128i *) (pcr+i));\n         __m128i cb_bytes = _mm_loadl_epi64((__m128i *) (pcb+i));\n         __m128i cr_biased = _mm_xor_si128(cr_bytes, signflip); // -128\n         __m128i cb_biased = _mm_xor_si128(cb_bytes, signflip); // -128\n\n         // unpack to short (and left-shift cr, cb by 8)\n         __m128i yw  = _mm_unpacklo_epi8(y_bias, y_bytes);\n         __m128i crw = _mm_unpacklo_epi8(_mm_setzero_si128(), cr_biased);\n         __m128i cbw = _mm_unpacklo_epi8(_mm_setzero_si128(), cb_biased);\n\n         // color transform\n         __m128i yws = _mm_srli_epi16(yw, 4);\n         __m128i cr0 = _mm_mulhi_epi16(cr_const0, crw);\n         __m128i cb0 = _mm_mulhi_epi16(cb_const0, cbw);\n         __m128i cb1 = _mm_mulhi_epi16(cbw, cb_const1);\n         __m128i cr1 = _mm_mulhi_epi16(crw, cr_const1);\n         __m128i rws = _mm_add_epi16(cr0, yws);\n         __m128i gwt = _mm_add_epi16(cb0, yws);\n         __m128i bws = _mm_add_epi16(yws, cb1);\n         __m128i gws = _mm_add_epi16(gwt, cr1);\n\n         // descale\n         __m128i rw = _mm_srai_epi16(rws, 4);\n         __m128i bw = _mm_srai_epi16(bws, 4);\n         __m128i gw = _mm_srai_epi16(gws, 4);\n\n         // back to byte, set up for transpose\n         __m128i brb = _mm_packus_epi16(rw, bw);\n         __m128i gxb = _mm_packus_epi16(gw, xw);\n\n         // transpose to interleave channels\n         __m128i t0 = _mm_unpacklo_epi8(brb, gxb);\n         __m128i t1 = _mm_unpackhi_epi8(brb, gxb);\n         __m128i o0 = _mm_unpacklo_epi16(t0, t1);\n         __m128i o1 = _mm_unpackhi_epi16(t0, t1);\n\n         // store\n         _mm_storeu_si128((__m128i *) (out + 0), o0);\n         _mm_storeu_si128((__m128i *) (out + 16), o1);\n         out += 32;\n      }\n   }\n#endif\n\n#ifdef STBI_NEON\n   // in this version, step=3 support would be easy to add. but is there demand?\n   if (step == 4) {\n      // this is a fairly straightforward implementation and not super-optimized.\n      uint8x8_t signflip = vdup_n_u8(0x80);\n      int16x8_t cr_const0 = vdupq_n_s16(   (short) ( 1.40200f*4096.0f+0.5f));\n      int16x8_t cr_const1 = vdupq_n_s16( - (short) ( 0.71414f*4096.0f+0.5f));\n      int16x8_t cb_const0 = vdupq_n_s16( - (short) ( 0.34414f*4096.0f+0.5f));\n      int16x8_t cb_const1 = vdupq_n_s16(   (short) ( 1.77200f*4096.0f+0.5f));\n\n      for (; i+7 < count; i += 8) {\n         // load\n         uint8x8_t y_bytes  = vld1_u8(y + i);\n         uint8x8_t cr_bytes = vld1_u8(pcr + i);\n         uint8x8_t cb_bytes = vld1_u8(pcb + i);\n         int8x8_t cr_biased = vreinterpret_s8_u8(vsub_u8(cr_bytes, signflip));\n         int8x8_t cb_biased = vreinterpret_s8_u8(vsub_u8(cb_bytes, signflip));\n\n         // expand to s16\n         int16x8_t yws = vreinterpretq_s16_u16(vshll_n_u8(y_bytes, 4));\n         int16x8_t crw = vshll_n_s8(cr_biased, 7);\n         int16x8_t cbw = vshll_n_s8(cb_biased, 7);\n\n         // color transform\n         int16x8_t cr0 = vqdmulhq_s16(crw, cr_const0);\n         int16x8_t cb0 = vqdmulhq_s16(cbw, cb_const0);\n         int16x8_t cr1 = vqdmulhq_s16(crw, cr_const1);\n         int16x8_t cb1 = vqdmulhq_s16(cbw, cb_const1);\n         int16x8_t rws = vaddq_s16(yws, cr0);\n         int16x8_t gws = vaddq_s16(vaddq_s16(yws, cb0), cr1);\n         int16x8_t bws = vaddq_s16(yws, cb1);\n\n         // undo scaling, round, convert to byte\n         uint8x8x4_t o;\n         o.val[0] = vqrshrun_n_s16(rws, 4);\n         o.val[1] = vqrshrun_n_s16(gws, 4);\n         o.val[2] = vqrshrun_n_s16(bws, 4);\n         o.val[3] = vdup_n_u8(255);\n\n         // store, interleaving r/g/b/a\n         vst4_u8(out, o);\n         out += 8*4;\n      }\n   }\n#endif\n\n   for (; i < count; ++i) {\n      int y_fixed = (y[i] << 20) + (1<<19); // rounding\n      int r,g,b;\n      int cr = pcr[i] - 128;\n      int cb = pcb[i] - 128;\n      r = y_fixed + cr* stbi__float2fixed(1.40200f);\n      g = y_fixed + cr*-stbi__float2fixed(0.71414f) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000);\n      b = y_fixed                                   +   cb* stbi__float2fixed(1.77200f);\n      r >>= 20;\n      g >>= 20;\n      b >>= 20;\n      if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; }\n      if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; }\n      if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; }\n      out[0] = (stbi_uc)r;\n      out[1] = (stbi_uc)g;\n      out[2] = (stbi_uc)b;\n      out[3] = 255;\n      out += step;\n   }\n}\n#endif\n\n// set up the kernels\nstatic void stbi__setup_jpeg(stbi__jpeg *j)\n{\n   j->idct_block_kernel = stbi__idct_block;\n   j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_row;\n   j->resample_row_hv_2_kernel = stbi__resample_row_hv_2;\n\n#ifdef STBI_SSE2\n   if (stbi__sse2_available()) {\n      j->idct_block_kernel = stbi__idct_simd;\n      j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd;\n      j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd;\n   }\n#endif\n\n#ifdef STBI_NEON\n   j->idct_block_kernel = stbi__idct_simd;\n   j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd;\n   j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd;\n#endif\n}\n\n// clean up the temporary component buffers\nstatic void stbi__cleanup_jpeg(stbi__jpeg *j)\n{\n   stbi__free_jpeg_components(j, j->s->img_n, 0);\n}\n\ntypedef struct\n{\n   resample_row_func resample;\n   stbi_uc *line0,*line1;\n   int hs,vs;   // expansion factor in each axis\n   int w_lores; // horizontal pixels pre-expansion\n   int ystep;   // how far through vertical expansion we are\n   int ypos;    // which pre-expansion row we're on\n} stbi__resample;\n\n// fast 0..255 * 0..255 => 0..255 rounded multiplication\nstatic stbi_uc stbi__blinn_8x8(stbi_uc x, stbi_uc y)\n{\n   unsigned int t = x*y + 128;\n   return (stbi_uc) ((t + (t >>8)) >> 8);\n}\n\nstatic stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp, int req_comp)\n{\n   int n, decode_n, is_rgb;\n   z->s->img_n = 0; // make stbi__cleanup_jpeg safe\n\n   // validate req_comp\n   if (req_comp < 0 || req_comp > 4) return stbi__errpuc(\"bad req_comp\", \"Internal error\");\n\n   // load a jpeg image from whichever source, but leave in YCbCr format\n   if (!stbi__decode_jpeg_image(z)) { stbi__cleanup_jpeg(z); return NULL; }\n\n   // determine actual number of components to generate\n   n = req_comp ? req_comp : z->s->img_n >= 3 ? 3 : 1;\n\n   is_rgb = z->s->img_n == 3 && (z->rgb == 3 || (z->app14_color_transform == 0 && !z->jfif));\n\n   if (z->s->img_n == 3 && n < 3 && !is_rgb)\n      decode_n = 1;\n   else\n      decode_n = z->s->img_n;\n\n   // nothing to do if no components requested; check this now to avoid\n   // accessing uninitialized coutput[0] later\n   if (decode_n <= 0) { stbi__cleanup_jpeg(z); return NULL; }\n\n   // resample and color-convert\n   {\n      int k;\n      unsigned int i,j;\n      stbi_uc *output;\n      stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL };\n\n      stbi__resample res_comp[4];\n\n      for (k=0; k < decode_n; ++k) {\n         stbi__resample *r = &res_comp[k];\n\n         // allocate line buffer big enough for upsampling off the edges\n         // with upsample factor of 4\n         z->img_comp[k].linebuf = (stbi_uc *) stbi__malloc(z->s->img_x + 3);\n         if (!z->img_comp[k].linebuf) { stbi__cleanup_jpeg(z); return stbi__errpuc(\"outofmem\", \"Out of memory\"); }\n\n         r->hs      = z->img_h_max / z->img_comp[k].h;\n         r->vs      = z->img_v_max / z->img_comp[k].v;\n         r->ystep   = r->vs >> 1;\n         r->w_lores = (z->s->img_x + r->hs-1) / r->hs;\n         r->ypos    = 0;\n         r->line0   = r->line1 = z->img_comp[k].data;\n\n         if      (r->hs == 1 && r->vs == 1) r->resample = resample_row_1;\n         else if (r->hs == 1 && r->vs == 2) r->resample = stbi__resample_row_v_2;\n         else if (r->hs == 2 && r->vs == 1) r->resample = stbi__resample_row_h_2;\n         else if (r->hs == 2 && r->vs == 2) r->resample = z->resample_row_hv_2_kernel;\n         else                               r->resample = stbi__resample_row_generic;\n      }\n\n      // can't error after this so, this is safe\n      output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1);\n      if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc(\"outofmem\", \"Out of memory\"); }\n\n      // now go ahead and resample\n      for (j=0; j < z->s->img_y; ++j) {\n         stbi_uc *out = output + n * z->s->img_x * j;\n         for (k=0; k < decode_n; ++k) {\n            stbi__resample *r = &res_comp[k];\n            int y_bot = r->ystep >= (r->vs >> 1);\n            coutput[k] = r->resample(z->img_comp[k].linebuf,\n                                     y_bot ? r->line1 : r->line0,\n                                     y_bot ? r->line0 : r->line1,\n                                     r->w_lores, r->hs);\n            if (++r->ystep >= r->vs) {\n               r->ystep = 0;\n               r->line0 = r->line1;\n               if (++r->ypos < z->img_comp[k].y)\n                  r->line1 += z->img_comp[k].w2;\n            }\n         }\n         if (n >= 3) {\n            stbi_uc *y = coutput[0];\n            if (z->s->img_n == 3) {\n               if (is_rgb) {\n                  for (i=0; i < z->s->img_x; ++i) {\n                     out[0] = y[i];\n                     out[1] = coutput[1][i];\n                     out[2] = coutput[2][i];\n                     out[3] = 255;\n                     out += n;\n                  }\n               } else {\n                  z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n);\n               }\n            } else if (z->s->img_n == 4) {\n               if (z->app14_color_transform == 0) { // CMYK\n                  for (i=0; i < z->s->img_x; ++i) {\n                     stbi_uc m = coutput[3][i];\n                     out[0] = stbi__blinn_8x8(coutput[0][i], m);\n                     out[1] = stbi__blinn_8x8(coutput[1][i], m);\n                     out[2] = stbi__blinn_8x8(coutput[2][i], m);\n                     out[3] = 255;\n                     out += n;\n                  }\n               } else if (z->app14_color_transform == 2) { // YCCK\n                  z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n);\n                  for (i=0; i < z->s->img_x; ++i) {\n                     stbi_uc m = coutput[3][i];\n                     out[0] = stbi__blinn_8x8(255 - out[0], m);\n                     out[1] = stbi__blinn_8x8(255 - out[1], m);\n                     out[2] = stbi__blinn_8x8(255 - out[2], m);\n                     out += n;\n                  }\n               } else { // YCbCr + alpha?  Ignore the fourth channel for now\n                  z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n);\n               }\n            } else\n               for (i=0; i < z->s->img_x; ++i) {\n                  out[0] = out[1] = out[2] = y[i];\n                  out[3] = 255; // not used if n==3\n                  out += n;\n               }\n         } else {\n            if (is_rgb) {\n               if (n == 1)\n                  for (i=0; i < z->s->img_x; ++i)\n                     *out++ = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]);\n               else {\n                  for (i=0; i < z->s->img_x; ++i, out += 2) {\n                     out[0] = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]);\n                     out[1] = 255;\n                  }\n               }\n            } else if (z->s->img_n == 4 && z->app14_color_transform == 0) {\n               for (i=0; i < z->s->img_x; ++i) {\n                  stbi_uc m = coutput[3][i];\n                  stbi_uc r = stbi__blinn_8x8(coutput[0][i], m);\n                  stbi_uc g = stbi__blinn_8x8(coutput[1][i], m);\n                  stbi_uc b = stbi__blinn_8x8(coutput[2][i], m);\n                  out[0] = stbi__compute_y(r, g, b);\n                  out[1] = 255;\n                  out += n;\n               }\n            } else if (z->s->img_n == 4 && z->app14_color_transform == 2) {\n               for (i=0; i < z->s->img_x; ++i) {\n                  out[0] = stbi__blinn_8x8(255 - coutput[0][i], coutput[3][i]);\n                  out[1] = 255;\n                  out += n;\n               }\n            } else {\n               stbi_uc *y = coutput[0];\n               if (n == 1)\n                  for (i=0; i < z->s->img_x; ++i) out[i] = y[i];\n               else\n                  for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; }\n            }\n         }\n      }\n      stbi__cleanup_jpeg(z);\n      *out_x = z->s->img_x;\n      *out_y = z->s->img_y;\n      if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output\n      return output;\n   }\n}\n\nstatic void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)\n{\n   unsigned char* result;\n   stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg));\n   if (!j) return stbi__errpuc(\"outofmem\", \"Out of memory\");\n   memset(j, 0, sizeof(stbi__jpeg));\n   STBI_NOTUSED(ri);\n   j->s = s;\n   stbi__setup_jpeg(j);\n   result = load_jpeg_image(j, x,y,comp,req_comp);\n   STBI_FREE(j);\n   return result;\n}\n\nstatic int stbi__jpeg_test(stbi__context *s)\n{\n   int r;\n   stbi__jpeg* j = (stbi__jpeg*)stbi__malloc(sizeof(stbi__jpeg));\n   if (!j) return stbi__err(\"outofmem\", \"Out of memory\");\n   memset(j, 0, sizeof(stbi__jpeg));\n   j->s = s;\n   stbi__setup_jpeg(j);\n   r = stbi__decode_jpeg_header(j, STBI__SCAN_type);\n   stbi__rewind(s);\n   STBI_FREE(j);\n   return r;\n}\n\nstatic int stbi__jpeg_info_raw(stbi__jpeg *j, int *x, int *y, int *comp)\n{\n   if (!stbi__decode_jpeg_header(j, STBI__SCAN_header)) {\n      stbi__rewind( j->s );\n      return 0;\n   }\n   if (x) *x = j->s->img_x;\n   if (y) *y = j->s->img_y;\n   if (comp) *comp = j->s->img_n >= 3 ? 3 : 1;\n   return 1;\n}\n\nstatic int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp)\n{\n   int result;\n   stbi__jpeg* j = (stbi__jpeg*) (stbi__malloc(sizeof(stbi__jpeg)));\n   if (!j) return stbi__err(\"outofmem\", \"Out of memory\");\n   memset(j, 0, sizeof(stbi__jpeg));\n   j->s = s;\n   result = stbi__jpeg_info_raw(j, x, y, comp);\n   STBI_FREE(j);\n   return result;\n}\n#endif\n\n// public domain zlib decode    v0.2  Sean Barrett 2006-11-18\n//    simple implementation\n//      - all input must be provided in an upfront buffer\n//      - all output is written to a single output buffer (can malloc/realloc)\n//    performance\n//      - fast huffman\n\n#ifndef STBI_NO_ZLIB\n\n// fast-way is faster to check than jpeg huffman, but slow way is slower\n#define STBI__ZFAST_BITS  9 // accelerate all cases in default tables\n#define STBI__ZFAST_MASK  ((1 << STBI__ZFAST_BITS) - 1)\n#define STBI__ZNSYMS 288 // number of symbols in literal/length alphabet\n\n// zlib-style huffman encoding\n// (jpegs packs from left, zlib from right, so can't share code)\ntypedef struct\n{\n   stbi__uint16 fast[1 << STBI__ZFAST_BITS];\n   stbi__uint16 firstcode[16];\n   int maxcode[17];\n   stbi__uint16 firstsymbol[16];\n   stbi_uc  size[STBI__ZNSYMS];\n   stbi__uint16 value[STBI__ZNSYMS];\n} stbi__zhuffman;\n\nstbi_inline static int stbi__bitreverse16(int n)\n{\n  n = ((n & 0xAAAA) >>  1) | ((n & 0x5555) << 1);\n  n = ((n & 0xCCCC) >>  2) | ((n & 0x3333) << 2);\n  n = ((n & 0xF0F0) >>  4) | ((n & 0x0F0F) << 4);\n  n = ((n & 0xFF00) >>  8) | ((n & 0x00FF) << 8);\n  return n;\n}\n\nstbi_inline static int stbi__bit_reverse(int v, int bits)\n{\n   STBI_ASSERT(bits <= 16);\n   // to bit reverse n bits, reverse 16 and shift\n   // e.g. 11 bits, bit reverse and shift away 5\n   return stbi__bitreverse16(v) >> (16-bits);\n}\n\nstatic int stbi__zbuild_huffman(stbi__zhuffman *z, const stbi_uc *sizelist, int num)\n{\n   int i,k=0;\n   int code, next_code[16], sizes[17];\n\n   // DEFLATE spec for generating codes\n   memset(sizes, 0, sizeof(sizes));\n   memset(z->fast, 0, sizeof(z->fast));\n   for (i=0; i < num; ++i)\n      ++sizes[sizelist[i]];\n   sizes[0] = 0;\n   for (i=1; i < 16; ++i)\n      if (sizes[i] > (1 << i))\n         return stbi__err(\"bad sizes\", \"Corrupt PNG\");\n   code = 0;\n   for (i=1; i < 16; ++i) {\n      next_code[i] = code;\n      z->firstcode[i] = (stbi__uint16) code;\n      z->firstsymbol[i] = (stbi__uint16) k;\n      code = (code + sizes[i]);\n      if (sizes[i])\n         if (code-1 >= (1 << i)) return stbi__err(\"bad codelengths\",\"Corrupt PNG\");\n      z->maxcode[i] = code << (16-i); // preshift for inner loop\n      code <<= 1;\n      k += sizes[i];\n   }\n   z->maxcode[16] = 0x10000; // sentinel\n   for (i=0; i < num; ++i) {\n      int s = sizelist[i];\n      if (s) {\n         int c = next_code[s] - z->firstcode[s] + z->firstsymbol[s];\n         stbi__uint16 fastv = (stbi__uint16) ((s << 9) | i);\n         z->size [c] = (stbi_uc     ) s;\n         z->value[c] = (stbi__uint16) i;\n         if (s <= STBI__ZFAST_BITS) {\n            int j = stbi__bit_reverse(next_code[s],s);\n            while (j < (1 << STBI__ZFAST_BITS)) {\n               z->fast[j] = fastv;\n               j += (1 << s);\n            }\n         }\n         ++next_code[s];\n      }\n   }\n   return 1;\n}\n\n// zlib-from-memory implementation for PNG reading\n//    because PNG allows splitting the zlib stream arbitrarily,\n//    and it's annoying structurally to have PNG call ZLIB call PNG,\n//    we require PNG read all the IDATs and combine them into a single\n//    memory buffer\n\ntypedef struct\n{\n   stbi_uc *zbuffer, *zbuffer_end;\n   int num_bits;\n   int hit_zeof_once;\n   stbi__uint32 code_buffer;\n\n   char *zout;\n   char *zout_start;\n   char *zout_end;\n   int   z_expandable;\n\n   stbi__zhuffman z_length, z_distance;\n} stbi__zbuf;\n\nstbi_inline static int stbi__zeof(stbi__zbuf *z)\n{\n   return (z->zbuffer >= z->zbuffer_end);\n}\n\nstbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z)\n{\n   return stbi__zeof(z) ? 0 : *z->zbuffer++;\n}\n\nstatic void stbi__fill_bits(stbi__zbuf *z)\n{\n   do {\n      if (z->code_buffer >= (1U << z->num_bits)) {\n        z->zbuffer = z->zbuffer_end;  /* treat this as EOF so we fail. */\n        return;\n      }\n      z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits;\n      z->num_bits += 8;\n   } while (z->num_bits <= 24);\n}\n\nstbi_inline static unsigned int stbi__zreceive(stbi__zbuf *z, int n)\n{\n   unsigned int k;\n   if (z->num_bits < n) stbi__fill_bits(z);\n   k = z->code_buffer & ((1 << n) - 1);\n   z->code_buffer >>= n;\n   z->num_bits -= n;\n   return k;\n}\n\nstatic int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z)\n{\n   int b,s,k;\n   // not resolved by fast table, so compute it the slow way\n   // use jpeg approach, which requires MSbits at top\n   k = stbi__bit_reverse(a->code_buffer, 16);\n   for (s=STBI__ZFAST_BITS+1; ; ++s)\n      if (k < z->maxcode[s])\n         break;\n   if (s >= 16) return -1; // invalid code!\n   // code size is s, so:\n   b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s];\n   if (b >= STBI__ZNSYMS) return -1; // some data was corrupt somewhere!\n   if (z->size[b] != s) return -1;  // was originally an assert, but report failure instead.\n   a->code_buffer >>= s;\n   a->num_bits -= s;\n   return z->value[b];\n}\n\nstbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z)\n{\n   int b,s;\n   if (a->num_bits < 16) {\n      if (stbi__zeof(a)) {\n         if (!a->hit_zeof_once) {\n            // This is the first time we hit eof, insert 16 extra padding btis\n            // to allow us to keep going; if we actually consume any of them\n            // though, that is invalid data. This is caught later.\n            a->hit_zeof_once = 1;\n            a->num_bits += 16; // add 16 implicit zero bits\n         } else {\n            // We already inserted our extra 16 padding bits and are again\n            // out, this stream is actually prematurely terminated.\n            return -1;\n         }\n      } else {\n         stbi__fill_bits(a);\n      }\n   }\n   b = z->fast[a->code_buffer & STBI__ZFAST_MASK];\n   if (b) {\n      s = b >> 9;\n      a->code_buffer >>= s;\n      a->num_bits -= s;\n      return b & 511;\n   }\n   return stbi__zhuffman_decode_slowpath(a, z);\n}\n\nstatic int stbi__zexpand(stbi__zbuf *z, char *zout, int n)  // need to make room for n bytes\n{\n   char *q;\n   unsigned int cur, limit, old_limit;\n   z->zout = zout;\n   if (!z->z_expandable) return stbi__err(\"output buffer limit\",\"Corrupt PNG\");\n   cur   = (unsigned int) (z->zout - z->zout_start);\n   limit = old_limit = (unsigned) (z->zout_end - z->zout_start);\n   if (UINT_MAX - cur < (unsigned) n) return stbi__err(\"outofmem\", \"Out of memory\");\n   while (cur + n > limit) {\n      if(limit > UINT_MAX / 2) return stbi__err(\"outofmem\", \"Out of memory\");\n      limit *= 2;\n   }\n   q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit);\n   STBI_NOTUSED(old_limit);\n   if (q == NULL) return stbi__err(\"outofmem\", \"Out of memory\");\n   z->zout_start = q;\n   z->zout       = q + cur;\n   z->zout_end   = q + limit;\n   return 1;\n}\n\nstatic const int stbi__zlength_base[31] = {\n   3,4,5,6,7,8,9,10,11,13,\n   15,17,19,23,27,31,35,43,51,59,\n   67,83,99,115,131,163,195,227,258,0,0 };\n\nstatic const int stbi__zlength_extra[31]=\n{ 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0 };\n\nstatic const int stbi__zdist_base[32] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,\n257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0};\n\nstatic const int stbi__zdist_extra[32] =\n{ 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13};\n\nstatic int stbi__parse_huffman_block(stbi__zbuf *a)\n{\n   char *zout = a->zout;\n   for(;;) {\n      int z = stbi__zhuffman_decode(a, &a->z_length);\n      if (z < 256) {\n         if (z < 0) return stbi__err(\"bad huffman code\",\"Corrupt PNG\"); // error in huffman codes\n         if (zout >= a->zout_end) {\n            if (!stbi__zexpand(a, zout, 1)) return 0;\n            zout = a->zout;\n         }\n         *zout++ = (char) z;\n      } else {\n         stbi_uc *p;\n         int len,dist;\n         if (z == 256) {\n            a->zout = zout;\n            if (a->hit_zeof_once && a->num_bits < 16) {\n               // The first time we hit zeof, we inserted 16 extra zero bits into our bit\n               // buffer so the decoder can just do its speculative decoding. But if we\n               // actually consumed any of those bits (which is the case when num_bits < 16),\n               // the stream actually read past the end so it is malformed.\n               return stbi__err(\"unexpected end\",\"Corrupt PNG\");\n            }\n            return 1;\n         }\n         if (z >= 286) return stbi__err(\"bad huffman code\",\"Corrupt PNG\"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data\n         z -= 257;\n         len = stbi__zlength_base[z];\n         if (stbi__zlength_extra[z]) len += stbi__zreceive(a, stbi__zlength_extra[z]);\n         z = stbi__zhuffman_decode(a, &a->z_distance);\n         if (z < 0 || z >= 30) return stbi__err(\"bad huffman code\",\"Corrupt PNG\"); // per DEFLATE, distance codes 30 and 31 must not appear in compressed data\n         dist = stbi__zdist_base[z];\n         if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]);\n         if (zout - a->zout_start < dist) return stbi__err(\"bad dist\",\"Corrupt PNG\");\n         if (len > a->zout_end - zout) {\n            if (!stbi__zexpand(a, zout, len)) return 0;\n            zout = a->zout;\n         }\n         p = (stbi_uc *) (zout - dist);\n         if (dist == 1) { // run of one byte; common in images.\n            stbi_uc v = *p;\n            if (len) { do *zout++ = v; while (--len); }\n         } else {\n            if (len) { do *zout++ = *p++; while (--len); }\n         }\n      }\n   }\n}\n\nstatic int stbi__compute_huffman_codes(stbi__zbuf *a)\n{\n   static const stbi_uc length_dezigzag[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 };\n   stbi__zhuffman z_codelength;\n   stbi_uc lencodes[286+32+137];//padding for maximum single op\n   stbi_uc codelength_sizes[19];\n   int i,n;\n\n   int hlit  = stbi__zreceive(a,5) + 257;\n   int hdist = stbi__zreceive(a,5) + 1;\n   int hclen = stbi__zreceive(a,4) + 4;\n   int ntot  = hlit + hdist;\n\n   memset(codelength_sizes, 0, sizeof(codelength_sizes));\n   for (i=0; i < hclen; ++i) {\n      int s = stbi__zreceive(a,3);\n      codelength_sizes[length_dezigzag[i]] = (stbi_uc) s;\n   }\n   if (!stbi__zbuild_huffman(&z_codelength, codelength_sizes, 19)) return 0;\n\n   n = 0;\n   while (n < ntot) {\n      int c = stbi__zhuffman_decode(a, &z_codelength);\n      if (c < 0 || c >= 19) return stbi__err(\"bad codelengths\", \"Corrupt PNG\");\n      if (c < 16)\n         lencodes[n++] = (stbi_uc) c;\n      else {\n         stbi_uc fill = 0;\n         if (c == 16) {\n            c = stbi__zreceive(a,2)+3;\n            if (n == 0) return stbi__err(\"bad codelengths\", \"Corrupt PNG\");\n            fill = lencodes[n-1];\n         } else if (c == 17) {\n            c = stbi__zreceive(a,3)+3;\n         } else if (c == 18) {\n            c = stbi__zreceive(a,7)+11;\n         } else {\n            return stbi__err(\"bad codelengths\", \"Corrupt PNG\");\n         }\n         if (ntot - n < c) return stbi__err(\"bad codelengths\", \"Corrupt PNG\");\n         memset(lencodes+n, fill, c);\n         n += c;\n      }\n   }\n   if (n != ntot) return stbi__err(\"bad codelengths\",\"Corrupt PNG\");\n   if (!stbi__zbuild_huffman(&a->z_length, lencodes, hlit)) return 0;\n   if (!stbi__zbuild_huffman(&a->z_distance, lencodes+hlit, hdist)) return 0;\n   return 1;\n}\n\nstatic int stbi__parse_uncompressed_block(stbi__zbuf *a)\n{\n   stbi_uc header[4];\n   int len,nlen,k;\n   if (a->num_bits & 7)\n      stbi__zreceive(a, a->num_bits & 7); // discard\n   // drain the bit-packed data into header\n   k = 0;\n   while (a->num_bits > 0) {\n      header[k++] = (stbi_uc) (a->code_buffer & 255); // suppress MSVC run-time check\n      a->code_buffer >>= 8;\n      a->num_bits -= 8;\n   }\n   if (a->num_bits < 0) return stbi__err(\"zlib corrupt\",\"Corrupt PNG\");\n   // now fill header the normal way\n   while (k < 4)\n      header[k++] = stbi__zget8(a);\n   len  = header[1] * 256 + header[0];\n   nlen = header[3] * 256 + header[2];\n   if (nlen != (len ^ 0xffff)) return stbi__err(\"zlib corrupt\",\"Corrupt PNG\");\n   if (a->zbuffer + len > a->zbuffer_end) return stbi__err(\"read past buffer\",\"Corrupt PNG\");\n   if (a->zout + len > a->zout_end)\n      if (!stbi__zexpand(a, a->zout, len)) return 0;\n   memcpy(a->zout, a->zbuffer, len);\n   a->zbuffer += len;\n   a->zout += len;\n   return 1;\n}\n\nstatic int stbi__parse_zlib_header(stbi__zbuf *a)\n{\n   int cmf   = stbi__zget8(a);\n   int cm    = cmf & 15;\n   /* int cinfo = cmf >> 4; */\n   int flg   = stbi__zget8(a);\n   if (stbi__zeof(a)) return stbi__err(\"bad zlib header\",\"Corrupt PNG\"); // zlib spec\n   if ((cmf*256+flg) % 31 != 0) return stbi__err(\"bad zlib header\",\"Corrupt PNG\"); // zlib spec\n   if (flg & 32) return stbi__err(\"no preset dict\",\"Corrupt PNG\"); // preset dictionary not allowed in png\n   if (cm != 8) return stbi__err(\"bad compression\",\"Corrupt PNG\"); // DEFLATE required for png\n   // window = 1 << (8 + cinfo)... but who cares, we fully buffer output\n   return 1;\n}\n\nstatic const stbi_uc stbi__zdefault_length[STBI__ZNSYMS] =\n{\n   8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,\n   8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,\n   8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,\n   8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,\n   8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,\n   9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,\n   9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,\n   9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,\n   7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8\n};\nstatic const stbi_uc stbi__zdefault_distance[32] =\n{\n   5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5\n};\n/*\nInit algorithm:\n{\n   int i;   // use <= to match clearly with spec\n   for (i=0; i <= 143; ++i)     stbi__zdefault_length[i]   = 8;\n   for (   ; i <= 255; ++i)     stbi__zdefault_length[i]   = 9;\n   for (   ; i <= 279; ++i)     stbi__zdefault_length[i]   = 7;\n   for (   ; i <= 287; ++i)     stbi__zdefault_length[i]   = 8;\n\n   for (i=0; i <=  31; ++i)     stbi__zdefault_distance[i] = 5;\n}\n*/\n\nstatic int stbi__parse_zlib(stbi__zbuf *a, int parse_header)\n{\n   int final, type;\n   if (parse_header)\n      if (!stbi__parse_zlib_header(a)) return 0;\n   a->num_bits = 0;\n   a->code_buffer = 0;\n   a->hit_zeof_once = 0;\n   do {\n      final = stbi__zreceive(a,1);\n      type = stbi__zreceive(a,2);\n      if (type == 0) {\n         if (!stbi__parse_uncompressed_block(a)) return 0;\n      } else if (type == 3) {\n         return 0;\n      } else {\n         if (type == 1) {\n            // use fixed code lengths\n            if (!stbi__zbuild_huffman(&a->z_length  , stbi__zdefault_length  , STBI__ZNSYMS)) return 0;\n            if (!stbi__zbuild_huffman(&a->z_distance, stbi__zdefault_distance,  32)) return 0;\n         } else {\n            if (!stbi__compute_huffman_codes(a)) return 0;\n         }\n         if (!stbi__parse_huffman_block(a)) return 0;\n      }\n   } while (!final);\n   return 1;\n}\n\nstatic int stbi__do_zlib(stbi__zbuf *a, char *obuf, int olen, int exp, int parse_header)\n{\n   a->zout_start = obuf;\n   a->zout       = obuf;\n   a->zout_end   = obuf + olen;\n   a->z_expandable = exp;\n\n   return stbi__parse_zlib(a, parse_header);\n}\n\nSTBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen)\n{\n   stbi__zbuf a;\n   char *p = (char *) stbi__malloc(initial_size);\n   if (p == NULL) return NULL;\n   a.zbuffer = (stbi_uc *) buffer;\n   a.zbuffer_end = (stbi_uc *) buffer + len;\n   if (stbi__do_zlib(&a, p, initial_size, 1, 1)) {\n      if (outlen) *outlen = (int) (a.zout - a.zout_start);\n      return a.zout_start;\n   } else {\n      STBI_FREE(a.zout_start);\n      return NULL;\n   }\n}\n\nSTBIDEF char *stbi_zlib_decode_malloc(char const *buffer, int len, int *outlen)\n{\n   return stbi_zlib_decode_malloc_guesssize(buffer, len, 16384, outlen);\n}\n\nSTBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header)\n{\n   stbi__zbuf a;\n   char *p = (char *) stbi__malloc(initial_size);\n   if (p == NULL) return NULL;\n   a.zbuffer = (stbi_uc *) buffer;\n   a.zbuffer_end = (stbi_uc *) buffer + len;\n   if (stbi__do_zlib(&a, p, initial_size, 1, parse_header)) {\n      if (outlen) *outlen = (int) (a.zout - a.zout_start);\n      return a.zout_start;\n   } else {\n      STBI_FREE(a.zout_start);\n      return NULL;\n   }\n}\n\nSTBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, char const *ibuffer, int ilen)\n{\n   stbi__zbuf a;\n   a.zbuffer = (stbi_uc *) ibuffer;\n   a.zbuffer_end = (stbi_uc *) ibuffer + ilen;\n   if (stbi__do_zlib(&a, obuffer, olen, 0, 1))\n      return (int) (a.zout - a.zout_start);\n   else\n      return -1;\n}\n\nSTBIDEF char *stbi_zlib_decode_noheader_malloc(char const *buffer, int len, int *outlen)\n{\n   stbi__zbuf a;\n   char *p = (char *) stbi__malloc(16384);\n   if (p == NULL) return NULL;\n   a.zbuffer = (stbi_uc *) buffer;\n   a.zbuffer_end = (stbi_uc *) buffer+len;\n   if (stbi__do_zlib(&a, p, 16384, 1, 0)) {\n      if (outlen) *outlen = (int) (a.zout - a.zout_start);\n      return a.zout_start;\n   } else {\n      STBI_FREE(a.zout_start);\n      return NULL;\n   }\n}\n\nSTBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen)\n{\n   stbi__zbuf a;\n   a.zbuffer = (stbi_uc *) ibuffer;\n   a.zbuffer_end = (stbi_uc *) ibuffer + ilen;\n   if (stbi__do_zlib(&a, obuffer, olen, 0, 0))\n      return (int) (a.zout - a.zout_start);\n   else\n      return -1;\n}\n#endif\n\n// public domain \"baseline\" PNG decoder   v0.10  Sean Barrett 2006-11-18\n//    simple implementation\n//      - only 8-bit samples\n//      - no CRC checking\n//      - allocates lots of intermediate memory\n//        - avoids problem of streaming data between subsystems\n//        - avoids explicit window management\n//    performance\n//      - uses stb_zlib, a PD zlib implementation with fast huffman decoding\n\n#ifndef STBI_NO_PNG\ntypedef struct\n{\n   stbi__uint32 length;\n   stbi__uint32 type;\n} stbi__pngchunk;\n\nstatic stbi__pngchunk stbi__get_chunk_header(stbi__context *s)\n{\n   stbi__pngchunk c;\n   c.length = stbi__get32be(s);\n   c.type   = stbi__get32be(s);\n   return c;\n}\n\nstatic int stbi__check_png_header(stbi__context *s)\n{\n   static const stbi_uc png_sig[8] = { 137,80,78,71,13,10,26,10 };\n   int i;\n   for (i=0; i < 8; ++i)\n      if (stbi__get8(s) != png_sig[i]) return stbi__err(\"bad png sig\",\"Not a PNG\");\n   return 1;\n}\n\ntypedef struct\n{\n   stbi__context *s;\n   stbi_uc *idata, *expanded, *out;\n   int depth;\n} stbi__png;\n\n\nenum {\n   STBI__F_none=0,\n   STBI__F_sub=1,\n   STBI__F_up=2,\n   STBI__F_avg=3,\n   STBI__F_paeth=4,\n   // synthetic filter used for first scanline to avoid needing a dummy row of 0s\n   STBI__F_avg_first\n};\n\nstatic stbi_uc first_row_filter[5] =\n{\n   STBI__F_none,\n   STBI__F_sub,\n   STBI__F_none,\n   STBI__F_avg_first,\n   STBI__F_sub // Paeth with b=c=0 turns out to be equivalent to sub\n};\n\nstatic int stbi__paeth(int a, int b, int c)\n{\n   // This formulation looks very different from the reference in the PNG spec, but is\n   // actually equivalent and has favorable data dependencies and admits straightforward\n   // generation of branch-free code, which helps performance significantly.\n   int thresh = c*3 - (a + b);\n   int lo = a < b ? a : b;\n   int hi = a < b ? b : a;\n   int t0 = (hi <= thresh) ? lo : c;\n   int t1 = (thresh <= lo) ? hi : t0;\n   return t1;\n}\n\nstatic const stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 };\n\n// adds an extra all-255 alpha channel\n// dest == src is legal\n// img_n must be 1 or 3\nstatic void stbi__create_png_alpha_expand8(stbi_uc *dest, stbi_uc *src, stbi__uint32 x, int img_n)\n{\n   int i;\n   // must process data backwards since we allow dest==src\n   if (img_n == 1) {\n      for (i=x-1; i >= 0; --i) {\n         dest[i*2+1] = 255;\n         dest[i*2+0] = src[i];\n      }\n   } else {\n      STBI_ASSERT(img_n == 3);\n      for (i=x-1; i >= 0; --i) {\n         dest[i*4+3] = 255;\n         dest[i*4+2] = src[i*3+2];\n         dest[i*4+1] = src[i*3+1];\n         dest[i*4+0] = src[i*3+0];\n      }\n   }\n}\n\n// create the png data from post-deflated data\nstatic int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color)\n{\n   int bytes = (depth == 16 ? 2 : 1);\n   stbi__context *s = a->s;\n   stbi__uint32 i,j,stride = x*out_n*bytes;\n   stbi__uint32 img_len, img_width_bytes;\n   stbi_uc *filter_buf;\n   int all_ok = 1;\n   int k;\n   int img_n = s->img_n; // copy it into a local for later\n\n   int output_bytes = out_n*bytes;\n   int filter_bytes = img_n*bytes;\n   int width = x;\n\n   STBI_ASSERT(out_n == s->img_n || out_n == s->img_n+1);\n   a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0); // extra bytes to write off the end into\n   if (!a->out) return stbi__err(\"outofmem\", \"Out of memory\");\n\n   // note: error exits here don't need to clean up a->out individually,\n   // stbi__do_png always does on error.\n   if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) return stbi__err(\"too large\", \"Corrupt PNG\");\n   img_width_bytes = (((img_n * x * depth) + 7) >> 3);\n   if (!stbi__mad2sizes_valid(img_width_bytes, y, img_width_bytes)) return stbi__err(\"too large\", \"Corrupt PNG\");\n   img_len = (img_width_bytes + 1) * y;\n\n   // we used to check for exact match between raw_len and img_len on non-interlaced PNGs,\n   // but issue #276 reported a PNG in the wild that had extra data at the end (all zeros),\n   // so just check for raw_len < img_len always.\n   if (raw_len < img_len) return stbi__err(\"not enough pixels\",\"Corrupt PNG\");\n\n   // Allocate two scan lines worth of filter workspace buffer.\n   filter_buf = (stbi_uc *) stbi__malloc_mad2(img_width_bytes, 2, 0);\n   if (!filter_buf) return stbi__err(\"outofmem\", \"Out of memory\");\n\n   // Filtering for low-bit-depth images\n   if (depth < 8) {\n      filter_bytes = 1;\n      width = img_width_bytes;\n   }\n\n   for (j=0; j < y; ++j) {\n      // cur/prior filter buffers alternate\n      stbi_uc *cur = filter_buf + (j & 1)*img_width_bytes;\n      stbi_uc *prior = filter_buf + (~j & 1)*img_width_bytes;\n      stbi_uc *dest = a->out + stride*j;\n      int nk = width * filter_bytes;\n      int filter = *raw++;\n\n      // check filter type\n      if (filter > 4) {\n         all_ok = stbi__err(\"invalid filter\",\"Corrupt PNG\");\n         break;\n      }\n\n      // if first row, use special filter that doesn't sample previous row\n      if (j == 0) filter = first_row_filter[filter];\n\n      // perform actual filtering\n      switch (filter) {\n      case STBI__F_none:\n         memcpy(cur, raw, nk);\n         break;\n      case STBI__F_sub:\n         memcpy(cur, raw, filter_bytes);\n         for (k = filter_bytes; k < nk; ++k)\n            cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]);\n         break;\n      case STBI__F_up:\n         for (k = 0; k < nk; ++k)\n            cur[k] = STBI__BYTECAST(raw[k] + prior[k]);\n         break;\n      case STBI__F_avg:\n         for (k = 0; k < filter_bytes; ++k)\n            cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1));\n         for (k = filter_bytes; k < nk; ++k)\n            cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1));\n         break;\n      case STBI__F_paeth:\n         for (k = 0; k < filter_bytes; ++k)\n            cur[k] = STBI__BYTECAST(raw[k] + prior[k]); // prior[k] == stbi__paeth(0,prior[k],0)\n         for (k = filter_bytes; k < nk; ++k)\n            cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes], prior[k], prior[k-filter_bytes]));\n         break;\n      case STBI__F_avg_first:\n         memcpy(cur, raw, filter_bytes);\n         for (k = filter_bytes; k < nk; ++k)\n            cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1));\n         break;\n      }\n\n      raw += nk;\n\n      // expand decoded bits in cur to dest, also adding an extra alpha channel if desired\n      if (depth < 8) {\n         stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1; // scale grayscale values to 0..255 range\n         stbi_uc *in = cur;\n         stbi_uc *out = dest;\n         stbi_uc inb = 0;\n         stbi__uint32 nsmp = x*img_n;\n\n         // expand bits to bytes first\n         if (depth == 4) {\n            for (i=0; i < nsmp; ++i) {\n               if ((i & 1) == 0) inb = *in++;\n               *out++ = scale * (inb >> 4);\n               inb <<= 4;\n            }\n         } else if (depth == 2) {\n            for (i=0; i < nsmp; ++i) {\n               if ((i & 3) == 0) inb = *in++;\n               *out++ = scale * (inb >> 6);\n               inb <<= 2;\n            }\n         } else {\n            STBI_ASSERT(depth == 1);\n            for (i=0; i < nsmp; ++i) {\n               if ((i & 7) == 0) inb = *in++;\n               *out++ = scale * (inb >> 7);\n               inb <<= 1;\n            }\n         }\n\n         // insert alpha=255 values if desired\n         if (img_n != out_n)\n            stbi__create_png_alpha_expand8(dest, dest, x, img_n);\n      } else if (depth == 8) {\n         if (img_n == out_n)\n            memcpy(dest, cur, x*img_n);\n         else\n            stbi__create_png_alpha_expand8(dest, cur, x, img_n);\n      } else if (depth == 16) {\n         // convert the image data from big-endian to platform-native\n         stbi__uint16 *dest16 = (stbi__uint16*)dest;\n         stbi__uint32 nsmp = x*img_n;\n\n         if (img_n == out_n) {\n            for (i = 0; i < nsmp; ++i, ++dest16, cur += 2)\n               *dest16 = (cur[0] << 8) | cur[1];\n         } else {\n            STBI_ASSERT(img_n+1 == out_n);\n            if (img_n == 1) {\n               for (i = 0; i < x; ++i, dest16 += 2, cur += 2) {\n                  dest16[0] = (cur[0] << 8) | cur[1];\n                  dest16[1] = 0xffff;\n               }\n            } else {\n               STBI_ASSERT(img_n == 3);\n               for (i = 0; i < x; ++i, dest16 += 4, cur += 6) {\n                  dest16[0] = (cur[0] << 8) | cur[1];\n                  dest16[1] = (cur[2] << 8) | cur[3];\n                  dest16[2] = (cur[4] << 8) | cur[5];\n                  dest16[3] = 0xffff;\n               }\n            }\n         }\n      }\n   }\n\n   STBI_FREE(filter_buf);\n   if (!all_ok) return 0;\n\n   return 1;\n}\n\nstatic int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint32 image_data_len, int out_n, int depth, int color, int interlaced)\n{\n   int bytes = (depth == 16 ? 2 : 1);\n   int out_bytes = out_n * bytes;\n   stbi_uc *final;\n   int p;\n   if (!interlaced)\n      return stbi__create_png_image_raw(a, image_data, image_data_len, out_n, a->s->img_x, a->s->img_y, depth, color);\n\n   // de-interlacing\n   final = (stbi_uc *) stbi__malloc_mad3(a->s->img_x, a->s->img_y, out_bytes, 0);\n   if (!final) return stbi__err(\"outofmem\", \"Out of memory\");\n   for (p=0; p < 7; ++p) {\n      int xorig[] = { 0,4,0,2,0,1,0 };\n      int yorig[] = { 0,0,4,0,2,0,1 };\n      int xspc[]  = { 8,8,4,4,2,2,1 };\n      int yspc[]  = { 8,8,8,4,4,2,2 };\n      int i,j,x,y;\n      // pass1_x[4] = 0, pass1_x[5] = 1, pass1_x[12] = 1\n      x = (a->s->img_x - xorig[p] + xspc[p]-1) / xspc[p];\n      y = (a->s->img_y - yorig[p] + yspc[p]-1) / yspc[p];\n      if (x && y) {\n         stbi__uint32 img_len = ((((a->s->img_n * x * depth) + 7) >> 3) + 1) * y;\n         if (!stbi__create_png_image_raw(a, image_data, image_data_len, out_n, x, y, depth, color)) {\n            STBI_FREE(final);\n            return 0;\n         }\n         for (j=0; j < y; ++j) {\n            for (i=0; i < x; ++i) {\n               int out_y = j*yspc[p]+yorig[p];\n               int out_x = i*xspc[p]+xorig[p];\n               memcpy(final + out_y*a->s->img_x*out_bytes + out_x*out_bytes,\n                      a->out + (j*x+i)*out_bytes, out_bytes);\n            }\n         }\n         STBI_FREE(a->out);\n         image_data += img_len;\n         image_data_len -= img_len;\n      }\n   }\n   a->out = final;\n\n   return 1;\n}\n\nstatic int stbi__compute_transparency(stbi__png *z, stbi_uc tc[3], int out_n)\n{\n   stbi__context *s = z->s;\n   stbi__uint32 i, pixel_count = s->img_x * s->img_y;\n   stbi_uc *p = z->out;\n\n   // compute color-based transparency, assuming we've\n   // already got 255 as the alpha value in the output\n   STBI_ASSERT(out_n == 2 || out_n == 4);\n\n   if (out_n == 2) {\n      for (i=0; i < pixel_count; ++i) {\n         p[1] = (p[0] == tc[0] ? 0 : 255);\n         p += 2;\n      }\n   } else {\n      for (i=0; i < pixel_count; ++i) {\n         if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2])\n            p[3] = 0;\n         p += 4;\n      }\n   }\n   return 1;\n}\n\nstatic int stbi__compute_transparency16(stbi__png *z, stbi__uint16 tc[3], int out_n)\n{\n   stbi__context *s = z->s;\n   stbi__uint32 i, pixel_count = s->img_x * s->img_y;\n   stbi__uint16 *p = (stbi__uint16*) z->out;\n\n   // compute color-based transparency, assuming we've\n   // already got 65535 as the alpha value in the output\n   STBI_ASSERT(out_n == 2 || out_n == 4);\n\n   if (out_n == 2) {\n      for (i = 0; i < pixel_count; ++i) {\n         p[1] = (p[0] == tc[0] ? 0 : 65535);\n         p += 2;\n      }\n   } else {\n      for (i = 0; i < pixel_count; ++i) {\n         if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2])\n            p[3] = 0;\n         p += 4;\n      }\n   }\n   return 1;\n}\n\nstatic int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int pal_img_n)\n{\n   stbi__uint32 i, pixel_count = a->s->img_x * a->s->img_y;\n   stbi_uc *p, *temp_out, *orig = a->out;\n\n   p = (stbi_uc *) stbi__malloc_mad2(pixel_count, pal_img_n, 0);\n   if (p == NULL) return stbi__err(\"outofmem\", \"Out of memory\");\n\n   // between here and free(out) below, exitting would leak\n   temp_out = p;\n\n   if (pal_img_n == 3) {\n      for (i=0; i < pixel_count; ++i) {\n         int n = orig[i]*4;\n         p[0] = palette[n  ];\n         p[1] = palette[n+1];\n         p[2] = palette[n+2];\n         p += 3;\n      }\n   } else {\n      for (i=0; i < pixel_count; ++i) {\n         int n = orig[i]*4;\n         p[0] = palette[n  ];\n         p[1] = palette[n+1];\n         p[2] = palette[n+2];\n         p[3] = palette[n+3];\n         p += 4;\n      }\n   }\n   STBI_FREE(a->out);\n   a->out = temp_out;\n\n   STBI_NOTUSED(len);\n\n   return 1;\n}\n\nstatic int stbi__unpremultiply_on_load_global = 0;\nstatic int stbi__de_iphone_flag_global = 0;\n\nSTBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply)\n{\n   stbi__unpremultiply_on_load_global = flag_true_if_should_unpremultiply;\n}\n\nSTBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert)\n{\n   stbi__de_iphone_flag_global = flag_true_if_should_convert;\n}\n\n#ifndef STBI_THREAD_LOCAL\n#define stbi__unpremultiply_on_load  stbi__unpremultiply_on_load_global\n#define stbi__de_iphone_flag  stbi__de_iphone_flag_global\n#else\nstatic STBI_THREAD_LOCAL int stbi__unpremultiply_on_load_local, stbi__unpremultiply_on_load_set;\nstatic STBI_THREAD_LOCAL int stbi__de_iphone_flag_local, stbi__de_iphone_flag_set;\n\nSTBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply)\n{\n   stbi__unpremultiply_on_load_local = flag_true_if_should_unpremultiply;\n   stbi__unpremultiply_on_load_set = 1;\n}\n\nSTBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert)\n{\n   stbi__de_iphone_flag_local = flag_true_if_should_convert;\n   stbi__de_iphone_flag_set = 1;\n}\n\n#define stbi__unpremultiply_on_load  (stbi__unpremultiply_on_load_set           \\\n                                       ? stbi__unpremultiply_on_load_local      \\\n                                       : stbi__unpremultiply_on_load_global)\n#define stbi__de_iphone_flag  (stbi__de_iphone_flag_set                         \\\n                                ? stbi__de_iphone_flag_local                    \\\n                                : stbi__de_iphone_flag_global)\n#endif // STBI_THREAD_LOCAL\n\nstatic void stbi__de_iphone(stbi__png *z)\n{\n   stbi__context *s = z->s;\n   stbi__uint32 i, pixel_count = s->img_x * s->img_y;\n   stbi_uc *p = z->out;\n\n   if (s->img_out_n == 3) {  // convert bgr to rgb\n      for (i=0; i < pixel_count; ++i) {\n         stbi_uc t = p[0];\n         p[0] = p[2];\n         p[2] = t;\n         p += 3;\n      }\n   } else {\n      STBI_ASSERT(s->img_out_n == 4);\n      if (stbi__unpremultiply_on_load) {\n         // convert bgr to rgb and unpremultiply\n         for (i=0; i < pixel_count; ++i) {\n            stbi_uc a = p[3];\n            stbi_uc t = p[0];\n            if (a) {\n               stbi_uc half = a / 2;\n               p[0] = (p[2] * 255 + half) / a;\n               p[1] = (p[1] * 255 + half) / a;\n               p[2] = ( t   * 255 + half) / a;\n            } else {\n               p[0] = p[2];\n               p[2] = t;\n            }\n            p += 4;\n         }\n      } else {\n         // convert bgr to rgb\n         for (i=0; i < pixel_count; ++i) {\n            stbi_uc t = p[0];\n            p[0] = p[2];\n            p[2] = t;\n            p += 4;\n         }\n      }\n   }\n}\n\n#define STBI__PNG_TYPE(a,b,c,d)  (((unsigned) (a) << 24) + ((unsigned) (b) << 16) + ((unsigned) (c) << 8) + (unsigned) (d))\n\nstatic int stbi__parse_png_file(stbi__png *z, int scan, int req_comp)\n{\n   stbi_uc palette[1024], pal_img_n=0;\n   stbi_uc has_trans=0, tc[3]={0};\n   stbi__uint16 tc16[3];\n   stbi__uint32 ioff=0, idata_limit=0, i, pal_len=0;\n   int first=1,k,interlace=0, color=0, is_iphone=0;\n   stbi__context *s = z->s;\n\n   z->expanded = NULL;\n   z->idata = NULL;\n   z->out = NULL;\n\n   if (!stbi__check_png_header(s)) return 0;\n\n   if (scan == STBI__SCAN_type) return 1;\n\n   for (;;) {\n      stbi__pngchunk c = stbi__get_chunk_header(s);\n      switch (c.type) {\n         case STBI__PNG_TYPE('C','g','B','I'):\n            is_iphone = 1;\n            stbi__skip(s, c.length);\n            break;\n         case STBI__PNG_TYPE('I','H','D','R'): {\n            int comp,filter;\n            if (!first) return stbi__err(\"multiple IHDR\",\"Corrupt PNG\");\n            first = 0;\n            if (c.length != 13) return stbi__err(\"bad IHDR len\",\"Corrupt PNG\");\n            s->img_x = stbi__get32be(s);\n            s->img_y = stbi__get32be(s);\n            if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err(\"too large\",\"Very large image (corrupt?)\");\n            if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err(\"too large\",\"Very large image (corrupt?)\");\n            z->depth = stbi__get8(s);  if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16)  return stbi__err(\"1/2/4/8/16-bit only\",\"PNG not supported: 1/2/4/8/16-bit only\");\n            color = stbi__get8(s);  if (color > 6)         return stbi__err(\"bad ctype\",\"Corrupt PNG\");\n            if (color == 3 && z->depth == 16)                  return stbi__err(\"bad ctype\",\"Corrupt PNG\");\n            if (color == 3) pal_img_n = 3; else if (color & 1) return stbi__err(\"bad ctype\",\"Corrupt PNG\");\n            comp  = stbi__get8(s);  if (comp) return stbi__err(\"bad comp method\",\"Corrupt PNG\");\n            filter= stbi__get8(s);  if (filter) return stbi__err(\"bad filter method\",\"Corrupt PNG\");\n            interlace = stbi__get8(s); if (interlace>1) return stbi__err(\"bad interlace method\",\"Corrupt PNG\");\n            if (!s->img_x || !s->img_y) return stbi__err(\"0-pixel image\",\"Corrupt PNG\");\n            if (!pal_img_n) {\n               s->img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0);\n               if ((1 << 30) / s->img_x / s->img_n < s->img_y) return stbi__err(\"too large\", \"Image too large to decode\");\n            } else {\n               // if paletted, then pal_n is our final components, and\n               // img_n is # components to decompress/filter.\n               s->img_n = 1;\n               if ((1 << 30) / s->img_x / 4 < s->img_y) return stbi__err(\"too large\",\"Corrupt PNG\");\n            }\n            // even with SCAN_header, have to scan to see if we have a tRNS\n            break;\n         }\n\n         case STBI__PNG_TYPE('P','L','T','E'):  {\n            if (first) return stbi__err(\"first not IHDR\", \"Corrupt PNG\");\n            if (c.length > 256*3) return stbi__err(\"invalid PLTE\",\"Corrupt PNG\");\n            pal_len = c.length / 3;\n            if (pal_len * 3 != c.length) return stbi__err(\"invalid PLTE\",\"Corrupt PNG\");\n            for (i=0; i < pal_len; ++i) {\n               palette[i*4+0] = stbi__get8(s);\n               palette[i*4+1] = stbi__get8(s);\n               palette[i*4+2] = stbi__get8(s);\n               palette[i*4+3] = 255;\n            }\n            break;\n         }\n\n         case STBI__PNG_TYPE('t','R','N','S'): {\n            if (first) return stbi__err(\"first not IHDR\", \"Corrupt PNG\");\n            if (z->idata) return stbi__err(\"tRNS after IDAT\",\"Corrupt PNG\");\n            if (pal_img_n) {\n               if (scan == STBI__SCAN_header) { s->img_n = 4; return 1; }\n               if (pal_len == 0) return stbi__err(\"tRNS before PLTE\",\"Corrupt PNG\");\n               if (c.length > pal_len) return stbi__err(\"bad tRNS len\",\"Corrupt PNG\");\n               pal_img_n = 4;\n               for (i=0; i < c.length; ++i)\n                  palette[i*4+3] = stbi__get8(s);\n            } else {\n               if (!(s->img_n & 1)) return stbi__err(\"tRNS with alpha\",\"Corrupt PNG\");\n               if (c.length != (stbi__uint32) s->img_n*2) return stbi__err(\"bad tRNS len\",\"Corrupt PNG\");\n               has_trans = 1;\n               // non-paletted with tRNS = constant alpha. if header-scanning, we can stop now.\n               if (scan == STBI__SCAN_header) { ++s->img_n; return 1; }\n               if (z->depth == 16) {\n                  for (k = 0; k < s->img_n && k < 3; ++k) // extra loop test to suppress false GCC warning\n                     tc16[k] = (stbi__uint16)stbi__get16be(s); // copy the values as-is\n               } else {\n                  for (k = 0; k < s->img_n && k < 3; ++k)\n                     tc[k] = (stbi_uc)(stbi__get16be(s) & 255) * stbi__depth_scale_table[z->depth]; // non 8-bit images will be larger\n               }\n            }\n            break;\n         }\n\n         case STBI__PNG_TYPE('I','D','A','T'): {\n            if (first) return stbi__err(\"first not IHDR\", \"Corrupt PNG\");\n            if (pal_img_n && !pal_len) return stbi__err(\"no PLTE\",\"Corrupt PNG\");\n            if (scan == STBI__SCAN_header) {\n               // header scan definitely stops at first IDAT\n               if (pal_img_n)\n                  s->img_n = pal_img_n;\n               return 1;\n            }\n            if (c.length > (1u << 30)) return stbi__err(\"IDAT size limit\", \"IDAT section larger than 2^30 bytes\");\n            if ((int)(ioff + c.length) < (int)ioff) return 0;\n            if (ioff + c.length > idata_limit) {\n               stbi__uint32 idata_limit_old = idata_limit;\n               stbi_uc *p;\n               if (idata_limit == 0) idata_limit = c.length > 4096 ? c.length : 4096;\n               while (ioff + c.length > idata_limit)\n                  idata_limit *= 2;\n               STBI_NOTUSED(idata_limit_old);\n               p = (stbi_uc *) STBI_REALLOC_SIZED(z->idata, idata_limit_old, idata_limit); if (p == NULL) return stbi__err(\"outofmem\", \"Out of memory\");\n               z->idata = p;\n            }\n            if (!stbi__getn(s, z->idata+ioff,c.length)) return stbi__err(\"outofdata\",\"Corrupt PNG\");\n            ioff += c.length;\n            break;\n         }\n\n         case STBI__PNG_TYPE('I','E','N','D'): {\n            stbi__uint32 raw_len, bpl;\n            if (first) return stbi__err(\"first not IHDR\", \"Corrupt PNG\");\n            if (scan != STBI__SCAN_load) return 1;\n            if (z->idata == NULL) return stbi__err(\"no IDAT\",\"Corrupt PNG\");\n            // initial guess for decoded data size to avoid unnecessary reallocs\n            bpl = (s->img_x * z->depth + 7) / 8; // bytes per line, per component\n            raw_len = bpl * s->img_y * s->img_n /* pixels */ + s->img_y /* filter mode per row */;\n            z->expanded = (stbi_uc *) stbi_zlib_decode_malloc_guesssize_headerflag((char *) z->idata, ioff, raw_len, (int *) &raw_len, !is_iphone);\n            if (z->expanded == NULL) return 0; // zlib should set error\n            STBI_FREE(z->idata); z->idata = NULL;\n            if ((req_comp == s->img_n+1 && req_comp != 3 && !pal_img_n) || has_trans)\n               s->img_out_n = s->img_n+1;\n            else\n               s->img_out_n = s->img_n;\n            if (!stbi__create_png_image(z, z->expanded, raw_len, s->img_out_n, z->depth, color, interlace)) return 0;\n            if (has_trans) {\n               if (z->depth == 16) {\n                  if (!stbi__compute_transparency16(z, tc16, s->img_out_n)) return 0;\n               } else {\n                  if (!stbi__compute_transparency(z, tc, s->img_out_n)) return 0;\n               }\n            }\n            if (is_iphone && stbi__de_iphone_flag && s->img_out_n > 2)\n               stbi__de_iphone(z);\n            if (pal_img_n) {\n               // pal_img_n == 3 or 4\n               s->img_n = pal_img_n; // record the actual colors we had\n               s->img_out_n = pal_img_n;\n               if (req_comp >= 3) s->img_out_n = req_comp;\n               if (!stbi__expand_png_palette(z, palette, pal_len, s->img_out_n))\n                  return 0;\n            } else if (has_trans) {\n               // non-paletted image with tRNS -> source image has (constant) alpha\n               ++s->img_n;\n            }\n            STBI_FREE(z->expanded); z->expanded = NULL;\n            // end of PNG chunk, read and skip CRC\n            stbi__get32be(s);\n            return 1;\n         }\n\n         default:\n            // if critical, fail\n            if (first) return stbi__err(\"first not IHDR\", \"Corrupt PNG\");\n            if ((c.type & (1 << 29)) == 0) {\n               #ifndef STBI_NO_FAILURE_STRINGS\n               // not threadsafe\n               static char invalid_chunk[] = \"XXXX PNG chunk not known\";\n               invalid_chunk[0] = STBI__BYTECAST(c.type >> 24);\n               invalid_chunk[1] = STBI__BYTECAST(c.type >> 16);\n               invalid_chunk[2] = STBI__BYTECAST(c.type >>  8);\n               invalid_chunk[3] = STBI__BYTECAST(c.type >>  0);\n               #endif\n               return stbi__err(invalid_chunk, \"PNG not supported: unknown PNG chunk type\");\n            }\n            stbi__skip(s, c.length);\n            break;\n      }\n      // end of PNG chunk, read and skip CRC\n      stbi__get32be(s);\n   }\n}\n\nstatic void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, stbi__result_info *ri)\n{\n   void *result=NULL;\n   if (req_comp < 0 || req_comp > 4) return stbi__errpuc(\"bad req_comp\", \"Internal error\");\n   if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) {\n      if (p->depth <= 8)\n         ri->bits_per_channel = 8;\n      else if (p->depth == 16)\n         ri->bits_per_channel = 16;\n      else\n         return stbi__errpuc(\"bad bits_per_channel\", \"PNG not supported: unsupported color depth\");\n      result = p->out;\n      p->out = NULL;\n      if (req_comp && req_comp != p->s->img_out_n) {\n         if (ri->bits_per_channel == 8)\n            result = stbi__convert_format((unsigned char *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y);\n         else\n            result = stbi__convert_format16((stbi__uint16 *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y);\n         p->s->img_out_n = req_comp;\n         if (result == NULL) return result;\n      }\n      *x = p->s->img_x;\n      *y = p->s->img_y;\n      if (n) *n = p->s->img_n;\n   }\n   STBI_FREE(p->out);      p->out      = NULL;\n   STBI_FREE(p->expanded); p->expanded = NULL;\n   STBI_FREE(p->idata);    p->idata    = NULL;\n\n   return result;\n}\n\nstatic void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)\n{\n   stbi__png p;\n   p.s = s;\n   return stbi__do_png(&p, x,y,comp,req_comp, ri);\n}\n\nstatic int stbi__png_test(stbi__context *s)\n{\n   int r;\n   r = stbi__check_png_header(s);\n   stbi__rewind(s);\n   return r;\n}\n\nstatic int stbi__png_info_raw(stbi__png *p, int *x, int *y, int *comp)\n{\n   if (!stbi__parse_png_file(p, STBI__SCAN_header, 0)) {\n      stbi__rewind( p->s );\n      return 0;\n   }\n   if (x) *x = p->s->img_x;\n   if (y) *y = p->s->img_y;\n   if (comp) *comp = p->s->img_n;\n   return 1;\n}\n\nstatic int stbi__png_info(stbi__context *s, int *x, int *y, int *comp)\n{\n   stbi__png p;\n   p.s = s;\n   return stbi__png_info_raw(&p, x, y, comp);\n}\n\nstatic int stbi__png_is16(stbi__context *s)\n{\n   stbi__png p;\n   p.s = s;\n   if (!stbi__png_info_raw(&p, NULL, NULL, NULL))\n\t   return 0;\n   if (p.depth != 16) {\n      stbi__rewind(p.s);\n      return 0;\n   }\n   return 1;\n}\n#endif\n\n// Microsoft/Windows BMP image\n\n#ifndef STBI_NO_BMP\nstatic int stbi__bmp_test_raw(stbi__context *s)\n{\n   int r;\n   int sz;\n   if (stbi__get8(s) != 'B') return 0;\n   if (stbi__get8(s) != 'M') return 0;\n   stbi__get32le(s); // discard filesize\n   stbi__get16le(s); // discard reserved\n   stbi__get16le(s); // discard reserved\n   stbi__get32le(s); // discard data offset\n   sz = stbi__get32le(s);\n   r = (sz == 12 || sz == 40 || sz == 56 || sz == 108 || sz == 124);\n   return r;\n}\n\nstatic int stbi__bmp_test(stbi__context *s)\n{\n   int r = stbi__bmp_test_raw(s);\n   stbi__rewind(s);\n   return r;\n}\n\n\n// returns 0..31 for the highest set bit\nstatic int stbi__high_bit(unsigned int z)\n{\n   int n=0;\n   if (z == 0) return -1;\n   if (z >= 0x10000) { n += 16; z >>= 16; }\n   if (z >= 0x00100) { n +=  8; z >>=  8; }\n   if (z >= 0x00010) { n +=  4; z >>=  4; }\n   if (z >= 0x00004) { n +=  2; z >>=  2; }\n   if (z >= 0x00002) { n +=  1;/* >>=  1;*/ }\n   return n;\n}\n\nstatic int stbi__bitcount(unsigned int a)\n{\n   a = (a & 0x55555555) + ((a >>  1) & 0x55555555); // max 2\n   a = (a & 0x33333333) + ((a >>  2) & 0x33333333); // max 4\n   a = (a + (a >> 4)) & 0x0f0f0f0f; // max 8 per 4, now 8 bits\n   a = (a + (a >> 8)); // max 16 per 8 bits\n   a = (a + (a >> 16)); // max 32 per 8 bits\n   return a & 0xff;\n}\n\n// extract an arbitrarily-aligned N-bit value (N=bits)\n// from v, and then make it 8-bits long and fractionally\n// extend it to full full range.\nstatic int stbi__shiftsigned(unsigned int v, int shift, int bits)\n{\n   static unsigned int mul_table[9] = {\n      0,\n      0xff/*0b11111111*/, 0x55/*0b01010101*/, 0x49/*0b01001001*/, 0x11/*0b00010001*/,\n      0x21/*0b00100001*/, 0x41/*0b01000001*/, 0x81/*0b10000001*/, 0x01/*0b00000001*/,\n   };\n   static unsigned int shift_table[9] = {\n      0, 0,0,1,0,2,4,6,0,\n   };\n   if (shift < 0)\n      v <<= -shift;\n   else\n      v >>= shift;\n   STBI_ASSERT(v < 256);\n   v >>= (8-bits);\n   STBI_ASSERT(bits >= 0 && bits <= 8);\n   return (int) ((unsigned) v * mul_table[bits]) >> shift_table[bits];\n}\n\ntypedef struct\n{\n   int bpp, offset, hsz;\n   unsigned int mr,mg,mb,ma, all_a;\n   int extra_read;\n} stbi__bmp_data;\n\nstatic int stbi__bmp_set_mask_defaults(stbi__bmp_data *info, int compress)\n{\n   // BI_BITFIELDS specifies masks explicitly, don't override\n   if (compress == 3)\n      return 1;\n\n   if (compress == 0) {\n      if (info->bpp == 16) {\n         info->mr = 31u << 10;\n         info->mg = 31u <<  5;\n         info->mb = 31u <<  0;\n      } else if (info->bpp == 32) {\n         info->mr = 0xffu << 16;\n         info->mg = 0xffu <<  8;\n         info->mb = 0xffu <<  0;\n         info->ma = 0xffu << 24;\n         info->all_a = 0; // if all_a is 0 at end, then we loaded alpha channel but it was all 0\n      } else {\n         // otherwise, use defaults, which is all-0\n         info->mr = info->mg = info->mb = info->ma = 0;\n      }\n      return 1;\n   }\n   return 0; // error\n}\n\nstatic void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info)\n{\n   int hsz;\n   if (stbi__get8(s) != 'B' || stbi__get8(s) != 'M') return stbi__errpuc(\"not BMP\", \"Corrupt BMP\");\n   stbi__get32le(s); // discard filesize\n   stbi__get16le(s); // discard reserved\n   stbi__get16le(s); // discard reserved\n   info->offset = stbi__get32le(s);\n   info->hsz = hsz = stbi__get32le(s);\n   info->mr = info->mg = info->mb = info->ma = 0;\n   info->extra_read = 14;\n\n   if (info->offset < 0) return stbi__errpuc(\"bad BMP\", \"bad BMP\");\n\n   if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) return stbi__errpuc(\"unknown BMP\", \"BMP type not supported: unknown\");\n   if (hsz == 12) {\n      s->img_x = stbi__get16le(s);\n      s->img_y = stbi__get16le(s);\n   } else {\n      s->img_x = stbi__get32le(s);\n      s->img_y = stbi__get32le(s);\n   }\n   if (stbi__get16le(s) != 1) return stbi__errpuc(\"bad BMP\", \"bad BMP\");\n   info->bpp = stbi__get16le(s);\n   if (hsz != 12) {\n      int compress = stbi__get32le(s);\n      if (compress == 1 || compress == 2) return stbi__errpuc(\"BMP RLE\", \"BMP type not supported: RLE\");\n      if (compress >= 4) return stbi__errpuc(\"BMP JPEG/PNG\", \"BMP type not supported: unsupported compression\"); // this includes PNG/JPEG modes\n      if (compress == 3 && info->bpp != 16 && info->bpp != 32) return stbi__errpuc(\"bad BMP\", \"bad BMP\"); // bitfields requires 16 or 32 bits/pixel\n      stbi__get32le(s); // discard sizeof\n      stbi__get32le(s); // discard hres\n      stbi__get32le(s); // discard vres\n      stbi__get32le(s); // discard colorsused\n      stbi__get32le(s); // discard max important\n      if (hsz == 40 || hsz == 56) {\n         if (hsz == 56) {\n            stbi__get32le(s);\n            stbi__get32le(s);\n            stbi__get32le(s);\n            stbi__get32le(s);\n         }\n         if (info->bpp == 16 || info->bpp == 32) {\n            if (compress == 0) {\n               stbi__bmp_set_mask_defaults(info, compress);\n            } else if (compress == 3) {\n               info->mr = stbi__get32le(s);\n               info->mg = stbi__get32le(s);\n               info->mb = stbi__get32le(s);\n               info->extra_read += 12;\n               // not documented, but generated by photoshop and handled by mspaint\n               if (info->mr == info->mg && info->mg == info->mb) {\n                  // ?!?!?\n                  return stbi__errpuc(\"bad BMP\", \"bad BMP\");\n               }\n            } else\n               return stbi__errpuc(\"bad BMP\", \"bad BMP\");\n         }\n      } else {\n         // V4/V5 header\n         int i;\n         if (hsz != 108 && hsz != 124)\n            return stbi__errpuc(\"bad BMP\", \"bad BMP\");\n         info->mr = stbi__get32le(s);\n         info->mg = stbi__get32le(s);\n         info->mb = stbi__get32le(s);\n         info->ma = stbi__get32le(s);\n         if (compress != 3) // override mr/mg/mb unless in BI_BITFIELDS mode, as per docs\n            stbi__bmp_set_mask_defaults(info, compress);\n         stbi__get32le(s); // discard color space\n         for (i=0; i < 12; ++i)\n            stbi__get32le(s); // discard color space parameters\n         if (hsz == 124) {\n            stbi__get32le(s); // discard rendering intent\n            stbi__get32le(s); // discard offset of profile data\n            stbi__get32le(s); // discard size of profile data\n            stbi__get32le(s); // discard reserved\n         }\n      }\n   }\n   return (void *) 1;\n}\n\n\nstatic void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)\n{\n   stbi_uc *out;\n   unsigned int mr=0,mg=0,mb=0,ma=0, all_a;\n   stbi_uc pal[256][4];\n   int psize=0,i,j,width;\n   int flip_vertically, pad, target;\n   stbi__bmp_data info;\n   STBI_NOTUSED(ri);\n\n   info.all_a = 255;\n   if (stbi__bmp_parse_header(s, &info) == NULL)\n      return NULL; // error code already set\n\n   flip_vertically = ((int) s->img_y) > 0;\n   s->img_y = abs((int) s->img_y);\n\n   if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc(\"too large\",\"Very large image (corrupt?)\");\n   if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc(\"too large\",\"Very large image (corrupt?)\");\n\n   mr = info.mr;\n   mg = info.mg;\n   mb = info.mb;\n   ma = info.ma;\n   all_a = info.all_a;\n\n   if (info.hsz == 12) {\n      if (info.bpp < 24)\n         psize = (info.offset - info.extra_read - 24) / 3;\n   } else {\n      if (info.bpp < 16)\n         psize = (info.offset - info.extra_read - info.hsz) >> 2;\n   }\n   if (psize == 0) {\n      // accept some number of extra bytes after the header, but if the offset points either to before\n      // the header ends or implies a large amount of extra data, reject the file as malformed\n      int bytes_read_so_far = s->callback_already_read + (int)(s->img_buffer - s->img_buffer_original);\n      int header_limit = 1024; // max we actually read is below 256 bytes currently.\n      int extra_data_limit = 256*4; // what ordinarily goes here is a palette; 256 entries*4 bytes is its max size.\n      if (bytes_read_so_far <= 0 || bytes_read_so_far > header_limit) {\n         return stbi__errpuc(\"bad header\", \"Corrupt BMP\");\n      }\n      // we established that bytes_read_so_far is positive and sensible.\n      // the first half of this test rejects offsets that are either too small positives, or\n      // negative, and guarantees that info.offset >= bytes_read_so_far > 0. this in turn\n      // ensures the number computed in the second half of the test can't overflow.\n      if (info.offset < bytes_read_so_far || info.offset - bytes_read_so_far > extra_data_limit) {\n         return stbi__errpuc(\"bad offset\", \"Corrupt BMP\");\n      } else {\n         stbi__skip(s, info.offset - bytes_read_so_far);\n      }\n   }\n\n   if (info.bpp == 24 && ma == 0xff000000)\n      s->img_n = 3;\n   else\n      s->img_n = ma ? 4 : 3;\n   if (req_comp && req_comp >= 3) // we can directly decode 3 or 4\n      target = req_comp;\n   else\n      target = s->img_n; // if they want monochrome, we'll post-convert\n\n   // sanity-check size\n   if (!stbi__mad3sizes_valid(target, s->img_x, s->img_y, 0))\n      return stbi__errpuc(\"too large\", \"Corrupt BMP\");\n\n   out = (stbi_uc *) stbi__malloc_mad3(target, s->img_x, s->img_y, 0);\n   if (!out) return stbi__errpuc(\"outofmem\", \"Out of memory\");\n   if (info.bpp < 16) {\n      int z=0;\n      if (psize == 0 || psize > 256) { STBI_FREE(out); return stbi__errpuc(\"invalid\", \"Corrupt BMP\"); }\n      for (i=0; i < psize; ++i) {\n         pal[i][2] = stbi__get8(s);\n         pal[i][1] = stbi__get8(s);\n         pal[i][0] = stbi__get8(s);\n         if (info.hsz != 12) stbi__get8(s);\n         pal[i][3] = 255;\n      }\n      stbi__skip(s, info.offset - info.extra_read - info.hsz - psize * (info.hsz == 12 ? 3 : 4));\n      if (info.bpp == 1) width = (s->img_x + 7) >> 3;\n      else if (info.bpp == 4) width = (s->img_x + 1) >> 1;\n      else if (info.bpp == 8) width = s->img_x;\n      else { STBI_FREE(out); return stbi__errpuc(\"bad bpp\", \"Corrupt BMP\"); }\n      pad = (-width)&3;\n      if (info.bpp == 1) {\n         for (j=0; j < (int) s->img_y; ++j) {\n            int bit_offset = 7, v = stbi__get8(s);\n            for (i=0; i < (int) s->img_x; ++i) {\n               int color = (v>>bit_offset)&0x1;\n               out[z++] = pal[color][0];\n               out[z++] = pal[color][1];\n               out[z++] = pal[color][2];\n               if (target == 4) out[z++] = 255;\n               if (i+1 == (int) s->img_x) break;\n               if((--bit_offset) < 0) {\n                  bit_offset = 7;\n                  v = stbi__get8(s);\n               }\n            }\n            stbi__skip(s, pad);\n         }\n      } else {\n         for (j=0; j < (int) s->img_y; ++j) {\n            for (i=0; i < (int) s->img_x; i += 2) {\n               int v=stbi__get8(s),v2=0;\n               if (info.bpp == 4) {\n                  v2 = v & 15;\n                  v >>= 4;\n               }\n               out[z++] = pal[v][0];\n               out[z++] = pal[v][1];\n               out[z++] = pal[v][2];\n               if (target == 4) out[z++] = 255;\n               if (i+1 == (int) s->img_x) break;\n               v = (info.bpp == 8) ? stbi__get8(s) : v2;\n               out[z++] = pal[v][0];\n               out[z++] = pal[v][1];\n               out[z++] = pal[v][2];\n               if (target == 4) out[z++] = 255;\n            }\n            stbi__skip(s, pad);\n         }\n      }\n   } else {\n      int rshift=0,gshift=0,bshift=0,ashift=0,rcount=0,gcount=0,bcount=0,acount=0;\n      int z = 0;\n      int easy=0;\n      stbi__skip(s, info.offset - info.extra_read - info.hsz);\n      if (info.bpp == 24) width = 3 * s->img_x;\n      else if (info.bpp == 16) width = 2*s->img_x;\n      else /* bpp = 32 and pad = 0 */ width=0;\n      pad = (-width) & 3;\n      if (info.bpp == 24) {\n         easy = 1;\n      } else if (info.bpp == 32) {\n         if (mb == 0xff && mg == 0xff00 && mr == 0x00ff0000 && ma == 0xff000000)\n            easy = 2;\n      }\n      if (!easy) {\n         if (!mr || !mg || !mb) { STBI_FREE(out); return stbi__errpuc(\"bad masks\", \"Corrupt BMP\"); }\n         // right shift amt to put high bit in position #7\n         rshift = stbi__high_bit(mr)-7; rcount = stbi__bitcount(mr);\n         gshift = stbi__high_bit(mg)-7; gcount = stbi__bitcount(mg);\n         bshift = stbi__high_bit(mb)-7; bcount = stbi__bitcount(mb);\n         ashift = stbi__high_bit(ma)-7; acount = stbi__bitcount(ma);\n         if (rcount > 8 || gcount > 8 || bcount > 8 || acount > 8) { STBI_FREE(out); return stbi__errpuc(\"bad masks\", \"Corrupt BMP\"); }\n      }\n      for (j=0; j < (int) s->img_y; ++j) {\n         if (easy) {\n            for (i=0; i < (int) s->img_x; ++i) {\n               unsigned char a;\n               out[z+2] = stbi__get8(s);\n               out[z+1] = stbi__get8(s);\n               out[z+0] = stbi__get8(s);\n               z += 3;\n               a = (easy == 2 ? stbi__get8(s) : 255);\n               all_a |= a;\n               if (target == 4) out[z++] = a;\n            }\n         } else {\n            int bpp = info.bpp;\n            for (i=0; i < (int) s->img_x; ++i) {\n               stbi__uint32 v = (bpp == 16 ? (stbi__uint32) stbi__get16le(s) : stbi__get32le(s));\n               unsigned int a;\n               out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mr, rshift, rcount));\n               out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mg, gshift, gcount));\n               out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mb, bshift, bcount));\n               a = (ma ? stbi__shiftsigned(v & ma, ashift, acount) : 255);\n               all_a |= a;\n               if (target == 4) out[z++] = STBI__BYTECAST(a);\n            }\n         }\n         stbi__skip(s, pad);\n      }\n   }\n\n   // if alpha channel is all 0s, replace with all 255s\n   if (target == 4 && all_a == 0)\n      for (i=4*s->img_x*s->img_y-1; i >= 0; i -= 4)\n         out[i] = 255;\n\n   if (flip_vertically) {\n      stbi_uc t;\n      for (j=0; j < (int) s->img_y>>1; ++j) {\n         stbi_uc *p1 = out +      j     *s->img_x*target;\n         stbi_uc *p2 = out + (s->img_y-1-j)*s->img_x*target;\n         for (i=0; i < (int) s->img_x*target; ++i) {\n            t = p1[i]; p1[i] = p2[i]; p2[i] = t;\n         }\n      }\n   }\n\n   if (req_comp && req_comp != target) {\n      out = stbi__convert_format(out, target, req_comp, s->img_x, s->img_y);\n      if (out == NULL) return out; // stbi__convert_format frees input on failure\n   }\n\n   *x = s->img_x;\n   *y = s->img_y;\n   if (comp) *comp = s->img_n;\n   return out;\n}\n#endif\n\n// Targa Truevision - TGA\n// by Jonathan Dummer\n#ifndef STBI_NO_TGA\n// returns STBI_rgb or whatever, 0 on error\nstatic int stbi__tga_get_comp(int bits_per_pixel, int is_grey, int* is_rgb16)\n{\n   // only RGB or RGBA (incl. 16bit) or grey allowed\n   if (is_rgb16) *is_rgb16 = 0;\n   switch(bits_per_pixel) {\n      case 8:  return STBI_grey;\n      case 16: if(is_grey) return STBI_grey_alpha;\n               // fallthrough\n      case 15: if(is_rgb16) *is_rgb16 = 1;\n               return STBI_rgb;\n      case 24: // fallthrough\n      case 32: return bits_per_pixel/8;\n      default: return 0;\n   }\n}\n\nstatic int stbi__tga_info(stbi__context *s, int *x, int *y, int *comp)\n{\n    int tga_w, tga_h, tga_comp, tga_image_type, tga_bits_per_pixel, tga_colormap_bpp;\n    int sz, tga_colormap_type;\n    stbi__get8(s);                   // discard Offset\n    tga_colormap_type = stbi__get8(s); // colormap type\n    if( tga_colormap_type > 1 ) {\n        stbi__rewind(s);\n        return 0;      // only RGB or indexed allowed\n    }\n    tga_image_type = stbi__get8(s); // image type\n    if ( tga_colormap_type == 1 ) { // colormapped (paletted) image\n        if (tga_image_type != 1 && tga_image_type != 9) {\n            stbi__rewind(s);\n            return 0;\n        }\n        stbi__skip(s,4);       // skip index of first colormap entry and number of entries\n        sz = stbi__get8(s);    //   check bits per palette color entry\n        if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) {\n            stbi__rewind(s);\n            return 0;\n        }\n        stbi__skip(s,4);       // skip image x and y origin\n        tga_colormap_bpp = sz;\n    } else { // \"normal\" image w/o colormap - only RGB or grey allowed, +/- RLE\n        if ( (tga_image_type != 2) && (tga_image_type != 3) && (tga_image_type != 10) && (tga_image_type != 11) ) {\n            stbi__rewind(s);\n            return 0; // only RGB or grey allowed, +/- RLE\n        }\n        stbi__skip(s,9); // skip colormap specification and image x/y origin\n        tga_colormap_bpp = 0;\n    }\n    tga_w = stbi__get16le(s);\n    if( tga_w < 1 ) {\n        stbi__rewind(s);\n        return 0;   // test width\n    }\n    tga_h = stbi__get16le(s);\n    if( tga_h < 1 ) {\n        stbi__rewind(s);\n        return 0;   // test height\n    }\n    tga_bits_per_pixel = stbi__get8(s); // bits per pixel\n    stbi__get8(s); // ignore alpha bits\n    if (tga_colormap_bpp != 0) {\n        if((tga_bits_per_pixel != 8) && (tga_bits_per_pixel != 16)) {\n            // when using a colormap, tga_bits_per_pixel is the size of the indexes\n            // I don't think anything but 8 or 16bit indexes makes sense\n            stbi__rewind(s);\n            return 0;\n        }\n        tga_comp = stbi__tga_get_comp(tga_colormap_bpp, 0, NULL);\n    } else {\n        tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3) || (tga_image_type == 11), NULL);\n    }\n    if(!tga_comp) {\n      stbi__rewind(s);\n      return 0;\n    }\n    if (x) *x = tga_w;\n    if (y) *y = tga_h;\n    if (comp) *comp = tga_comp;\n    return 1;                   // seems to have passed everything\n}\n\nstatic int stbi__tga_test(stbi__context *s)\n{\n   int res = 0;\n   int sz, tga_color_type;\n   stbi__get8(s);      //   discard Offset\n   tga_color_type = stbi__get8(s);   //   color type\n   if ( tga_color_type > 1 ) goto errorEnd;   //   only RGB or indexed allowed\n   sz = stbi__get8(s);   //   image type\n   if ( tga_color_type == 1 ) { // colormapped (paletted) image\n      if (sz != 1 && sz != 9) goto errorEnd; // colortype 1 demands image type 1 or 9\n      stbi__skip(s,4);       // skip index of first colormap entry and number of entries\n      sz = stbi__get8(s);    //   check bits per palette color entry\n      if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) goto errorEnd;\n      stbi__skip(s,4);       // skip image x and y origin\n   } else { // \"normal\" image w/o colormap\n      if ( (sz != 2) && (sz != 3) && (sz != 10) && (sz != 11) ) goto errorEnd; // only RGB or grey allowed, +/- RLE\n      stbi__skip(s,9); // skip colormap specification and image x/y origin\n   }\n   if ( stbi__get16le(s) < 1 ) goto errorEnd;      //   test width\n   if ( stbi__get16le(s) < 1 ) goto errorEnd;      //   test height\n   sz = stbi__get8(s);   //   bits per pixel\n   if ( (tga_color_type == 1) && (sz != 8) && (sz != 16) ) goto errorEnd; // for colormapped images, bpp is size of an index\n   if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) goto errorEnd;\n\n   res = 1; // if we got this far, everything's good and we can return 1 instead of 0\n\nerrorEnd:\n   stbi__rewind(s);\n   return res;\n}\n\n// read 16bit value and convert to 24bit RGB\nstatic void stbi__tga_read_rgb16(stbi__context *s, stbi_uc* out)\n{\n   stbi__uint16 px = (stbi__uint16)stbi__get16le(s);\n   stbi__uint16 fiveBitMask = 31;\n   // we have 3 channels with 5bits each\n   int r = (px >> 10) & fiveBitMask;\n   int g = (px >> 5) & fiveBitMask;\n   int b = px & fiveBitMask;\n   // Note that this saves the data in RGB(A) order, so it doesn't need to be swapped later\n   out[0] = (stbi_uc)((r * 255)/31);\n   out[1] = (stbi_uc)((g * 255)/31);\n   out[2] = (stbi_uc)((b * 255)/31);\n\n   // some people claim that the most significant bit might be used for alpha\n   // (possibly if an alpha-bit is set in the \"image descriptor byte\")\n   // but that only made 16bit test images completely translucent..\n   // so let's treat all 15 and 16bit TGAs as RGB with no alpha.\n}\n\nstatic void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)\n{\n   //   read in the TGA header stuff\n   int tga_offset = stbi__get8(s);\n   int tga_indexed = stbi__get8(s);\n   int tga_image_type = stbi__get8(s);\n   int tga_is_RLE = 0;\n   int tga_palette_start = stbi__get16le(s);\n   int tga_palette_len = stbi__get16le(s);\n   int tga_palette_bits = stbi__get8(s);\n   int tga_x_origin = stbi__get16le(s);\n   int tga_y_origin = stbi__get16le(s);\n   int tga_width = stbi__get16le(s);\n   int tga_height = stbi__get16le(s);\n   int tga_bits_per_pixel = stbi__get8(s);\n   int tga_comp, tga_rgb16=0;\n   int tga_inverted = stbi__get8(s);\n   // int tga_alpha_bits = tga_inverted & 15; // the 4 lowest bits - unused (useless?)\n   //   image data\n   unsigned char *tga_data;\n   unsigned char *tga_palette = NULL;\n   int i, j;\n   unsigned char raw_data[4] = {0};\n   int RLE_count = 0;\n   int RLE_repeating = 0;\n   int read_next_pixel = 1;\n   STBI_NOTUSED(ri);\n   STBI_NOTUSED(tga_x_origin); // @TODO\n   STBI_NOTUSED(tga_y_origin); // @TODO\n\n   if (tga_height > STBI_MAX_DIMENSIONS) return stbi__errpuc(\"too large\",\"Very large image (corrupt?)\");\n   if (tga_width > STBI_MAX_DIMENSIONS) return stbi__errpuc(\"too large\",\"Very large image (corrupt?)\");\n\n   //   do a tiny bit of precessing\n   if ( tga_image_type >= 8 )\n   {\n      tga_image_type -= 8;\n      tga_is_RLE = 1;\n   }\n   tga_inverted = 1 - ((tga_inverted >> 5) & 1);\n\n   //   If I'm paletted, then I'll use the number of bits from the palette\n   if ( tga_indexed ) tga_comp = stbi__tga_get_comp(tga_palette_bits, 0, &tga_rgb16);\n   else tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3), &tga_rgb16);\n\n   if(!tga_comp) // shouldn't really happen, stbi__tga_test() should have ensured basic consistency\n      return stbi__errpuc(\"bad format\", \"Can't find out TGA pixelformat\");\n\n   //   tga info\n   *x = tga_width;\n   *y = tga_height;\n   if (comp) *comp = tga_comp;\n\n   if (!stbi__mad3sizes_valid(tga_width, tga_height, tga_comp, 0))\n      return stbi__errpuc(\"too large\", \"Corrupt TGA\");\n\n   tga_data = (unsigned char*)stbi__malloc_mad3(tga_width, tga_height, tga_comp, 0);\n   if (!tga_data) return stbi__errpuc(\"outofmem\", \"Out of memory\");\n\n   // skip to the data's starting position (offset usually = 0)\n   stbi__skip(s, tga_offset );\n\n   if ( !tga_indexed && !tga_is_RLE && !tga_rgb16 ) {\n      for (i=0; i < tga_height; ++i) {\n         int row = tga_inverted ? tga_height -i - 1 : i;\n         stbi_uc *tga_row = tga_data + row*tga_width*tga_comp;\n         stbi__getn(s, tga_row, tga_width * tga_comp);\n      }\n   } else  {\n      //   do I need to load a palette?\n      if ( tga_indexed)\n      {\n         if (tga_palette_len == 0) {  /* you have to have at least one entry! */\n            STBI_FREE(tga_data);\n            return stbi__errpuc(\"bad palette\", \"Corrupt TGA\");\n         }\n\n         //   any data to skip? (offset usually = 0)\n         stbi__skip(s, tga_palette_start );\n         //   load the palette\n         tga_palette = (unsigned char*)stbi__malloc_mad2(tga_palette_len, tga_comp, 0);\n         if (!tga_palette) {\n            STBI_FREE(tga_data);\n            return stbi__errpuc(\"outofmem\", \"Out of memory\");\n         }\n         if (tga_rgb16) {\n            stbi_uc *pal_entry = tga_palette;\n            STBI_ASSERT(tga_comp == STBI_rgb);\n            for (i=0; i < tga_palette_len; ++i) {\n               stbi__tga_read_rgb16(s, pal_entry);\n               pal_entry += tga_comp;\n            }\n         } else if (!stbi__getn(s, tga_palette, tga_palette_len * tga_comp)) {\n               STBI_FREE(tga_data);\n               STBI_FREE(tga_palette);\n               return stbi__errpuc(\"bad palette\", \"Corrupt TGA\");\n         }\n      }\n      //   load the data\n      for (i=0; i < tga_width * tga_height; ++i)\n      {\n         //   if I'm in RLE mode, do I need to get a RLE stbi__pngchunk?\n         if ( tga_is_RLE )\n         {\n            if ( RLE_count == 0 )\n            {\n               //   yep, get the next byte as a RLE command\n               int RLE_cmd = stbi__get8(s);\n               RLE_count = 1 + (RLE_cmd & 127);\n               RLE_repeating = RLE_cmd >> 7;\n               read_next_pixel = 1;\n            } else if ( !RLE_repeating )\n            {\n               read_next_pixel = 1;\n            }\n         } else\n         {\n            read_next_pixel = 1;\n         }\n         //   OK, if I need to read a pixel, do it now\n         if ( read_next_pixel )\n         {\n            //   load however much data we did have\n            if ( tga_indexed )\n            {\n               // read in index, then perform the lookup\n               int pal_idx = (tga_bits_per_pixel == 8) ? stbi__get8(s) : stbi__get16le(s);\n               if ( pal_idx >= tga_palette_len ) {\n                  // invalid index\n                  pal_idx = 0;\n               }\n               pal_idx *= tga_comp;\n               for (j = 0; j < tga_comp; ++j) {\n                  raw_data[j] = tga_palette[pal_idx+j];\n               }\n            } else if(tga_rgb16) {\n               STBI_ASSERT(tga_comp == STBI_rgb);\n               stbi__tga_read_rgb16(s, raw_data);\n            } else {\n               //   read in the data raw\n               for (j = 0; j < tga_comp; ++j) {\n                  raw_data[j] = stbi__get8(s);\n               }\n            }\n            //   clear the reading flag for the next pixel\n            read_next_pixel = 0;\n         } // end of reading a pixel\n\n         // copy data\n         for (j = 0; j < tga_comp; ++j)\n           tga_data[i*tga_comp+j] = raw_data[j];\n\n         //   in case we're in RLE mode, keep counting down\n         --RLE_count;\n      }\n      //   do I need to invert the image?\n      if ( tga_inverted )\n      {\n         for (j = 0; j*2 < tga_height; ++j)\n         {\n            int index1 = j * tga_width * tga_comp;\n            int index2 = (tga_height - 1 - j) * tga_width * tga_comp;\n            for (i = tga_width * tga_comp; i > 0; --i)\n            {\n               unsigned char temp = tga_data[index1];\n               tga_data[index1] = tga_data[index2];\n               tga_data[index2] = temp;\n               ++index1;\n               ++index2;\n            }\n         }\n      }\n      //   clear my palette, if I had one\n      if ( tga_palette != NULL )\n      {\n         STBI_FREE( tga_palette );\n      }\n   }\n\n   // swap RGB - if the source data was RGB16, it already is in the right order\n   if (tga_comp >= 3 && !tga_rgb16)\n   {\n      unsigned char* tga_pixel = tga_data;\n      for (i=0; i < tga_width * tga_height; ++i)\n      {\n         unsigned char temp = tga_pixel[0];\n         tga_pixel[0] = tga_pixel[2];\n         tga_pixel[2] = temp;\n         tga_pixel += tga_comp;\n      }\n   }\n\n   // convert to target component count\n   if (req_comp && req_comp != tga_comp)\n      tga_data = stbi__convert_format(tga_data, tga_comp, req_comp, tga_width, tga_height);\n\n   //   the things I do to get rid of an error message, and yet keep\n   //   Microsoft's C compilers happy... [8^(\n   tga_palette_start = tga_palette_len = tga_palette_bits =\n         tga_x_origin = tga_y_origin = 0;\n   STBI_NOTUSED(tga_palette_start);\n   //   OK, done\n   return tga_data;\n}\n#endif\n\n// *************************************************************************************************\n// Photoshop PSD loader -- PD by Thatcher Ulrich, integration by Nicolas Schulz, tweaked by STB\n\n#ifndef STBI_NO_PSD\nstatic int stbi__psd_test(stbi__context *s)\n{\n   int r = (stbi__get32be(s) == 0x38425053);\n   stbi__rewind(s);\n   return r;\n}\n\nstatic int stbi__psd_decode_rle(stbi__context *s, stbi_uc *p, int pixelCount)\n{\n   int count, nleft, len;\n\n   count = 0;\n   while ((nleft = pixelCount - count) > 0) {\n      len = stbi__get8(s);\n      if (len == 128) {\n         // No-op.\n      } else if (len < 128) {\n         // Copy next len+1 bytes literally.\n         len++;\n         if (len > nleft) return 0; // corrupt data\n         count += len;\n         while (len) {\n            *p = stbi__get8(s);\n            p += 4;\n            len--;\n         }\n      } else if (len > 128) {\n         stbi_uc   val;\n         // Next -len+1 bytes in the dest are replicated from next source byte.\n         // (Interpret len as a negative 8-bit int.)\n         len = 257 - len;\n         if (len > nleft) return 0; // corrupt data\n         val = stbi__get8(s);\n         count += len;\n         while (len) {\n            *p = val;\n            p += 4;\n            len--;\n         }\n      }\n   }\n\n   return 1;\n}\n\nstatic void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc)\n{\n   int pixelCount;\n   int channelCount, compression;\n   int channel, i;\n   int bitdepth;\n   int w,h;\n   stbi_uc *out;\n   STBI_NOTUSED(ri);\n\n   // Check identifier\n   if (stbi__get32be(s) != 0x38425053)   // \"8BPS\"\n      return stbi__errpuc(\"not PSD\", \"Corrupt PSD image\");\n\n   // Check file type version.\n   if (stbi__get16be(s) != 1)\n      return stbi__errpuc(\"wrong version\", \"Unsupported version of PSD image\");\n\n   // Skip 6 reserved bytes.\n   stbi__skip(s, 6 );\n\n   // Read the number of channels (R, G, B, A, etc).\n   channelCount = stbi__get16be(s);\n   if (channelCount < 0 || channelCount > 16)\n      return stbi__errpuc(\"wrong channel count\", \"Unsupported number of channels in PSD image\");\n\n   // Read the rows and columns of the image.\n   h = stbi__get32be(s);\n   w = stbi__get32be(s);\n\n   if (h > STBI_MAX_DIMENSIONS) return stbi__errpuc(\"too large\",\"Very large image (corrupt?)\");\n   if (w > STBI_MAX_DIMENSIONS) return stbi__errpuc(\"too large\",\"Very large image (corrupt?)\");\n\n   // Make sure the depth is 8 bits.\n   bitdepth = stbi__get16be(s);\n   if (bitdepth != 8 && bitdepth != 16)\n      return stbi__errpuc(\"unsupported bit depth\", \"PSD bit depth is not 8 or 16 bit\");\n\n   // Make sure the color mode is RGB.\n   // Valid options are:\n   //   0: Bitmap\n   //   1: Grayscale\n   //   2: Indexed color\n   //   3: RGB color\n   //   4: CMYK color\n   //   7: Multichannel\n   //   8: Duotone\n   //   9: Lab color\n   if (stbi__get16be(s) != 3)\n      return stbi__errpuc(\"wrong color format\", \"PSD is not in RGB color format\");\n\n   // Skip the Mode Data.  (It's the palette for indexed color; other info for other modes.)\n   stbi__skip(s,stbi__get32be(s) );\n\n   // Skip the image resources.  (resolution, pen tool paths, etc)\n   stbi__skip(s, stbi__get32be(s) );\n\n   // Skip the reserved data.\n   stbi__skip(s, stbi__get32be(s) );\n\n   // Find out if the data is compressed.\n   // Known values:\n   //   0: no compression\n   //   1: RLE compressed\n   compression = stbi__get16be(s);\n   if (compression > 1)\n      return stbi__errpuc(\"bad compression\", \"PSD has an unknown compression format\");\n\n   // Check size\n   if (!stbi__mad3sizes_valid(4, w, h, 0))\n      return stbi__errpuc(\"too large\", \"Corrupt PSD\");\n\n   // Create the destination image.\n\n   if (!compression && bitdepth == 16 && bpc == 16) {\n      out = (stbi_uc *) stbi__malloc_mad3(8, w, h, 0);\n      ri->bits_per_channel = 16;\n   } else\n      out = (stbi_uc *) stbi__malloc(4 * w*h);\n\n   if (!out) return stbi__errpuc(\"outofmem\", \"Out of memory\");\n   pixelCount = w*h;\n\n   // Initialize the data to zero.\n   //memset( out, 0, pixelCount * 4 );\n\n   // Finally, the image data.\n   if (compression) {\n      // RLE as used by .PSD and .TIFF\n      // Loop until you get the number of unpacked bytes you are expecting:\n      //     Read the next source byte into n.\n      //     If n is between 0 and 127 inclusive, copy the next n+1 bytes literally.\n      //     Else if n is between -127 and -1 inclusive, copy the next byte -n+1 times.\n      //     Else if n is 128, noop.\n      // Endloop\n\n      // The RLE-compressed data is preceded by a 2-byte data count for each row in the data,\n      // which we're going to just skip.\n      stbi__skip(s, h * channelCount * 2 );\n\n      // Read the RLE data by channel.\n      for (channel = 0; channel < 4; channel++) {\n         stbi_uc *p;\n\n         p = out+channel;\n         if (channel >= channelCount) {\n            // Fill this channel with default data.\n            for (i = 0; i < pixelCount; i++, p += 4)\n               *p = (channel == 3 ? 255 : 0);\n         } else {\n            // Read the RLE data.\n            if (!stbi__psd_decode_rle(s, p, pixelCount)) {\n               STBI_FREE(out);\n               return stbi__errpuc(\"corrupt\", \"bad RLE data\");\n            }\n         }\n      }\n\n   } else {\n      // We're at the raw image data.  It's each channel in order (Red, Green, Blue, Alpha, ...)\n      // where each channel consists of an 8-bit (or 16-bit) value for each pixel in the image.\n\n      // Read the data by channel.\n      for (channel = 0; channel < 4; channel++) {\n         if (channel >= channelCount) {\n            // Fill this channel with default data.\n            if (bitdepth == 16 && bpc == 16) {\n               stbi__uint16 *q = ((stbi__uint16 *) out) + channel;\n               stbi__uint16 val = channel == 3 ? 65535 : 0;\n               for (i = 0; i < pixelCount; i++, q += 4)\n                  *q = val;\n            } else {\n               stbi_uc *p = out+channel;\n               stbi_uc val = channel == 3 ? 255 : 0;\n               for (i = 0; i < pixelCount; i++, p += 4)\n                  *p = val;\n            }\n         } else {\n            if (ri->bits_per_channel == 16) {    // output bpc\n               stbi__uint16 *q = ((stbi__uint16 *) out) + channel;\n               for (i = 0; i < pixelCount; i++, q += 4)\n                  *q = (stbi__uint16) stbi__get16be(s);\n            } else {\n               stbi_uc *p = out+channel;\n               if (bitdepth == 16) {  // input bpc\n                  for (i = 0; i < pixelCount; i++, p += 4)\n                     *p = (stbi_uc) (stbi__get16be(s) >> 8);\n               } else {\n                  for (i = 0; i < pixelCount; i++, p += 4)\n                     *p = stbi__get8(s);\n               }\n            }\n         }\n      }\n   }\n\n   // remove weird white matte from PSD\n   if (channelCount >= 4) {\n      if (ri->bits_per_channel == 16) {\n         for (i=0; i < w*h; ++i) {\n            stbi__uint16 *pixel = (stbi__uint16 *) out + 4*i;\n            if (pixel[3] != 0 && pixel[3] != 65535) {\n               float a = pixel[3] / 65535.0f;\n               float ra = 1.0f / a;\n               float inv_a = 65535.0f * (1 - ra);\n               pixel[0] = (stbi__uint16) (pixel[0]*ra + inv_a);\n               pixel[1] = (stbi__uint16) (pixel[1]*ra + inv_a);\n               pixel[2] = (stbi__uint16) (pixel[2]*ra + inv_a);\n            }\n         }\n      } else {\n         for (i=0; i < w*h; ++i) {\n            unsigned char *pixel = out + 4*i;\n            if (pixel[3] != 0 && pixel[3] != 255) {\n               float a = pixel[3] / 255.0f;\n               float ra = 1.0f / a;\n               float inv_a = 255.0f * (1 - ra);\n               pixel[0] = (unsigned char) (pixel[0]*ra + inv_a);\n               pixel[1] = (unsigned char) (pixel[1]*ra + inv_a);\n               pixel[2] = (unsigned char) (pixel[2]*ra + inv_a);\n            }\n         }\n      }\n   }\n\n   // convert to desired output format\n   if (req_comp && req_comp != 4) {\n      if (ri->bits_per_channel == 16)\n         out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, 4, req_comp, w, h);\n      else\n         out = stbi__convert_format(out, 4, req_comp, w, h);\n      if (out == NULL) return out; // stbi__convert_format frees input on failure\n   }\n\n   if (comp) *comp = 4;\n   *y = h;\n   *x = w;\n\n   return out;\n}\n#endif\n\n// *************************************************************************************************\n// Softimage PIC loader\n// by Tom Seddon\n//\n// See http://softimage.wiki.softimage.com/index.php/INFO:_PIC_file_format\n// See http://ozviz.wasp.uwa.edu.au/~pbourke/dataformats/softimagepic/\n\n#ifndef STBI_NO_PIC\nstatic int stbi__pic_is4(stbi__context *s,const char *str)\n{\n   int i;\n   for (i=0; i<4; ++i)\n      if (stbi__get8(s) != (stbi_uc)str[i])\n         return 0;\n\n   return 1;\n}\n\nstatic int stbi__pic_test_core(stbi__context *s)\n{\n   int i;\n\n   if (!stbi__pic_is4(s,\"\\x53\\x80\\xF6\\x34\"))\n      return 0;\n\n   for(i=0;i<84;++i)\n      stbi__get8(s);\n\n   if (!stbi__pic_is4(s,\"PICT\"))\n      return 0;\n\n   return 1;\n}\n\ntypedef struct\n{\n   stbi_uc size,type,channel;\n} stbi__pic_packet;\n\nstatic stbi_uc *stbi__readval(stbi__context *s, int channel, stbi_uc *dest)\n{\n   int mask=0x80, i;\n\n   for (i=0; i<4; ++i, mask>>=1) {\n      if (channel & mask) {\n         if (stbi__at_eof(s)) return stbi__errpuc(\"bad file\",\"PIC file too short\");\n         dest[i]=stbi__get8(s);\n      }\n   }\n\n   return dest;\n}\n\nstatic void stbi__copyval(int channel,stbi_uc *dest,const stbi_uc *src)\n{\n   int mask=0x80,i;\n\n   for (i=0;i<4; ++i, mask>>=1)\n      if (channel&mask)\n         dest[i]=src[i];\n}\n\nstatic stbi_uc *stbi__pic_load_core(stbi__context *s,int width,int height,int *comp, stbi_uc *result)\n{\n   int act_comp=0,num_packets=0,y,chained;\n   stbi__pic_packet packets[10];\n\n   // this will (should...) cater for even some bizarre stuff like having data\n    // for the same channel in multiple packets.\n   do {\n      stbi__pic_packet *packet;\n\n      if (num_packets==sizeof(packets)/sizeof(packets[0]))\n         return stbi__errpuc(\"bad format\",\"too many packets\");\n\n      packet = &packets[num_packets++];\n\n      chained = stbi__get8(s);\n      packet->size    = stbi__get8(s);\n      packet->type    = stbi__get8(s);\n      packet->channel = stbi__get8(s);\n\n      act_comp |= packet->channel;\n\n      if (stbi__at_eof(s))          return stbi__errpuc(\"bad file\",\"file too short (reading packets)\");\n      if (packet->size != 8)  return stbi__errpuc(\"bad format\",\"packet isn't 8bpp\");\n   } while (chained);\n\n   *comp = (act_comp & 0x10 ? 4 : 3); // has alpha channel?\n\n   for(y=0; y<height; ++y) {\n      int packet_idx;\n\n      for(packet_idx=0; packet_idx < num_packets; ++packet_idx) {\n         stbi__pic_packet *packet = &packets[packet_idx];\n         stbi_uc *dest = result+y*width*4;\n\n         switch (packet->type) {\n            default:\n               return stbi__errpuc(\"bad format\",\"packet has bad compression type\");\n\n            case 0: {//uncompressed\n               int x;\n\n               for(x=0;x<width;++x, dest+=4)\n                  if (!stbi__readval(s,packet->channel,dest))\n                     return 0;\n               break;\n            }\n\n            case 1://Pure RLE\n               {\n                  int left=width, i;\n\n                  while (left>0) {\n                     stbi_uc count,value[4];\n\n                     count=stbi__get8(s);\n                     if (stbi__at_eof(s))   return stbi__errpuc(\"bad file\",\"file too short (pure read count)\");\n\n                     if (count > left)\n                        count = (stbi_uc) left;\n\n                     if (!stbi__readval(s,packet->channel,value))  return 0;\n\n                     for(i=0; i<count; ++i,dest+=4)\n                        stbi__copyval(packet->channel,dest,value);\n                     left -= count;\n                  }\n               }\n               break;\n\n            case 2: {//Mixed RLE\n               int left=width;\n               while (left>0) {\n                  int count = stbi__get8(s), i;\n                  if (stbi__at_eof(s))  return stbi__errpuc(\"bad file\",\"file too short (mixed read count)\");\n\n                  if (count >= 128) { // Repeated\n                     stbi_uc value[4];\n\n                     if (count==128)\n                        count = stbi__get16be(s);\n                     else\n                        count -= 127;\n                     if (count > left)\n                        return stbi__errpuc(\"bad file\",\"scanline overrun\");\n\n                     if (!stbi__readval(s,packet->channel,value))\n                        return 0;\n\n                     for(i=0;i<count;++i, dest += 4)\n                        stbi__copyval(packet->channel,dest,value);\n                  } else { // Raw\n                     ++count;\n                     if (count>left) return stbi__errpuc(\"bad file\",\"scanline overrun\");\n\n                     for(i=0;i<count;++i, dest+=4)\n                        if (!stbi__readval(s,packet->channel,dest))\n                           return 0;\n                  }\n                  left-=count;\n               }\n               break;\n            }\n         }\n      }\n   }\n\n   return result;\n}\n\nstatic void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_comp, stbi__result_info *ri)\n{\n   stbi_uc *result;\n   int i, x,y, internal_comp;\n   STBI_NOTUSED(ri);\n\n   if (!comp) comp = &internal_comp;\n\n   for (i=0; i<92; ++i)\n      stbi__get8(s);\n\n   x = stbi__get16be(s);\n   y = stbi__get16be(s);\n\n   if (y > STBI_MAX_DIMENSIONS) return stbi__errpuc(\"too large\",\"Very large image (corrupt?)\");\n   if (x > STBI_MAX_DIMENSIONS) return stbi__errpuc(\"too large\",\"Very large image (corrupt?)\");\n\n   if (stbi__at_eof(s))  return stbi__errpuc(\"bad file\",\"file too short (pic header)\");\n   if (!stbi__mad3sizes_valid(x, y, 4, 0)) return stbi__errpuc(\"too large\", \"PIC image too large to decode\");\n\n   stbi__get32be(s); //skip `ratio'\n   stbi__get16be(s); //skip `fields'\n   stbi__get16be(s); //skip `pad'\n\n   // intermediate buffer is RGBA\n   result = (stbi_uc *) stbi__malloc_mad3(x, y, 4, 0);\n   if (!result) return stbi__errpuc(\"outofmem\", \"Out of memory\");\n   memset(result, 0xff, x*y*4);\n\n   if (!stbi__pic_load_core(s,x,y,comp, result)) {\n      STBI_FREE(result);\n      result=0;\n   }\n   *px = x;\n   *py = y;\n   if (req_comp == 0) req_comp = *comp;\n   result=stbi__convert_format(result,4,req_comp,x,y);\n\n   return result;\n}\n\nstatic int stbi__pic_test(stbi__context *s)\n{\n   int r = stbi__pic_test_core(s);\n   stbi__rewind(s);\n   return r;\n}\n#endif\n\n// *************************************************************************************************\n// GIF loader -- public domain by Jean-Marc Lienher -- simplified/shrunk by stb\n\n#ifndef STBI_NO_GIF\ntypedef struct\n{\n   stbi__int16 prefix;\n   stbi_uc first;\n   stbi_uc suffix;\n} stbi__gif_lzw;\n\ntypedef struct\n{\n   int w,h;\n   stbi_uc *out;                 // output buffer (always 4 components)\n   stbi_uc *background;          // The current \"background\" as far as a gif is concerned\n   stbi_uc *history;\n   int flags, bgindex, ratio, transparent, eflags;\n   stbi_uc  pal[256][4];\n   stbi_uc lpal[256][4];\n   stbi__gif_lzw codes[8192];\n   stbi_uc *color_table;\n   int parse, step;\n   int lflags;\n   int start_x, start_y;\n   int max_x, max_y;\n   int cur_x, cur_y;\n   int line_size;\n   int delay;\n} stbi__gif;\n\nstatic int stbi__gif_test_raw(stbi__context *s)\n{\n   int sz;\n   if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') return 0;\n   sz = stbi__get8(s);\n   if (sz != '9' && sz != '7') return 0;\n   if (stbi__get8(s) != 'a') return 0;\n   return 1;\n}\n\nstatic int stbi__gif_test(stbi__context *s)\n{\n   int r = stbi__gif_test_raw(s);\n   stbi__rewind(s);\n   return r;\n}\n\nstatic void stbi__gif_parse_colortable(stbi__context *s, stbi_uc pal[256][4], int num_entries, int transp)\n{\n   int i;\n   for (i=0; i < num_entries; ++i) {\n      pal[i][2] = stbi__get8(s);\n      pal[i][1] = stbi__get8(s);\n      pal[i][0] = stbi__get8(s);\n      pal[i][3] = transp == i ? 0 : 255;\n   }\n}\n\nstatic int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_info)\n{\n   stbi_uc version;\n   if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8')\n      return stbi__err(\"not GIF\", \"Corrupt GIF\");\n\n   version = stbi__get8(s);\n   if (version != '7' && version != '9')    return stbi__err(\"not GIF\", \"Corrupt GIF\");\n   if (stbi__get8(s) != 'a')                return stbi__err(\"not GIF\", \"Corrupt GIF\");\n\n   stbi__g_failure_reason = \"\";\n   g->w = stbi__get16le(s);\n   g->h = stbi__get16le(s);\n   g->flags = stbi__get8(s);\n   g->bgindex = stbi__get8(s);\n   g->ratio = stbi__get8(s);\n   g->transparent = -1;\n\n   if (g->w > STBI_MAX_DIMENSIONS) return stbi__err(\"too large\",\"Very large image (corrupt?)\");\n   if (g->h > STBI_MAX_DIMENSIONS) return stbi__err(\"too large\",\"Very large image (corrupt?)\");\n\n   if (comp != 0) *comp = 4;  // can't actually tell whether it's 3 or 4 until we parse the comments\n\n   if (is_info) return 1;\n\n   if (g->flags & 0x80)\n      stbi__gif_parse_colortable(s,g->pal, 2 << (g->flags & 7), -1);\n\n   return 1;\n}\n\nstatic int stbi__gif_info_raw(stbi__context *s, int *x, int *y, int *comp)\n{\n   stbi__gif* g = (stbi__gif*) stbi__malloc(sizeof(stbi__gif));\n   if (!g) return stbi__err(\"outofmem\", \"Out of memory\");\n   if (!stbi__gif_header(s, g, comp, 1)) {\n      STBI_FREE(g);\n      stbi__rewind( s );\n      return 0;\n   }\n   if (x) *x = g->w;\n   if (y) *y = g->h;\n   STBI_FREE(g);\n   return 1;\n}\n\nstatic void stbi__out_gif_code(stbi__gif *g, stbi__uint16 code)\n{\n   stbi_uc *p, *c;\n   int idx;\n\n   // recurse to decode the prefixes, since the linked-list is backwards,\n   // and working backwards through an interleaved image would be nasty\n   if (g->codes[code].prefix >= 0)\n      stbi__out_gif_code(g, g->codes[code].prefix);\n\n   if (g->cur_y >= g->max_y) return;\n\n   idx = g->cur_x + g->cur_y;\n   p = &g->out[idx];\n   g->history[idx / 4] = 1;\n\n   c = &g->color_table[g->codes[code].suffix * 4];\n   if (c[3] > 128) { // don't render transparent pixels;\n      p[0] = c[2];\n      p[1] = c[1];\n      p[2] = c[0];\n      p[3] = c[3];\n   }\n   g->cur_x += 4;\n\n   if (g->cur_x >= g->max_x) {\n      g->cur_x = g->start_x;\n      g->cur_y += g->step;\n\n      while (g->cur_y >= g->max_y && g->parse > 0) {\n         g->step = (1 << g->parse) * g->line_size;\n         g->cur_y = g->start_y + (g->step >> 1);\n         --g->parse;\n      }\n   }\n}\n\nstatic stbi_uc *stbi__process_gif_raster(stbi__context *s, stbi__gif *g)\n{\n   stbi_uc lzw_cs;\n   stbi__int32 len, init_code;\n   stbi__uint32 first;\n   stbi__int32 codesize, codemask, avail, oldcode, bits, valid_bits, clear;\n   stbi__gif_lzw *p;\n\n   lzw_cs = stbi__get8(s);\n   if (lzw_cs > 12) return NULL;\n   clear = 1 << lzw_cs;\n   first = 1;\n   codesize = lzw_cs + 1;\n   codemask = (1 << codesize) - 1;\n   bits = 0;\n   valid_bits = 0;\n   for (init_code = 0; init_code < clear; init_code++) {\n      g->codes[init_code].prefix = -1;\n      g->codes[init_code].first = (stbi_uc) init_code;\n      g->codes[init_code].suffix = (stbi_uc) init_code;\n   }\n\n   // support no starting clear code\n   avail = clear+2;\n   oldcode = -1;\n\n   len = 0;\n   for(;;) {\n      if (valid_bits < codesize) {\n         if (len == 0) {\n            len = stbi__get8(s); // start new block\n            if (len == 0)\n               return g->out;\n         }\n         --len;\n         bits |= (stbi__int32) stbi__get8(s) << valid_bits;\n         valid_bits += 8;\n      } else {\n         stbi__int32 code = bits & codemask;\n         bits >>= codesize;\n         valid_bits -= codesize;\n         // @OPTIMIZE: is there some way we can accelerate the non-clear path?\n         if (code == clear) {  // clear code\n            codesize = lzw_cs + 1;\n            codemask = (1 << codesize) - 1;\n            avail = clear + 2;\n            oldcode = -1;\n            first = 0;\n         } else if (code == clear + 1) { // end of stream code\n            stbi__skip(s, len);\n            while ((len = stbi__get8(s)) > 0)\n               stbi__skip(s,len);\n            return g->out;\n         } else if (code <= avail) {\n            if (first) {\n               return stbi__errpuc(\"no clear code\", \"Corrupt GIF\");\n            }\n\n            if (oldcode >= 0) {\n               p = &g->codes[avail++];\n               if (avail > 8192) {\n                  return stbi__errpuc(\"too many codes\", \"Corrupt GIF\");\n               }\n\n               p->prefix = (stbi__int16) oldcode;\n               p->first = g->codes[oldcode].first;\n               p->suffix = (code == avail) ? p->first : g->codes[code].first;\n            } else if (code == avail)\n               return stbi__errpuc(\"illegal code in raster\", \"Corrupt GIF\");\n\n            stbi__out_gif_code(g, (stbi__uint16) code);\n\n            if ((avail & codemask) == 0 && avail <= 0x0FFF) {\n               codesize++;\n               codemask = (1 << codesize) - 1;\n            }\n\n            oldcode = code;\n         } else {\n            return stbi__errpuc(\"illegal code in raster\", \"Corrupt GIF\");\n         }\n      }\n   }\n}\n\n// this function is designed to support animated gifs, although stb_image doesn't support it\n// two back is the image from two frames ago, used for a very specific disposal format\nstatic stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, int req_comp, stbi_uc *two_back)\n{\n   int dispose;\n   int first_frame;\n   int pi;\n   int pcount;\n   STBI_NOTUSED(req_comp);\n\n   // on first frame, any non-written pixels get the background colour (non-transparent)\n   first_frame = 0;\n   if (g->out == 0) {\n      if (!stbi__gif_header(s, g, comp,0)) return 0; // stbi__g_failure_reason set by stbi__gif_header\n      if (!stbi__mad3sizes_valid(4, g->w, g->h, 0))\n         return stbi__errpuc(\"too large\", \"GIF image is too large\");\n      pcount = g->w * g->h;\n      g->out = (stbi_uc *) stbi__malloc(4 * pcount);\n      g->background = (stbi_uc *) stbi__malloc(4 * pcount);\n      g->history = (stbi_uc *) stbi__malloc(pcount);\n      if (!g->out || !g->background || !g->history)\n         return stbi__errpuc(\"outofmem\", \"Out of memory\");\n\n      // image is treated as \"transparent\" at the start - ie, nothing overwrites the current background;\n      // background colour is only used for pixels that are not rendered first frame, after that \"background\"\n      // color refers to the color that was there the previous frame.\n      memset(g->out, 0x00, 4 * pcount);\n      memset(g->background, 0x00, 4 * pcount); // state of the background (starts transparent)\n      memset(g->history, 0x00, pcount);        // pixels that were affected previous frame\n      first_frame = 1;\n   } else {\n      // second frame - how do we dispose of the previous one?\n      dispose = (g->eflags & 0x1C) >> 2;\n      pcount = g->w * g->h;\n\n      if ((dispose == 3) && (two_back == 0)) {\n         dispose = 2; // if I don't have an image to revert back to, default to the old background\n      }\n\n      if (dispose == 3) { // use previous graphic\n         for (pi = 0; pi < pcount; ++pi) {\n            if (g->history[pi]) {\n               memcpy( &g->out[pi * 4], &two_back[pi * 4], 4 );\n            }\n         }\n      } else if (dispose == 2) {\n         // restore what was changed last frame to background before that frame;\n         for (pi = 0; pi < pcount; ++pi) {\n            if (g->history[pi]) {\n               memcpy( &g->out[pi * 4], &g->background[pi * 4], 4 );\n            }\n         }\n      } else {\n         // This is a non-disposal case eithe way, so just\n         // leave the pixels as is, and they will become the new background\n         // 1: do not dispose\n         // 0:  not specified.\n      }\n\n      // background is what out is after the undoing of the previou frame;\n      memcpy( g->background, g->out, 4 * g->w * g->h );\n   }\n\n   // clear my history;\n   memset( g->history, 0x00, g->w * g->h );        // pixels that were affected previous frame\n\n   for (;;) {\n      int tag = stbi__get8(s);\n      switch (tag) {\n         case 0x2C: /* Image Descriptor */\n         {\n            stbi__int32 x, y, w, h;\n            stbi_uc *o;\n\n            x = stbi__get16le(s);\n            y = stbi__get16le(s);\n            w = stbi__get16le(s);\n            h = stbi__get16le(s);\n            if (((x + w) > (g->w)) || ((y + h) > (g->h)))\n               return stbi__errpuc(\"bad Image Descriptor\", \"Corrupt GIF\");\n\n            g->line_size = g->w * 4;\n            g->start_x = x * 4;\n            g->start_y = y * g->line_size;\n            g->max_x   = g->start_x + w * 4;\n            g->max_y   = g->start_y + h * g->line_size;\n            g->cur_x   = g->start_x;\n            g->cur_y   = g->start_y;\n\n            // if the width of the specified rectangle is 0, that means\n            // we may not see *any* pixels or the image is malformed;\n            // to make sure this is caught, move the current y down to\n            // max_y (which is what out_gif_code checks).\n            if (w == 0)\n               g->cur_y = g->max_y;\n\n            g->lflags = stbi__get8(s);\n\n            if (g->lflags & 0x40) {\n               g->step = 8 * g->line_size; // first interlaced spacing\n               g->parse = 3;\n            } else {\n               g->step = g->line_size;\n               g->parse = 0;\n            }\n\n            if (g->lflags & 0x80) {\n               stbi__gif_parse_colortable(s,g->lpal, 2 << (g->lflags & 7), g->eflags & 0x01 ? g->transparent : -1);\n               g->color_table = (stbi_uc *) g->lpal;\n            } else if (g->flags & 0x80) {\n               g->color_table = (stbi_uc *) g->pal;\n            } else\n               return stbi__errpuc(\"missing color table\", \"Corrupt GIF\");\n\n            o = stbi__process_gif_raster(s, g);\n            if (!o) return NULL;\n\n            // if this was the first frame,\n            pcount = g->w * g->h;\n            if (first_frame && (g->bgindex > 0)) {\n               // if first frame, any pixel not drawn to gets the background color\n               for (pi = 0; pi < pcount; ++pi) {\n                  if (g->history[pi] == 0) {\n                     g->pal[g->bgindex][3] = 255; // just in case it was made transparent, undo that; It will be reset next frame if need be;\n                     memcpy( &g->out[pi * 4], &g->pal[g->bgindex], 4 );\n                  }\n               }\n            }\n\n            return o;\n         }\n\n         case 0x21: // Comment Extension.\n         {\n            int len;\n            int ext = stbi__get8(s);\n            if (ext == 0xF9) { // Graphic Control Extension.\n               len = stbi__get8(s);\n               if (len == 4) {\n                  g->eflags = stbi__get8(s);\n                  g->delay = 10 * stbi__get16le(s); // delay - 1/100th of a second, saving as 1/1000ths.\n\n                  // unset old transparent\n                  if (g->transparent >= 0) {\n                     g->pal[g->transparent][3] = 255;\n                  }\n                  if (g->eflags & 0x01) {\n                     g->transparent = stbi__get8(s);\n                     if (g->transparent >= 0) {\n                        g->pal[g->transparent][3] = 0;\n                     }\n                  } else {\n                     // don't need transparent\n                     stbi__skip(s, 1);\n                     g->transparent = -1;\n                  }\n               } else {\n                  stbi__skip(s, len);\n                  break;\n               }\n            }\n            while ((len = stbi__get8(s)) != 0) {\n               stbi__skip(s, len);\n            }\n            break;\n         }\n\n         case 0x3B: // gif stream termination code\n            return (stbi_uc *) s; // using '1' causes warning on some compilers\n\n         default:\n            return stbi__errpuc(\"unknown code\", \"Corrupt GIF\");\n      }\n   }\n}\n\nstatic void *stbi__load_gif_main_outofmem(stbi__gif *g, stbi_uc *out, int **delays)\n{\n   STBI_FREE(g->out);\n   STBI_FREE(g->history);\n   STBI_FREE(g->background);\n\n   if (out) STBI_FREE(out);\n   if (delays && *delays) STBI_FREE(*delays);\n   return stbi__errpuc(\"outofmem\", \"Out of memory\");\n}\n\nstatic void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp)\n{\n   if (stbi__gif_test(s)) {\n      int layers = 0;\n      stbi_uc *u = 0;\n      stbi_uc *out = 0;\n      stbi_uc *two_back = 0;\n      stbi__gif g;\n      int stride;\n      int out_size = 0;\n      int delays_size = 0;\n\n      STBI_NOTUSED(out_size);\n      STBI_NOTUSED(delays_size);\n\n      memset(&g, 0, sizeof(g));\n      if (delays) {\n         *delays = 0;\n      }\n\n      do {\n         u = stbi__gif_load_next(s, &g, comp, req_comp, two_back);\n         if (u == (stbi_uc *) s) u = 0;  // end of animated gif marker\n\n         if (u) {\n            *x = g.w;\n            *y = g.h;\n            ++layers;\n            stride = g.w * g.h * 4;\n\n            if (out) {\n               void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride );\n               if (!tmp)\n                  return stbi__load_gif_main_outofmem(&g, out, delays);\n               else {\n                   out = (stbi_uc*) tmp;\n                   out_size = layers * stride;\n               }\n\n               if (delays) {\n                  int *new_delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers );\n                  if (!new_delays)\n                     return stbi__load_gif_main_outofmem(&g, out, delays);\n                  *delays = new_delays;\n                  delays_size = layers * sizeof(int);\n               }\n            } else {\n               out = (stbi_uc*)stbi__malloc( layers * stride );\n               if (!out)\n                  return stbi__load_gif_main_outofmem(&g, out, delays);\n               out_size = layers * stride;\n               if (delays) {\n                  *delays = (int*) stbi__malloc( layers * sizeof(int) );\n                  if (!*delays)\n                     return stbi__load_gif_main_outofmem(&g, out, delays);\n                  delays_size = layers * sizeof(int);\n               }\n            }\n            memcpy( out + ((layers - 1) * stride), u, stride );\n            if (layers >= 2) {\n               two_back = out - 2 * stride;\n            }\n\n            if (delays) {\n               (*delays)[layers - 1U] = g.delay;\n            }\n         }\n      } while (u != 0);\n\n      // free temp buffer;\n      STBI_FREE(g.out);\n      STBI_FREE(g.history);\n      STBI_FREE(g.background);\n\n      // do the final conversion after loading everything;\n      if (req_comp && req_comp != 4)\n         out = stbi__convert_format(out, 4, req_comp, layers * g.w, g.h);\n\n      *z = layers;\n      return out;\n   } else {\n      return stbi__errpuc(\"not GIF\", \"Image was not as a gif type.\");\n   }\n}\n\nstatic void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)\n{\n   stbi_uc *u = 0;\n   stbi__gif g;\n   memset(&g, 0, sizeof(g));\n   STBI_NOTUSED(ri);\n\n   u = stbi__gif_load_next(s, &g, comp, req_comp, 0);\n   if (u == (stbi_uc *) s) u = 0;  // end of animated gif marker\n   if (u) {\n      *x = g.w;\n      *y = g.h;\n\n      // moved conversion to after successful load so that the same\n      // can be done for multiple frames.\n      if (req_comp && req_comp != 4)\n         u = stbi__convert_format(u, 4, req_comp, g.w, g.h);\n   } else if (g.out) {\n      // if there was an error and we allocated an image buffer, free it!\n      STBI_FREE(g.out);\n   }\n\n   // free buffers needed for multiple frame loading;\n   STBI_FREE(g.history);\n   STBI_FREE(g.background);\n\n   return u;\n}\n\nstatic int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp)\n{\n   return stbi__gif_info_raw(s,x,y,comp);\n}\n#endif\n\n// *************************************************************************************************\n// Radiance RGBE HDR loader\n// originally by Nicolas Schulz\n#ifndef STBI_NO_HDR\nstatic int stbi__hdr_test_core(stbi__context *s, const char *signature)\n{\n   int i;\n   for (i=0; signature[i]; ++i)\n      if (stbi__get8(s) != signature[i])\n          return 0;\n   stbi__rewind(s);\n   return 1;\n}\n\nstatic int stbi__hdr_test(stbi__context* s)\n{\n   int r = stbi__hdr_test_core(s, \"#?RADIANCE\\n\");\n   stbi__rewind(s);\n   if(!r) {\n       r = stbi__hdr_test_core(s, \"#?RGBE\\n\");\n       stbi__rewind(s);\n   }\n   return r;\n}\n\n#define STBI__HDR_BUFLEN  1024\nstatic char *stbi__hdr_gettoken(stbi__context *z, char *buffer)\n{\n   int len=0;\n   char c = '\\0';\n\n   c = (char) stbi__get8(z);\n\n   while (!stbi__at_eof(z) && c != '\\n') {\n      buffer[len++] = c;\n      if (len == STBI__HDR_BUFLEN-1) {\n         // flush to end of line\n         while (!stbi__at_eof(z) && stbi__get8(z) != '\\n')\n            ;\n         break;\n      }\n      c = (char) stbi__get8(z);\n   }\n\n   buffer[len] = 0;\n   return buffer;\n}\n\nstatic void stbi__hdr_convert(float *output, stbi_uc *input, int req_comp)\n{\n   if ( input[3] != 0 ) {\n      float f1;\n      // Exponent\n      f1 = (float) ldexp(1.0f, input[3] - (int)(128 + 8));\n      if (req_comp <= 2)\n         output[0] = (input[0] + input[1] + input[2]) * f1 / 3;\n      else {\n         output[0] = input[0] * f1;\n         output[1] = input[1] * f1;\n         output[2] = input[2] * f1;\n      }\n      if (req_comp == 2) output[1] = 1;\n      if (req_comp == 4) output[3] = 1;\n   } else {\n      switch (req_comp) {\n         case 4: output[3] = 1; /* fallthrough */\n         case 3: output[0] = output[1] = output[2] = 0;\n                 break;\n         case 2: output[1] = 1; /* fallthrough */\n         case 1: output[0] = 0;\n                 break;\n      }\n   }\n}\n\nstatic float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)\n{\n   char buffer[STBI__HDR_BUFLEN];\n   char *token;\n   int valid = 0;\n   int width, height;\n   stbi_uc *scanline;\n   float *hdr_data;\n   int len;\n   unsigned char count, value;\n   int i, j, k, c1,c2, z;\n   const char *headerToken;\n   STBI_NOTUSED(ri);\n\n   // Check identifier\n   headerToken = stbi__hdr_gettoken(s,buffer);\n   if (strcmp(headerToken, \"#?RADIANCE\") != 0 && strcmp(headerToken, \"#?RGBE\") != 0)\n      return stbi__errpf(\"not HDR\", \"Corrupt HDR image\");\n\n   // Parse header\n   for(;;) {\n      token = stbi__hdr_gettoken(s,buffer);\n      if (token[0] == 0) break;\n      if (strcmp(token, \"FORMAT=32-bit_rle_rgbe\") == 0) valid = 1;\n   }\n\n   if (!valid)    return stbi__errpf(\"unsupported format\", \"Unsupported HDR format\");\n\n   // Parse width and height\n   // can't use sscanf() if we're not using stdio!\n   token = stbi__hdr_gettoken(s,buffer);\n   if (strncmp(token, \"-Y \", 3))  return stbi__errpf(\"unsupported data layout\", \"Unsupported HDR format\");\n   token += 3;\n   height = (int) strtol(token, &token, 10);\n   while (*token == ' ') ++token;\n   if (strncmp(token, \"+X \", 3))  return stbi__errpf(\"unsupported data layout\", \"Unsupported HDR format\");\n   token += 3;\n   width = (int) strtol(token, NULL, 10);\n\n   if (height > STBI_MAX_DIMENSIONS) return stbi__errpf(\"too large\",\"Very large image (corrupt?)\");\n   if (width > STBI_MAX_DIMENSIONS) return stbi__errpf(\"too large\",\"Very large image (corrupt?)\");\n\n   *x = width;\n   *y = height;\n\n   if (comp) *comp = 3;\n   if (req_comp == 0) req_comp = 3;\n\n   if (!stbi__mad4sizes_valid(width, height, req_comp, sizeof(float), 0))\n      return stbi__errpf(\"too large\", \"HDR image is too large\");\n\n   // Read data\n   hdr_data = (float *) stbi__malloc_mad4(width, height, req_comp, sizeof(float), 0);\n   if (!hdr_data)\n      return stbi__errpf(\"outofmem\", \"Out of memory\");\n\n   // Load image data\n   // image data is stored as some number of sca\n   if ( width < 8 || width >= 32768) {\n      // Read flat data\n      for (j=0; j < height; ++j) {\n         for (i=0; i < width; ++i) {\n            stbi_uc rgbe[4];\n           main_decode_loop:\n            stbi__getn(s, rgbe, 4);\n            stbi__hdr_convert(hdr_data + j * width * req_comp + i * req_comp, rgbe, req_comp);\n         }\n      }\n   } else {\n      // Read RLE-encoded data\n      scanline = NULL;\n\n      for (j = 0; j < height; ++j) {\n         c1 = stbi__get8(s);\n         c2 = stbi__get8(s);\n         len = stbi__get8(s);\n         if (c1 != 2 || c2 != 2 || (len & 0x80)) {\n            // not run-length encoded, so we have to actually use THIS data as a decoded\n            // pixel (note this can't be a valid pixel--one of RGB must be >= 128)\n            stbi_uc rgbe[4];\n            rgbe[0] = (stbi_uc) c1;\n            rgbe[1] = (stbi_uc) c2;\n            rgbe[2] = (stbi_uc) len;\n            rgbe[3] = (stbi_uc) stbi__get8(s);\n            stbi__hdr_convert(hdr_data, rgbe, req_comp);\n            i = 1;\n            j = 0;\n            STBI_FREE(scanline);\n            goto main_decode_loop; // yes, this makes no sense\n         }\n         len <<= 8;\n         len |= stbi__get8(s);\n         if (len != width) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf(\"invalid decoded scanline length\", \"corrupt HDR\"); }\n         if (scanline == NULL) {\n            scanline = (stbi_uc *) stbi__malloc_mad2(width, 4, 0);\n            if (!scanline) {\n               STBI_FREE(hdr_data);\n               return stbi__errpf(\"outofmem\", \"Out of memory\");\n            }\n         }\n\n         for (k = 0; k < 4; ++k) {\n            int nleft;\n            i = 0;\n            while ((nleft = width - i) > 0) {\n               count = stbi__get8(s);\n               if (count > 128) {\n                  // Run\n                  value = stbi__get8(s);\n                  count -= 128;\n                  if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf(\"corrupt\", \"bad RLE data in HDR\"); }\n                  for (z = 0; z < count; ++z)\n                     scanline[i++ * 4 + k] = value;\n               } else {\n                  // Dump\n                  if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf(\"corrupt\", \"bad RLE data in HDR\"); }\n                  for (z = 0; z < count; ++z)\n                     scanline[i++ * 4 + k] = stbi__get8(s);\n               }\n            }\n         }\n         for (i=0; i < width; ++i)\n            stbi__hdr_convert(hdr_data+(j*width + i)*req_comp, scanline + i*4, req_comp);\n      }\n      if (scanline)\n         STBI_FREE(scanline);\n   }\n\n   return hdr_data;\n}\n\nstatic int stbi__hdr_info(stbi__context *s, int *x, int *y, int *comp)\n{\n   char buffer[STBI__HDR_BUFLEN];\n   char *token;\n   int valid = 0;\n   int dummy;\n\n   if (!x) x = &dummy;\n   if (!y) y = &dummy;\n   if (!comp) comp = &dummy;\n\n   if (stbi__hdr_test(s) == 0) {\n       stbi__rewind( s );\n       return 0;\n   }\n\n   for(;;) {\n      token = stbi__hdr_gettoken(s,buffer);\n      if (token[0] == 0) break;\n      if (strcmp(token, \"FORMAT=32-bit_rle_rgbe\") == 0) valid = 1;\n   }\n\n   if (!valid) {\n       stbi__rewind( s );\n       return 0;\n   }\n   token = stbi__hdr_gettoken(s,buffer);\n   if (strncmp(token, \"-Y \", 3)) {\n       stbi__rewind( s );\n       return 0;\n   }\n   token += 3;\n   *y = (int) strtol(token, &token, 10);\n   while (*token == ' ') ++token;\n   if (strncmp(token, \"+X \", 3)) {\n       stbi__rewind( s );\n       return 0;\n   }\n   token += 3;\n   *x = (int) strtol(token, NULL, 10);\n   *comp = 3;\n   return 1;\n}\n#endif // STBI_NO_HDR\n\n#ifndef STBI_NO_BMP\nstatic int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp)\n{\n   void *p;\n   stbi__bmp_data info;\n\n   info.all_a = 255;\n   p = stbi__bmp_parse_header(s, &info);\n   if (p == NULL) {\n      stbi__rewind( s );\n      return 0;\n   }\n   if (x) *x = s->img_x;\n   if (y) *y = s->img_y;\n   if (comp) {\n      if (info.bpp == 24 && info.ma == 0xff000000)\n         *comp = 3;\n      else\n         *comp = info.ma ? 4 : 3;\n   }\n   return 1;\n}\n#endif\n\n#ifndef STBI_NO_PSD\nstatic int stbi__psd_info(stbi__context *s, int *x, int *y, int *comp)\n{\n   int channelCount, dummy, depth;\n   if (!x) x = &dummy;\n   if (!y) y = &dummy;\n   if (!comp) comp = &dummy;\n   if (stbi__get32be(s) != 0x38425053) {\n       stbi__rewind( s );\n       return 0;\n   }\n   if (stbi__get16be(s) != 1) {\n       stbi__rewind( s );\n       return 0;\n   }\n   stbi__skip(s, 6);\n   channelCount = stbi__get16be(s);\n   if (channelCount < 0 || channelCount > 16) {\n       stbi__rewind( s );\n       return 0;\n   }\n   *y = stbi__get32be(s);\n   *x = stbi__get32be(s);\n   depth = stbi__get16be(s);\n   if (depth != 8 && depth != 16) {\n       stbi__rewind( s );\n       return 0;\n   }\n   if (stbi__get16be(s) != 3) {\n       stbi__rewind( s );\n       return 0;\n   }\n   *comp = 4;\n   return 1;\n}\n\nstatic int stbi__psd_is16(stbi__context *s)\n{\n   int channelCount, depth;\n   if (stbi__get32be(s) != 0x38425053) {\n       stbi__rewind( s );\n       return 0;\n   }\n   if (stbi__get16be(s) != 1) {\n       stbi__rewind( s );\n       return 0;\n   }\n   stbi__skip(s, 6);\n   channelCount = stbi__get16be(s);\n   if (channelCount < 0 || channelCount > 16) {\n       stbi__rewind( s );\n       return 0;\n   }\n   STBI_NOTUSED(stbi__get32be(s));\n   STBI_NOTUSED(stbi__get32be(s));\n   depth = stbi__get16be(s);\n   if (depth != 16) {\n       stbi__rewind( s );\n       return 0;\n   }\n   return 1;\n}\n#endif\n\n#ifndef STBI_NO_PIC\nstatic int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp)\n{\n   int act_comp=0,num_packets=0,chained,dummy;\n   stbi__pic_packet packets[10];\n\n   if (!x) x = &dummy;\n   if (!y) y = &dummy;\n   if (!comp) comp = &dummy;\n\n   if (!stbi__pic_is4(s,\"\\x53\\x80\\xF6\\x34\")) {\n      stbi__rewind(s);\n      return 0;\n   }\n\n   stbi__skip(s, 88);\n\n   *x = stbi__get16be(s);\n   *y = stbi__get16be(s);\n   if (stbi__at_eof(s)) {\n      stbi__rewind( s);\n      return 0;\n   }\n   if ( (*x) != 0 && (1 << 28) / (*x) < (*y)) {\n      stbi__rewind( s );\n      return 0;\n   }\n\n   stbi__skip(s, 8);\n\n   do {\n      stbi__pic_packet *packet;\n\n      if (num_packets==sizeof(packets)/sizeof(packets[0]))\n         return 0;\n\n      packet = &packets[num_packets++];\n      chained = stbi__get8(s);\n      packet->size    = stbi__get8(s);\n      packet->type    = stbi__get8(s);\n      packet->channel = stbi__get8(s);\n      act_comp |= packet->channel;\n\n      if (stbi__at_eof(s)) {\n          stbi__rewind( s );\n          return 0;\n      }\n      if (packet->size != 8) {\n          stbi__rewind( s );\n          return 0;\n      }\n   } while (chained);\n\n   *comp = (act_comp & 0x10 ? 4 : 3);\n\n   return 1;\n}\n#endif\n\n// *************************************************************************************************\n// Portable Gray Map and Portable Pixel Map loader\n// by Ken Miller\n//\n// PGM: http://netpbm.sourceforge.net/doc/pgm.html\n// PPM: http://netpbm.sourceforge.net/doc/ppm.html\n//\n// Known limitations:\n//    Does not support comments in the header section\n//    Does not support ASCII image data (formats P2 and P3)\n\n#ifndef STBI_NO_PNM\n\nstatic int      stbi__pnm_test(stbi__context *s)\n{\n   char p, t;\n   p = (char) stbi__get8(s);\n   t = (char) stbi__get8(s);\n   if (p != 'P' || (t != '5' && t != '6')) {\n       stbi__rewind( s );\n       return 0;\n   }\n   return 1;\n}\n\nstatic void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)\n{\n   stbi_uc *out;\n   STBI_NOTUSED(ri);\n\n   ri->bits_per_channel = stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n);\n   if (ri->bits_per_channel == 0)\n      return 0;\n\n   if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc(\"too large\",\"Very large image (corrupt?)\");\n   if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc(\"too large\",\"Very large image (corrupt?)\");\n\n   *x = s->img_x;\n   *y = s->img_y;\n   if (comp) *comp = s->img_n;\n\n   if (!stbi__mad4sizes_valid(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0))\n      return stbi__errpuc(\"too large\", \"PNM too large\");\n\n   out = (stbi_uc *) stbi__malloc_mad4(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0);\n   if (!out) return stbi__errpuc(\"outofmem\", \"Out of memory\");\n   if (!stbi__getn(s, out, s->img_n * s->img_x * s->img_y * (ri->bits_per_channel / 8))) {\n      STBI_FREE(out);\n      return stbi__errpuc(\"bad PNM\", \"PNM file truncated\");\n   }\n\n   if (req_comp && req_comp != s->img_n) {\n      if (ri->bits_per_channel == 16) {\n         out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, s->img_n, req_comp, s->img_x, s->img_y);\n      } else {\n         out = stbi__convert_format(out, s->img_n, req_comp, s->img_x, s->img_y);\n      }\n      if (out == NULL) return out; // stbi__convert_format frees input on failure\n   }\n   return out;\n}\n\nstatic int      stbi__pnm_isspace(char c)\n{\n   return c == ' ' || c == '\\t' || c == '\\n' || c == '\\v' || c == '\\f' || c == '\\r';\n}\n\nstatic void     stbi__pnm_skip_whitespace(stbi__context *s, char *c)\n{\n   for (;;) {\n      while (!stbi__at_eof(s) && stbi__pnm_isspace(*c))\n         *c = (char) stbi__get8(s);\n\n      if (stbi__at_eof(s) || *c != '#')\n         break;\n\n      while (!stbi__at_eof(s) && *c != '\\n' && *c != '\\r' )\n         *c = (char) stbi__get8(s);\n   }\n}\n\nstatic int      stbi__pnm_isdigit(char c)\n{\n   return c >= '0' && c <= '9';\n}\n\nstatic int      stbi__pnm_getinteger(stbi__context *s, char *c)\n{\n   int value = 0;\n\n   while (!stbi__at_eof(s) && stbi__pnm_isdigit(*c)) {\n      value = value*10 + (*c - '0');\n      *c = (char) stbi__get8(s);\n      if((value > 214748364) || (value == 214748364 && *c > '7'))\n          return stbi__err(\"integer parse overflow\", \"Parsing an integer in the PPM header overflowed a 32-bit int\");\n   }\n\n   return value;\n}\n\nstatic int      stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp)\n{\n   int maxv, dummy;\n   char c, p, t;\n\n   if (!x) x = &dummy;\n   if (!y) y = &dummy;\n   if (!comp) comp = &dummy;\n\n   stbi__rewind(s);\n\n   // Get identifier\n   p = (char) stbi__get8(s);\n   t = (char) stbi__get8(s);\n   if (p != 'P' || (t != '5' && t != '6')) {\n       stbi__rewind(s);\n       return 0;\n   }\n\n   *comp = (t == '6') ? 3 : 1;  // '5' is 1-component .pgm; '6' is 3-component .ppm\n\n   c = (char) stbi__get8(s);\n   stbi__pnm_skip_whitespace(s, &c);\n\n   *x = stbi__pnm_getinteger(s, &c); // read width\n   if(*x == 0)\n       return stbi__err(\"invalid width\", \"PPM image header had zero or overflowing width\");\n   stbi__pnm_skip_whitespace(s, &c);\n\n   *y = stbi__pnm_getinteger(s, &c); // read height\n   if (*y == 0)\n       return stbi__err(\"invalid width\", \"PPM image header had zero or overflowing width\");\n   stbi__pnm_skip_whitespace(s, &c);\n\n   maxv = stbi__pnm_getinteger(s, &c);  // read max value\n   if (maxv > 65535)\n      return stbi__err(\"max value > 65535\", \"PPM image supports only 8-bit and 16-bit images\");\n   else if (maxv > 255)\n      return 16;\n   else\n      return 8;\n}\n\nstatic int stbi__pnm_is16(stbi__context *s)\n{\n   if (stbi__pnm_info(s, NULL, NULL, NULL) == 16)\n\t   return 1;\n   return 0;\n}\n#endif\n\nstatic int stbi__info_main(stbi__context *s, int *x, int *y, int *comp)\n{\n   #ifndef STBI_NO_JPEG\n   if (stbi__jpeg_info(s, x, y, comp)) return 1;\n   #endif\n\n   #ifndef STBI_NO_PNG\n   if (stbi__png_info(s, x, y, comp))  return 1;\n   #endif\n\n   #ifndef STBI_NO_GIF\n   if (stbi__gif_info(s, x, y, comp))  return 1;\n   #endif\n\n   #ifndef STBI_NO_BMP\n   if (stbi__bmp_info(s, x, y, comp))  return 1;\n   #endif\n\n   #ifndef STBI_NO_PSD\n   if (stbi__psd_info(s, x, y, comp))  return 1;\n   #endif\n\n   #ifndef STBI_NO_PIC\n   if (stbi__pic_info(s, x, y, comp))  return 1;\n   #endif\n\n   #ifndef STBI_NO_PNM\n   if (stbi__pnm_info(s, x, y, comp))  return 1;\n   #endif\n\n   #ifndef STBI_NO_HDR\n   if (stbi__hdr_info(s, x, y, comp))  return 1;\n   #endif\n\n   // test tga last because it's a crappy test!\n   #ifndef STBI_NO_TGA\n   if (stbi__tga_info(s, x, y, comp))\n       return 1;\n   #endif\n   return stbi__err(\"unknown image type\", \"Image not of any known type, or corrupt\");\n}\n\nstatic int stbi__is_16_main(stbi__context *s)\n{\n   #ifndef STBI_NO_PNG\n   if (stbi__png_is16(s))  return 1;\n   #endif\n\n   #ifndef STBI_NO_PSD\n   if (stbi__psd_is16(s))  return 1;\n   #endif\n\n   #ifndef STBI_NO_PNM\n   if (stbi__pnm_is16(s))  return 1;\n   #endif\n   return 0;\n}\n\n#ifndef STBI_NO_STDIO\nSTBIDEF int stbi_info(char const *filename, int *x, int *y, int *comp)\n{\n    FILE *f = stbi__fopen(filename, \"rb\");\n    int result;\n    if (!f) return stbi__err(\"can't fopen\", \"Unable to open file\");\n    result = stbi_info_from_file(f, x, y, comp);\n    fclose(f);\n    return result;\n}\n\nSTBIDEF int stbi_info_from_file(FILE *f, int *x, int *y, int *comp)\n{\n   int r;\n   stbi__context s;\n   long pos = ftell(f);\n   stbi__start_file(&s, f);\n   r = stbi__info_main(&s,x,y,comp);\n   fseek(f,pos,SEEK_SET);\n   return r;\n}\n\nSTBIDEF int stbi_is_16_bit(char const *filename)\n{\n    FILE *f = stbi__fopen(filename, \"rb\");\n    int result;\n    if (!f) return stbi__err(\"can't fopen\", \"Unable to open file\");\n    result = stbi_is_16_bit_from_file(f);\n    fclose(f);\n    return result;\n}\n\nSTBIDEF int stbi_is_16_bit_from_file(FILE *f)\n{\n   int r;\n   stbi__context s;\n   long pos = ftell(f);\n   stbi__start_file(&s, f);\n   r = stbi__is_16_main(&s);\n   fseek(f,pos,SEEK_SET);\n   return r;\n}\n#endif // !STBI_NO_STDIO\n\nSTBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp)\n{\n   stbi__context s;\n   stbi__start_mem(&s,buffer,len);\n   return stbi__info_main(&s,x,y,comp);\n}\n\nSTBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *c, void *user, int *x, int *y, int *comp)\n{\n   stbi__context s;\n   stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user);\n   return stbi__info_main(&s,x,y,comp);\n}\n\nSTBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len)\n{\n   stbi__context s;\n   stbi__start_mem(&s,buffer,len);\n   return stbi__is_16_main(&s);\n}\n\nSTBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *c, void *user)\n{\n   stbi__context s;\n   stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user);\n   return stbi__is_16_main(&s);\n}\n\n#endif // STB_IMAGE_IMPLEMENTATION\n\n/*\n   revision history:\n      2.20  (2019-02-07) support utf8 filenames in Windows; fix warnings and platform ifdefs\n      2.19  (2018-02-11) fix warning\n      2.18  (2018-01-30) fix warnings\n      2.17  (2018-01-29) change sbti__shiftsigned to avoid clang -O2 bug\n                         1-bit BMP\n                         *_is_16_bit api\n                         avoid warnings\n      2.16  (2017-07-23) all functions have 16-bit variants;\n                         STBI_NO_STDIO works again;\n                         compilation fixes;\n                         fix rounding in unpremultiply;\n                         optimize vertical flip;\n                         disable raw_len validation;\n                         documentation fixes\n      2.15  (2017-03-18) fix png-1,2,4 bug; now all Imagenet JPGs decode;\n                         warning fixes; disable run-time SSE detection on gcc;\n                         uniform handling of optional \"return\" values;\n                         thread-safe initialization of zlib tables\n      2.14  (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet JPGs\n      2.13  (2016-11-29) add 16-bit API, only supported for PNG right now\n      2.12  (2016-04-02) fix typo in 2.11 PSD fix that caused crashes\n      2.11  (2016-04-02) allocate large structures on the stack\n                         remove white matting for transparent PSD\n                         fix reported channel count for PNG & BMP\n                         re-enable SSE2 in non-gcc 64-bit\n                         support RGB-formatted JPEG\n                         read 16-bit PNGs (only as 8-bit)\n      2.10  (2016-01-22) avoid warning introduced in 2.09 by STBI_REALLOC_SIZED\n      2.09  (2016-01-16) allow comments in PNM files\n                         16-bit-per-pixel TGA (not bit-per-component)\n                         info() for TGA could break due to .hdr handling\n                         info() for BMP to shares code instead of sloppy parse\n                         can use STBI_REALLOC_SIZED if allocator doesn't support realloc\n                         code cleanup\n      2.08  (2015-09-13) fix to 2.07 cleanup, reading RGB PSD as RGBA\n      2.07  (2015-09-13) fix compiler warnings\n                         partial animated GIF support\n                         limited 16-bpc PSD support\n                         #ifdef unused functions\n                         bug with < 92 byte PIC,PNM,HDR,TGA\n      2.06  (2015-04-19) fix bug where PSD returns wrong '*comp' value\n      2.05  (2015-04-19) fix bug in progressive JPEG handling, fix warning\n      2.04  (2015-04-15) try to re-enable SIMD on MinGW 64-bit\n      2.03  (2015-04-12) extra corruption checking (mmozeiko)\n                         stbi_set_flip_vertically_on_load (nguillemot)\n                         fix NEON support; fix mingw support\n      2.02  (2015-01-19) fix incorrect assert, fix warning\n      2.01  (2015-01-17) fix various warnings; suppress SIMD on gcc 32-bit without -msse2\n      2.00b (2014-12-25) fix STBI_MALLOC in progressive JPEG\n      2.00  (2014-12-25) optimize JPG, including x86 SSE2 & NEON SIMD (ryg)\n                         progressive JPEG (stb)\n                         PGM/PPM support (Ken Miller)\n                         STBI_MALLOC,STBI_REALLOC,STBI_FREE\n                         GIF bugfix -- seemingly never worked\n                         STBI_NO_*, STBI_ONLY_*\n      1.48  (2014-12-14) fix incorrectly-named assert()\n      1.47  (2014-12-14) 1/2/4-bit PNG support, both direct and paletted (Omar Cornut & stb)\n                         optimize PNG (ryg)\n                         fix bug in interlaced PNG with user-specified channel count (stb)\n      1.46  (2014-08-26)\n              fix broken tRNS chunk (colorkey-style transparency) in non-paletted PNG\n      1.45  (2014-08-16)\n              fix MSVC-ARM internal compiler error by wrapping malloc\n      1.44  (2014-08-07)\n              various warning fixes from Ronny Chevalier\n      1.43  (2014-07-15)\n              fix MSVC-only compiler problem in code changed in 1.42\n      1.42  (2014-07-09)\n              don't define _CRT_SECURE_NO_WARNINGS (affects user code)\n              fixes to stbi__cleanup_jpeg path\n              added STBI_ASSERT to avoid requiring assert.h\n      1.41  (2014-06-25)\n              fix search&replace from 1.36 that messed up comments/error messages\n      1.40  (2014-06-22)\n              fix gcc struct-initialization warning\n      1.39  (2014-06-15)\n              fix to TGA optimization when req_comp != number of components in TGA;\n              fix to GIF loading because BMP wasn't rewinding (whoops, no GIFs in my test suite)\n              add support for BMP version 5 (more ignored fields)\n      1.38  (2014-06-06)\n              suppress MSVC warnings on integer casts truncating values\n              fix accidental rename of 'skip' field of I/O\n      1.37  (2014-06-04)\n              remove duplicate typedef\n      1.36  (2014-06-03)\n              convert to header file single-file library\n              if de-iphone isn't set, load iphone images color-swapped instead of returning NULL\n      1.35  (2014-05-27)\n              various warnings\n              fix broken STBI_SIMD path\n              fix bug where stbi_load_from_file no longer left file pointer in correct place\n              fix broken non-easy path for 32-bit BMP (possibly never used)\n              TGA optimization by Arseny Kapoulkine\n      1.34  (unknown)\n              use STBI_NOTUSED in stbi__resample_row_generic(), fix one more leak in tga failure case\n      1.33  (2011-07-14)\n              make stbi_is_hdr work in STBI_NO_HDR (as specified), minor compiler-friendly improvements\n      1.32  (2011-07-13)\n              support for \"info\" function for all supported filetypes (SpartanJ)\n      1.31  (2011-06-20)\n              a few more leak fixes, bug in PNG handling (SpartanJ)\n      1.30  (2011-06-11)\n              added ability to load files via callbacks to accomidate custom input streams (Ben Wenger)\n              removed deprecated format-specific test/load functions\n              removed support for installable file formats (stbi_loader) -- would have been broken for IO callbacks anyway\n              error cases in bmp and tga give messages and don't leak (Raymond Barbiero, grisha)\n              fix inefficiency in decoding 32-bit BMP (David Woo)\n      1.29  (2010-08-16)\n              various warning fixes from Aurelien Pocheville\n      1.28  (2010-08-01)\n              fix bug in GIF palette transparency (SpartanJ)\n      1.27  (2010-08-01)\n              cast-to-stbi_uc to fix warnings\n      1.26  (2010-07-24)\n              fix bug in file buffering for PNG reported by SpartanJ\n      1.25  (2010-07-17)\n              refix trans_data warning (Won Chun)\n      1.24  (2010-07-12)\n              perf improvements reading from files on platforms with lock-heavy fgetc()\n              minor perf improvements for jpeg\n              deprecated type-specific functions so we'll get feedback if they're needed\n              attempt to fix trans_data warning (Won Chun)\n      1.23    fixed bug in iPhone support\n      1.22  (2010-07-10)\n              removed image *writing* support\n              stbi_info support from Jetro Lauha\n              GIF support from Jean-Marc Lienher\n              iPhone PNG-extensions from James Brown\n              warning-fixes from Nicolas Schulz and Janez Zemva (i.stbi__err. Janez (U+017D)emva)\n      1.21    fix use of 'stbi_uc' in header (reported by jon blow)\n      1.20    added support for Softimage PIC, by Tom Seddon\n      1.19    bug in interlaced PNG corruption check (found by ryg)\n      1.18  (2008-08-02)\n              fix a threading bug (local mutable static)\n      1.17    support interlaced PNG\n      1.16    major bugfix - stbi__convert_format converted one too many pixels\n      1.15    initialize some fields for thread safety\n      1.14    fix threadsafe conversion bug\n              header-file-only version (#define STBI_HEADER_FILE_ONLY before including)\n      1.13    threadsafe\n      1.12    const qualifiers in the API\n      1.11    Support installable IDCT, colorspace conversion routines\n      1.10    Fixes for 64-bit (don't use \"unsigned long\")\n              optimized upsampling by Fabian \"ryg\" Giesen\n      1.09    Fix format-conversion for PSD code (bad global variables!)\n      1.08    Thatcher Ulrich's PSD code integrated by Nicolas Schulz\n      1.07    attempt to fix C++ warning/errors again\n      1.06    attempt to fix C++ warning/errors again\n      1.05    fix TGA loading to return correct *comp and use good luminance calc\n      1.04    default float alpha is 1, not 255; use 'void *' for stbi_image_free\n      1.03    bugfixes to STBI_NO_STDIO, STBI_NO_HDR\n      1.02    support for (subset of) HDR files, float interface for preferred access to them\n      1.01    fix bug: possible bug in handling right-side up bmps... not sure\n              fix bug: the stbi__bmp_load() and stbi__tga_load() functions didn't work at all\n      1.00    interface to zlib that skips zlib header\n      0.99    correct handling of alpha in palette\n      0.98    TGA loader by lonesock; dynamically add loaders (untested)\n      0.97    jpeg errors on too large a file; also catch another malloc failure\n      0.96    fix detection of invalid v value - particleman@mollyrocket forum\n      0.95    during header scan, seek to markers in case of padding\n      0.94    STBI_NO_STDIO to disable stdio usage; rename all #defines the same\n      0.93    handle jpegtran output; verbose errors\n      0.92    read 4,8,16,24,32-bit BMP files of several formats\n      0.91    output 24-bit Windows 3.0 BMP files\n      0.90    fix a few more warnings; bump version number to approach 1.0\n      0.61    bugfixes due to Marc LeBlanc, Christopher Lloyd\n      0.60    fix compiling as c++\n      0.59    fix warnings: merge Dave Moore's -Wall fixes\n      0.58    fix bug: zlib uncompressed mode len/nlen was wrong endian\n      0.57    fix bug: jpg last huffman symbol before marker was >9 bits but less than 16 available\n      0.56    fix bug: zlib uncompressed mode len vs. nlen\n      0.55    fix bug: restart_interval not initialized to 0\n      0.54    allow NULL for 'int *comp'\n      0.53    fix bug in png 3->4; speedup png decoding\n      0.52    png handles req_comp=3,4 directly; minor cleanup; jpeg comments\n      0.51    obey req_comp requests, 1-component jpegs return as 1-component,\n              on 'test' only check type, not whether we support this variant\n      0.50  (2006-11-19)\n              first released version\n*/\n\n\n/*\n------------------------------------------------------------------------------\nThis software is available under 2 licenses -- choose whichever you prefer.\n------------------------------------------------------------------------------\nALTERNATIVE A - MIT License\nCopyright (c) 2017 Sean Barrett\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n------------------------------------------------------------------------------\nALTERNATIVE B - Public Domain (www.unlicense.org)\nThis is free and unencumbered software released into the public domain.\nAnyone is free to copy, modify, publish, use, compile, sell, or distribute this\nsoftware, either in source code form or as a compiled binary, for any purpose,\ncommercial or non-commercial, and by any means.\nIn jurisdictions that recognize copyright laws, the author or authors of this\nsoftware dedicate any and all copyright interest in the software to the public\ndomain. We make this dedication for the benefit of the public at large and to\nthe detriment of our heirs and successors. We intend this dedication to be an\novert act of relinquishment in perpetuity of all present and future rights to\nthis software under copyright law.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\nACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n------------------------------------------------------------------------------\n*/"
  },
  {
    "path": "src/stb_image_write.h",
    "content": "/* stb_image_write - v1.16 - public domain - http://nothings.org/stb\n   writes out PNG/BMP/TGA/JPEG/HDR images to C stdio - Sean Barrett 2010-2015\n                                     no warranty implied; use at your own risk\n\n   Before #including,\n\n       #define STB_IMAGE_WRITE_IMPLEMENTATION\n\n   in the file that you want to have the implementation.\n\n   Will probably not work correctly with strict-aliasing optimizations.\n\nABOUT:\n\n   This header file is a library for writing images to C stdio or a callback.\n\n   The PNG output is not optimal; it is 20-50% larger than the file\n   written by a decent optimizing implementation; though providing a custom\n   zlib compress function (see STBIW_ZLIB_COMPRESS) can mitigate that.\n   This library is designed for source code compactness and simplicity,\n   not optimal image file size or run-time performance.\n\nBUILDING:\n\n   You can #define STBIW_ASSERT(x) before the #include to avoid using assert.h.\n   You can #define STBIW_MALLOC(), STBIW_REALLOC(), and STBIW_FREE() to replace\n   malloc,realloc,free.\n   You can #define STBIW_MEMMOVE() to replace memmove()\n   You can #define STBIW_ZLIB_COMPRESS to use a custom zlib-style compress function\n   for PNG compression (instead of the builtin one), it must have the following signature:\n   unsigned char * my_compress(unsigned char *data, int data_len, int *out_len, int quality);\n   The returned data will be freed with STBIW_FREE() (free() by default),\n   so it must be heap allocated with STBIW_MALLOC() (malloc() by default),\n\nUNICODE:\n\n   If compiling for Windows and you wish to use Unicode filenames, compile\n   with\n       #define STBIW_WINDOWS_UTF8\n   and pass utf8-encoded filenames. Call stbiw_convert_wchar_to_utf8 to convert\n   Windows wchar_t filenames to utf8.\n\nUSAGE:\n\n   There are five functions, one for each image file format:\n\n     int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes);\n     int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data);\n     int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data);\n     int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality);\n     int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data);\n\n     void stbi_flip_vertically_on_write(int flag); // flag is non-zero to flip data vertically\n\n   There are also five equivalent functions that use an arbitrary write function. You are\n   expected to open/close your file-equivalent before and after calling these:\n\n     int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data, int stride_in_bytes);\n     int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data);\n     int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data);\n     int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data);\n     int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality);\n\n   where the callback is:\n      void stbi_write_func(void *context, void *data, int size);\n\n   You can configure it with these global variables:\n      int stbi_write_tga_with_rle;             // defaults to true; set to 0 to disable RLE\n      int stbi_write_png_compression_level;    // defaults to 8; set to higher for more compression\n      int stbi_write_force_png_filter;         // defaults to -1; set to 0..5 to force a filter mode\n\n\n   You can define STBI_WRITE_NO_STDIO to disable the file variant of these\n   functions, so the library will not use stdio.h at all. However, this will\n   also disable HDR writing, because it requires stdio for formatted output.\n\n   Each function returns 0 on failure and non-0 on success.\n\n   The functions create an image file defined by the parameters. The image\n   is a rectangle of pixels stored from left-to-right, top-to-bottom.\n   Each pixel contains 'comp' channels of data stored interleaved with 8-bits\n   per channel, in the following order: 1=Y, 2=YA, 3=RGB, 4=RGBA. (Y is\n   monochrome color.) The rectangle is 'w' pixels wide and 'h' pixels tall.\n   The *data pointer points to the first byte of the top-left-most pixel.\n   For PNG, \"stride_in_bytes\" is the distance in bytes from the first byte of\n   a row of pixels to the first byte of the next row of pixels.\n\n   PNG creates output files with the same number of components as the input.\n   The BMP format expands Y to RGB in the file format and does not\n   output alpha.\n\n   PNG supports writing rectangles of data even when the bytes storing rows of\n   data are not consecutive in memory (e.g. sub-rectangles of a larger image),\n   by supplying the stride between the beginning of adjacent rows. The other\n   formats do not. (Thus you cannot write a native-format BMP through the BMP\n   writer, both because it is in BGR order and because it may have padding\n   at the end of the line.)\n\n   PNG allows you to set the deflate compression level by setting the global\n   variable 'stbi_write_png_compression_level' (it defaults to 8).\n\n   HDR expects linear float data. Since the format is always 32-bit rgb(e)\n   data, alpha (if provided) is discarded, and for monochrome data it is\n   replicated across all three channels.\n\n   TGA supports RLE or non-RLE compressed data. To use non-RLE-compressed\n   data, set the global variable 'stbi_write_tga_with_rle' to 0.\n\n   JPEG does ignore alpha channels in input data; quality is between 1 and 100.\n   Higher quality looks better but results in a bigger image.\n   JPEG baseline (no JPEG progressive).\n\nCREDITS:\n\n\n   Sean Barrett           -    PNG/BMP/TGA\n   Baldur Karlsson        -    HDR\n   Jean-Sebastien Guay    -    TGA monochrome\n   Tim Kelsey             -    misc enhancements\n   Alan Hickman           -    TGA RLE\n   Emmanuel Julien        -    initial file IO callback implementation\n   Jon Olick              -    original jo_jpeg.cpp code\n   Daniel Gibson          -    integrate JPEG, allow external zlib\n   Aarni Koskela          -    allow choosing PNG filter\n\n   bugfixes:\n      github:Chribba\n      Guillaume Chereau\n      github:jry2\n      github:romigrou\n      Sergio Gonzalez\n      Jonas Karlsson\n      Filip Wasil\n      Thatcher Ulrich\n      github:poppolopoppo\n      Patrick Boettcher\n      github:xeekworx\n      Cap Petschulat\n      Simon Rodriguez\n      Ivan Tikhonov\n      github:ignotion\n      Adam Schackart\n      Andrew Kensler\n\nLICENSE\n\n  See end of file for license information.\n\n*/\n\n#ifndef INCLUDE_STB_IMAGE_WRITE_H\n#define INCLUDE_STB_IMAGE_WRITE_H\n\n#include <stdlib.h>\n\n// if STB_IMAGE_WRITE_STATIC causes problems, try defining STBIWDEF to 'inline' or 'static inline'\n#ifndef STBIWDEF\n#ifdef STB_IMAGE_WRITE_STATIC\n#define STBIWDEF  static\n#else\n#ifdef __cplusplus\n#define STBIWDEF  extern \"C\"\n#else\n#define STBIWDEF  extern\n#endif\n#endif\n#endif\n\n#ifndef STB_IMAGE_WRITE_STATIC  // C++ forbids static forward declarations\nSTBIWDEF int stbi_write_tga_with_rle;\nSTBIWDEF int stbi_write_png_compression_level;\nSTBIWDEF int stbi_write_force_png_filter;\n#endif\n\n#ifndef STBI_WRITE_NO_STDIO\nSTBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void  *data, int stride_in_bytes);\nSTBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void  *data);\nSTBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void  *data);\nSTBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data);\nSTBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void  *data, int quality);\n\n#ifdef STBIW_WINDOWS_UTF8\nSTBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input);\n#endif\n#endif\n\ntypedef void stbi_write_func(void *context, void *data, int size);\n\nSTBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data, int stride_in_bytes);\nSTBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data);\nSTBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data);\nSTBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data);\nSTBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void  *data, int quality);\n\nSTBIWDEF void stbi_flip_vertically_on_write(int flip_boolean);\n\n#endif//INCLUDE_STB_IMAGE_WRITE_H\n\n#ifdef STB_IMAGE_WRITE_IMPLEMENTATION\n\n#ifdef _WIN32\n   #ifndef _CRT_SECURE_NO_WARNINGS\n   #define _CRT_SECURE_NO_WARNINGS\n   #endif\n   #ifndef _CRT_NONSTDC_NO_DEPRECATE\n   #define _CRT_NONSTDC_NO_DEPRECATE\n   #endif\n#endif\n\n#ifndef STBI_WRITE_NO_STDIO\n#include <stdio.h>\n#endif // STBI_WRITE_NO_STDIO\n\n#include <stdarg.h>\n#include <stdlib.h>\n#include <string.h>\n#include <math.h>\n\n#if defined(STBIW_MALLOC) && defined(STBIW_FREE) && (defined(STBIW_REALLOC) || defined(STBIW_REALLOC_SIZED))\n// ok\n#elif !defined(STBIW_MALLOC) && !defined(STBIW_FREE) && !defined(STBIW_REALLOC) && !defined(STBIW_REALLOC_SIZED)\n// ok\n#else\n#error \"Must define all or none of STBIW_MALLOC, STBIW_FREE, and STBIW_REALLOC (or STBIW_REALLOC_SIZED).\"\n#endif\n\n#ifndef STBIW_MALLOC\n#define STBIW_MALLOC(sz)        malloc(sz)\n#define STBIW_REALLOC(p,newsz)  realloc(p,newsz)\n#define STBIW_FREE(p)           free(p)\n#endif\n\n#ifndef STBIW_REALLOC_SIZED\n#define STBIW_REALLOC_SIZED(p,oldsz,newsz) STBIW_REALLOC(p,newsz)\n#endif\n\n\n#ifndef STBIW_MEMMOVE\n#define STBIW_MEMMOVE(a,b,sz) memmove(a,b,sz)\n#endif\n\n\n#ifndef STBIW_ASSERT\n#include <assert.h>\n#define STBIW_ASSERT(x) assert(x)\n#endif\n\n#define STBIW_UCHAR(x) (unsigned char) ((x) & 0xff)\n\n#ifdef STB_IMAGE_WRITE_STATIC\nstatic int stbi_write_png_compression_level = 8;\nstatic int stbi_write_tga_with_rle = 1;\nstatic int stbi_write_force_png_filter = -1;\n#else\nint stbi_write_png_compression_level = 8;\nint stbi_write_tga_with_rle = 1;\nint stbi_write_force_png_filter = -1;\n#endif\n\nstatic int stbi__flip_vertically_on_write = 0;\n\nSTBIWDEF void stbi_flip_vertically_on_write(int flag)\n{\n   stbi__flip_vertically_on_write = flag;\n}\n\ntypedef struct\n{\n   stbi_write_func *func;\n   void *context;\n   unsigned char buffer[64];\n   int buf_used;\n} stbi__write_context;\n\n// initialize a callback-based context\nstatic void stbi__start_write_callbacks(stbi__write_context *s, stbi_write_func *c, void *context)\n{\n   s->func    = c;\n   s->context = context;\n}\n\n#ifndef STBI_WRITE_NO_STDIO\n\nstatic void stbi__stdio_write(void *context, void *data, int size)\n{\n   fwrite(data,1,size,(FILE*) context);\n}\n\n#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8)\n#ifdef __cplusplus\n#define STBIW_EXTERN extern \"C\"\n#else\n#define STBIW_EXTERN extern\n#endif\nSTBIW_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide);\nSTBIW_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default);\n\nSTBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input)\n{\n   return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL);\n}\n#endif\n\nstatic FILE *stbiw__fopen(char const *filename, char const *mode)\n{\n   FILE *f;\n#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8)\n   wchar_t wMode[64];\n   wchar_t wFilename[1024];\n   if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename)))\n      return 0;\n\n   if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode)))\n      return 0;\n\n#if defined(_MSC_VER) && _MSC_VER >= 1400\n   if (0 != _wfopen_s(&f, wFilename, wMode))\n      f = 0;\n#else\n   f = _wfopen(wFilename, wMode);\n#endif\n\n#elif defined(_MSC_VER) && _MSC_VER >= 1400\n   if (0 != fopen_s(&f, filename, mode))\n      f=0;\n#else\n   f = fopen(filename, mode);\n#endif\n   return f;\n}\n\nstatic int stbi__start_write_file(stbi__write_context *s, const char *filename)\n{\n   FILE *f = stbiw__fopen(filename, \"wb\");\n   stbi__start_write_callbacks(s, stbi__stdio_write, (void *) f);\n   return f != NULL;\n}\n\nstatic void stbi__end_write_file(stbi__write_context *s)\n{\n   fclose((FILE *)s->context);\n}\n\n#endif // !STBI_WRITE_NO_STDIO\n\ntypedef unsigned int stbiw_uint32;\ntypedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1];\n\nstatic void stbiw__writefv(stbi__write_context *s, const char *fmt, va_list v)\n{\n   while (*fmt) {\n      switch (*fmt++) {\n         case ' ': break;\n         case '1': { unsigned char x = STBIW_UCHAR(va_arg(v, int));\n                     s->func(s->context,&x,1);\n                     break; }\n         case '2': { int x = va_arg(v,int);\n                     unsigned char b[2];\n                     b[0] = STBIW_UCHAR(x);\n                     b[1] = STBIW_UCHAR(x>>8);\n                     s->func(s->context,b,2);\n                     break; }\n         case '4': { stbiw_uint32 x = va_arg(v,int);\n                     unsigned char b[4];\n                     b[0]=STBIW_UCHAR(x);\n                     b[1]=STBIW_UCHAR(x>>8);\n                     b[2]=STBIW_UCHAR(x>>16);\n                     b[3]=STBIW_UCHAR(x>>24);\n                     s->func(s->context,b,4);\n                     break; }\n         default:\n            STBIW_ASSERT(0);\n            return;\n      }\n   }\n}\n\nstatic void stbiw__writef(stbi__write_context *s, const char *fmt, ...)\n{\n   va_list v;\n   va_start(v, fmt);\n   stbiw__writefv(s, fmt, v);\n   va_end(v);\n}\n\nstatic void stbiw__write_flush(stbi__write_context *s)\n{\n   if (s->buf_used) {\n      s->func(s->context, &s->buffer, s->buf_used);\n      s->buf_used = 0;\n   }\n}\n\nstatic void stbiw__putc(stbi__write_context *s, unsigned char c)\n{\n   s->func(s->context, &c, 1);\n}\n\nstatic void stbiw__write1(stbi__write_context *s, unsigned char a)\n{\n   if ((size_t)s->buf_used + 1 > sizeof(s->buffer))\n      stbiw__write_flush(s);\n   s->buffer[s->buf_used++] = a;\n}\n\nstatic void stbiw__write3(stbi__write_context *s, unsigned char a, unsigned char b, unsigned char c)\n{\n   int n;\n   if ((size_t)s->buf_used + 3 > sizeof(s->buffer))\n      stbiw__write_flush(s);\n   n = s->buf_used;\n   s->buf_used = n+3;\n   s->buffer[n+0] = a;\n   s->buffer[n+1] = b;\n   s->buffer[n+2] = c;\n}\n\nstatic void stbiw__write_pixel(stbi__write_context *s, int rgb_dir, int comp, int write_alpha, int expand_mono, unsigned char *d)\n{\n   unsigned char bg[3] = { 255, 0, 255}, px[3];\n   int k;\n\n   if (write_alpha < 0)\n      stbiw__write1(s, d[comp - 1]);\n\n   switch (comp) {\n      case 2: // 2 pixels = mono + alpha, alpha is written separately, so same as 1-channel case\n      case 1:\n         if (expand_mono)\n            stbiw__write3(s, d[0], d[0], d[0]); // monochrome bmp\n         else\n            stbiw__write1(s, d[0]);  // monochrome TGA\n         break;\n      case 4:\n         if (!write_alpha) {\n            // composite against pink background\n            for (k = 0; k < 3; ++k)\n               px[k] = bg[k] + ((d[k] - bg[k]) * d[3]) / 255;\n            stbiw__write3(s, px[1 - rgb_dir], px[1], px[1 + rgb_dir]);\n            break;\n         }\n         /* FALLTHROUGH */\n      case 3:\n         stbiw__write3(s, d[1 - rgb_dir], d[1], d[1 + rgb_dir]);\n         break;\n   }\n   if (write_alpha > 0)\n      stbiw__write1(s, d[comp - 1]);\n}\n\nstatic void stbiw__write_pixels(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, void *data, int write_alpha, int scanline_pad, int expand_mono)\n{\n   stbiw_uint32 zero = 0;\n   int i,j, j_end;\n\n   if (y <= 0)\n      return;\n\n   if (stbi__flip_vertically_on_write)\n      vdir *= -1;\n\n   if (vdir < 0) {\n      j_end = -1; j = y-1;\n   } else {\n      j_end =  y; j = 0;\n   }\n\n   for (; j != j_end; j += vdir) {\n      for (i=0; i < x; ++i) {\n         unsigned char *d = (unsigned char *) data + (j*x+i)*comp;\n         stbiw__write_pixel(s, rgb_dir, comp, write_alpha, expand_mono, d);\n      }\n      stbiw__write_flush(s);\n      s->func(s->context, &zero, scanline_pad);\n   }\n}\n\nstatic int stbiw__outfile(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, int expand_mono, void *data, int alpha, int pad, const char *fmt, ...)\n{\n   if (y < 0 || x < 0) {\n      return 0;\n   } else {\n      va_list v;\n      va_start(v, fmt);\n      stbiw__writefv(s, fmt, v);\n      va_end(v);\n      stbiw__write_pixels(s,rgb_dir,vdir,x,y,comp,data,alpha,pad, expand_mono);\n      return 1;\n   }\n}\n\nstatic int stbi_write_bmp_core(stbi__write_context *s, int x, int y, int comp, const void *data)\n{\n   if (comp != 4) {\n      // write RGB bitmap\n      int pad = (-x*3) & 3;\n      return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *) data,0,pad,\n              \"11 4 22 4\" \"4 44 22 444444\",\n              'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40,  // file header\n               40, x,y, 1,24, 0,0,0,0,0,0);             // bitmap header\n   } else {\n      // RGBA bitmaps need a v4 header\n      // use BI_BITFIELDS mode with 32bpp and alpha mask\n      // (straight BI_RGB with alpha mask doesn't work in most readers)\n      return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *)data,1,0,\n         \"11 4 22 4\" \"4 44 22 444444 4444 4 444 444 444 444\",\n         'B', 'M', 14+108+x*y*4, 0, 0, 14+108, // file header\n         108, x,y, 1,32, 3,0,0,0,0,0, 0xff0000,0xff00,0xff,0xff000000u, 0, 0,0,0, 0,0,0, 0,0,0, 0,0,0); // bitmap V4 header\n   }\n}\n\nSTBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data)\n{\n   stbi__write_context s = { 0 };\n   stbi__start_write_callbacks(&s, func, context);\n   return stbi_write_bmp_core(&s, x, y, comp, data);\n}\n\n#ifndef STBI_WRITE_NO_STDIO\nSTBIWDEF int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data)\n{\n   stbi__write_context s = { 0 };\n   if (stbi__start_write_file(&s,filename)) {\n      int r = stbi_write_bmp_core(&s, x, y, comp, data);\n      stbi__end_write_file(&s);\n      return r;\n   } else\n      return 0;\n}\n#endif //!STBI_WRITE_NO_STDIO\n\nstatic int stbi_write_tga_core(stbi__write_context *s, int x, int y, int comp, void *data)\n{\n   int has_alpha = (comp == 2 || comp == 4);\n   int colorbytes = has_alpha ? comp-1 : comp;\n   int format = colorbytes < 2 ? 3 : 2; // 3 color channels (RGB/RGBA) = 2, 1 color channel (Y/YA) = 3\n\n   if (y < 0 || x < 0)\n      return 0;\n\n   if (!stbi_write_tga_with_rle) {\n      return stbiw__outfile(s, -1, -1, x, y, comp, 0, (void *) data, has_alpha, 0,\n         \"111 221 2222 11\", 0, 0, format, 0, 0, 0, 0, 0, x, y, (colorbytes + has_alpha) * 8, has_alpha * 8);\n   } else {\n      int i,j,k;\n      int jend, jdir;\n\n      stbiw__writef(s, \"111 221 2222 11\", 0,0,format+8, 0,0,0, 0,0,x,y, (colorbytes + has_alpha) * 8, has_alpha * 8);\n\n      if (stbi__flip_vertically_on_write) {\n         j = 0;\n         jend = y;\n         jdir = 1;\n      } else {\n         j = y-1;\n         jend = -1;\n         jdir = -1;\n      }\n      for (; j != jend; j += jdir) {\n         unsigned char *row = (unsigned char *) data + j * x * comp;\n         int len;\n\n         for (i = 0; i < x; i += len) {\n            unsigned char *begin = row + i * comp;\n            int diff = 1;\n            len = 1;\n\n            if (i < x - 1) {\n               ++len;\n               diff = memcmp(begin, row + (i + 1) * comp, comp);\n               if (diff) {\n                  const unsigned char *prev = begin;\n                  for (k = i + 2; k < x && len < 128; ++k) {\n                     if (memcmp(prev, row + k * comp, comp)) {\n                        prev += comp;\n                        ++len;\n                     } else {\n                        --len;\n                        break;\n                     }\n                  }\n               } else {\n                  for (k = i + 2; k < x && len < 128; ++k) {\n                     if (!memcmp(begin, row + k * comp, comp)) {\n                        ++len;\n                     } else {\n                        break;\n                     }\n                  }\n               }\n            }\n\n            if (diff) {\n               unsigned char header = STBIW_UCHAR(len - 1);\n               stbiw__write1(s, header);\n               for (k = 0; k < len; ++k) {\n                  stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin + k * comp);\n               }\n            } else {\n               unsigned char header = STBIW_UCHAR(len - 129);\n               stbiw__write1(s, header);\n               stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin);\n            }\n         }\n      }\n      stbiw__write_flush(s);\n   }\n   return 1;\n}\n\nSTBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data)\n{\n   stbi__write_context s = { 0 };\n   stbi__start_write_callbacks(&s, func, context);\n   return stbi_write_tga_core(&s, x, y, comp, (void *) data);\n}\n\n#ifndef STBI_WRITE_NO_STDIO\nSTBIWDEF int stbi_write_tga(char const *filename, int x, int y, int comp, const void *data)\n{\n   stbi__write_context s = { 0 };\n   if (stbi__start_write_file(&s,filename)) {\n      int r = stbi_write_tga_core(&s, x, y, comp, (void *) data);\n      stbi__end_write_file(&s);\n      return r;\n   } else\n      return 0;\n}\n#endif\n\n// *************************************************************************************************\n// Radiance RGBE HDR writer\n// by Baldur Karlsson\n\n#define stbiw__max(a, b)  ((a) > (b) ? (a) : (b))\n\n#ifndef STBI_WRITE_NO_STDIO\n\nstatic void stbiw__linear_to_rgbe(unsigned char *rgbe, float *linear)\n{\n   int exponent;\n   float maxcomp = stbiw__max(linear[0], stbiw__max(linear[1], linear[2]));\n\n   if (maxcomp < 1e-32f) {\n      rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0;\n   } else {\n      float normalize = (float) frexp(maxcomp, &exponent) * 256.0f/maxcomp;\n\n      rgbe[0] = (unsigned char)(linear[0] * normalize);\n      rgbe[1] = (unsigned char)(linear[1] * normalize);\n      rgbe[2] = (unsigned char)(linear[2] * normalize);\n      rgbe[3] = (unsigned char)(exponent + 128);\n   }\n}\n\nstatic void stbiw__write_run_data(stbi__write_context *s, int length, unsigned char databyte)\n{\n   unsigned char lengthbyte = STBIW_UCHAR(length+128);\n   STBIW_ASSERT(length+128 <= 255);\n   s->func(s->context, &lengthbyte, 1);\n   s->func(s->context, &databyte, 1);\n}\n\nstatic void stbiw__write_dump_data(stbi__write_context *s, int length, unsigned char *data)\n{\n   unsigned char lengthbyte = STBIW_UCHAR(length);\n   STBIW_ASSERT(length <= 128); // inconsistent with spec but consistent with official code\n   s->func(s->context, &lengthbyte, 1);\n   s->func(s->context, data, length);\n}\n\nstatic void stbiw__write_hdr_scanline(stbi__write_context *s, int width, int ncomp, unsigned char *scratch, float *scanline)\n{\n   unsigned char scanlineheader[4] = { 2, 2, 0, 0 };\n   unsigned char rgbe[4];\n   float linear[3];\n   int x;\n\n   scanlineheader[2] = (width&0xff00)>>8;\n   scanlineheader[3] = (width&0x00ff);\n\n   /* skip RLE for images too small or large */\n   if (width < 8 || width >= 32768) {\n      for (x=0; x < width; x++) {\n         switch (ncomp) {\n            case 4: /* fallthrough */\n            case 3: linear[2] = scanline[x*ncomp + 2];\n                    linear[1] = scanline[x*ncomp + 1];\n                    linear[0] = scanline[x*ncomp + 0];\n                    break;\n            default:\n                    linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0];\n                    break;\n         }\n         stbiw__linear_to_rgbe(rgbe, linear);\n         s->func(s->context, rgbe, 4);\n      }\n   } else {\n      int c,r;\n      /* encode into scratch buffer */\n      for (x=0; x < width; x++) {\n         switch(ncomp) {\n            case 4: /* fallthrough */\n            case 3: linear[2] = scanline[x*ncomp + 2];\n                    linear[1] = scanline[x*ncomp + 1];\n                    linear[0] = scanline[x*ncomp + 0];\n                    break;\n            default:\n                    linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0];\n                    break;\n         }\n         stbiw__linear_to_rgbe(rgbe, linear);\n         scratch[x + width*0] = rgbe[0];\n         scratch[x + width*1] = rgbe[1];\n         scratch[x + width*2] = rgbe[2];\n         scratch[x + width*3] = rgbe[3];\n      }\n\n      s->func(s->context, scanlineheader, 4);\n\n      /* RLE each component separately */\n      for (c=0; c < 4; c++) {\n         unsigned char *comp = &scratch[width*c];\n\n         x = 0;\n         while (x < width) {\n            // find first run\n            r = x;\n            while (r+2 < width) {\n               if (comp[r] == comp[r+1] && comp[r] == comp[r+2])\n                  break;\n               ++r;\n            }\n            if (r+2 >= width)\n               r = width;\n            // dump up to first run\n            while (x < r) {\n               int len = r-x;\n               if (len > 128) len = 128;\n               stbiw__write_dump_data(s, len, &comp[x]);\n               x += len;\n            }\n            // if there's a run, output it\n            if (r+2 < width) { // same test as what we break out of in search loop, so only true if we break'd\n               // find next byte after run\n               while (r < width && comp[r] == comp[x])\n                  ++r;\n               // output run up to r\n               while (x < r) {\n                  int len = r-x;\n                  if (len > 127) len = 127;\n                  stbiw__write_run_data(s, len, comp[x]);\n                  x += len;\n               }\n            }\n         }\n      }\n   }\n}\n\nstatic int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, float *data)\n{\n   if (y <= 0 || x <= 0 || data == NULL)\n      return 0;\n   else {\n      // Each component is stored separately. Allocate scratch space for full output scanline.\n      unsigned char *scratch = (unsigned char *) STBIW_MALLOC(x*4);\n      int i, len;\n      char buffer[128];\n      char header[] = \"#?RADIANCE\\n# Written by stb_image_write.h\\nFORMAT=32-bit_rle_rgbe\\n\";\n      s->func(s->context, header, sizeof(header)-1);\n\n#ifdef __STDC_LIB_EXT1__\n      len = sprintf_s(buffer, sizeof(buffer), \"EXPOSURE=          1.0000000000000\\n\\n-Y %d +X %d\\n\", y, x);\n#else\n      len = sprintf(buffer, \"EXPOSURE=          1.0000000000000\\n\\n-Y %d +X %d\\n\", y, x);\n#endif\n      s->func(s->context, buffer, len);\n\n      for(i=0; i < y; i++)\n         stbiw__write_hdr_scanline(s, x, comp, scratch, data + comp*x*(stbi__flip_vertically_on_write ? y-1-i : i));\n      STBIW_FREE(scratch);\n      return 1;\n   }\n}\n\nSTBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const float *data)\n{\n   stbi__write_context s = { 0 };\n   stbi__start_write_callbacks(&s, func, context);\n   return stbi_write_hdr_core(&s, x, y, comp, (float *) data);\n}\n\nSTBIWDEF int stbi_write_hdr(char const *filename, int x, int y, int comp, const float *data)\n{\n   stbi__write_context s = { 0 };\n   if (stbi__start_write_file(&s,filename)) {\n      int r = stbi_write_hdr_core(&s, x, y, comp, (float *) data);\n      stbi__end_write_file(&s);\n      return r;\n   } else\n      return 0;\n}\n#endif // STBI_WRITE_NO_STDIO\n\n\n//////////////////////////////////////////////////////////////////////////////\n//\n// PNG writer\n//\n\n#ifndef STBIW_ZLIB_COMPRESS\n// stretchy buffer; stbiw__sbpush() == vector<>::push_back() -- stbiw__sbcount() == vector<>::size()\n#define stbiw__sbraw(a) ((int *) (void *) (a) - 2)\n#define stbiw__sbm(a)   stbiw__sbraw(a)[0]\n#define stbiw__sbn(a)   stbiw__sbraw(a)[1]\n\n#define stbiw__sbneedgrow(a,n)  ((a)==0 || stbiw__sbn(a)+n >= stbiw__sbm(a))\n#define stbiw__sbmaybegrow(a,n) (stbiw__sbneedgrow(a,(n)) ? stbiw__sbgrow(a,n) : 0)\n#define stbiw__sbgrow(a,n)  stbiw__sbgrowf((void **) &(a), (n), sizeof(*(a)))\n\n#define stbiw__sbpush(a, v)      (stbiw__sbmaybegrow(a,1), (a)[stbiw__sbn(a)++] = (v))\n#define stbiw__sbcount(a)        ((a) ? stbiw__sbn(a) : 0)\n#define stbiw__sbfree(a)         ((a) ? STBIW_FREE(stbiw__sbraw(a)),0 : 0)\n\nstatic void *stbiw__sbgrowf(void **arr, int increment, int itemsize)\n{\n   int m = *arr ? 2*stbiw__sbm(*arr)+increment : increment+1;\n   void *p = STBIW_REALLOC_SIZED(*arr ? stbiw__sbraw(*arr) : 0, *arr ? (stbiw__sbm(*arr)*itemsize + sizeof(int)*2) : 0, itemsize * m + sizeof(int)*2);\n   STBIW_ASSERT(p);\n   if (p) {\n      if (!*arr) ((int *) p)[1] = 0;\n      *arr = (void *) ((int *) p + 2);\n      stbiw__sbm(*arr) = m;\n   }\n   return *arr;\n}\n\nstatic unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount)\n{\n   while (*bitcount >= 8) {\n      stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer));\n      *bitbuffer >>= 8;\n      *bitcount -= 8;\n   }\n   return data;\n}\n\nstatic int stbiw__zlib_bitrev(int code, int codebits)\n{\n   int res=0;\n   while (codebits--) {\n      res = (res << 1) | (code & 1);\n      code >>= 1;\n   }\n   return res;\n}\n\nstatic unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit)\n{\n   int i;\n   for (i=0; i < limit && i < 258; ++i)\n      if (a[i] != b[i]) break;\n   return i;\n}\n\nstatic unsigned int stbiw__zhash(unsigned char *data)\n{\n   stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16);\n   hash ^= hash << 3;\n   hash += hash >> 5;\n   hash ^= hash << 4;\n   hash += hash >> 17;\n   hash ^= hash << 25;\n   hash += hash >> 6;\n   return hash;\n}\n\n#define stbiw__zlib_flush() (out = stbiw__zlib_flushf(out, &bitbuf, &bitcount))\n#define stbiw__zlib_add(code,codebits) \\\n      (bitbuf |= (code) << bitcount, bitcount += (codebits), stbiw__zlib_flush())\n#define stbiw__zlib_huffa(b,c)  stbiw__zlib_add(stbiw__zlib_bitrev(b,c),c)\n// default huffman tables\n#define stbiw__zlib_huff1(n)  stbiw__zlib_huffa(0x30 + (n), 8)\n#define stbiw__zlib_huff2(n)  stbiw__zlib_huffa(0x190 + (n)-144, 9)\n#define stbiw__zlib_huff3(n)  stbiw__zlib_huffa(0 + (n)-256,7)\n#define stbiw__zlib_huff4(n)  stbiw__zlib_huffa(0xc0 + (n)-280,8)\n#define stbiw__zlib_huff(n)  ((n) <= 143 ? stbiw__zlib_huff1(n) : (n) <= 255 ? stbiw__zlib_huff2(n) : (n) <= 279 ? stbiw__zlib_huff3(n) : stbiw__zlib_huff4(n))\n#define stbiw__zlib_huffb(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : stbiw__zlib_huff2(n))\n\n#define stbiw__ZHASH   16384\n\n#endif // STBIW_ZLIB_COMPRESS\n\nSTBIWDEF unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality)\n{\n#ifdef STBIW_ZLIB_COMPRESS\n   // user provided a zlib compress implementation, use that\n   return STBIW_ZLIB_COMPRESS(data, data_len, out_len, quality);\n#else // use builtin\n   static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 };\n   static unsigned char  lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4,  4,  5,  5,  5,  5,  0 };\n   static unsigned short distc[]   = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 };\n   static unsigned char  disteb[]  = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 };\n   unsigned int bitbuf=0;\n   int i,j, bitcount=0;\n   unsigned char *out = NULL;\n   unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(stbiw__ZHASH * sizeof(unsigned char**));\n   if (hash_table == NULL)\n      return NULL;\n   if (quality < 5) quality = 5;\n\n   stbiw__sbpush(out, 0x78);   // DEFLATE 32K window\n   stbiw__sbpush(out, 0x5e);   // FLEVEL = 1\n   stbiw__zlib_add(1,1);  // BFINAL = 1\n   stbiw__zlib_add(1,2);  // BTYPE = 1 -- fixed huffman\n\n   for (i=0; i < stbiw__ZHASH; ++i)\n      hash_table[i] = NULL;\n\n   i=0;\n   while (i < data_len-3) {\n      // hash next 3 bytes of data to be compressed\n      int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3;\n      unsigned char *bestloc = 0;\n      unsigned char **hlist = hash_table[h];\n      int n = stbiw__sbcount(hlist);\n      for (j=0; j < n; ++j) {\n         if (hlist[j]-data > i-32768) { // if entry lies within window\n            int d = stbiw__zlib_countm(hlist[j], data+i, data_len-i);\n            if (d >= best) { best=d; bestloc=hlist[j]; }\n         }\n      }\n      // when hash table entry is too long, delete half the entries\n      if (hash_table[h] && stbiw__sbn(hash_table[h]) == 2*quality) {\n         STBIW_MEMMOVE(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality);\n         stbiw__sbn(hash_table[h]) = quality;\n      }\n      stbiw__sbpush(hash_table[h],data+i);\n\n      if (bestloc) {\n         // \"lazy matching\" - check match at *next* byte, and if it's better, do cur byte as literal\n         h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1);\n         hlist = hash_table[h];\n         n = stbiw__sbcount(hlist);\n         for (j=0; j < n; ++j) {\n            if (hlist[j]-data > i-32767) {\n               int e = stbiw__zlib_countm(hlist[j], data+i+1, data_len-i-1);\n               if (e > best) { // if next match is better, bail on current match\n                  bestloc = NULL;\n                  break;\n               }\n            }\n         }\n      }\n\n      if (bestloc) {\n         int d = (int) (data+i - bestloc); // distance back\n         STBIW_ASSERT(d <= 32767 && best <= 258);\n         for (j=0; best > lengthc[j+1]-1; ++j);\n         stbiw__zlib_huff(j+257);\n         if (lengtheb[j]) stbiw__zlib_add(best - lengthc[j], lengtheb[j]);\n         for (j=0; d > distc[j+1]-1; ++j);\n         stbiw__zlib_add(stbiw__zlib_bitrev(j,5),5);\n         if (disteb[j]) stbiw__zlib_add(d - distc[j], disteb[j]);\n         i += best;\n      } else {\n         stbiw__zlib_huffb(data[i]);\n         ++i;\n      }\n   }\n   // write out final bytes\n   for (;i < data_len; ++i)\n      stbiw__zlib_huffb(data[i]);\n   stbiw__zlib_huff(256); // end of block\n   // pad with 0 bits to byte boundary\n   while (bitcount)\n      stbiw__zlib_add(0,1);\n\n   for (i=0; i < stbiw__ZHASH; ++i)\n      (void) stbiw__sbfree(hash_table[i]);\n   STBIW_FREE(hash_table);\n\n   // store uncompressed instead if compression was worse\n   if (stbiw__sbn(out) > data_len + 2 + ((data_len+32766)/32767)*5) {\n      stbiw__sbn(out) = 2;  // truncate to DEFLATE 32K window and FLEVEL = 1\n      for (j = 0; j < data_len;) {\n         int blocklen = data_len - j;\n         if (blocklen > 32767) blocklen = 32767;\n         stbiw__sbpush(out, data_len - j == blocklen); // BFINAL = ?, BTYPE = 0 -- no compression\n         stbiw__sbpush(out, STBIW_UCHAR(blocklen)); // LEN\n         stbiw__sbpush(out, STBIW_UCHAR(blocklen >> 8));\n         stbiw__sbpush(out, STBIW_UCHAR(~blocklen)); // NLEN\n         stbiw__sbpush(out, STBIW_UCHAR(~blocklen >> 8));\n         memcpy(out+stbiw__sbn(out), data+j, blocklen);\n         stbiw__sbn(out) += blocklen;\n         j += blocklen;\n      }\n   }\n\n   {\n      // compute adler32 on input\n      unsigned int s1=1, s2=0;\n      int blocklen = (int) (data_len % 5552);\n      j=0;\n      while (j < data_len) {\n         for (i=0; i < blocklen; ++i) { s1 += data[j+i]; s2 += s1; }\n         s1 %= 65521; s2 %= 65521;\n         j += blocklen;\n         blocklen = 5552;\n      }\n      stbiw__sbpush(out, STBIW_UCHAR(s2 >> 8));\n      stbiw__sbpush(out, STBIW_UCHAR(s2));\n      stbiw__sbpush(out, STBIW_UCHAR(s1 >> 8));\n      stbiw__sbpush(out, STBIW_UCHAR(s1));\n   }\n   *out_len = stbiw__sbn(out);\n   // make returned pointer freeable\n   STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len);\n   return (unsigned char *) stbiw__sbraw(out);\n#endif // STBIW_ZLIB_COMPRESS\n}\n\nstatic unsigned int stbiw__crc32(unsigned char *buffer, int len)\n{\n#ifdef STBIW_CRC32\n    return STBIW_CRC32(buffer, len);\n#else\n   static unsigned int crc_table[256] =\n   {\n      0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,\n      0x0eDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,\n      0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,\n      0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,\n      0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,\n      0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,\n      0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,\n      0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,\n      0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,\n      0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,\n      0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,\n      0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,\n      0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,\n      0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,\n      0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,\n      0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,\n      0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,\n      0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,\n      0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,\n      0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,\n      0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,\n      0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,\n      0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,\n      0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,\n      0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,\n      0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,\n      0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,\n      0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,\n      0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,\n      0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,\n      0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,\n      0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D\n   };\n\n   unsigned int crc = ~0u;\n   int i;\n   for (i=0; i < len; ++i)\n      crc = (crc >> 8) ^ crc_table[buffer[i] ^ (crc & 0xff)];\n   return ~crc;\n#endif\n}\n\n#define stbiw__wpng4(o,a,b,c,d) ((o)[0]=STBIW_UCHAR(a),(o)[1]=STBIW_UCHAR(b),(o)[2]=STBIW_UCHAR(c),(o)[3]=STBIW_UCHAR(d),(o)+=4)\n#define stbiw__wp32(data,v) stbiw__wpng4(data, (v)>>24,(v)>>16,(v)>>8,(v));\n#define stbiw__wptag(data,s) stbiw__wpng4(data, s[0],s[1],s[2],s[3])\n\nstatic void stbiw__wpcrc(unsigned char **data, int len)\n{\n   unsigned int crc = stbiw__crc32(*data - len - 4, len+4);\n   stbiw__wp32(*data, crc);\n}\n\nstatic unsigned char stbiw__paeth(int a, int b, int c)\n{\n   int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c);\n   if (pa <= pb && pa <= pc) return STBIW_UCHAR(a);\n   if (pb <= pc) return STBIW_UCHAR(b);\n   return STBIW_UCHAR(c);\n}\n\n// @OPTIMIZE: provide an option that always forces left-predict or paeth predict\nstatic void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int filter_type, signed char *line_buffer)\n{\n   static int mapping[] = { 0,1,2,3,4 };\n   static int firstmap[] = { 0,1,0,5,6 };\n   int *mymap = (y != 0) ? mapping : firstmap;\n   int i;\n   int type = mymap[filter_type];\n   unsigned char *z = pixels + stride_bytes * (stbi__flip_vertically_on_write ? height-1-y : y);\n   int signed_stride = stbi__flip_vertically_on_write ? -stride_bytes : stride_bytes;\n\n   if (type==0) {\n      memcpy(line_buffer, z, width*n);\n      return;\n   }\n\n   // first loop isn't optimized since it's just one pixel\n   for (i = 0; i < n; ++i) {\n      switch (type) {\n         case 1: line_buffer[i] = z[i]; break;\n         case 2: line_buffer[i] = z[i] - z[i-signed_stride]; break;\n         case 3: line_buffer[i] = z[i] - (z[i-signed_stride]>>1); break;\n         case 4: line_buffer[i] = (signed char) (z[i] - stbiw__paeth(0,z[i-signed_stride],0)); break;\n         case 5: line_buffer[i] = z[i]; break;\n         case 6: line_buffer[i] = z[i]; break;\n      }\n   }\n   switch (type) {\n      case 1: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-n]; break;\n      case 2: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break;\n      case 3: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-n] + z[i-signed_stride])>>1); break;\n      case 4: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], z[i-signed_stride], z[i-signed_stride-n]); break;\n      case 5: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-n]>>1); break;\n      case 6: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], 0,0); break;\n   }\n}\n\nSTBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len)\n{\n   int force_filter = stbi_write_force_png_filter;\n   int ctype[5] = { -1, 0, 4, 2, 6 };\n   unsigned char sig[8] = { 137,80,78,71,13,10,26,10 };\n   unsigned char *out,*o, *filt, *zlib;\n   signed char *line_buffer;\n   int j,zlen;\n\n   if (stride_bytes == 0)\n      stride_bytes = x * n;\n\n   if (force_filter >= 5) {\n      force_filter = -1;\n   }\n\n   filt = (unsigned char *) STBIW_MALLOC((x*n+1) * y); if (!filt) return 0;\n   line_buffer = (signed char *) STBIW_MALLOC(x * n); if (!line_buffer) { STBIW_FREE(filt); return 0; }\n   for (j=0; j < y; ++j) {\n      int filter_type;\n      if (force_filter > -1) {\n         filter_type = force_filter;\n         stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, force_filter, line_buffer);\n      } else { // Estimate the best filter by running through all of them:\n         int best_filter = 0, best_filter_val = 0x7fffffff, est, i;\n         for (filter_type = 0; filter_type < 5; filter_type++) {\n            stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, filter_type, line_buffer);\n\n            // Estimate the entropy of the line using this filter; the less, the better.\n            est = 0;\n            for (i = 0; i < x*n; ++i) {\n               est += abs((signed char) line_buffer[i]);\n            }\n            if (est < best_filter_val) {\n               best_filter_val = est;\n               best_filter = filter_type;\n            }\n         }\n         if (filter_type != best_filter) {  // If the last iteration already got us the best filter, don't redo it\n            stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, best_filter, line_buffer);\n            filter_type = best_filter;\n         }\n      }\n      // when we get here, filter_type contains the filter type, and line_buffer contains the data\n      filt[j*(x*n+1)] = (unsigned char) filter_type;\n      STBIW_MEMMOVE(filt+j*(x*n+1)+1, line_buffer, x*n);\n   }\n   STBIW_FREE(line_buffer);\n   zlib = stbi_zlib_compress(filt, y*( x*n+1), &zlen, stbi_write_png_compression_level);\n   STBIW_FREE(filt);\n   if (!zlib) return 0;\n\n   // each tag requires 12 bytes of overhead\n   out = (unsigned char *) STBIW_MALLOC(8 + 12+13 + 12+zlen + 12);\n   if (!out) return 0;\n   *out_len = 8 + 12+13 + 12+zlen + 12;\n\n   o=out;\n   STBIW_MEMMOVE(o,sig,8); o+= 8;\n   stbiw__wp32(o, 13); // header length\n   stbiw__wptag(o, \"IHDR\");\n   stbiw__wp32(o, x);\n   stbiw__wp32(o, y);\n   *o++ = 8;\n   *o++ = STBIW_UCHAR(ctype[n]);\n   *o++ = 0;\n   *o++ = 0;\n   *o++ = 0;\n   stbiw__wpcrc(&o,13);\n\n   stbiw__wp32(o, zlen);\n   stbiw__wptag(o, \"IDAT\");\n   STBIW_MEMMOVE(o, zlib, zlen);\n   o += zlen;\n   STBIW_FREE(zlib);\n   stbiw__wpcrc(&o, zlen);\n\n   stbiw__wp32(o,0);\n   stbiw__wptag(o, \"IEND\");\n   stbiw__wpcrc(&o,0);\n\n   STBIW_ASSERT(o == out + *out_len);\n\n   return out;\n}\n\n#ifndef STBI_WRITE_NO_STDIO\nSTBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes)\n{\n   FILE *f;\n   int len;\n   unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len);\n   if (png == NULL) return 0;\n\n   f = stbiw__fopen(filename, \"wb\");\n   if (!f) { STBIW_FREE(png); return 0; }\n   fwrite(png, 1, len, f);\n   fclose(f);\n   STBIW_FREE(png);\n   return 1;\n}\n#endif\n\nSTBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes)\n{\n   int len;\n   unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len);\n   if (png == NULL) return 0;\n   func(context, png, len);\n   STBIW_FREE(png);\n   return 1;\n}\n\n\n/* ***************************************************************************\n *\n * JPEG writer\n *\n * This is based on Jon Olick's jo_jpeg.cpp:\n * public domain Simple, Minimalistic JPEG writer - http://www.jonolick.com/code.html\n */\n\nstatic const unsigned char stbiw__jpg_ZigZag[] = { 0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18,\n      24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63 };\n\nstatic void stbiw__jpg_writeBits(stbi__write_context *s, int *bitBufP, int *bitCntP, const unsigned short *bs) {\n   int bitBuf = *bitBufP, bitCnt = *bitCntP;\n   bitCnt += bs[1];\n   bitBuf |= bs[0] << (24 - bitCnt);\n   while(bitCnt >= 8) {\n      unsigned char c = (bitBuf >> 16) & 255;\n      stbiw__putc(s, c);\n      if(c == 255) {\n         stbiw__putc(s, 0);\n      }\n      bitBuf <<= 8;\n      bitCnt -= 8;\n   }\n   *bitBufP = bitBuf;\n   *bitCntP = bitCnt;\n}\n\nstatic void stbiw__jpg_DCT(float *d0p, float *d1p, float *d2p, float *d3p, float *d4p, float *d5p, float *d6p, float *d7p) {\n   float d0 = *d0p, d1 = *d1p, d2 = *d2p, d3 = *d3p, d4 = *d4p, d5 = *d5p, d6 = *d6p, d7 = *d7p;\n   float z1, z2, z3, z4, z5, z11, z13;\n\n   float tmp0 = d0 + d7;\n   float tmp7 = d0 - d7;\n   float tmp1 = d1 + d6;\n   float tmp6 = d1 - d6;\n   float tmp2 = d2 + d5;\n   float tmp5 = d2 - d5;\n   float tmp3 = d3 + d4;\n   float tmp4 = d3 - d4;\n\n   // Even part\n   float tmp10 = tmp0 + tmp3;   // phase 2\n   float tmp13 = tmp0 - tmp3;\n   float tmp11 = tmp1 + tmp2;\n   float tmp12 = tmp1 - tmp2;\n\n   d0 = tmp10 + tmp11;       // phase 3\n   d4 = tmp10 - tmp11;\n\n   z1 = (tmp12 + tmp13) * 0.707106781f; // c4\n   d2 = tmp13 + z1;       // phase 5\n   d6 = tmp13 - z1;\n\n   // Odd part\n   tmp10 = tmp4 + tmp5;       // phase 2\n   tmp11 = tmp5 + tmp6;\n   tmp12 = tmp6 + tmp7;\n\n   // The rotator is modified from fig 4-8 to avoid extra negations.\n   z5 = (tmp10 - tmp12) * 0.382683433f; // c6\n   z2 = tmp10 * 0.541196100f + z5; // c2-c6\n   z4 = tmp12 * 1.306562965f + z5; // c2+c6\n   z3 = tmp11 * 0.707106781f; // c4\n\n   z11 = tmp7 + z3;      // phase 5\n   z13 = tmp7 - z3;\n\n   *d5p = z13 + z2;         // phase 6\n   *d3p = z13 - z2;\n   *d1p = z11 + z4;\n   *d7p = z11 - z4;\n\n   *d0p = d0;  *d2p = d2;  *d4p = d4;  *d6p = d6;\n}\n\nstatic void stbiw__jpg_calcBits(int val, unsigned short bits[2]) {\n   int tmp1 = val < 0 ? -val : val;\n   val = val < 0 ? val-1 : val;\n   bits[1] = 1;\n   while(tmp1 >>= 1) {\n      ++bits[1];\n   }\n   bits[0] = val & ((1<<bits[1])-1);\n}\n\nstatic int stbiw__jpg_processDU(stbi__write_context *s, int *bitBuf, int *bitCnt, float *CDU, int du_stride, float *fdtbl, int DC, const unsigned short HTDC[256][2], const unsigned short HTAC[256][2]) {\n   const unsigned short EOB[2] = { HTAC[0x00][0], HTAC[0x00][1] };\n   const unsigned short M16zeroes[2] = { HTAC[0xF0][0], HTAC[0xF0][1] };\n   int dataOff, i, j, n, diff, end0pos, x, y;\n   int DU[64];\n\n   // DCT rows\n   for(dataOff=0, n=du_stride*8; dataOff<n; dataOff+=du_stride) {\n      stbiw__jpg_DCT(&CDU[dataOff], &CDU[dataOff+1], &CDU[dataOff+2], &CDU[dataOff+3], &CDU[dataOff+4], &CDU[dataOff+5], &CDU[dataOff+6], &CDU[dataOff+7]);\n   }\n   // DCT columns\n   for(dataOff=0; dataOff<8; ++dataOff) {\n      stbiw__jpg_DCT(&CDU[dataOff], &CDU[dataOff+du_stride], &CDU[dataOff+du_stride*2], &CDU[dataOff+du_stride*3], &CDU[dataOff+du_stride*4],\n                     &CDU[dataOff+du_stride*5], &CDU[dataOff+du_stride*6], &CDU[dataOff+du_stride*7]);\n   }\n   // Quantize/descale/zigzag the coefficients\n   for(y = 0, j=0; y < 8; ++y) {\n      for(x = 0; x < 8; ++x,++j) {\n         float v;\n         i = y*du_stride+x;\n         v = CDU[i]*fdtbl[j];\n         // DU[stbiw__jpg_ZigZag[j]] = (int)(v < 0 ? ceilf(v - 0.5f) : floorf(v + 0.5f));\n         // ceilf() and floorf() are C99, not C89, but I /think/ they're not needed here anyway?\n         DU[stbiw__jpg_ZigZag[j]] = (int)(v < 0 ? v - 0.5f : v + 0.5f);\n      }\n   }\n\n   // Encode DC\n   diff = DU[0] - DC;\n   if (diff == 0) {\n      stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTDC[0]);\n   } else {\n      unsigned short bits[2];\n      stbiw__jpg_calcBits(diff, bits);\n      stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTDC[bits[1]]);\n      stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits);\n   }\n   // Encode ACs\n   end0pos = 63;\n   for(; (end0pos>0)&&(DU[end0pos]==0); --end0pos) {\n   }\n   // end0pos = first element in reverse order !=0\n   if(end0pos == 0) {\n      stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB);\n      return DU[0];\n   }\n   for(i = 1; i <= end0pos; ++i) {\n      int startpos = i;\n      int nrzeroes;\n      unsigned short bits[2];\n      for (; DU[i]==0 && i<=end0pos; ++i) {\n      }\n      nrzeroes = i-startpos;\n      if ( nrzeroes >= 16 ) {\n         int lng = nrzeroes>>4;\n         int nrmarker;\n         for (nrmarker=1; nrmarker <= lng; ++nrmarker)\n            stbiw__jpg_writeBits(s, bitBuf, bitCnt, M16zeroes);\n         nrzeroes &= 15;\n      }\n      stbiw__jpg_calcBits(DU[i], bits);\n      stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTAC[(nrzeroes<<4)+bits[1]]);\n      stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits);\n   }\n   if(end0pos != 63) {\n      stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB);\n   }\n   return DU[0];\n}\n\nstatic int stbi_write_jpg_core(stbi__write_context *s, int width, int height, int comp, const void* data, int quality) {\n   // Constants that don't pollute global namespace\n   static const unsigned char std_dc_luminance_nrcodes[] = {0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0};\n   static const unsigned char std_dc_luminance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11};\n   static const unsigned char std_ac_luminance_nrcodes[] = {0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d};\n   static const unsigned char std_ac_luminance_values[] = {\n      0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08,\n      0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0,0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28,\n      0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59,\n      0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89,\n      0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6,\n      0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2,\n      0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa\n   };\n   static const unsigned char std_dc_chrominance_nrcodes[] = {0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0};\n   static const unsigned char std_dc_chrominance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11};\n   static const unsigned char std_ac_chrominance_nrcodes[] = {0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77};\n   static const unsigned char std_ac_chrominance_values[] = {\n      0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91,\n      0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0,0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26,\n      0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,\n      0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87,\n      0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,\n      0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,\n      0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa\n   };\n   // Huffman tables\n   static const unsigned short YDC_HT[256][2] = { {0,2},{2,3},{3,3},{4,3},{5,3},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9}};\n   static const unsigned short UVDC_HT[256][2] = { {0,2},{1,2},{2,2},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9},{1022,10},{2046,11}};\n   static const unsigned short YAC_HT[256][2] = {\n      {10,4},{0,2},{1,2},{4,3},{11,4},{26,5},{120,7},{248,8},{1014,10},{65410,16},{65411,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {12,4},{27,5},{121,7},{502,9},{2038,11},{65412,16},{65413,16},{65414,16},{65415,16},{65416,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {28,5},{249,8},{1015,10},{4084,12},{65417,16},{65418,16},{65419,16},{65420,16},{65421,16},{65422,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {58,6},{503,9},{4085,12},{65423,16},{65424,16},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {59,6},{1016,10},{65430,16},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {122,7},{2039,11},{65438,16},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {123,7},{4086,12},{65446,16},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {250,8},{4087,12},{65454,16},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {504,9},{32704,15},{65462,16},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {505,9},{65470,16},{65471,16},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {506,9},{65479,16},{65480,16},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {1017,10},{65488,16},{65489,16},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {1018,10},{65497,16},{65498,16},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {2040,11},{65506,16},{65507,16},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {65515,16},{65516,16},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {2041,11},{65525,16},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0}\n   };\n   static const unsigned short UVAC_HT[256][2] = {\n      {0,2},{1,2},{4,3},{10,4},{24,5},{25,5},{56,6},{120,7},{500,9},{1014,10},{4084,12},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {11,4},{57,6},{246,8},{501,9},{2038,11},{4085,12},{65416,16},{65417,16},{65418,16},{65419,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {26,5},{247,8},{1015,10},{4086,12},{32706,15},{65420,16},{65421,16},{65422,16},{65423,16},{65424,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {27,5},{248,8},{1016,10},{4087,12},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{65430,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {58,6},{502,9},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{65438,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {59,6},{1017,10},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{65446,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {121,7},{2039,11},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{65454,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {122,7},{2040,11},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{65462,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {249,8},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{65470,16},{65471,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {503,9},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{65479,16},{65480,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {504,9},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{65488,16},{65489,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {505,9},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{65497,16},{65498,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {506,9},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{65506,16},{65507,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {2041,11},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{65515,16},{65516,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {16352,14},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{65525,16},{0,0},{0,0},{0,0},{0,0},{0,0},\n      {1018,10},{32707,15},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0}\n   };\n   static const int YQT[] = {16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22,\n                             37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99};\n   static const int UVQT[] = {17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99,\n                              99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99};\n   static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f,\n                                 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f };\n\n   int row, col, i, k, subsample;\n   float fdtbl_Y[64], fdtbl_UV[64];\n   unsigned char YTable[64], UVTable[64];\n\n   if(!data || !width || !height || comp > 4 || comp < 1) {\n      return 0;\n   }\n\n   quality = quality ? quality : 90;\n   subsample = quality <= 90 ? 1 : 0;\n   quality = quality < 1 ? 1 : quality > 100 ? 100 : quality;\n   quality = quality < 50 ? 5000 / quality : 200 - quality * 2;\n\n   for(i = 0; i < 64; ++i) {\n      int uvti, yti = (YQT[i]*quality+50)/100;\n      YTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (yti < 1 ? 1 : yti > 255 ? 255 : yti);\n      uvti = (UVQT[i]*quality+50)/100;\n      UVTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (uvti < 1 ? 1 : uvti > 255 ? 255 : uvti);\n   }\n\n   for(row = 0, k = 0; row < 8; ++row) {\n      for(col = 0; col < 8; ++col, ++k) {\n         fdtbl_Y[k]  = 1 / (YTable [stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]);\n         fdtbl_UV[k] = 1 / (UVTable[stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]);\n      }\n   }\n\n   // Write Headers\n   {\n      static const unsigned char head0[] = { 0xFF,0xD8,0xFF,0xE0,0,0x10,'J','F','I','F',0,1,1,0,0,1,0,1,0,0,0xFF,0xDB,0,0x84,0 };\n      static const unsigned char head2[] = { 0xFF,0xDA,0,0xC,3,1,0,2,0x11,3,0x11,0,0x3F,0 };\n      const unsigned char head1[] = { 0xFF,0xC0,0,0x11,8,(unsigned char)(height>>8),STBIW_UCHAR(height),(unsigned char)(width>>8),STBIW_UCHAR(width),\n                                      3,1,(unsigned char)(subsample?0x22:0x11),0,2,0x11,1,3,0x11,1,0xFF,0xC4,0x01,0xA2,0 };\n      s->func(s->context, (void*)head0, sizeof(head0));\n      s->func(s->context, (void*)YTable, sizeof(YTable));\n      stbiw__putc(s, 1);\n      s->func(s->context, UVTable, sizeof(UVTable));\n      s->func(s->context, (void*)head1, sizeof(head1));\n      s->func(s->context, (void*)(std_dc_luminance_nrcodes+1), sizeof(std_dc_luminance_nrcodes)-1);\n      s->func(s->context, (void*)std_dc_luminance_values, sizeof(std_dc_luminance_values));\n      stbiw__putc(s, 0x10); // HTYACinfo\n      s->func(s->context, (void*)(std_ac_luminance_nrcodes+1), sizeof(std_ac_luminance_nrcodes)-1);\n      s->func(s->context, (void*)std_ac_luminance_values, sizeof(std_ac_luminance_values));\n      stbiw__putc(s, 1); // HTUDCinfo\n      s->func(s->context, (void*)(std_dc_chrominance_nrcodes+1), sizeof(std_dc_chrominance_nrcodes)-1);\n      s->func(s->context, (void*)std_dc_chrominance_values, sizeof(std_dc_chrominance_values));\n      stbiw__putc(s, 0x11); // HTUACinfo\n      s->func(s->context, (void*)(std_ac_chrominance_nrcodes+1), sizeof(std_ac_chrominance_nrcodes)-1);\n      s->func(s->context, (void*)std_ac_chrominance_values, sizeof(std_ac_chrominance_values));\n      s->func(s->context, (void*)head2, sizeof(head2));\n   }\n\n   // Encode 8x8 macroblocks\n   {\n      static const unsigned short fillBits[] = {0x7F, 7};\n      int DCY=0, DCU=0, DCV=0;\n      int bitBuf=0, bitCnt=0;\n      // comp == 2 is grey+alpha (alpha is ignored)\n      int ofsG = comp > 2 ? 1 : 0, ofsB = comp > 2 ? 2 : 0;\n      const unsigned char *dataR = (const unsigned char *)data;\n      const unsigned char *dataG = dataR + ofsG;\n      const unsigned char *dataB = dataR + ofsB;\n      int x, y, pos;\n      if(subsample) {\n         for(y = 0; y < height; y += 16) {\n            for(x = 0; x < width; x += 16) {\n               float Y[256], U[256], V[256];\n               for(row = y, pos = 0; row < y+16; ++row) {\n                  // row >= height => use last input row\n                  int clamped_row = (row < height) ? row : height - 1;\n                  int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp;\n                  for(col = x; col < x+16; ++col, ++pos) {\n                     // if col >= width => use pixel from last input column\n                     int p = base_p + ((col < width) ? col : (width-1))*comp;\n                     float r = dataR[p], g = dataG[p], b = dataB[p];\n                     Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128;\n                     U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b;\n                     V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b;\n                  }\n               }\n               DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+0,   16, fdtbl_Y, DCY, YDC_HT, YAC_HT);\n               DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+8,   16, fdtbl_Y, DCY, YDC_HT, YAC_HT);\n               DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+128, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT);\n               DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+136, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT);\n\n               // subsample U,V\n               {\n                  float subU[64], subV[64];\n                  int yy, xx;\n                  for(yy = 0, pos = 0; yy < 8; ++yy) {\n                     for(xx = 0; xx < 8; ++xx, ++pos) {\n                        int j = yy*32+xx*2;\n                        subU[pos] = (U[j+0] + U[j+1] + U[j+16] + U[j+17]) * 0.25f;\n                        subV[pos] = (V[j+0] + V[j+1] + V[j+16] + V[j+17]) * 0.25f;\n                     }\n                  }\n                  DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subU, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT);\n                  DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subV, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT);\n               }\n            }\n         }\n      } else {\n         for(y = 0; y < height; y += 8) {\n            for(x = 0; x < width; x += 8) {\n               float Y[64], U[64], V[64];\n               for(row = y, pos = 0; row < y+8; ++row) {\n                  // row >= height => use last input row\n                  int clamped_row = (row < height) ? row : height - 1;\n                  int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp;\n                  for(col = x; col < x+8; ++col, ++pos) {\n                     // if col >= width => use pixel from last input column\n                     int p = base_p + ((col < width) ? col : (width-1))*comp;\n                     float r = dataR[p], g = dataG[p], b = dataB[p];\n                     Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128;\n                     U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b;\n                     V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b;\n                  }\n               }\n\n               DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y, 8, fdtbl_Y,  DCY, YDC_HT, YAC_HT);\n               DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, U, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT);\n               DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, V, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT);\n            }\n         }\n      }\n\n      // Do the bit alignment of the EOI marker\n      stbiw__jpg_writeBits(s, &bitBuf, &bitCnt, fillBits);\n   }\n\n   // EOI\n   stbiw__putc(s, 0xFF);\n   stbiw__putc(s, 0xD9);\n\n   return 1;\n}\n\nSTBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality)\n{\n   stbi__write_context s = { 0 };\n   stbi__start_write_callbacks(&s, func, context);\n   return stbi_write_jpg_core(&s, x, y, comp, (void *) data, quality);\n}\n\n\n#ifndef STBI_WRITE_NO_STDIO\nSTBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality)\n{\n   stbi__write_context s = { 0 };\n   if (stbi__start_write_file(&s,filename)) {\n      int r = stbi_write_jpg_core(&s, x, y, comp, data, quality);\n      stbi__end_write_file(&s);\n      return r;\n   } else\n      return 0;\n}\n#endif\n\n#endif // STB_IMAGE_WRITE_IMPLEMENTATION\n\n/* Revision history\n      1.16  (2021-07-11)\n             make Deflate code emit uncompressed blocks when it would otherwise expand\n             support writing BMPs with alpha channel\n      1.15  (2020-07-13) unknown\n      1.14  (2020-02-02) updated JPEG writer to downsample chroma channels\n      1.13\n      1.12\n      1.11  (2019-08-11)\n\n      1.10  (2019-02-07)\n             support utf8 filenames in Windows; fix warnings and platform ifdefs\n      1.09  (2018-02-11)\n             fix typo in zlib quality API, improve STB_I_W_STATIC in C++\n      1.08  (2018-01-29)\n             add stbi__flip_vertically_on_write, external zlib, zlib quality, choose PNG filter\n      1.07  (2017-07-24)\n             doc fix\n      1.06 (2017-07-23)\n             writing JPEG (using Jon Olick's code)\n      1.05   ???\n      1.04 (2017-03-03)\n             monochrome BMP expansion\n      1.03   ???\n      1.02 (2016-04-02)\n             avoid allocating large structures on the stack\n      1.01 (2016-01-16)\n             STBIW_REALLOC_SIZED: support allocators with no realloc support\n             avoid race-condition in crc initialization\n             minor compile issues\n      1.00 (2015-09-14)\n             installable file IO function\n      0.99 (2015-09-13)\n             warning fixes; TGA rle support\n      0.98 (2015-04-08)\n             added STBIW_MALLOC, STBIW_ASSERT etc\n      0.97 (2015-01-18)\n             fixed HDR asserts, rewrote HDR rle logic\n      0.96 (2015-01-17)\n             add HDR output\n             fix monochrome BMP\n      0.95 (2014-08-17)\n             add monochrome TGA output\n      0.94 (2014-05-31)\n             rename private functions to avoid conflicts with stb_image.h\n      0.93 (2014-05-27)\n             warning fixes\n      0.92 (2010-08-01)\n             casts to unsigned char to fix warnings\n      0.91 (2010-07-17)\n             first public release\n      0.90   first internal release\n*/\n\n/*\n------------------------------------------------------------------------------\nThis software is available under 2 licenses -- choose whichever you prefer.\n------------------------------------------------------------------------------\nALTERNATIVE A - MIT License\nCopyright (c) 2017 Sean Barrett\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n------------------------------------------------------------------------------\nALTERNATIVE B - Public Domain (www.unlicense.org)\nThis is free and unencumbered software released into the public domain.\nAnyone is free to copy, modify, publish, use, compile, sell, or distribute this\nsoftware, either in source code form or as a compiled binary, for any purpose,\ncommercial or non-commercial, and by any means.\nIn jurisdictions that recognize copyright laws, the author or authors of this\nsoftware dedicate any and all copyright interest in the software to the public\ndomain. We make this dedication for the benefit of the public at large and to\nthe detriment of our heirs and successors. We intend this dedication to be an\novert act of relinquishment in perpetuity of all present and future rights to\nthis software under copyright law.\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\nACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n------------------------------------------------------------------------------\n*/"
  },
  {
    "path": "src/stream.cpp",
    "content": "/**\n * @file src/stream.cpp\n * @brief Definitions for the streaming protocols.\n */\n#include \"process.h\"\n\n#include <future>\n#include <iomanip>\n#include <queue>\n#include <unordered_map>\n\n#include <fstream>\n#include <openssl/err.h>\n\n#include <boost/atomic.hpp>\n#include <boost/container/flat_map.hpp>\n#include <boost/endian/arithmetic.hpp>\n#include <boost/make_shared.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/thread/lock_guard.hpp>\n\n#include \"abr.h\"\n\nextern \"C\" {\n// clang-format off\n#include <moonlight-common-c/src/Limelight-internal.h>\n#include \"rswrapper.h\"\n// clang-format on\n}\n\n#ifndef DATA_SHARDS_MAX\n  #define DATA_SHARDS_MAX 255\n#endif\n\n#include \"config.h\"\n#include \"display_device/session.h\"\n#include \"globals.h\"\n#include \"rtsp.h\"\n#include \"input.h\"\n#include \"logging.h\"\n#include \"network.h\"\n#include \"stream.h\"\n#include \"sync.h\"\n#include \"system_tray.h\"\n#include \"thread_safe.h\"\n#include \"utility.h\"\n\n#include \"platform/common.h\"\n\n#define IDX_START_A 0\n#define IDX_START_B 1\n#define IDX_INVALIDATE_REF_FRAMES 2\n#define IDX_LOSS_STATS 3\n#define IDX_INPUT_DATA 5\n#define IDX_RUMBLE_DATA 6\n#define IDX_TERMINATION 7\n#define IDX_PERIODIC_PING 8\n#define IDX_REQUEST_IDR_FRAME 9\n#define IDX_ENCRYPTED 10\n#define IDX_HDR_MODE 11\n#define IDX_RUMBLE_TRIGGER_DATA 12\n#define IDX_SET_MOTION_EVENT 13\n#define IDX_SET_RGB_LED 14\n#define IDX_SET_ADAPTIVE_TRIGGERS 15\n#define IDX_MIC_DATA 16\n#define IDX_MIC_CONFIG 17\n#define IDX_DYNAMIC_PARAM_CHANGE 18  // 统一动态参数调整消息类型（支持码率、分辨率等）\n#define IDX_RESOLUTION_CHANGE 19  // 分辨率变化通知\n\nstatic const short packetTypes[] = {\n  0x0305,  // Start A\n  0x0307,  // Start B\n  0x0301,  // Invalidate reference frames\n  0x0201,  // Loss Stats\n  0x0204,  // Frame Stats (unused)\n  0x0206,  // Input data\n  0x010b,  // Rumble data\n  0x0109,  // Termination\n  0x0200,  // Periodic Ping\n  0x0302,  // IDR frame\n  0x0001,  // fully encrypted\n  0x010e,  // HDR mode\n  0x5500,  // Rumble triggers (Sunshine protocol extension)\n  0x5501,  // Set motion event (Sunshine protocol extension)\n  0x5502,  // Set RGB LED (Sunshine protocol extension)\n  0x5503,  // Set Adaptive triggers (Sunshine protocol extension)\n  0x5504,  // Microphone data (Sunshine protocol extension)\n  0x5505,  // Microphone config (Sunshine protocol extension)\n  0x5506,  // Dynamic parameter change (Sunshine protocol extension) - 统一动态参数调整\n  0x5507,  // Resolution change (Sunshine protocol extension) - 分辨率变化通知\n};\n\nnamespace asio = boost::asio;\nnamespace sys = boost::system;\n\nusing asio::ip::tcp;\nusing asio::ip::udp;\n\nusing namespace std::literals;\n\nnamespace stream {\n\n  enum class socket_e : int {\n    video,  ///< Video\n    audio,  ///< Audio\n    microphone,  ///< Microphone\n  };\n\n#pragma pack(push, 1)\n\n  struct video_short_frame_header_t {\n    uint8_t *\n    payload() {\n      return (uint8_t *) (this + 1);\n    }\n\n    std::uint8_t headerType;  // Always 0x01 for short headers\n\n    // Sunshine extension\n    // Frame processing latency, in 1/10 ms units\n    //     zero when the frame is repeated or there is no backend implementation\n    boost::endian::little_uint16_at frame_processing_latency;\n\n    // Currently known values:\n    // 1 = Normal P-frame\n    // 2 = IDR-frame\n    // 4 = P-frame with intra-refresh blocks\n    // 5 = P-frame after reference frame invalidation\n    std::uint8_t frameType;\n\n    // Length of the final packet payload for codecs that cannot handle\n    // zero padding, such as AV1 (Sunshine extension).\n    boost::endian::little_uint16_at lastPayloadLen;\n\n    std::uint8_t unknown[2];\n  };\n\n  static_assert(\n    sizeof(video_short_frame_header_t) == 8,\n    \"Short frame header must be 8 bytes\");\n\n  struct video_packet_raw_t {\n    uint8_t *\n    payload() {\n      return (uint8_t *) (this + 1);\n    }\n\n    RTP_PACKET rtp;\n    char reserved[4];\n\n    NV_VIDEO_PACKET packet;\n  };\n\n  struct video_packet_enc_prefix_t {\n    std::uint8_t iv[12];  // 12-byte IV is ideal for AES-GCM\n    std::uint32_t frameNumber;\n    std::uint8_t tag[16];\n  };\n\n  struct audio_packet_t {\n    RTP_PACKET rtp;\n  };\n\n  struct control_header_v2 {\n    std::uint16_t type;\n    std::uint16_t payloadLength;\n\n    uint8_t *\n    payload() {\n      return (uint8_t *) (this + 1);\n    }\n  };\n\n  struct control_terminate_t {\n    control_header_v2 header;\n\n    std::uint32_t ec;\n  };\n\n  struct control_rumble_t {\n    control_header_v2 header;\n\n    std::uint32_t useless;\n\n    std::uint16_t id;\n    std::uint16_t lowfreq;\n    std::uint16_t highfreq;\n  };\n\n  struct control_rumble_triggers_t {\n    control_header_v2 header;\n\n    std::uint16_t id;\n    std::uint16_t left;\n    std::uint16_t right;\n  };\n\n  struct control_set_motion_event_t {\n    control_header_v2 header;\n\n    std::uint16_t id;\n    std::uint16_t reportrate;\n    std::uint8_t type;\n  };\n\n  struct control_set_rgb_led_t {\n    control_header_v2 header;\n\n    std::uint16_t id;\n    std::uint8_t r;\n    std::uint8_t g;\n    std::uint8_t b;\n  };\n\n  struct control_adaptive_triggers_t {\n    control_header_v2 header;\n\n    std::uint16_t id;\n    /**\n     * 0x04 - Right trigger\n     * 0x08 - Left trigger\n     */\n    std::uint8_t event_flags;\n    std::uint8_t type_left;\n    std::uint8_t type_right;\n    std::uint8_t left[DS_EFFECT_PAYLOAD_SIZE];\n    std::uint8_t right[DS_EFFECT_PAYLOAD_SIZE];\n  };\n\n  struct control_hdr_mode_t {\n    control_header_v2 header;\n\n    std::uint8_t enabled;\n\n    // Sunshine protocol extension\n    SS_HDR_METADATA metadata;\n  };\n\n  struct control_resolution_change_t {\n    control_header_v2 header;\n\n    std::uint32_t width;\n    std::uint32_t height;\n  };\n\n  typedef struct control_encrypted_t {\n    std::uint16_t encryptedHeaderType;  // Always LE 0x0001\n    std::uint16_t length;  // sizeof(seq) + 16 byte tag + secondary header and data\n\n    // seq is accepted as an arbitrary value in Moonlight\n    std::uint32_t seq;  // Monotonically increasing sequence number (used as IV for AES-GCM)\n\n    uint8_t *\n    payload() {\n      return (uint8_t *) (this + 1);\n    }\n    // encrypted control_header_v2 and payload data follow\n  } *control_encrypted_p;\n\n  struct audio_fec_packet_t {\n    RTP_PACKET rtp;\n    AUDIO_FEC_HEADER fecHeader;\n  };\n\n  struct mic_packet_t {\n    RTP_PACKET rtp;\n  };\n\n  // 扩展的RTP包结构，支持16位包类型\n  struct rtp_packet_ext_t {\n    std::uint8_t header;\n    std::uint16_t packetType;  // 16位包类型\n    std::uint16_t sequenceNumber;\n    std::uint32_t timestamp;\n    std::uint32_t ssrc;\n  };\n\n#pragma pack(pop)\n\n  constexpr std::size_t\n  round_to_pkcs7_padded(std::size_t size) {\n    return ((size + 15) / 16) * 16;\n  }\n  constexpr std::size_t MAX_AUDIO_PACKET_SIZE = 1400;\n\n  using audio_aes_t = std::array<char, round_to_pkcs7_padded(MAX_AUDIO_PACKET_SIZE)>;\n\n  using av_session_id_t = std::variant<asio::ip::address, std::string>;  // IP address or SS-Ping-Payload from RTSP handshake\n  using message_queue_t = std::shared_ptr<safe::queue_t<std::pair<udp::endpoint, std::string>>>;\n  using message_queue_queue_t = std::shared_ptr<safe::queue_t<std::tuple<socket_e, av_session_id_t, message_queue_t>>>;\n\n  // return bytes written on success\n  // return -1 on error\n  static inline int\n  encode_audio(bool encrypted, const audio::buffer_t &plaintext, uint8_t *destination, crypto::aes_t &iv, crypto::cipher::cbc_t &cbc) {\n    // If encryption isn't enabled\n    if (!encrypted) {\n      std::copy(std::begin(plaintext), std::end(plaintext), destination);\n      return plaintext.size();\n    }\n\n    return cbc.encrypt(std::string_view { (char *) std::begin(plaintext), plaintext.size() }, destination, &iv);\n  }\n\n  static inline void\n  while_starting_do_nothing(std::atomic<session::state_e> &state) {\n    while (state.load(std::memory_order_acquire) == session::state_e::STARTING) {\n      std::this_thread::sleep_for(1ms);\n    }\n  }\n\n  class control_server_t {\n  public:\n    int\n    bind(net::af_e address_family, std::uint16_t port) {\n      _host = net::host_create(address_family, _addr, port);\n\n      return !(bool) _host;\n    }\n\n    // Get session associated with address.\n    // If none are found, try to find a session not yet claimed. (It will be marked by a port of value 0\n    // If none of those are found, return nullptr\n    session_t *\n    get_session(const net::peer_t peer, uint32_t connect_data);\n\n    // Circular dependency:\n    //   iterate refers to session\n    //   session refers to broadcast_ctx_t\n    //   broadcast_ctx_t refers to control_server_t\n    // Therefore, iterate is implemented further down the source file\n    void\n    iterate(std::chrono::milliseconds timeout);\n\n    /**\n     * @brief Call the handler for a given control stream message.\n     * @param type The message type.\n     * @param session The session the message was received on.\n     * @param payload The payload of the message.\n     * @param reinjected `true` if this message is being reprocessed after decryption.\n     */\n    void\n    call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected);\n\n    void\n    map(uint16_t type, std::function<void(session_t *, const std::string_view &)> cb) {\n      _map_type_cb.emplace(type, std::move(cb));\n    }\n\n    int\n    send(const std::string_view &payload, net::peer_t peer) {\n      auto packet = enet_packet_create(payload.data(), payload.size(), ENET_PACKET_FLAG_RELIABLE);\n      if (enet_peer_send(peer, 0, packet)) {\n        enet_packet_destroy(packet);\n\n        return -1;\n      }\n\n      return 0;\n    }\n\n    void\n    flush() {\n      enet_host_flush(_host.get());\n    }\n\n    // Callbacks\n    std::unordered_map<std::uint16_t, std::function<void(session_t *, const std::string_view &)>> _map_type_cb;\n\n    // All active sessions (including those still waiting for a peer to connect)\n    sync_util::sync_t<std::vector<session_t *>> _sessions;\n\n    // ENet peer to session mapping for sessions with a peer connected\n    sync_util::sync_t<std::unordered_map<net::peer_t, session_t *>> _peer_to_session;\n\n    ENetAddress _addr;\n    net::host_t _host;\n  };\n\n  struct broadcast_ctx_t {\n    message_queue_queue_t message_queue_queue;\n\n    std::thread recv_thread;\n    std::thread video_thread;\n    std::thread audio_thread;\n    std::thread control_thread;\n    std::thread mic_thread;\n\n    asio::io_context io_context;\n    asio::io_context mic_io_context;\n\n    udp::socket video_sock { io_context };\n    udp::socket audio_sock { io_context };\n    udp::socket mic_sock { mic_io_context };\n\n    control_server_t control_server;\n\n    boost::atomic<bool> mic_socket_enabled { false };\n    boost::atomic<int> mic_sessions_count { 0 };  // 需要麦克风的会话数\n\n    // Per-client 麦克风加密上下文（以客户端 IP 为 key）\n    struct mic_cipher_ctx_t {\n      crypto::cipher::cbc_t cipher;\n      crypto::aes_t iv;\n\n      mic_cipher_ctx_t(const crypto::aes_t &key, bool padding, std::uint32_t avRiKeyId)\n          : cipher(key, padding), iv(16) {\n        // 初始化 IV：前 4 字节存储 baseIv（大端序）\n        // baseIv 对应客户端的 remoteInputAesIv 的前 4 字节\n        // avRiKeyId 就是 launch_session.iv 的前 4 字节（大端序），与 remoteInputAesIv 的前 4 字节相同\n        *(std::uint32_t *) iv.data() = util::endian::big<std::uint32_t>(avRiKeyId);\n        // 其余字节保持为 0（IV 是 16 字节，但只使用前 4 字节）\n        std::memset(iv.data() + 4, 0, 12);\n      }\n\n      mic_cipher_ctx_t(mic_cipher_ctx_t &&) noexcept = default;\n      mic_cipher_ctx_t &operator=(mic_cipher_ctx_t &&) noexcept = default;\n    };\n\n    // Per-client 加密表：IP -> shared_ptr<cipher_ctx>\n    // shared_ptr 允许在锁外安全使用 cipher_ctx（即使 map 中的条目被其他线程移除，\n    // 持有 shared_ptr 的线程仍可安全完成解密操作）\n    // 使用 boost::container::flat_map 获得更好的缓存局部性（客户端数 N≤5，线性扫描比哈希更快）\n    boost::container::flat_map<std::string, boost::shared_ptr<mic_cipher_ctx_t>> mic_ciphers;\n    boost::mutex mic_cipher_mutex;\n\n    // TODO: 未来版本应当强制启用麦克风加密，防止被窃听\n    boost::atomic<bool> mic_reject_plaintext { false };\n\n    std::map<std::string, std::string> client_ip_to_name;\n    boost::mutex client_name_mutex;\n  };\n\n  struct session_t {\n    config_t config;\n\n    safe::mail_t mail;\n\n    std::shared_ptr<input::input_t> input;\n\n    std::thread audioThread;\n    std::thread videoThread;\n\n    std::chrono::steady_clock::time_point pingTimeout;\n\n    safe::shared_t<broadcast_ctx_t>::ptr_t broadcast_ref;\n\n    boost::asio::ip::address localAddress;\n\n    // 添加客户端名称字段\n    std::string client_name;\n\n    struct {\n      std::string ping_payload;\n\n      int lowseq;\n      udp::endpoint peer;\n\n      std::optional<crypto::cipher::gcm_t> cipher;\n      std::uint64_t gcm_iv_counter;\n\n      safe::mail_raw_t::event_t<bool> idr_events;\n      safe::mail_raw_t::event_t<std::pair<int64_t, int64_t>> invalidate_ref_frames_events;\n      safe::mail_raw_t::event_t<video::dynamic_param_t> dynamic_param_change_events;  // 新增：动态参数调整事件\n\n      std::unique_ptr<platf::deinit_t> qos;\n    } video;\n\n    struct {\n      crypto::cipher::cbc_t cipher;\n      std::string ping_payload;\n\n      std::uint16_t sequenceNumber;\n      // avRiKeyId == util::endian::big(First (sizeof(avRiKeyId)) bytes of launch_session->iv)\n      std::uint32_t avRiKeyId;\n      std::uint32_t timestamp;\n      udp::endpoint peer;\n\n      util::buffer_t<char> shards;\n      util::buffer_t<uint8_t *> shards_p;\n\n      audio_fec_packet_t fec_packet;\n      std::unique_ptr<platf::deinit_t> qos;\n\n      bool enable_mic;\n    } audio;\n\n    struct {\n      crypto::cipher::gcm_t cipher;\n      crypto::aes_t legacy_input_enc_iv;  // Only used when the client doesn't support full control stream encryption\n      crypto::aes_t incoming_iv;\n      crypto::aes_t outgoing_iv;\n\n      std::uint32_t connect_data;  // Used for new clients with ML_FF_SESSION_ID_V1\n      std::string expected_peer_address;  // Only used for legacy clients without ML_FF_SESSION_ID_V1\n\n      net::peer_t peer;\n      std::uint32_t seq;\n\n      platf::feedback_queue_t feedback_queue;\n      safe::mail_raw_t::event_t<video::hdr_info_t> hdr_queue;\n      safe::mail_raw_t::event_t<std::pair<std::uint32_t, std::uint32_t>> resolution_change_queue;  // width, height\n    } control;\n\n    std::uint32_t launch_session_id;\n\n    // 保存 launch_session 的关键字段，用于动态参数更新\n    bool enable_sops { false };\n    bool enable_hdr { false };\n    float max_nits { 1000.0f };\n    float min_nits { 0.001f };\n    float max_full_nits { 1000.0f };\n\n    safe::mail_raw_t::event_t<bool> shutdown_event;\n    safe::signal_t controlEnd;\n\n    std::atomic<session::state_e> state;\n\n    // Current total bitrate for this session (including FEC overhead) in Kbps\n    // This is the user-configured bitrate, not the encoding bitrate\n    std::atomic<int> current_total_bitrate { 0 };\n\n    // 标识这是仅控制流会话（只作为输入设备，不传输视频/音频）\n    bool control_only { false };\n  };\n\n  /**\n   * First part of cipher must be struct of type control_encrypted_t\n   *\n   * returns empty string_view on failure\n   * returns string_view pointing to payload data\n   */\n  template <std::size_t max_payload_size>\n  static inline std::string_view\n  encode_control(session_t *session, const std::string_view &plaintext, std::array<std::uint8_t, max_payload_size> &tagged_cipher) {\n    static_assert(\n      max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size),\n      \"max_payload_size >= sizeof(control_encrypted_t) + sizeof(crypto::cipher::tag_size)\");\n\n    if (session->config.controlProtocolType != 13) {\n      return plaintext;\n    }\n\n    auto seq = session->control.seq++;\n\n    auto &iv = session->control.outgoing_iv;\n    if (session->config.encryptionFlagsEnabled & SS_ENC_CONTROL_V2) {\n      // We use the deterministic IV construction algorithm specified in NIST SP 800-38D\n      // Section 8.2.1. The sequence number is our \"invocation\" field and the 'CH' in the\n      // high bytes is the \"fixed\" field. Because each client provides their own unique\n      // key, our values in the fixed field need only uniquely identify each independent\n      // use of the client's key with AES-GCM in our code.\n      //\n      // The sequence number is 32 bits long which allows for 2^32 control stream messages\n      // to be sent to each client before the IV repeats.\n      iv.resize(12);\n      std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv));\n      iv[10] = 'H';  // Host originated\n      iv[11] = 'C';  // Control stream\n    }\n    else {\n      // Nvidia's old style encryption uses a 16-byte IV\n      iv.resize(16);\n\n      iv[0] = (std::uint8_t) seq;\n    }\n\n    auto packet = (control_encrypted_p) tagged_cipher.data();\n\n    auto bytes = session->control.cipher.encrypt(plaintext, packet->payload(), &iv);\n    if (bytes <= 0) {\n      BOOST_LOG(error) << \"Couldn't encrypt control data\"sv;\n      return {};\n    }\n\n    std::uint16_t packet_length = bytes + crypto::cipher::tag_size + sizeof(control_encrypted_t::seq);\n\n    packet->encryptedHeaderType = util::endian::little(0x0001);\n    packet->length = util::endian::little(packet_length);\n    packet->seq = util::endian::little(seq);\n\n    return std::string_view { (char *) tagged_cipher.data(), packet_length + sizeof(control_encrypted_t) - sizeof(control_encrypted_t::seq) };\n  }\n\n  /**\n   * @brief 确保麦克风 socket 处于打开状态。\n   * 如果 socket 已关闭（上次会话结束时被关闭），则重新 open + bind。\n   * @param ctx broadcast 上下文\n   * @return true 如果 socket 已打开或成功重新打开\n   */\n  bool\n  ensure_mic_sock_open(broadcast_ctx_t &ctx) {\n    if (ctx.mic_sock.is_open()) {\n      return true;\n    }\n\n    auto address_family = net::af_from_enum_string(config::sunshine.address_family);\n    auto protocol = address_family == net::IPV4 ? udp::v4() : udp::v6();\n    auto mic_port = net::map_port(MIC_STREAM_PORT);\n    boost::system::error_code ec;\n\n    ctx.mic_sock.open(protocol, ec);\n    if (ec) {\n      BOOST_LOG(error) << \"Couldn't re-open Microphone socket: \"sv << ec.message();\n      return false;\n    }\n\n    ctx.mic_sock.bind(udp::endpoint(protocol, mic_port), ec);\n    if (ec) {\n      BOOST_LOG(error) << \"Couldn't re-bind Microphone socket to port [\"sv << mic_port << \"]: \"sv << ec.message();\n      ctx.mic_sock.close();\n      return false;\n    }\n\n    BOOST_LOG(info) << \"Microphone socket re-opened on port \" << mic_port;\n    return true;\n  }\n\n  /**\n   * @brief 重置麦克风加密状态（清除所有客户端的加密上下文）。\n   * 在所有麦克风会话结束或 broadcast 结束时调用。\n   */\n  void\n  reset_mic_encryption(broadcast_ctx_t &ctx) {\n    boost::lock_guard<boost::mutex> lg(ctx.mic_cipher_mutex);\n    ctx.mic_ciphers.clear();\n  }\n\n  /**\n   * @brief 移除指定客户端的麦克风加密上下文。\n   * 在单个客户端会话结束时调用，不影响其他客户端的加密状态。\n   * @param ctx broadcast 上下文\n   * @param client_ip 客户端 IP 地址字符串\n   */\n  void\n  remove_mic_encryption(broadcast_ctx_t &ctx, const std::string &client_ip) {\n    boost::lock_guard<boost::mutex> lg(ctx.mic_cipher_mutex);\n    ctx.mic_ciphers.erase(client_ip);\n  }\n\n  /**\n   * @brief 为会话设置麦克风接收。\n   * 统一处理 mic_sessions_count 递增、socket 打开、加密上下文注册。\n   * 如果 socket 打开失败，回滚计数并跳过麦克风启用。\n   * @param session 当前会话\n   * @return true 如果麦克风设置成功\n   */\n  bool\n  setup_mic_for_session(session_t &session) {\n    auto &ctx = *session.broadcast_ref.get();\n\n    ctx.mic_sessions_count.fetch_add(1);\n\n    // 确保 mic socket 处于打开状态（上次会话结束时可能已关闭）\n    if (!ensure_mic_sock_open(ctx)) {\n      BOOST_LOG(error) << \"Failed to ensure mic socket is open, microphone will be unavailable for \" << session.client_name;\n      // 回滚计数 — socket 未打开不应算有效 mic 会话\n      ctx.mic_sessions_count.fetch_sub(1);\n      return false;\n    }\n\n    ctx.mic_socket_enabled.store(true);\n\n    // 注册客户端 IP → 名称映射（用于麦克风统计日志）\n    std::string client_ip = session.audio.peer.address().to_string();\n    {\n      boost::lock_guard<boost::mutex> lg(ctx.client_name_mutex);\n      ctx.client_ip_to_name[client_ip] = session.client_name;\n      BOOST_LOG(debug) << \"Registered client mapping: \" << client_ip << \" -> \" << session.client_name;\n    }\n\n    // 检查是否需要启用 MIC 加密\n    bool should_enable_mic_encryption = (session.config.encryptionFlagsEnabled & SS_ENC_MIC) != 0;\n    if (should_enable_mic_encryption) {\n      boost::lock_guard<boost::mutex> lg(ctx.mic_cipher_mutex);\n      // Per-client cipher：用当前会话的密钥为该客户端创建独立的加密上下文\n      ctx.mic_ciphers[client_ip] = boost::make_shared<broadcast_ctx_t::mic_cipher_ctx_t>(\n        session.audio.cipher.key, session.audio.cipher.padding, session.audio.avRiKeyId);\n      BOOST_LOG(info) << \"Client \" << session.client_name << \": Microphone encryption ENABLED (per-client cipher registered for \" << client_ip << \")\";\n    }\n    else {\n      // 该客户端未启用加密，移除其加密上下文（如果有的话）\n      remove_mic_encryption(ctx, client_ip);\n      BOOST_LOG(info) << \"Client \" << session.client_name << \": Microphone encryption DISABLED\";\n    }\n\n    return true;\n  }\n\n  int\n  start_broadcast(broadcast_ctx_t &ctx);\n  void\n  end_broadcast(broadcast_ctx_t &ctx);\n\n  static auto broadcast_shared = safe::make_shared<broadcast_ctx_t>(start_broadcast, end_broadcast);\n\n  session_t *\n  control_server_t::get_session(const net::peer_t peer, uint32_t connect_data) {\n    {\n      // Fast path - look up existing session by peer\n      auto lg = _peer_to_session.lock();\n      auto it = _peer_to_session->find(peer);\n      if (it != _peer_to_session->end()) {\n        return it->second;\n      }\n    }\n\n    // Slow path - process new session\n    TUPLE_2D(peer_port, peer_addr, platf::from_sockaddr_ex((sockaddr *) &peer->address.address));\n    auto lg = _sessions.lock();\n    for (auto pos = std::begin(*_sessions); pos != std::end(*_sessions); ++pos) {\n      auto session_p = *pos;\n\n      // Skip sessions that are already established\n      if (session_p->control.peer) {\n        continue;\n      }\n\n      // Identify the connection by the unique connect data if the client supports it.\n      // Only fall back to IP address matching for clients without session ID support.\n      if (session_p->config.mlFeatureFlags & ML_FF_SESSION_ID_V1) {\n        if (session_p->control.connect_data != connect_data) {\n          continue;\n        }\n        else {\n          BOOST_LOG(debug) << \"Initialized new control stream session by connect data match [v2]\"sv;\n        }\n      }\n      else {\n        if (session_p->control.expected_peer_address != peer_addr) {\n          continue;\n        }\n        else {\n          BOOST_LOG(debug) << \"Initialized new control stream session by IP address match [v1]\"sv;\n        }\n      }\n\n      // Once the control stream connection is established, RTSP session state can be torn down\n      rtsp_stream::launch_session_clear(session_p->launch_session_id);\n\n      session_p->control.peer = peer;\n\n      // Use the local address from the control connection as the source address\n      // for other communications to the client. This is necessary to ensure\n      // proper routing on multi-homed hosts.\n      auto local_address = platf::from_sockaddr((sockaddr *) &peer->localAddress.address);\n      session_p->localAddress = boost::asio::ip::make_address(local_address);\n\n      BOOST_LOG(debug) << \"Control local address [\"sv << local_address << ']';\n      BOOST_LOG(debug) << \"Control peer address [\"sv << peer_addr << ':' << peer_port << ']';\n\n      // Insert this into the map for O(1) lookups in the future\n      auto ptslg = _peer_to_session.lock();\n      _peer_to_session->emplace(peer, session_p);\n      return session_p;\n    }\n\n    return nullptr;\n  }\n\n  /**\n   * @brief Call the handler for a given control stream message.\n   * @param type The message type.\n   * @param session The session the message was received on.\n   * @param payload The payload of the message.\n   * @param reinjected `true` if this message is being reprocessed after decryption.\n   */\n  void\n  control_server_t::call(std::uint16_t type, session_t *session, const std::string_view &payload, bool reinjected) {\n    // If we are using the encrypted control stream protocol, drop any messages that come off the wire unencrypted\n    if (session->config.controlProtocolType == 13 && !reinjected && type != packetTypes[IDX_ENCRYPTED]) {\n      BOOST_LOG(error) << \"Dropping unencrypted message on encrypted control stream: \"sv << util::hex(type).to_string_view();\n      return;\n    }\n\n    auto cb = _map_type_cb.find(type);\n    if (cb == std::end(_map_type_cb)) {\n      BOOST_LOG(debug)\n        << \"type [Unknown] { \"sv << util::hex(type).to_string_view() << \" }\"sv << std::endl\n        << \"---data---\"sv << std::endl\n        << util::hex_vec(payload) << std::endl\n        << \"---end data---\"sv;\n    }\n    else {\n      cb->second(session, payload);\n    }\n  }\n\n  void\n  control_server_t::iterate(std::chrono::milliseconds timeout) {\n    ENetEvent event;\n    auto res = enet_host_service(_host.get(), &event, timeout.count());\n\n    if (res > 0) {\n      auto session = get_session(event.peer, event.data);\n      if (!session) {\n        BOOST_LOG(warning) << \"Rejected connection from [\"sv << platf::from_sockaddr((sockaddr *) &event.peer->address.address) << \"]: it's not properly set up\"sv;\n        enet_peer_disconnect_now(event.peer, 0);\n\n        return;\n      }\n\n      session->pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout;\n\n      switch (event.type) {\n        case ENET_EVENT_TYPE_RECEIVE: {\n          net::packet_t packet { event.packet };\n\n          auto type = *(std::uint16_t *) packet->data;\n          std::string_view payload { (char *) packet->data + sizeof(type), packet->dataLength - sizeof(type) };\n\n          call(type, session, payload, false);\n        } break;\n        case ENET_EVENT_TYPE_CONNECT:\n          BOOST_LOG(info) << \"CLIENT CONNECTED\"sv;\n          break;\n        case ENET_EVENT_TYPE_DISCONNECT:\n          BOOST_LOG(info) << \"CLIENT DISCONNECTED\"sv;\n          // No more clients to send video data to ^_^\n          if (session->state == session::state_e::RUNNING) {\n            session::stop(*session);\n          }\n          break;\n        case ENET_EVENT_TYPE_NONE:\n          break;\n      }\n    }\n  }\n\n  namespace fec {\n    using rs_t = util::safe_ptr<reed_solomon, [](reed_solomon *rs) { reed_solomon_release(rs); }>;\n\n    struct fec_t {\n      size_t data_shards;\n      size_t nr_shards;\n      size_t percentage;\n\n      size_t blocksize;\n      size_t prefixsize;\n      util::buffer_t<char> shards;\n      util::buffer_t<char> headers;\n      util::buffer_t<uint8_t *> shards_p;\n\n      std::vector<platf::buffer_descriptor_t> payload_buffers;\n\n      char *\n      data(size_t el) {\n        return (char *) shards_p[el];\n      }\n\n      char *\n      prefix(size_t el) {\n        return prefixsize ? &headers[el * prefixsize] : nullptr;\n      }\n\n      size_t\n      size() const {\n        return nr_shards;\n      }\n    };\n\n    static fec_t\n    encode(const std::string_view &payload, size_t blocksize, size_t fecpercentage, size_t minparityshards, size_t prefixsize) {\n      auto payload_size = payload.size();\n\n      auto pad = payload_size % blocksize != 0;\n\n      auto aligned_data_shards = payload_size / blocksize;\n      auto data_shards = aligned_data_shards + (pad ? 1 : 0);\n      auto parity_shards = (data_shards * fecpercentage + 99) / 100;\n\n      // increase the FEC percentage for this frame if the parity shard minimum is not met\n      if (parity_shards < minparityshards && fecpercentage != 0) {\n        parity_shards = minparityshards;\n        fecpercentage = (100 * parity_shards) / data_shards;\n\n        BOOST_LOG(verbose) << \"Increasing FEC percentage to \"sv << fecpercentage << \" to meet parity shard minimum\"sv << std::endl;\n      }\n\n      auto nr_shards = data_shards + parity_shards;\n\n      // If we need to store a zero-padded data shard, allocate that first to\n      // to keep the shards in order and reduce buffer fragmentation\n      auto parity_shard_offset = pad ? 1 : 0;\n      util::buffer_t<char> shards { (parity_shard_offset + parity_shards) * blocksize };\n      util::buffer_t<uint8_t *> shards_p { nr_shards };\n      std::vector<platf::buffer_descriptor_t> payload_buffers;\n      payload_buffers.reserve(2);\n\n      // Point into the payload buffer for all except the final padded data shard\n      auto next = std::begin(payload);\n      for (auto x = 0; x < aligned_data_shards; ++x) {\n        shards_p[x] = (uint8_t *) next;\n        next += blocksize;\n      }\n      payload_buffers.emplace_back(std::begin(payload), aligned_data_shards * blocksize);\n\n      // If the last data shard needs to be zero-padded, we must use the shards buffer\n      if (pad) {\n        shards_p[aligned_data_shards] = (uint8_t *) &shards[0];\n\n        // GCC doesn't figure out that std::copy_n() can be replaced with memcpy() here\n        // and ends up compiling a horribly slow element-by-element copy loop, so we\n        // help it by using memcpy()/memset() directly.\n        auto copy_len = std::min<size_t>(blocksize, std::end(payload) - next);\n        std::memcpy(shards_p[aligned_data_shards], next, copy_len);\n        if (copy_len < blocksize) {\n          // Zero any additional space after the end of the payload\n          std::memset(shards_p[aligned_data_shards] + copy_len, 0, blocksize - copy_len);\n        }\n      }\n\n      // Add a payload buffer describing the shard buffer\n      payload_buffers.emplace_back(std::begin(shards), shards.size());\n\n      if (fecpercentage != 0) {\n        // Point into our allocated buffer for the parity shards\n        for (auto x = 0; x < parity_shards; ++x) {\n          shards_p[data_shards + x] = (uint8_t *) &shards[(parity_shard_offset + x) * blocksize];\n        }\n\n        // packets = parity_shards + data_shards\n        rs_t rs { reed_solomon_new(data_shards, parity_shards) };\n\n        reed_solomon_encode(rs.get(), shards_p.begin(), nr_shards, blocksize);\n      }\n\n      return {\n        data_shards,\n        nr_shards,\n        fecpercentage,\n        blocksize,\n        prefixsize,\n        std::move(shards),\n        util::buffer_t<char> { nr_shards * prefixsize },\n        std::move(shards_p),\n        std::move(payload_buffers),\n      };\n    }\n  }  // namespace fec\n\n  /**\n   * @brief Combines two buffers and inserts new buffers at each slice boundary of the result.\n   * @param insert_size The number of bytes to insert.\n   * @param slice_size The number of bytes between insertions.\n   * @param data1 The first data buffer.\n   * @param data2 The second data buffer.\n   */\n  std::vector<uint8_t>\n  concat_and_insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data1, const std::string_view &data2) {\n    auto data_size = data1.size() + data2.size();\n    auto pad = data_size % slice_size != 0;\n    auto elements = data_size / slice_size + (pad ? 1 : 0);\n\n    std::vector<uint8_t> result;\n    result.resize(elements * insert_size + data_size);\n\n    auto next = std::begin(data1);\n    auto end = std::end(data1);\n    for (auto x = 0; x < elements; ++x) {\n      void *p = &result[x * (insert_size + slice_size)];\n\n      // For the last iteration, only copy to the end of the data\n      if (x == elements - 1) {\n        slice_size = data_size - (x * slice_size);\n      }\n\n      // Test if this slice will extend into the next buffer\n      if (next + slice_size > end) {\n        // Copy the first portion from the first buffer\n        auto copy_len = end - next;\n        std::copy(next, end, (char *) p + insert_size);\n\n        // Copy the remaining portion from the second buffer\n        next = std::begin(data2);\n        end = std::end(data2);\n        std::copy(next, next + (slice_size - copy_len), (char *) p + copy_len + insert_size);\n        next += slice_size - copy_len;\n      }\n      else {\n        std::copy(next, next + slice_size, (char *) p + insert_size);\n        next += slice_size;\n      }\n    }\n\n    return result;\n  }\n\n  std::vector<uint8_t>\n  replace(const std::string_view &original, const std::string_view &old, const std::string_view &_new) {\n    std::vector<uint8_t> replaced;\n    replaced.reserve(original.size() + _new.size() - old.size());\n\n    auto begin = std::begin(original);\n    auto end = std::end(original);\n    auto next = std::search(begin, end, std::begin(old), std::end(old));\n\n    std::copy(begin, next, std::back_inserter(replaced));\n    if (next != end) {\n      std::copy(std::begin(_new), std::end(_new), std::back_inserter(replaced));\n      std::copy(next + old.size(), end, std::back_inserter(replaced));\n    }\n\n    return replaced;\n  }\n\n  /**\n   * @brief Pass gamepad feedback data back to the client.\n   * @param session The session object.\n   * @param msg The message to pass.\n   * @return 0 on success.\n   */\n  int\n  send_feedback_msg(session_t *session, platf::gamepad_feedback_msg_t &msg) {\n    if (!session->control.peer) {\n      BOOST_LOG(warning) << \"Couldn't send gamepad feedback data, still waiting for PING from Moonlight\"sv;\n      // Still waiting for PING from Moonlight\n      return -1;\n    }\n\n    std::string payload;\n    if (msg.type == platf::gamepad_feedback_e::rumble) {\n      control_rumble_t plaintext;\n      plaintext.header.type = packetTypes[IDX_RUMBLE_DATA];\n      plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2);\n\n      auto &data = msg.data.rumble;\n\n      plaintext.useless = 0xC0FFEE;\n      plaintext.id = util::endian::little(msg.id);\n      plaintext.lowfreq = util::endian::little(data.lowfreq);\n      plaintext.highfreq = util::endian::little(data.highfreq);\n\n      BOOST_LOG(verbose) << \"Rumble: \"sv << msg.id << \" :: \"sv << util::hex(data.lowfreq).to_string_view() << \" :: \"sv << util::hex(data.highfreq).to_string_view();\n      std::array<std::uint8_t,\n        sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>\n        encrypted_payload;\n\n      payload = encode_control(session, util::view(plaintext), encrypted_payload);\n    }\n    else if (msg.type == platf::gamepad_feedback_e::rumble_triggers) {\n      control_rumble_triggers_t plaintext;\n      plaintext.header.type = packetTypes[IDX_RUMBLE_TRIGGER_DATA];\n      plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2);\n\n      auto &data = msg.data.rumble_triggers;\n\n      plaintext.id = util::endian::little(msg.id);\n      plaintext.left = util::endian::little(data.left_trigger);\n      plaintext.right = util::endian::little(data.right_trigger);\n\n      BOOST_LOG(verbose) << \"Rumble triggers: \"sv << msg.id << \" :: \"sv << util::hex(data.left_trigger).to_string_view() << \" :: \"sv << util::hex(data.right_trigger).to_string_view();\n      std::array<std::uint8_t,\n        sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>\n        encrypted_payload;\n\n      payload = encode_control(session, util::view(plaintext), encrypted_payload);\n    }\n    else if (msg.type == platf::gamepad_feedback_e::set_motion_event_state) {\n      control_set_motion_event_t plaintext;\n      plaintext.header.type = packetTypes[IDX_SET_MOTION_EVENT];\n      plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2);\n\n      auto &data = msg.data.motion_event_state;\n\n      plaintext.id = util::endian::little(msg.id);\n      plaintext.reportrate = util::endian::little(data.report_rate);\n      plaintext.type = data.motion_type;\n\n      BOOST_LOG(verbose) << \"Motion event state: \"sv << msg.id << \" :: \"sv << util::hex(data.report_rate).to_string_view() << \" :: \"sv << util::hex(data.motion_type).to_string_view();\n      std::array<std::uint8_t,\n        sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>\n        encrypted_payload;\n\n      payload = encode_control(session, util::view(plaintext), encrypted_payload);\n    }\n    else if (msg.type == platf::gamepad_feedback_e::set_rgb_led) {\n      control_set_rgb_led_t plaintext;\n      plaintext.header.type = packetTypes[IDX_SET_RGB_LED];\n      plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2);\n\n      auto &data = msg.data.rgb_led;\n\n      plaintext.id = util::endian::little(msg.id);\n      plaintext.r = data.r;\n      plaintext.g = data.g;\n      plaintext.b = data.b;\n\n      BOOST_LOG(verbose) << \"RGB: \"sv << msg.id << \" :: \"sv << util::hex(data.r).to_string_view() << util::hex(data.g).to_string_view() << util::hex(data.b).to_string_view();\n      std::array<std::uint8_t,\n        sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>\n        encrypted_payload;\n\n      payload = encode_control(session, util::view(plaintext), encrypted_payload);\n    }\n    else if (msg.type == platf::gamepad_feedback_e::set_adaptive_triggers) {\n      control_adaptive_triggers_t plaintext;\n      plaintext.header.type = packetTypes[IDX_SET_ADAPTIVE_TRIGGERS];\n      plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2);\n\n      plaintext.id = util::endian::little(msg.id);\n      plaintext.event_flags = msg.data.adaptive_triggers.event_flags;\n      plaintext.type_left = msg.data.adaptive_triggers.type_left;\n      std::ranges::copy(msg.data.adaptive_triggers.left, plaintext.left);\n      plaintext.type_right = msg.data.adaptive_triggers.type_right;\n      std::ranges::copy(msg.data.adaptive_triggers.right, plaintext.right);\n\n      std::array<std::uint8_t, sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>\n        encrypted_payload;\n\n      payload = encode_control(session, util::view(plaintext), encrypted_payload);\n    }\n    else {\n      BOOST_LOG(error) << \"Unknown gamepad feedback message type\"sv;\n      return -1;\n    }\n\n    if (session->broadcast_ref->control_server.send(payload, session->control.peer)) {\n      TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address));\n      BOOST_LOG(warning) << \"Couldn't send gamepad feedback to [\"sv << addr << ':' << port << ']';\n\n      return -1;\n    }\n\n    return 0;\n  }\n\n  int\n  send_hdr_mode(session_t *session, video::hdr_info_t hdr_info) {\n    if (!session->control.peer) {\n      BOOST_LOG(warning) << \"Couldn't send HDR mode, still waiting for PING from Moonlight\"sv;\n      // Still waiting for PING from Moonlight\n      return -1;\n    }\n\n    control_hdr_mode_t plaintext {};\n    plaintext.header.type = packetTypes[IDX_HDR_MODE];\n    plaintext.header.payloadLength = sizeof(control_hdr_mode_t) - sizeof(control_header_v2);\n\n    plaintext.enabled = hdr_info->enabled;\n    plaintext.metadata = hdr_info->metadata;\n\n    std::array<std::uint8_t,\n      sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>\n      encrypted_payload;\n\n    auto payload = encode_control(session, util::view(plaintext), encrypted_payload);\n    if (session->broadcast_ref->control_server.send(payload, session->control.peer)) {\n      TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address));\n      BOOST_LOG(warning) << \"Couldn't send HDR mode to [\"sv << addr << ':' << port << ']';\n\n      return -1;\n    }\n\n    BOOST_LOG(debug) << \"Sent HDR mode: \" << hdr_info->enabled;\n    return 0;\n  }\n\n  int\n  send_resolution_change(session_t *session, std::uint32_t width, std::uint32_t height) {\n    if (!session->control.peer) {\n      BOOST_LOG(warning) << \"Couldn't send resolution change, still waiting for PING from Moonlight\"sv;\n      // Still waiting for PING from Moonlight\n      return -1;\n    }\n\n    control_resolution_change_t plaintext {};\n    plaintext.header.type = packetTypes[IDX_RESOLUTION_CHANGE];\n    plaintext.header.payloadLength = sizeof(control_resolution_change_t) - sizeof(control_header_v2);\n\n    plaintext.width = util::endian::little(width);\n    plaintext.height = util::endian::little(height);\n\n    std::array<std::uint8_t,\n      sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>\n      encrypted_payload;\n\n    auto payload = encode_control(session, util::view(plaintext), encrypted_payload);\n    if (session->broadcast_ref->control_server.send(payload, session->control.peer)) {\n      TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address));\n      BOOST_LOG(warning) << \"Couldn't send resolution change to [\"sv << addr << ':' << port << ']';\n\n      return -1;\n    }\n\n    BOOST_LOG(debug) << \"Sent resolution change: \" << width << \"x\" << height;\n    return 0;\n  }\n\n  void\n  controlBroadcastThread(control_server_t *server) {\n    server->map(packetTypes[IDX_PERIODIC_PING], [](session_t *session, const std::string_view &payload) {\n      BOOST_LOG(verbose) << \"type [IDX_PERIODIC_PING]\"sv;\n    });\n\n    server->map(packetTypes[IDX_START_A], [&](session_t *session, const std::string_view &payload) {\n      BOOST_LOG(debug) << \"type [IDX_START_A]\"sv;\n    });\n\n    server->map(packetTypes[IDX_START_B], [&](session_t *session, const std::string_view &payload) {\n      BOOST_LOG(debug) << \"type [IDX_START_B]\"sv;\n    });\n\n    server->map(packetTypes[IDX_LOSS_STATS], [&](session_t *session, const std::string_view &payload) {\n      int32_t *stats = (int32_t *) payload.data();\n      auto count = stats[0];\n      std::chrono::milliseconds t { stats[1] };\n\n      auto lastGoodFrame = stats[3];\n\n      BOOST_LOG(verbose)\n        << \"type [IDX_LOSS_STATS]\"sv << std::endl\n        << \"---begin stats---\" << std::endl\n        << \"loss count since last report [\" << count << ']' << std::endl\n        << \"time in milli since last report [\" << t.count() << ']' << std::endl\n        << \"last good frame [\" << lastGoodFrame << ']' << std::endl\n        << \"---end stats---\";\n    });\n\n    server->map(packetTypes[IDX_REQUEST_IDR_FRAME], [&](session_t *session, const std::string_view &payload) {\n      BOOST_LOG(debug) << \"type [IDX_REQUEST_IDR_FRAME]\"sv;\n\n      session->video.idr_events->raise(true);\n    });\n\n    // 辅助函数：处理分辨率变更\n    auto handle_resolution_change = [](session_t *session, int new_width, int new_height) {\n      int old_width = session->config.monitor.width;\n      int old_height = session->config.monitor.height;\n      \n      BOOST_LOG(info) << \"Dynamic resolution change requested: \" << old_width << \"x\" << old_height \n                      << \" -> \" << new_width << \"x\" << new_height;\n\n      // 验证分辨率范围\n      constexpr int MAX_RESOLUTION = 16384;\n      if (new_width <= 0 || new_width > MAX_RESOLUTION || new_height <= 0 || new_height > MAX_RESOLUTION) {\n        BOOST_LOG(warning) << \"Invalid resolution value: \" << new_width << \"x\" << new_height;\n        return;\n      }\n\n      // 检查分辨率是否真的改变了\n      if (old_width == new_width && old_height == new_height) {\n        BOOST_LOG(debug) << \"Resolution unchanged, ignoring request\";\n        return;\n      }\n\n      // 检测是否是旋转导致的宽高互换（例如：1920x1080 -> 1080x1920）\n      bool is_rotation = (old_width == new_height && old_height == new_width);\n      if (is_rotation) {\n        BOOST_LOG(info) << \"Detected display rotation: width and height swapped\";\n      }\n\n      // 更新会话配置\n      session->config.monitor.width = new_width;\n      session->config.monitor.height = new_height;\n\n      // 创建临时的 launch_session_t 来更新显示设备配置\n      // 注意：必须按照结构体声明顺序初始化字段\n      rtsp_stream::launch_session_t temp_launch_session {};\n      temp_launch_session.id = session->launch_session_id;\n      temp_launch_session.client_name = session->client_name;\n      temp_launch_session.width = new_width;\n      temp_launch_session.height = new_height;\n      temp_launch_session.fps = session->config.monitor.framerate;\n      temp_launch_session.enable_hdr = session->enable_hdr;\n      temp_launch_session.enable_sops = session->enable_sops;\n      temp_launch_session.max_nits = session->max_nits;\n      temp_launch_session.min_nits = session->min_nits;\n      temp_launch_session.max_full_nits = session->max_full_nits;\n\n      // 更新显示设备配置（重新配置模式）\n      // 注意：这也会触发捕获端和编码器的重新初始化，以适配新的分辨率\n      if (is_rotation) {\n        BOOST_LOG(info) << \"Reconfiguring display device for rotation: \" << old_width << \"x\" << old_height \n                        << \" -> \" << new_width << \"x\" << new_height;\n      }\n      else {\n        BOOST_LOG(info) << \"Reconfiguring display device for new resolution: \" << old_width << \"x\" << old_height \n                        << \" -> \" << new_width << \"x\" << new_height;\n      }\n      \n      display_device::session_t::get().configure_display(config::video, temp_launch_session, true);\n\n      // 请求 IDR 帧以确保客户端能正确显示新分辨率\n      // 这对于旋转场景特别重要，因为宽高互换需要新的关键帧\n      session->video.idr_events->raise(true);\n\n      // 注意：编码器和触摸端口的更新会在捕获端重新初始化时自动处理\n      // - 编码器会在重新初始化时使用新的宽高（通过 config.monitor.width/height）\n      // - 触摸端口会在视频捕获循环中通过 make_port() 自动更新\n      BOOST_LOG(info) << \"Resolution change completed: \" << new_width << \"x\" << new_height \n                      << (is_rotation ? \" (rotation detected)\" : \"\");\n    };\n\n    // 统一动态参数更新协议 (IDX_DYNAMIC_PARAM_CHANGE)\n    // Payload 格式：\n    // - 参数类型 (int, 4字节): 0=分辨率, 1=FPS, 2=码率, 3=QP, 4=FEC, 5=预设, 6=自适应量化, 7=多遍编码, 8=VBV缓冲区\n    // - 参数值：\n    //   * 分辨率 (类型0): 2个int (8字节, width和height)\n    //   * FPS (类型1): 1个float (4字节)\n    //   * 其他单值参数（码率、QP等）: 1个int (4字节)\n    server->map(packetTypes[IDX_DYNAMIC_PARAM_CHANGE], [&, handle_resolution_change](session_t *session, const std::string_view &payload) {\n      BOOST_LOG(debug) << \"type [IDX_DYNAMIC_PARAM_CHANGE]\"sv;\n\n      constexpr size_t MIN_PAYLOAD_SIZE = sizeof(int);\n      if (payload.size() < MIN_PAYLOAD_SIZE) {\n        BOOST_LOG(warning) << \"Invalid payload size for dynamic param change. Expected at least \" \n                           << MIN_PAYLOAD_SIZE << \" bytes, got \" << payload.size();\n        return;\n      }\n\n      const int param_type = *reinterpret_cast<const int *>(payload.data());\n      \n      if (param_type < 0 || param_type >= static_cast<int>(video::dynamic_param_type_e::MAX_PARAM_TYPE)) {\n        BOOST_LOG(warning) << \"Invalid parameter type: \" << param_type;\n        return;\n      }\n\n      const auto param_type_enum = static_cast<video::dynamic_param_type_e>(param_type);\n      \n      // 处理分辨率变更（需要两个int值）\n      if (param_type_enum == video::dynamic_param_type_e::RESOLUTION) {\n        constexpr size_t RESOLUTION_PAYLOAD_SIZE = sizeof(int) * 3;  // 类型 + width + height\n        if (payload.size() < RESOLUTION_PAYLOAD_SIZE) {\n          BOOST_LOG(warning) << \"Invalid payload size for resolution change. Expected \" \n                             << RESOLUTION_PAYLOAD_SIZE << \" bytes, got \" << payload.size();\n          return;\n        }\n\n        const auto *resolution_data = reinterpret_cast<const int *>(payload.data());\n        handle_resolution_change(session, resolution_data[1], resolution_data[2]);\n        return;\n      }\n\n      // 处理FPS变更（需要float值）\n      if (param_type_enum == video::dynamic_param_type_e::FPS) {\n        constexpr size_t FPS_PAYLOAD_SIZE = sizeof(int) + sizeof(float);\n        if (payload.size() < FPS_PAYLOAD_SIZE) {\n          BOOST_LOG(warning) << \"Invalid payload size for FPS change. Expected \" \n                             << FPS_PAYLOAD_SIZE << \" bytes, got \" << payload.size();\n          return;\n        }\n\n        const float new_fps = *reinterpret_cast<const float *>(payload.data() + sizeof(int));\n        \n        if (new_fps <= 0.0f || new_fps > 1000.0f) {\n          BOOST_LOG(warning) << \"Invalid FPS value: \" << new_fps;\n          return;\n        }\n\n        session->config.monitor.framerate = static_cast<int>(new_fps);\n        \n        video::dynamic_param_t param;\n        param.type = video::dynamic_param_type_e::FPS;\n        param.value.float_value = new_fps;\n        param.valid = true;\n        session->video.dynamic_param_change_events->raise(param);\n        \n        BOOST_LOG(info) << \"Dynamic FPS change: \" << new_fps << \" fps\";\n        return;\n      }\n\n      // 处理其他单值参数（码率、QP等，使用int值）\n      constexpr size_t INT_PARAM_PAYLOAD_SIZE = sizeof(int) * 2;\n      if (payload.size() < INT_PARAM_PAYLOAD_SIZE) {\n        BOOST_LOG(warning) << \"Invalid payload size for dynamic param change. Expected at least \" \n                           << INT_PARAM_PAYLOAD_SIZE << \" bytes, got \" << payload.size();\n        return;\n      }\n\n      const int param_value = reinterpret_cast<const int *>(payload.data())[1];\n\n      video::dynamic_param_t param;\n      param.type = param_type_enum;\n      param.valid = true;\n\n      // 参数验证和处理的辅助lambda\n      auto validate_and_raise = [&](bool valid, auto value, const char *name, const char *unit = \"\") {\n        if (valid) {\n          if constexpr (std::is_same_v<decltype(value), bool>) {\n            param.value.bool_value = value;\n          } else {\n            param.value.int_value = value;\n          }\n          session->video.dynamic_param_change_events->raise(param);\n          BOOST_LOG(info) << \"Dynamic \" << name << \" change: \" << value << unit;\n          return true;\n        }\n        BOOST_LOG(warning) << \"Invalid \" << name << \" value: \" << param_value;\n        return false;\n      };\n\n      switch (param_type_enum) {\n        case video::dynamic_param_type_e::BITRATE:\n          if (validate_and_raise(param_value > 0 && param_value <= 800000, param_value, \"bitrate\", \" Kbps\")) {\n            session->current_total_bitrate = param_value;\n          }\n          break;\n        case video::dynamic_param_type_e::QP:\n          validate_and_raise(param_value >= 0 && param_value <= 51, param_value, \"QP\");\n          break;\n        case video::dynamic_param_type_e::FEC_PERCENTAGE:\n          validate_and_raise(param_value >= 0 && param_value <= 100, param_value, \"FEC percentage\", \"%\");\n          break;\n        case video::dynamic_param_type_e::ADAPTIVE_QUANTIZATION: {\n          bool enabled = (param_value != 0);\n          param.value.bool_value = enabled;\n          session->video.dynamic_param_change_events->raise(param);\n          BOOST_LOG(info) << \"Dynamic adaptive quantization change: \" << (enabled ? \"enabled\" : \"disabled\");\n          break;\n        }\n        case video::dynamic_param_type_e::MULTI_PASS:\n          validate_and_raise(param_value >= 0 && param_value <= 2, param_value, \"multi-pass\");\n          break;\n        case video::dynamic_param_type_e::VBV_BUFFER_SIZE:\n          validate_and_raise(param_value > 0, param_value, \"VBV buffer size\", \" Kbps\");\n          break;\n        case video::dynamic_param_type_e::PRESET:\n          param.value.int_value = param_value;\n          session->video.dynamic_param_change_events->raise(param);\n          BOOST_LOG(info) << \"Dynamic preset change: \" << param_value;\n          break;\n        default:\n          BOOST_LOG(warning) << \"Unsupported parameter type: \" << param_type;\n          break;\n      }\n    });\n\n    server->map(packetTypes[IDX_INVALIDATE_REF_FRAMES], [&](session_t *session, const std::string_view &payload) {\n      auto frames = (std::int64_t *) payload.data();\n      auto firstFrame = frames[0];\n      auto lastFrame = frames[1];\n\n      BOOST_LOG(debug)\n        << \"type [IDX_INVALIDATE_REF_FRAMES]\"sv << std::endl\n        << \"firstFrame [\" << firstFrame << ']' << std::endl\n        << \"lastFrame [\" << lastFrame << ']';\n\n      session->video.invalidate_ref_frames_events->raise(std::make_pair(firstFrame, lastFrame));\n    });\n\n    server->map(packetTypes[IDX_INPUT_DATA], [&](session_t *session, const std::string_view &payload) {\n      BOOST_LOG(debug) << \"type [IDX_INPUT_DATA]\"sv;\n\n      auto tagged_cipher_length = util::endian::big(*(int32_t *) payload.data());\n      std::string_view tagged_cipher { payload.data() + sizeof(tagged_cipher_length), (size_t) tagged_cipher_length };\n\n      std::vector<uint8_t> plaintext;\n\n      auto &cipher = session->control.cipher;\n      auto &iv = session->control.legacy_input_enc_iv;\n      if (cipher.decrypt(tagged_cipher, plaintext, &iv)) {\n        // something went wrong :(\n\n        BOOST_LOG(error) << \"Failed to verify tag\"sv;\n\n        session::stop(*session);\n        return;\n      }\n\n      if (tagged_cipher_length >= 16 + iv.size()) {\n        std::copy(payload.end() - 16, payload.end(), std::begin(iv));\n      }\n\n      input::passthrough(session->input, std::move(plaintext));\n    });\n\n    server->map(packetTypes[IDX_ENCRYPTED], [server](session_t *session, const std::string_view &payload) {\n      BOOST_LOG(verbose) << \"type [IDX_ENCRYPTED]\"sv;\n\n      auto header = (control_encrypted_p) (payload.data() - 2);\n\n      auto length = util::endian::little(header->length);\n      auto seq = util::endian::little(header->seq);\n\n      if (length < (16 + 4 + 4)) {\n        BOOST_LOG(warning) << \"Control: Runt packet\"sv;\n        return;\n      }\n\n      auto tagged_cipher_length = length - 4;\n      std::string_view tagged_cipher { (char *) header->payload(), (size_t) tagged_cipher_length };\n\n      auto &cipher = session->control.cipher;\n      auto &iv = session->control.incoming_iv;\n      if (session->config.encryptionFlagsEnabled & SS_ENC_CONTROL_V2) {\n        // We use the deterministic IV construction algorithm specified in NIST SP 800-38D\n        // Section 8.2.1. The sequence number is our \"invocation\" field and the 'CC' in the\n        // high bytes is the \"fixed\" field. Because each client provides their own unique\n        // key, our values in the fixed field need only uniquely identify each independent\n        // use of the client's key with AES-GCM in our code.\n        //\n        // The sequence number is 32 bits long which allows for 2^32 control stream messages\n        // to be received from each client before the IV repeats.\n        iv.resize(12);\n        std::copy_n((uint8_t *) &seq, sizeof(seq), std::begin(iv));\n        iv[10] = 'C';  // Client originated\n        iv[11] = 'C';  // Control stream\n      }\n      else {\n        // Nvidia's old style encryption uses a 16-byte IV\n        iv.resize(16);\n\n        iv[0] = (std::uint8_t) seq;\n      }\n\n      std::vector<uint8_t> plaintext;\n      if (cipher.decrypt(tagged_cipher, plaintext, &iv)) {\n        // something went wrong :(\n\n        BOOST_LOG(error) << \"Failed to verify tag\"sv;\n\n        session::stop(*session);\n        return;\n      }\n\n      auto type = *(std::uint16_t *) plaintext.data();\n      std::string_view next_payload { (char *) plaintext.data() + 4, plaintext.size() - 4 };\n\n      if (type == packetTypes[IDX_ENCRYPTED]) {\n        BOOST_LOG(error) << \"Bad packet type [IDX_ENCRYPTED] found\"sv;\n        session::stop(*session);\n        return;\n      }\n\n      // IDX_INPUT_DATA callback will attempt to decrypt unencrypted data, therefore we need pass it directly\n      if (type == packetTypes[IDX_INPUT_DATA]) {\n        plaintext.erase(std::begin(plaintext), std::begin(plaintext) + 4);\n        input::passthrough(session->input, std::move(plaintext));\n      }\n      else {\n        server->call(type, session, next_payload, true);\n      }\n    });\n\n    // This thread handles latency-sensitive control messages\n    platf::adjust_thread_priority(platf::thread_priority_e::critical);\n\n    // Check for both the full shutdown event and the shutdown event for this\n    // broadcast to ensure we can inform connected clients of our graceful\n    // termination when we shut down.\n    auto shutdown_event = mail::man->event<bool>(mail::shutdown);\n    auto broadcast_shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);\n    while (!shutdown_event->peek() && !broadcast_shutdown_event->peek()) {\n      bool has_session_awaiting_peer = false;\n\n      {\n        auto lg = server->_sessions.lock();\n\n        auto now = std::chrono::steady_clock::now();\n\n        KITTY_WHILE_LOOP(auto pos = std::begin(*server->_sessions), pos != std::end(*server->_sessions), {\n          // Don't perform additional session processing if we're shutting down\n          if (shutdown_event->peek() || broadcast_shutdown_event->peek()) {\n            break;\n          }\n\n          auto session = *pos;\n\n          if (now > session->pingTimeout) {\n            auto address = session->control.peer ? platf::from_sockaddr((sockaddr *) &session->control.peer->address.address) : session->control.expected_peer_address;\n            BOOST_LOG(info) << address << \": Ping Timeout\"sv;\n            session::stop(*session);\n          }\n\n          if (session->state.load(std::memory_order_acquire) == session::state_e::STOPPING) {\n            pos = server->_sessions->erase(pos);\n\n            if (session->control.peer) {\n              {\n                auto ptslg = server->_peer_to_session.lock();\n                server->_peer_to_session->erase(session->control.peer);\n              }\n\n              enet_peer_disconnect_now(session->control.peer, 0);\n            }\n\n            session->controlEnd.raise(true);\n            continue;\n          }\n\n          // Remember if we have a session that's waiting for a peer to connect to the\n          // control stream. This ensures the clients are properly notified even when\n          // the app terminates before they finish connecting.\n          if (!session->control.peer) {\n            has_session_awaiting_peer = true;\n          }\n          else {\n            auto &feedback_queue = session->control.feedback_queue;\n            while (feedback_queue->peek()) {\n              auto feedback_msg = feedback_queue->pop();\n\n              send_feedback_msg(session, *feedback_msg);\n            }\n\n            auto &hdr_queue = session->control.hdr_queue;\n            while (session->control.peer && hdr_queue->peek()) {\n              auto hdr_info = hdr_queue->pop();\n\n              send_hdr_mode(session, std::move(hdr_info));\n            }\n\n            auto &resolution_change_queue = session->control.resolution_change_queue;\n            while (session->control.peer && resolution_change_queue->peek()) {\n              auto resolution = resolution_change_queue->pop();\n              \n              if (resolution) {\n                send_resolution_change(session, resolution->first, resolution->second);\n              }\n            }\n          }\n\n          ++pos;\n        })\n      }\n\n      // Don't break until any pending sessions either expire or connect\n      if (proc::proc.running() == 0 && !has_session_awaiting_peer) {\n        BOOST_LOG(info) << \"Process terminated\"sv;\n        break;\n      }\n\n      server->iterate(150ms);\n    }\n\n    // Let all remaining connections know the server is shutting down\n    // reason: graceful termination\n    std::uint32_t reason = 0x80030023;\n\n    control_terminate_t plaintext;\n    plaintext.header.type = packetTypes[IDX_TERMINATION];\n    plaintext.header.payloadLength = sizeof(plaintext.ec);\n    plaintext.ec = util::endian::big<uint32_t>(reason);\n\n    std::array<std::uint8_t,\n      sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>\n      encrypted_payload;\n\n    auto lg = server->_sessions.lock();\n    for (auto pos = std::begin(*server->_sessions); pos != std::end(*server->_sessions); ++pos) {\n      auto session = *pos;\n\n      // We may not have gotten far enough to have an ENet connection yet\n      if (session->control.peer) {\n        auto payload = encode_control(session, util::view(plaintext), encrypted_payload);\n\n        if (server->send(payload, session->control.peer)) {\n          TUPLE_2D(port, addr, platf::from_sockaddr_ex((sockaddr *) &session->control.peer->address.address));\n          BOOST_LOG(warning) << \"Couldn't send termination code to [\"sv << addr << ':' << port << ']';\n        }\n      }\n\n      session->shutdown_event->raise(true);\n      session->controlEnd.raise(true);\n    }\n\n    server->flush();\n  }\n\n  void\n  micRecvThread(broadcast_ctx_t &ctx) {\n    auto broadcast_shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);\n    auto &mic_io = ctx.mic_io_context;\n\n    udp::endpoint peer;\n    std::array<char, 2048> mic_recv_buffer;\n    bool mic_device_initialized = false;\n\n    // 麦克风统计结构体（按客户端地址分组）\n    struct MicStats {\n      uint64_t total_packets = 0;\n      uint64_t decrypt_success = 0;\n      uint64_t decrypt_failed = 0;\n      uint64_t invalid_data = 0;\n    };\n    std::map<std::string, MicStats> client_stats;\n\n    // // SSRC验证辅助函数\n    // auto validate_mic_ssrc = [](uint32_t ssrc, const std::string &client_id) -> bool {\n    //   if (ssrc != MIC_PACKET_MAGIC) {\n    //     BOOST_LOG(warning) << \"Client \" << client_id << \" received invalid microphone packet type (SSRC: 0x\" \n    //                       << std::hex << ssrc << std::dec << \")\";\n    //     return false;\n    //   }\n    //   return true;\n    // };\n\n    auto process_audio_data = [&](const uint8_t *audio_data, size_t data_size, uint16_t sequence_number, const std::string &peer_addr, const std::string &client_ip) {\n      if (!ctx.mic_socket_enabled.load()) {\n        return;\n      }\n\n      // 更新统计\n      auto &stats = client_stats[peer_addr];\n      stats.total_packets++;\n\n      // 查找该客户端的 per-client 加密上下文\n      // 仅在锁内拷贝 shared_ptr，解密和写入在锁外进行\n      // 避免在持有 mutex 期间调用可能阻塞的 write_mic_data（含 WASAPI Sleep）\n      boost::shared_ptr<broadcast_ctx_t::mic_cipher_ctx_t> cipher_ctx;\n      {\n        boost::lock_guard<boost::mutex> lg(ctx.mic_cipher_mutex);\n        auto it = ctx.mic_ciphers.find(client_ip);\n        if (it != ctx.mic_ciphers.end()) {\n          cipher_ctx = it->second;\n        }\n      }\n      if (cipher_ctx) {\n          // 根据 sequenceNumber 更新 IV\n          // 客户端使用: baseIv[0:4] (Big Endian) + (sequenceNumber - 1) & 0xFFFF\n          // 这与音频加密不同，音频加密使用: avRiKeyId + sequenceNumber\n          // cipher_ctx->iv 的前 4 字节存储的是 baseIv（大端序），对应客户端的 remoteInputAesIv\n          crypto::aes_t current_iv(16);  // 确保是 16 字节\n          uint32_t baseIvVal = util::endian::big<std::uint32_t>(*(std::uint32_t *) cipher_ctx->iv.data());\n          // 服务端收到的 sequence_number 就是包里的实际值，直接使用即可（不需要减1），客户端减1是因为它的 sequenceNumber 变量在写入包后就递增了\n          uint32_t ivSeq = baseIvVal + (sequence_number & 0xFFFF);\n          *(std::uint32_t *) current_iv.data() = util::endian::big<std::uint32_t>(ivSeq);\n          // 确保后 12 字节为 0（客户端构建 IV 时后 12 字节也是 0）\n          std::memset(current_iv.data() + 4, 0, 12);\n          std::vector<std::uint8_t> plaintext;\n          std::string_view cipher_view((const char *) audio_data, data_size);\n          if (cipher_ctx->cipher.decrypt(cipher_view, plaintext, &current_iv) != 0) {\n            // 解密失败：可能是网络损坏包、IV不匹配、或密钥错误\n            stats.decrypt_failed++;\n            return;  // 丢弃数据包\n          }\n\n          stats.decrypt_success++;\n\n          if (plaintext.size() > 0) {\n            // 简单的有效性检查：Opus 数据不应该全是 0 或全是 0xFF\n            bool looks_valid = true;\n            if (plaintext.size() >= 4) {\n              uint8_t first_byte = plaintext[0];\n              uint8_t second_byte = plaintext[1];\n              uint8_t third_byte = plaintext[2];\n              uint8_t fourth_byte = plaintext[3];\n              bool all_zero = (first_byte == 0 && second_byte == 0 && third_byte == 0 && fourth_byte == 0);\n              bool all_ff = (first_byte == 0xFF && second_byte == 0xFF && third_byte == 0xFF && fourth_byte == 0xFF);\n              if (all_zero || all_ff) {\n                looks_valid = false;\n                stats.invalid_data++;\n              }\n            }\n            // 注意：如果 plaintext.size() < 4，无法验证，假设有效并继续处理\n\n            if (!looks_valid) {\n              return;  // 丢弃数据包\n            }\n          }\n\n          // 解密成功且数据看起来有效\n          audio::write_mic_data(plaintext.data(), plaintext.size(), sequence_number);\n          return;\n      }\n\n      // 该客户端没有注册加密上下文 — 视为明文数据\n\n      // 安全模式：拒绝明文数据\n      if (ctx.mic_reject_plaintext.load()) {\n        BOOST_LOG(warning) << \"Rejected plaintext microphone data (mic_reject_plaintext enabled)\";\n        stats.decrypt_failed++;\n        return;\n      }\n\n      // 未加密数据或加密未启用，直接处理\n      // 也要统计未加密数据\n      stats.decrypt_success++;  // 明文数据算作\"成功\"\n      audio::write_mic_data(audio_data, data_size, sequence_number);\n    };\n\n    std::function<void(const boost::system::error_code, size_t)> mic_recv_func;\n    mic_recv_func = [&](const boost::system::error_code &ec, size_t received_bytes) {\n      if (!ctx.mic_socket_enabled.load()) {\n        return;\n      }\n\n      // 致命错误（socket 已关闭/无效）：不重新注册接收，让 mic_io.run() 自然退出\n      if (ec) {\n        if (ec == boost::asio::error::operation_aborted ||\n            ec == boost::asio::error::bad_descriptor ||\n            ec == boost::system::errc::bad_file_descriptor ||\n            ec == boost::system::errc::not_a_socket) {\n          BOOST_LOG(debug) << \"Mic socket closed: \"sv << ec.message();\n          return;\n        }\n      }\n\n      // fail_guard：在此之后的任何 return 都会重新注册 async_receive_from\n      // 包括瞬态错误（connection_refused/reset）和数据处理\n      auto fg = util::fail_guard([&]() {\n        if (ctx.mic_socket_enabled.load()) {\n          ctx.mic_sock.async_receive_from(asio::buffer(mic_recv_buffer), peer, 0, mic_recv_func);\n        }\n      });\n\n      // 瞬态错误（connection_refused/reset）：记录但继续接收\n      // 这些通常是 ICMP 错误（客户端断开、端口不可达等），不应停止整个接收\n      if (ec) {\n        if (ec == boost::system::errc::connection_refused ||\n            ec == boost::system::errc::connection_reset) {\n          BOOST_LOG(debug) << \"Mic socket transient error (ignored): \"sv << ec.message();\n        }\n        else {\n          BOOST_LOG(error) << \"Mic socket error: \"sv << ec.message();\n        }\n        return;  // fail_guard 会重新注册接收\n      }\n\n      if (received_bytes < sizeof(RTP_PACKET)) {\n        return;\n      }\n\n      // 获取客户端标识：设备名拼接IP地址\n      std::string client_ip = peer.address().to_string();\n      std::string client_id;\n      {\n        boost::lock_guard<boost::mutex> lg(ctx.client_name_mutex);\n        auto it = ctx.client_ip_to_name.find(client_ip);\n        if (it != ctx.client_ip_to_name.end()) {\n          client_id = it->second + \"@\" + client_ip;  // 设备名@IP\n        } else {\n          client_id = \"@\" + client_ip;  // 回退到IP（未知设备名时）\n        }\n      }\n\n      // 尝试16位扩展包类型\n      if (received_bytes >= sizeof(rtp_packet_ext_t)) {\n        auto *header_ext = (rtp_packet_ext_t *) mic_recv_buffer.data();\n        if (header_ext->packetType == packetTypes[IDX_MIC_DATA]) {\n          size_t header_size = sizeof(rtp_packet_ext_t);\n          if (received_bytes > header_size) {\n            uint16_t sequence_number = util::endian::little(header_ext->sequenceNumber);\n            // uint32_t ssrc = util::endian::little(header_ext->ssrc);  // 小端序\n            // if (!validate_mic_ssrc(ssrc, client_id)) {\n            //   return;\n            // }\n            process_audio_data(reinterpret_cast<const uint8_t *>(mic_recv_buffer.data()) + header_size, received_bytes - header_size, sequence_number, client_id, client_ip);\n          }\n          return;\n        }\n      }\n\n      // 8位包类型\n      auto *header = (mic_packet_t *) mic_recv_buffer.data();\n      if (header->rtp.packetType == MIC_PACKET_TYPE_OPUS) {\n        size_t header_size = sizeof(mic_packet_t);\n        if (received_bytes > header_size) {\n          // 客户端按小端序发送序列号（MicrophoneStream.java 使用 LITTLE_ENDIAN）\n          // 服务端必须按小端序读取，否则会读错（比如 1 会读成 256）\n          uint16_t sequence_number = util::endian::little(header->rtp.sequenceNumber);\n          // uint32_t ssrc = util::endian::little(header->rtp.ssrc);  // 小端序\n          // if (!validate_mic_ssrc(ssrc, client_id)) {\n          //   return;\n          // }\n          size_t data_size = received_bytes - header_size;\n          \n          // BOOST_LOG(verbose) << \"Received MIC packet: total=\" << received_bytes \n          //                 << \" bytes, header=\" << header_size \n          //                 << \" bytes, data=\" << data_size \n          //                 << \" bytes, sequenceNumber=\" << sequence_number << \" (little-endian)\"\n          //                 << \" from \" << client_id;\n          process_audio_data(reinterpret_cast<const uint8_t *>(mic_recv_buffer.data()) + header_size, data_size, sequence_number, client_id, client_ip);\n        }\n      }\n    };\n\n    BOOST_LOG(debug) << \"Starting microphone receive thread\";\n\n    auto retry_delay = 300ms;  // 初始重试延迟，指数退避到最大5秒\n\n    while (!broadcast_shutdown_event->peek()) {\n      if (!ctx.mic_socket_enabled.load()) {\n        retry_delay = 300ms;  // 会话结束时重置延迟\n\n        // 重置设备初始化标志，下次会话重新初始化麦克风设备\n        // （处理音频设备在运行中被卸载/重装的情况）\n        if (mic_device_initialized) {\n          audio::release_mic_redirect_device();\n          mic_device_initialized = false;\n          BOOST_LOG(debug) << \"Microphone device released, will re-initialize on next session\";\n        }\n\n        std::this_thread::sleep_for(100ms);\n        continue;\n      }\n\n      // 延迟初始化麦克风设备\n      if (!mic_device_initialized) {\n        if (audio::init_mic_redirect_device() != 0) {\n          std::this_thread::sleep_for(retry_delay);\n          retry_delay = std::min(retry_delay * 2, 5000ms);  // 指数退避，最大5秒\n          continue;\n        }\n        mic_device_initialized = true;\n      }\n\n      ctx.mic_sock.async_receive_from(asio::buffer(mic_recv_buffer), peer, 0, mic_recv_func);\n\n      while (ctx.mic_socket_enabled.load() && !broadcast_shutdown_event->peek()) {\n        mic_io.run();\n      }\n      mic_io.restart();  // 重置 io_context，以便下次会话可以重新进入 mic_io.run()\n    }\n\n    if (mic_device_initialized) {\n      audio::release_mic_redirect_device();\n    }\n\n    // 打印所有客户端的麦克风解密统计\n    if (!client_stats.empty()) {\n      BOOST_LOG(info) << \"=== Microphone Decryption Stats Summary ===\";\n      for (const auto &[client, stats] : client_stats) {\n        if (stats.total_packets > 0) {\n          double success_rate = (double)stats.decrypt_success / stats.total_packets * 100.0;\n          BOOST_LOG(info) << \"Client \" << client << \": \"\n                         << \"total=\" << stats.total_packets\n                         << \", success=\" << stats.decrypt_success << \" (\" << std::fixed << std::setprecision(1) << success_rate << \"%)\"\n                         << \", failed=\" << stats.decrypt_failed\n                         << \", invalid=\" << stats.invalid_data;\n        }\n      }\n    }\n\n    BOOST_LOG(debug) << \"Microphone receive thread ended\";\n  }\n\n  void\n  recvThread(broadcast_ctx_t &ctx) {\n    std::unordered_map<av_session_id_t, message_queue_t> peer_to_video_session;\n    std::unordered_map<av_session_id_t, message_queue_t> peer_to_audio_session;\n\n    auto &video_sock = ctx.video_sock;\n    auto &audio_sock = ctx.audio_sock;\n    auto &message_queue_queue = ctx.message_queue_queue;\n    auto broadcast_shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);\n    auto &io = ctx.io_context;\n\n    udp::endpoint peer;\n    std::array<std::array<char, 2048>, 2> buffers;\n    std::array<std::function<void(const boost::system::error_code, size_t)>, 2> recv_funcs;\n\n    // 统一处理PING包逻辑\n    auto handle_ping = [](auto &session_map, auto &peer, auto &buf, size_t bytes, std::string_view type_str) {\n      try {\n        if (bytes == 4) {\n          if (auto it = session_map.find(peer.address()); it != std::end(session_map)) {\n            BOOST_LOG(debug) << \"RAISE: \"sv << peer.address().to_string() << ':' << peer.port() << \" :: \" << type_str;\n            it->second->raise(peer, std::string { buf.data(), bytes });\n          }\n        }\n        else if (bytes >= sizeof(SS_PING)) {\n          auto ping = (PSS_PING) buf.data();\n          if (auto it = session_map.find(std::string { ping->payload, sizeof(ping->payload) }); it != std::end(session_map)) {\n            BOOST_LOG(debug) << \"RAISE: \"sv << peer.address().to_string() << ':' << peer.port() << \" :: \" << type_str;\n            it->second->raise(peer, std::string { buf.data(), bytes });\n          }\n        }\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(error) << \"Error processing packet: \" << e.what();\n      }\n    };\n\n    // 更新会话映射\n    auto update_session_map = [](auto &message_queue_queue, auto &video_map, auto &audio_map) {\n      while (message_queue_queue->peek()) {\n        if (auto message_queue_opt = message_queue_queue->pop()) {\n          auto [socket_type, session_id, message_queue] = *message_queue_opt;\n          auto &target_map = socket_type == socket_e::video ? video_map :\n                                                              (socket_type == socket_e::audio ? audio_map : throw std::runtime_error(\"Unknown socket type\"));\n\n          if (message_queue) {\n            target_map.emplace(session_id, message_queue);\n          }\n          else {\n            target_map.erase(session_id);\n          }\n        }\n      }\n    };\n\n    // 初始化接收函数\n    auto init_recv_func = [&](auto &sock, size_t buf_idx, auto &session_map, std::string_view type_str) {\n      recv_funcs[buf_idx] = [&, buf_idx, type_str](const boost::system::error_code &ec, size_t bytes) {\n        // 静默处理正常关闭错误\n        if (ec == boost::asio::error::operation_aborted ||\n            ec == boost::asio::error::bad_descriptor) {\n          return;  // Socket已关闭，不重新调度\n        }\n\n        // 静默处理网络连接错误\n        if (ec == boost::system::errc::connection_refused ||\n            ec == boost::system::errc::connection_reset) {\n          return;  // 连接错误，不重新调度\n        }\n\n        // 如果有其他错误，记录并返回\n        if (ec) {\n          BOOST_LOG(error) << type_str << \" receive error: \"sv << ec.message();\n          return;  // 有错误，不重新调度\n        }\n\n        BOOST_LOG(verbose) << \"Recv: \"sv << peer.address().to_string() << ':' << peer.port() << \" :: \" << type_str;\n\n        update_session_map(message_queue_queue, peer_to_video_session, peer_to_audio_session);\n        if (bytes == 0) {\n          BOOST_LOG(warning) << \"Received empty packet\";\n          // 即使是空包，也继续接收\n        }\n        else {\n          handle_ping(session_map, peer, buffers[buf_idx], bytes, type_str);\n        }\n\n        // 只有在成功接收数据后才重新调度\n        try {\n          sock.async_receive_from(asio::buffer(buffers[buf_idx]), peer, 0, recv_funcs[buf_idx]);\n        }\n        catch (const std::exception &e) {\n          BOOST_LOG(error) << \"Failed to restart async receive: \" << e.what();\n        }\n      };\n    };\n\n    try {\n      init_recv_func(video_sock, 0, peer_to_video_session, \"VIDEO\");\n      init_recv_func(audio_sock, 1, peer_to_audio_session, \"AUDIO\");\n\n      video_sock.async_receive_from(asio::buffer(buffers[0]), peer, 0, recv_funcs[0]);\n      audio_sock.async_receive_from(asio::buffer(buffers[1]), peer, 0, recv_funcs[1]);\n\n      while (!broadcast_shutdown_event->peek()) {\n        io.run();\n      }\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(fatal) << \"recvThread exception: \" << e.what();\n    }\n  }\n\n  void\n  videoBroadcastThread(udp::socket &sock) {\n    auto shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);\n    auto packets = mail::man->queue<video::packet_t>(mail::video_packets);\n    auto video_epoch = std::chrono::steady_clock::now();\n\n    // Video traffic is sent on this thread\n    platf::adjust_thread_priority(platf::thread_priority_e::high);\n\n    logging::min_max_avg_periodic_logger<double> frame_processing_latency_logger(debug, \"Frame processing latency\", \"ms\");\n\n    logging::time_delta_periodic_logger frame_send_batch_latency_logger(debug, \"Network: each send_batch() latency\");\n    logging::time_delta_periodic_logger frame_fec_latency_logger(debug, \"Network: each FEC block latency\");\n    logging::time_delta_periodic_logger frame_network_latency_logger(debug, \"Network: frame's overall network latency\");\n\n    crypto::aes_t iv(12);\n\n    auto timer = platf::create_high_precision_timer();\n    if (!timer || !*timer) {\n      BOOST_LOG(error) << \"Failed to create timer, aborting video broadcast thread\";\n      return;\n    }\n\n    auto ratecontrol_next_frame_start = std::chrono::steady_clock::now();\n\n    while (auto packet = packets->pop()) {\n      if (shutdown_event->peek()) {\n        break;\n      }\n\n      frame_network_latency_logger.first_point_now();\n\n      auto session = (session_t *) packet->channel_data;\n      auto lowseq = session->video.lowseq;\n\n      std::string_view payload { (char *) packet->data(), packet->data_size() };\n      std::vector<uint8_t> payload_with_replacements;\n\n      // Apply replacements on the packet payload before performing any other operations.\n      // We need to know the final frame size to calculate the last packet size, and we\n      // must avoid matching replacements against the frame header or any other non-video\n      // part of the payload.\n      if (packet->is_idr() && packet->replacements) {\n        for (auto &replacement : *packet->replacements) {\n          auto frame_old = replacement.old;\n          auto frame_new = replacement._new;\n\n          payload_with_replacements = replace(payload, frame_old, frame_new);\n          payload = { (char *) payload_with_replacements.data(), payload_with_replacements.size() };\n        }\n      }\n\n      video_short_frame_header_t frame_header = {};\n      frame_header.headerType = 0x01;  // Short header type\n      frame_header.frameType = packet->is_idr()                     ? 2 :\n                               packet->after_ref_frame_invalidation ? 5 :\n                                                                      1;\n      frame_header.lastPayloadLen = (payload.size() + sizeof(frame_header)) % (session->config.packetsize - sizeof(NV_VIDEO_PACKET));\n      if (frame_header.lastPayloadLen == 0) {\n        frame_header.lastPayloadLen = session->config.packetsize - sizeof(NV_VIDEO_PACKET);\n      }\n\n      if (packet->frame_timestamp) {\n        auto duration_to_latency = [](const std::chrono::steady_clock::duration &duration) {\n          const auto duration_us = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();\n          return (uint16_t) std::clamp<decltype(duration_us)>((duration_us + 50) / 100, 0, std::numeric_limits<uint16_t>::max());\n        };\n\n        uint16_t latency = duration_to_latency(std::chrono::steady_clock::now() - *packet->frame_timestamp);\n        frame_header.frame_processing_latency = latency;\n        frame_processing_latency_logger.collect_and_log(latency / 10.);\n      }\n      else {\n        frame_header.frame_processing_latency = 0;\n      }\n\n      auto fecPercentage = config::stream.fec_percentage;\n\n      // Insert space for packet headers\n      auto blocksize = session->config.packetsize + MAX_RTP_HEADER_SIZE;\n      auto payload_blocksize = blocksize - sizeof(video_packet_raw_t);\n      auto payload_new = concat_and_insert(sizeof(video_packet_raw_t), payload_blocksize,\n        std::string_view { (char *) &frame_header, sizeof(frame_header) }, payload);\n\n      payload = std::string_view { (char *) payload_new.data(), payload_new.size() };\n\n      // There are 2 bits for FEC block count for a maximum of 4 FEC blocks\n      constexpr auto MAX_FEC_BLOCKS = 4;\n\n      // The max number of data shards per block is found by solving this system of equations for D:\n      // D = 255 - P\n      // P = D * F\n      // which results in the solution:\n      // D = 255 / (1 + F)\n      // multiplied by 100 since F is the percentage as an integer:\n      // D = (255 * 100) / (100 + F)\n      auto max_data_shards_per_fec_block = (DATA_SHARDS_MAX * 100) / (100 + fecPercentage);\n\n      // Compute the number of FEC blocks needed for this frame using the block size and max shards\n      auto max_data_per_fec_block = max_data_shards_per_fec_block * blocksize;\n      auto fec_blocks_needed = (payload.size() + (max_data_per_fec_block - 1)) / max_data_per_fec_block;\n\n      // If the number of FEC blocks needed exceeds the protocol limit, turn off FEC for this frame.\n      // For normal FEC percentages, this should only happen for enormous frames (over 800 packets at 20%).\n      if (fec_blocks_needed > MAX_FEC_BLOCKS) {\n        BOOST_LOG(warning) << \"Skipping FEC for abnormally large encoded frame (needed \"sv << fec_blocks_needed << \" FEC blocks)\"sv;\n        fecPercentage = 0;\n        fec_blocks_needed = MAX_FEC_BLOCKS;\n      }\n\n      std::array<std::string_view, MAX_FEC_BLOCKS> fec_blocks;\n      decltype(fec_blocks)::iterator\n        fec_blocks_begin = std::begin(fec_blocks),\n        fec_blocks_end = std::begin(fec_blocks) + fec_blocks_needed;\n\n      BOOST_LOG(verbose) << \"Generating \"sv << fec_blocks_needed << \" FEC blocks\"sv;\n\n      // Align individual FEC blocks to blocksize\n      auto unaligned_size = payload.size() / fec_blocks_needed;\n      auto aligned_size = ((unaligned_size + (blocksize - 1)) / blocksize) * blocksize;\n\n      // If we exceed the 10-bit FEC packet index (which means our frame exceeded 4096 packets),\n      // the frame will be unrecoverable. Log an error for this case.\n      if (aligned_size / blocksize >= 1024) {\n        BOOST_LOG(error) << \"Encoder produced a frame too large to send! Is the encoder broken? (needed \"sv << (aligned_size / blocksize) << \" packets)\"sv;\n      }\n\n      // Split the data into aligned FEC blocks\n      for (int x = 0; x < fec_blocks_needed; ++x) {\n        if (x == fec_blocks_needed - 1) {\n          // The last block must extend to the end of the payload\n          fec_blocks[x] = payload.substr(x * aligned_size);\n        }\n        else {\n          // Earlier blocks just extend to the next block offset\n          fec_blocks[x] = payload.substr(x * aligned_size, aligned_size);\n        }\n      }\n\n      try {\n        // Use around 80% of 1Gbps          1Gbps            percent    ms     packet      byte\n        size_t ratecontrol_packets_in_1ms = std::giga::num * 80 / 100 / 1000 / blocksize / 8;\n\n        // Send less than 64K in a single batch.\n        // On Windows, batches above 64K seem to bypass SO_SNDBUF regardless of its size,\n        // appear in \"Other I/O\" and begin waiting for interrupts.\n        // This gives inconsistent performance so we'd rather avoid it.\n        size_t send_batch_size = 64 * 1024 / blocksize;\n        // Also don't exceed 64 packets, which can happen when Moonlight requests\n        // unusually small packet size.\n        // Generic Segmentation Offload on Linux can't do more than 64.\n        send_batch_size = std::min<size_t>(64, send_batch_size);\n\n        // Don't ignore the last ratecontrol group of the previous frame\n        auto ratecontrol_frame_start = std::max(ratecontrol_next_frame_start, std::chrono::steady_clock::now());\n\n        size_t ratecontrol_frame_packets_sent = 0;\n        size_t ratecontrol_group_packets_sent = 0;\n\n        auto blockIndex = 0;\n        std::for_each(fec_blocks_begin, fec_blocks_end, [&](std::string_view &current_payload) {\n          auto packets = (current_payload.size() + (blocksize - 1)) / blocksize;\n\n          for (int x = 0; x < packets; ++x) {\n            auto *inspect = (video_packet_raw_t *) &current_payload[x * blocksize];\n\n            inspect->packet.frameIndex = packet->frame_index();\n            inspect->packet.streamPacketIndex = ((uint32_t) lowseq + x) << 8;\n\n            // Match multiFecFlags with Moonlight\n            inspect->packet.multiFecFlags = 0x10;\n            inspect->packet.multiFecBlocks = (blockIndex << 4) | ((fec_blocks_needed - 1) << 6);\n\n            inspect->packet.flags = FLAG_CONTAINS_PIC_DATA;\n            if (x == 0) {\n              inspect->packet.flags |= FLAG_SOF;\n            }\n            if (x == packets - 1) {\n              inspect->packet.flags |= FLAG_EOF;\n            }\n          }\n\n          frame_fec_latency_logger.first_point_now();\n          // If video encryption is enabled, we allocate space for the encryption header before each shard\n          auto shards = fec::encode(current_payload, blocksize, fecPercentage, session->config.minRequiredFecPackets,\n            session->video.cipher ? sizeof(video_packet_enc_prefix_t) : 0);\n          frame_fec_latency_logger.second_point_now_and_log();\n\n          auto peer_address = session->video.peer.address();\n          auto batch_info = platf::batched_send_info_t {\n            shards.headers.begin(),\n            shards.prefixsize,\n            shards.payload_buffers,\n            shards.blocksize,\n            0,\n            0,\n            (uintptr_t) sock.native_handle(),\n            peer_address,\n            session->video.peer.port(),\n            session->localAddress,\n          };\n\n          size_t next_shard_to_send = 0;\n\n          // RTP video timestamps use a 90 KHz clock and the frame_timestamp from when the frame was captured\n          // When a timestamp isn't available (duplicate frames), the timestamp from rate control is used instead.\n          bool frame_is_dupe = false;\n          if (!packet->frame_timestamp) {\n            packet->frame_timestamp = ratecontrol_next_frame_start;\n            frame_is_dupe = true;\n          }\n          using rtp_tick = std::chrono::duration<uint32_t, std::ratio<1, 90000>>;\n          uint32_t timestamp = std::chrono::round<rtp_tick>(*packet->frame_timestamp - video_epoch).count();\n\n          // set FEC info now that we know for sure what our percentage will be for this frame\n          for (auto x = 0; x < shards.size(); ++x) {\n            auto *inspect = (video_packet_raw_t *) shards.data(x);\n\n            inspect->packet.fecInfo =\n              (x << 12 |\n                shards.data_shards << 22 |\n                shards.percentage << 4);\n\n            inspect->rtp.header = 0x80 | FLAG_EXTENSION;\n            inspect->rtp.sequenceNumber = util::endian::big<uint16_t>(lowseq + x);\n            inspect->rtp.timestamp = util::endian::big<uint32_t>(timestamp);\n\n            inspect->packet.multiFecBlocks = (blockIndex << 4) | ((fec_blocks_needed - 1) << 6);\n            inspect->packet.frameIndex = packet->frame_index();\n\n            // Encrypt this shard if video encryption is enabled\n            if (session->video.cipher) {\n              // We use the deterministic IV construction algorithm specified in NIST SP 800-38D\n              // Section 8.2.1. The sequence number is our \"invocation\" field and the 'V' in the\n              // high bytes is the \"fixed\" field. Because each client provides their own unique\n              // key, our values in the fixed field need only uniquely identify each independent\n              // use of the client's key with AES-GCM in our code.\n              //\n              // The IV counter is 64 bits long which allows for 2^64 encrypted video packets\n              // to be sent to each client before the IV repeats.\n              std::copy_n((uint8_t *) &session->video.gcm_iv_counter, sizeof(session->video.gcm_iv_counter), std::begin(iv));\n              iv[11] = 'V';  // Video stream\n              session->video.gcm_iv_counter++;\n\n              // Encrypt the target buffer in place\n              auto *prefix = (video_packet_enc_prefix_t *) shards.prefix(x);\n              prefix->frameNumber = packet->frame_index();\n              std::copy(std::begin(iv), std::end(iv), prefix->iv);\n              session->video.cipher->encrypt(std::string_view { (char *) inspect, (size_t) blocksize },\n                prefix->tag, (uint8_t *) inspect, &iv);\n            }\n\n            if (x - next_shard_to_send + 1 >= send_batch_size ||\n                x + 1 == shards.size()) {\n              // Do pacing within the frame.\n              // Also trigger pacing before the first send_batch() of the frame\n              // to account for the last send_batch() of the previous frame.\n              if (ratecontrol_group_packets_sent >= ratecontrol_packets_in_1ms ||\n                  ratecontrol_frame_packets_sent == 0) {\n                auto due = ratecontrol_frame_start +\n                           std::chrono::duration_cast<std::chrono::nanoseconds>(1ms) *\n                             ratecontrol_frame_packets_sent / ratecontrol_packets_in_1ms;\n\n                auto now = std::chrono::steady_clock::now();\n                if (now < due) {\n                  timer->sleep_for(due - now);\n                }\n\n                ratecontrol_group_packets_sent = 0;\n              }\n\n              size_t current_batch_size = x - next_shard_to_send + 1;\n              batch_info.block_offset = next_shard_to_send;\n              batch_info.block_count = current_batch_size;\n\n              frame_send_batch_latency_logger.first_point_now();\n              // Use a batched send if it's supported on this platform\n              if (!platf::send_batch(batch_info)) {\n                // Batched send is not available, so send each packet individually\n                BOOST_LOG(verbose) << \"Falling back to unbatched send\"sv;\n                for (auto y = 0; y < current_batch_size; y++) {\n                  auto send_info = platf::send_info_t {\n                    shards.prefix(next_shard_to_send + y),\n                    shards.prefixsize,\n                    shards.data(next_shard_to_send + y),\n                    shards.blocksize,\n                    (uintptr_t) sock.native_handle(),\n                    peer_address,\n                    session->video.peer.port(),\n                    session->localAddress,\n                  };\n\n                  platf::send(send_info);\n                }\n              }\n              frame_send_batch_latency_logger.second_point_now_and_log();\n\n              ratecontrol_group_packets_sent += current_batch_size;\n              ratecontrol_frame_packets_sent += current_batch_size;\n              next_shard_to_send = x + 1;\n            }\n          }\n\n          // remember this in case the next frame comes immediately\n          ratecontrol_next_frame_start = ratecontrol_frame_start +\n                                         std::chrono::duration_cast<std::chrono::nanoseconds>(1ms) *\n                                           ratecontrol_frame_packets_sent / ratecontrol_packets_in_1ms;\n\n          frame_network_latency_logger.second_point_now_and_log();\n\n          BOOST_LOG(verbose) << \"Sent Frame seq [\"sv << packet->frame_index() << \"] pts [\"sv << timestamp\n                             << \"] shards [\"sv << shards.size() << \"/\"sv << shards.percentage << \"%]\"sv\n                             << (frame_is_dupe ? \" Dupe\" : \"\")\n                             << (packet->is_idr() ? \" Key\" : \"\")\n                             << (packet->after_ref_frame_invalidation ? \" RFI\" : \"\");\n\n          ++blockIndex;\n          lowseq += shards.size();\n        });\n\n        session->video.lowseq = lowseq;\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(error) << \"Broadcast video failed \"sv << e.what();\n        std::this_thread::sleep_for(100ms);\n      }\n    }\n\n    shutdown_event->raise(true);\n  }\n\n  void\n  audioBroadcastThread(udp::socket &sock) {\n    auto shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);\n    auto packets = mail::man->queue<audio::packet_t>(mail::audio_packets);\n\n    audio_packet_t audio_packet;\n    fec::rs_t rs { reed_solomon_new(RTPA_DATA_SHARDS, RTPA_FEC_SHARDS) };\n    crypto::aes_t iv(16);\n\n    // For unknown reasons, the RS parity matrix computed by our RS implementation\n    // doesn't match the one Nvidia uses for audio data. I'm not exactly sure why,\n    // but we can simply replace it with the matrix generated by OpenFEC which\n    // works correctly. This is possible because the data and FEC shard count is\n    // constant and known in advance.\n    const unsigned char parity[] = { 0x77, 0x40, 0x38, 0x0e, 0xc7, 0xa7, 0x0d, 0x6c };\n    memcpy(rs.get()->p, parity, sizeof(parity));\n\n    audio_packet.rtp.header = 0x80;\n    audio_packet.rtp.packetType = 97;\n    audio_packet.rtp.ssrc = 0;\n\n    // Audio traffic is sent on this thread\n    platf::adjust_thread_priority(platf::thread_priority_e::high);\n\n    while (auto packet = packets->pop()) {\n      if (shutdown_event->peek()) {\n        break;\n      }\n\n      TUPLE_2D_REF(channel_data, packet_data, *packet);\n      auto session = (session_t *) channel_data;\n\n      auto sequenceNumber = session->audio.sequenceNumber;\n      auto timestamp = session->audio.timestamp;\n\n      *(std::uint32_t *) iv.data() = util::endian::big<std::uint32_t>(session->audio.avRiKeyId + sequenceNumber);\n\n      auto &shards_p = session->audio.shards_p;\n\n      // 检查客户端是否启用了音频加密\n      bool audio_encryption_enabled = (session->config.encryptionFlagsEnabled & SS_ENC_AUDIO) != 0;\n      if (sequenceNumber == 0) {\n        // 只在第一个包时记录一次，避免日志过多\n        BOOST_LOG(info) << \"Audio encryption status: encryptionFlagsEnabled=0x\" \n                        << std::hex << session->config.encryptionFlagsEnabled << std::dec\n                        << \", SS_ENC_AUDIO (0x04) check: \" << (audio_encryption_enabled ? \"enabled\" : \"disabled\")\n                        << \", will \" << (audio_encryption_enabled ? \"ENCRYPT\" : \"NOT encrypt\") << \" audio data\";\n      }\n\n      size_t plaintext_size = packet_data.size();\n      \n      // 验证 cipher 是否已初始化\n      if (sequenceNumber == 0) {\n        bool cipher_initialized = (session->audio.cipher.key.size() > 0);\n        BOOST_LOG(info) << \"Audio cipher status: initialized=\" << (cipher_initialized ? \"yes\" : \"no\")\n                        << \", key_size=\" << session->audio.cipher.key.size();\n      }\n      \n      auto bytes = encode_audio(audio_encryption_enabled, packet_data,\n        shards_p[sequenceNumber % RTPA_DATA_SHARDS], iv, session->audio.cipher);\n      \n      if (sequenceNumber == 0) {\n        // 验证加密是否真的执行了\n        if (audio_encryption_enabled) {\n          // 加密后的大小应该大于等于明文（因为 PKCS5 填充）\n          bool encryption_applied = (bytes >= plaintext_size && bytes % 16 == 0);\n          BOOST_LOG(info) << \"Audio packet encryption: plaintext_size=\" << plaintext_size \n                          << \", encrypted_size=\" << bytes\n                          << \", encryption \" << (encryption_applied ? \"SUCCESS (data is encrypted)\" : \"FAILED (data may not be encrypted)\");\n        } else {\n          BOOST_LOG(info) << \"Audio packet: plaintext_size=\" << plaintext_size \n                          << \", output_size=\" << bytes\n                          << \" (NOT encrypted, sent as plaintext)\";\n        }\n      }\n      if (bytes < 0) {\n        BOOST_LOG(error) << \"Couldn't encode audio packet\"sv;\n        break;\n      }\n\n      BOOST_LOG(verbose) << \"Audio [seq \"sv << sequenceNumber << \", pts \"sv << timestamp << \"] ::  send...\"sv;\n\n      audio_packet.rtp.sequenceNumber = util::endian::big(sequenceNumber);\n      audio_packet.rtp.timestamp = util::endian::big(timestamp);\n\n      session->audio.sequenceNumber++;\n      session->audio.timestamp += session->config.audio.packetDuration;\n\n      auto peer_address = session->audio.peer.address();\n      try {\n        auto send_info = platf::send_info_t {\n          (const char *) &audio_packet,\n          sizeof(audio_packet),\n          (const char *) shards_p[sequenceNumber % RTPA_DATA_SHARDS],\n          (size_t) bytes,\n          (uintptr_t) sock.native_handle(),\n          peer_address,\n          session->audio.peer.port(),\n          session->localAddress,\n        };\n        platf::send(send_info);\n\n        auto &fec_packet = session->audio.fec_packet;\n        // initialize the FEC header at the beginning of the FEC block\n        if (sequenceNumber % RTPA_DATA_SHARDS == 0) {\n          fec_packet.fecHeader.baseSequenceNumber = util::endian::big(sequenceNumber);\n          fec_packet.fecHeader.baseTimestamp = util::endian::big(timestamp);\n        }\n\n        // generate parity shards at the end of the FEC block\n        if ((sequenceNumber + 1) % RTPA_DATA_SHARDS == 0) {\n          reed_solomon_encode(rs.get(), shards_p.begin(), RTPA_TOTAL_SHARDS, bytes);\n\n          for (auto x = 0; x < RTPA_FEC_SHARDS; ++x) {\n            fec_packet.rtp.sequenceNumber = util::endian::big<std::uint16_t>(sequenceNumber + x + 1);\n            fec_packet.fecHeader.fecShardIndex = x;\n\n            auto send_info = platf::send_info_t {\n              (const char *) &fec_packet,\n              sizeof(fec_packet),\n              (const char *) shards_p[RTPA_DATA_SHARDS + x],\n              (size_t) bytes,\n              (uintptr_t) sock.native_handle(),\n              peer_address,\n              session->audio.peer.port(),\n              session->localAddress,\n            };\n            platf::send(send_info);\n            BOOST_LOG(verbose) << \"Audio FEC [\"sv << (sequenceNumber & ~(RTPA_DATA_SHARDS - 1)) << ' ' << x << \"] ::  send...\"sv;\n          }\n        }\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(error) << \"Broadcast audio failed \"sv << e.what();\n        std::this_thread::sleep_for(100ms);\n      }\n    }\n\n    shutdown_event->raise(true);\n  }\n\n  int\n  start_broadcast(broadcast_ctx_t &ctx) {\n    auto address_family = net::af_from_enum_string(config::sunshine.address_family);\n    auto protocol = address_family == net::IPV4 ? udp::v4() : udp::v6();\n    auto control_port = net::map_port(CONTROL_PORT);\n    auto video_port = net::map_port(VIDEO_STREAM_PORT);\n    auto audio_port = net::map_port(AUDIO_STREAM_PORT);\n    auto mic_port = net::map_port(MIC_STREAM_PORT);\n\n    if (ctx.control_server.bind(address_family, control_port)) {\n      BOOST_LOG(error) << \"Couldn't bind Control server to port [\"sv << control_port << \"], likely another process already bound to the port\"sv;\n\n      return -1;\n    }\n\n    boost::system::error_code ec;\n    ctx.video_sock.open(protocol, ec);\n    if (ec) {\n      BOOST_LOG(fatal) << \"Couldn't open socket for Video server: \"sv << ec.message();\n\n      return -1;\n    }\n\n    // Set video socket send buffer size (SO_SENDBUF) to 1MB\n    try {\n      ctx.video_sock.set_option(boost::asio::socket_base::send_buffer_size(1024 * 1024));\n    }\n    catch (...) {\n      BOOST_LOG(error) << \"Failed to set video socket send buffer size (SO_SENDBUF)\";\n    }\n\n    auto bind_addr_str = net::get_bind_address(address_family);\n    const auto bind_addr = boost::asio::ip::make_address(bind_addr_str, ec);\n    if (ec) {\n      BOOST_LOG(fatal) << \"Invalid bind address: \"sv << bind_addr_str << \" - \" << ec.message();\n      return -1;\n    }\n\n    ctx.video_sock.bind(udp::endpoint(bind_addr, video_port), ec);\n    if (ec) {\n      BOOST_LOG(fatal) << \"Couldn't bind Video server to port [\"sv << video_port << \"]: \"sv << ec.message();\n\n      return -1;\n    }\n\n    ctx.audio_sock.open(protocol, ec);\n    if (ec) {\n      BOOST_LOG(fatal) << \"Couldn't open socket for Audio server: \"sv << ec.message();\n\n      return -1;\n    }\n\n    ctx.audio_sock.bind(udp::endpoint(bind_addr, audio_port), ec);\n    if (ec) {\n      BOOST_LOG(fatal) << \"Couldn't bind Audio server to port [\"sv << audio_port << \"]: \"sv << ec.message();\n\n      return -1;\n    }\n\n    // 仅在启用麦克风串流时启动麦克风socket\n    if (config::audio.stream_mic) {\n      ctx.mic_sock.open(protocol, ec);\n      if (ec) {\n        BOOST_LOG(fatal) << \"Couldn't open socket for Microphone server: \"sv << ec.message();\n        return -1;\n      }\n      ctx.mic_sock.bind(udp::endpoint(protocol, mic_port), ec);\n      if (ec) {\n        BOOST_LOG(fatal) << \"Couldn't bind Microphone server to port [\"sv << mic_port << \"]: \"sv << ec.message();\n        return -1;\n      }\n      ctx.mic_socket_enabled.store(true);\n      BOOST_LOG(info) << \"Microphone socket started on port \" << mic_port;\n    } else {\n      BOOST_LOG(info) << \"Microphone streaming disabled by config\";\n    }\n\n    ctx.message_queue_queue = std::make_shared<message_queue_queue_t::element_type>(30);\n\n    ctx.video_thread = std::thread { videoBroadcastThread, std::ref(ctx.video_sock) };\n    ctx.audio_thread = std::thread { audioBroadcastThread, std::ref(ctx.audio_sock) };\n    ctx.control_thread = std::thread { controlBroadcastThread, &ctx.control_server };\n\n    ctx.recv_thread = std::thread { recvThread, std::ref(ctx) };\n    ctx.mic_thread = std::thread { micRecvThread, std::ref(ctx) };\n\n    return 0;\n  }\n\n  void\n  end_broadcast(broadcast_ctx_t &ctx) {\n    auto broadcast_shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);\n\n    broadcast_shutdown_event->raise(true);\n\n    auto video_packets = mail::man->queue<video::packet_t>(mail::video_packets);\n    auto audio_packets = mail::man->queue<audio::packet_t>(mail::audio_packets);\n\n    // Minimize delay stopping video/audio threads\n    video_packets->stop();\n    audio_packets->stop();\n\n    ctx.message_queue_queue->stop();\n    ctx.io_context.stop();\n    ctx.mic_io_context.stop();\n\n    ctx.video_sock.close();\n    ctx.audio_sock.close();\n\n    if (ctx.mic_socket_enabled.load()) {\n      ctx.mic_socket_enabled.store(false);\n      ctx.mic_sock.close();\n      ctx.mic_sessions_count.store(0);\n      \n      reset_mic_encryption(ctx);\n      \n      BOOST_LOG(debug) << \"Microphone socket closed and encryption context securely cleared\";\n    }\n\n    video_packets.reset();\n    audio_packets.reset();\n\n    BOOST_LOG(debug) << \"Waiting for main listening thread to end...\"sv;\n    ctx.recv_thread.join();\n    BOOST_LOG(debug) << \"Waiting for main video thread to end...\"sv;\n    ctx.video_thread.join();\n    BOOST_LOG(debug) << \"Waiting for main audio thread to end...\"sv;\n    ctx.audio_thread.join();\n    BOOST_LOG(debug) << \"Waiting for main control thread to end...\"sv;\n    ctx.control_thread.join();\n    BOOST_LOG(debug) << \"Waiting for microphone thread to end...\"sv;\n    ctx.mic_thread.join();\n    BOOST_LOG(debug) << \"All broadcasting threads ended\"sv;\n\n    broadcast_shutdown_event->reset();\n  }\n\n  int\n  recv_ping(session_t *session, decltype(broadcast_shared)::ptr_t ref, socket_e type, std::string_view expected_payload, udp::endpoint &peer, std::chrono::milliseconds timeout) {\n    auto messages = std::make_shared<message_queue_t::element_type>(30);\n    av_session_id_t session_id = std::string { expected_payload };\n\n    // Only allow matches on the peer address for legacy clients\n    if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1)) {\n      ref->message_queue_queue->raise(type, peer.address(), messages);\n    }\n    ref->message_queue_queue->raise(type, session_id, messages);\n\n    auto fg = util::fail_guard([&]() {\n      messages->stop();\n\n      // remove message queue from session\n      if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1)) {\n        ref->message_queue_queue->raise(type, peer.address(), nullptr);\n      }\n      ref->message_queue_queue->raise(type, session_id, nullptr);\n    });\n\n    auto start_time = std::chrono::steady_clock::now();\n    auto current_time = start_time;\n\n    while (current_time - start_time < config::stream.ping_timeout) {\n      auto delta_time = current_time - start_time;\n\n      auto msg_opt = messages->pop(config::stream.ping_timeout - delta_time);\n      if (!msg_opt) {\n        break;\n      }\n\n      TUPLE_2D_REF(recv_peer, msg, *msg_opt);\n      if (msg.find(expected_payload) != std::string::npos) {\n        // Match the new PING payload format\n        BOOST_LOG(debug) << \"Received ping [v2] from \"sv << recv_peer.address() << ':' << recv_peer.port() << \" [\"sv << util::hex_vec(msg) << ']';\n      }\n      else if (!(session->config.mlFeatureFlags & ML_FF_SESSION_ID_V1) && msg == \"PING\"sv) {\n        // Match the legacy fixed PING payload only if the new type is not supported\n        BOOST_LOG(debug) << \"Received ping [v1] from \"sv << recv_peer.address() << ':' << recv_peer.port() << \" [\"sv << util::hex_vec(msg) << ']';\n      }\n      else {\n        BOOST_LOG(debug) << \"Received non-ping from \"sv << recv_peer.address() << ':' << recv_peer.port() << \" [\"sv << util::hex_vec(msg) << ']';\n        current_time = std::chrono::steady_clock::now();\n        continue;\n      }\n\n      // Update connection details.\n      peer = recv_peer;\n      return 0;\n    }\n\n    BOOST_LOG(error) << \"Initial Ping Timeout\"sv;\n    return -1;\n  }\n\n  void\n  videoThread(session_t *session) {\n    auto fg = util::fail_guard([&]() {\n      session::stop(*session);\n    });\n\n    while_starting_do_nothing(session->state);\n\n    auto ref = broadcast_shared.ref();\n    auto error = recv_ping(session, ref, socket_e::video, session->video.ping_payload, session->video.peer, config::stream.ping_timeout);\n    if (error < 0) {\n      return;\n    }\n\n    // Enable local prioritization and QoS tagging on video traffic if requested by the client\n    auto address = session->video.peer.address();\n    session->video.qos = platf::enable_socket_qos(ref->video_sock.native_handle(), address,\n      session->video.peer.port(), platf::qos_data_type_e::video, session->config.videoQosType != 0);\n\n    BOOST_LOG(debug) << \"Start capturing Video\"sv;\n    // Debug: Log the display_name before calling video::capture\n    BOOST_LOG(debug) << \"stream.cpp: session->config.monitor.display_name = [\" << (session->config.monitor.display_name.empty() ? \"<empty>\" : session->config.monitor.display_name) << \"]\";\n    video::capture(session->mail, session->config.monitor, session, session->video.dynamic_param_change_events);\n  }\n\n  void\n  audioThread(session_t *session) {\n    auto fg = util::fail_guard([&]() {\n      session::stop(*session);\n    });\n\n    while_starting_do_nothing(session->state);\n\n    auto ref = broadcast_shared.ref();\n    auto error = recv_ping(session, ref, socket_e::audio, session->audio.ping_payload, session->audio.peer, config::stream.ping_timeout);\n    if (error < 0) {\n      return;\n    }\n\n    // Enable local prioritization and QoS tagging on audio traffic if requested by the client\n    auto address = session->audio.peer.address();\n    session->audio.qos = platf::enable_socket_qos(ref->audio_sock.native_handle(), address,\n      session->audio.peer.port(), platf::qos_data_type_e::audio, session->config.audioQosType != 0);\n\n    BOOST_LOG(debug) << \"Start capturing Audio\"sv;\n    audio::capture(session->mail, session->config.audio, session);\n  }\n\n  namespace session {\n    std::atomic_uint running_sessions;\n    std::atomic_uint running_non_control_only_sessions;  // 跟踪非仅控制流会话的数量\n\n    state_e\n    state(session_t &session) {\n      return session.state.load(std::memory_order_relaxed);\n    }\n\n    void\n    stop(session_t &session) {\n      while_starting_do_nothing(session.state);\n      auto expected = state_e::RUNNING;\n      auto already_stopping = !session.state.compare_exchange_strong(expected, state_e::STOPPING);\n      if (already_stopping) {\n        return;\n      }\n\n      session.shutdown_event->raise(true);\n    }\n\n    void\n    join(session_t &session) {\n      // Current Nvidia drivers have a bug where NVENC can deadlock the encoder thread with hardware-accelerated\n      // GPU scheduling enabled. If this happens, we will terminate ourselves and the service can restart.\n      // The alternative is that Sunshine can never start another session until it's manually restarted.\n      auto task = []() {\n        BOOST_LOG(fatal) << \"Hang detected! Session failed to terminate in 10 seconds.\"sv;\n        logging::log_flush();\n        lifetime::debug_trap();\n      };\n      auto force_kill = task_pool.pushDelayed(task, 10s).task_id;\n      auto fg = util::fail_guard([&force_kill]() {\n        // Cancel the kill task if we manage to return from this function\n        task_pool.cancel(force_kill);\n      });\n\n      // 仅控制流会话没有视频/音频线程\n      if (!session.control_only) {\n        BOOST_LOG(debug) << \"Waiting for video to end...\"sv;\n        session.videoThread.join();\n        BOOST_LOG(debug) << \"Waiting for audio to end...\"sv;\n        session.audioThread.join();\n      }\n      else {\n        BOOST_LOG(debug) << \"Control-only session: skipping video/audio thread join\"sv;\n      }\n      BOOST_LOG(debug) << \"Waiting for control to end...\"sv;\n      session.controlEnd.view();\n      // Reset input on session stop to avoid stuck repeated keys\n      BOOST_LOG(debug) << \"Resetting Input...\"sv;\n      input::reset(session.input);\n\n      // 对于仅控制流会话，只减少总会话计数，不调用 streaming_will_stop\n      // 只有当所有非控制流会话都结束时才调用 streaming_will_stop\n      if (session.control_only) {\n        --running_sessions;\n        BOOST_LOG(debug) << \"Control-only session ended (remaining sessions: \"sv << running_sessions.load() << \")\"sv;\n      }\n      else {\n        // 非仅控制流会话：减少两个计数器\n        --running_sessions;\n        // If this is the last non-control-only session, invoke the platform callbacks\n        if (--running_non_control_only_sessions == 0) {\n          // 最后一个会话结束时，确保麦克风socket已关闭\n          if (session.broadcast_ref->mic_socket_enabled.load()) {\n            session.broadcast_ref->mic_socket_enabled.store(false);\n            session.broadcast_ref->mic_sessions_count.store(0);\n            session.broadcast_ref->mic_sock.close();\n            reset_mic_encryption(*session.broadcast_ref.get());\n            BOOST_LOG(debug) << \"Microphone socket closed (last session ended)\";\n          }\n\n          bool restore_display_state { true };\n          if (proc::proc.running()) {\n#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1\n            system_tray::update_tray_pausing(proc::proc.get_last_run_app_name());\n#endif\n\n            // TODO: make this configurable per app\n            restore_display_state = false;\n          }\n\n          if (restore_display_state) {\n            display_device::session_t::get().restore_state();\n          }\n\n          platf::streaming_will_stop();\n        }\n        else {\n          // 非最后一个会话：如果当前会话启用了麦克风，减少计数\n          if (session.audio.enable_mic) {\n            int remaining_count = session.broadcast_ref->mic_sessions_count.fetch_sub(1) - 1;\n            if (remaining_count == 0) {\n              // 没有会话需要麦克风了，关闭socket并清除所有加密上下文\n              session.broadcast_ref->mic_socket_enabled.store(false);\n              session.broadcast_ref->mic_sock.close();\n              reset_mic_encryption(*session.broadcast_ref.get());\n              BOOST_LOG(debug) << \"Microphone socket closed (no sessions require it)\";\n            }\n            else {\n              // 只移除当前客户端的加密上下文，保留其他客户端的\n              std::string client_ip = session.audio.peer.address().to_string();\n              remove_mic_encryption(*session.broadcast_ref.get(), client_ip);\n              BOOST_LOG(debug) << \"Microphone sessions remaining: \" << remaining_count << \" (removed cipher for \" << client_ip << \")\";\n            }\n          }\n        }\n      }\n\n      // Clean up ABR state for this client\n      abr::cleanup(session.client_name);\n\n      BOOST_LOG(debug) << \"Session ended\"sv;\n    }\n\n    int\n    start(session_t &session, const std::string &addr_string) {\n      session.input = input::alloc(session.mail);\n\n      session.broadcast_ref = broadcast_shared.ref();\n      if (!session.broadcast_ref) {\n        return -1;\n      }\n\n      session.control.expected_peer_address = addr_string;\n      if (session.control_only) {\n        BOOST_LOG(info) << \"Starting control-only session from [\"sv << addr_string << \"] - will only handle input control\"sv;\n      }\n      else {\n        BOOST_LOG(debug) << \"Expecting incoming session connections from \"sv << addr_string;\n      }\n\n      // Insert this session into the session list\n      {\n        auto lg = session.broadcast_ref->control_server._sessions.lock();\n        session.broadcast_ref->control_server._sessions->push_back(&session);\n      }\n\n      auto addr = boost::asio::ip::make_address(addr_string);\n      session.video.peer.address(addr);\n      session.video.peer.port(0);\n\n      session.audio.peer.address(addr);\n      session.audio.peer.port(0);\n\n      session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout;\n\n      // 仅控制流会话不启动视频/音频线程\n      if (!session.control_only) {\n        session.audioThread = std::thread { audioThread, &session };\n        session.videoThread = std::thread { videoThread, &session };\n      }\n      else {\n        BOOST_LOG(debug) << \"Control-only session: skipping video and audio thread creation\"sv;\n      }\n\n      session.state.store(state_e::RUNNING, std::memory_order_relaxed);\n\n      // 仅控制流会话不触发 streaming_will_start 回调，因为它们不传输视频/音频\n      // 但它们仍然需要被计入 running_sessions，以便正确管理会话\n      if (session.control_only) {\n        // 仅控制流会话：只增加总会话计数，不调用平台回调\n        ++running_sessions;\n        BOOST_LOG(debug) << \"Control-only session started (total sessions: \"sv << running_sessions.load() << \")\"sv;\n      }\n      else {\n        // 非仅控制流会话：增加两个计数器\n        ++running_sessions;\n        // If this is the first non-control-only session, invoke the platform callbacks\n        if (++running_non_control_only_sessions == 1) {\n          // 根据会话的麦克风启用标志管理麦克风socket\n          if (session.audio.enable_mic) {\n            setup_mic_for_session(session);\n          }\n          else {\n            // 如果第一个会话不需要麦克风，关闭麦克风socket\n            session.broadcast_ref->mic_socket_enabled.store(false);\n            session.broadcast_ref->mic_sock.close();\n            BOOST_LOG(info) << \"Client \" << session.client_name << \": Microphone socket closed (session doesn't require it)\";\n          }\n\n          platf::streaming_will_start();\n#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1\n          system_tray::update_tray_playing(proc::proc.get_last_run_app_name());\n#endif\n        }\n        else {\n          // 非第一个会话：如果启用麦克风\n          if (session.audio.enable_mic) {\n            setup_mic_for_session(session);\n          }\n        }\n      }\n\n      return 0;\n    }\n\n    std::shared_ptr<session_t>\n    alloc(config_t &config, rtsp_stream::launch_session_t &launch_session) {\n      auto session = std::make_shared<session_t>();\n\n      auto mail = std::make_shared<safe::mail_raw_t>();\n\n      session->shutdown_event = mail->event<bool>(mail::shutdown);\n      session->launch_session_id = launch_session.id;\n\n      // 设置客户端名称\n      session->client_name = launch_session.client_name;\n\n      // 保存 launch_session 的关键字段，用于后续动态参数更新\n      session->enable_sops = launch_session.enable_sops;\n      session->enable_hdr = launch_session.enable_hdr;\n      session->max_nits = launch_session.max_nits;\n      session->min_nits = launch_session.min_nits;\n      session->max_full_nits = launch_session.max_full_nits;\n\n      session->config = config;\n\n      // Initialize current total bitrate (including FEC) from config\n      // config.monitor.bitrate is the encoding bitrate (excluding FEC)\n      // We need to convert it to total bitrate (including FEC)\n      int encoding_bitrate = config.monitor.bitrate;\n      int fec_percentage = config::stream.fec_percentage;\n      if (fec_percentage > 0 && fec_percentage <= 80) {\n        // Convert encoding bitrate to total bitrate: total = encoding * 100 / (100 - fec_percentage)\n        session->current_total_bitrate = (int) (encoding_bitrate * 100.f / (100 - fec_percentage));\n      }\n      else {\n        // If FEC percentage is 0 or > 80%, encoding bitrate equals total bitrate\n        session->current_total_bitrate = encoding_bitrate;\n      }\n\n      session->control.connect_data = launch_session.control_connect_data;\n      session->control.feedback_queue = mail->queue<platf::gamepad_feedback_msg_t>(mail::gamepad_feedback);\n      session->control.hdr_queue = mail->event<video::hdr_info_t>(mail::hdr);\n      session->control.resolution_change_queue = mail->event<std::pair<std::uint32_t, std::uint32_t>>(mail::resolution_change);\n      session->control.legacy_input_enc_iv = launch_session.iv;\n      session->control.cipher = crypto::cipher::gcm_t {\n        launch_session.gcm_key, false\n      };\n\n      session->video.idr_events = mail->event<bool>(mail::idr);\n      session->video.invalidate_ref_frames_events = mail->event<std::pair<int64_t, int64_t>>(mail::invalidate_ref_frames);\n      session->video.dynamic_param_change_events = mail->event<video::dynamic_param_t>(mail::dynamic_param_change);\n      session->video.lowseq = 0;\n      session->video.ping_payload = launch_session.av_ping_payload;\n      if (config.encryptionFlagsEnabled & SS_ENC_VIDEO) {\n        BOOST_LOG(info) << \"Video encryption enabled\"sv;\n        session->video.cipher = crypto::cipher::gcm_t {\n          launch_session.gcm_key, false\n        };\n        session->video.gcm_iv_counter = 0;\n      }\n\n      constexpr auto max_block_size = crypto::cipher::round_to_pkcs7_padded(2048);\n\n      util::buffer_t<char> shards { RTPA_TOTAL_SHARDS * max_block_size };\n      util::buffer_t<uint8_t *> shards_p { RTPA_TOTAL_SHARDS };\n\n      for (auto x = 0; x < RTPA_TOTAL_SHARDS; ++x) {\n        shards_p[x] = (uint8_t *) &shards[x * max_block_size];\n      }\n\n      // Audio FEC spans multiple audio packets,\n      // therefore its session specific\n      session->audio.shards = std::move(shards);\n      session->audio.shards_p = std::move(shards_p);\n\n      session->audio.fec_packet.rtp.header = 0x80;\n      session->audio.fec_packet.rtp.packetType = 127;\n      session->audio.fec_packet.rtp.timestamp = 0;\n      session->audio.fec_packet.rtp.ssrc = 0;\n\n      session->audio.fec_packet.fecHeader.payloadType = 97;\n      session->audio.fec_packet.fecHeader.ssrc = 0;\n\n      session->audio.cipher = crypto::cipher::cbc_t {\n        launch_session.gcm_key, true\n      };\n\n      session->audio.ping_payload = launch_session.av_ping_payload;\n      session->audio.avRiKeyId = util::endian::big(*(std::uint32_t *) launch_session.iv.data());\n      session->audio.sequenceNumber = 0;\n      session->audio.timestamp = 0;\n\n      session->audio.enable_mic = launch_session.enable_mic;\n\n      session->control_only = launch_session.control_only;\n\n      session->control.peer = nullptr;\n      session->state.store(state_e::STOPPED, std::memory_order_relaxed);\n\n      session->mail = std::move(mail);\n\n      return session;\n    }\n\n    bool\n    change_dynamic_param_for_client(const std::string &client_name, const video::dynamic_param_t &param) {\n      // 先检查是否有活动的广播引用，避免在无活跃session时\n      // 触发start_broadcast/end_broadcast循环（\"僵尸广播\"），\n      // 这可能阻塞HTTPS服务器线程导致客户端显示主机离线\n      if (!broadcast_shared.has_ref()) {\n        return false;\n      }\n\n      auto broadcast_ref = broadcast_shared.ref();\n      if (!broadcast_ref) {\n        BOOST_LOG(warning) << \"No broadcast context available when changing dynamic parameter for client\";\n        return false;\n      }\n\n      auto lg = broadcast_ref->control_server._sessions.lock();\n      for (auto session_p : *broadcast_ref->control_server._sessions) {\n        if (session_p->client_name == client_name &&\n            session_p->state.load(std::memory_order_relaxed) == state_e::RUNNING) {\n          // Update session's current total bitrate if this is a bitrate change\n          if (param.type == video::dynamic_param_type_e::BITRATE && param.valid) {\n            // The param.value.int_value is the total bitrate (user-configured, including FEC)\n            session_p->current_total_bitrate = param.value.int_value;\n            BOOST_LOG(info) << \"Updated session total bitrate for client '\" << client_name\n                            << \"': \" << param.value.int_value << \" Kbps (including FEC)\";\n          }\n\n          session_p->video.dynamic_param_change_events->raise(param);\n          BOOST_LOG(info) << \"Sent dynamic parameter change event to client '\" << client_name\n                          << \"': type=\" << (int) param.type;\n          return true;\n        }\n      }\n\n      BOOST_LOG(warning) << \"No active session found for client: \" << client_name;\n      return false;\n    }\n\n    std::vector<session_info_t>\n    get_all_sessions_info() {\n      std::vector<session_info_t> sessions_info;\n\n      // 关键修复：先检查是否有活动的引用，避免触发 start_broadcast\n      // 如果没有活动的引用，说明没有活动的 session，直接返回空列表\n      if (!broadcast_shared.has_ref()) {\n        return sessions_info;\n      }\n\n      auto broadcast_ref = broadcast_shared.ref();\n      if (!broadcast_ref) {\n        BOOST_LOG(warning) << \"No broadcast context when getting all sessions info\";\n        return sessions_info;\n      }\n\n      // 在持有锁的情况下，快速复制会话的基本信息\n      // 由于存储的是原始指针，我们需要在持有锁时快速访问\n      auto lg = broadcast_ref->control_server._sessions.lock();\n      for (auto session_p : *broadcast_ref->control_server._sessions) {\n        // 双重检查：确保会话指针仍然有效\n        if (!session_p) {\n          continue;\n        }\n\n        try {\n          session_info_t info;\n\n          info.client_name = session_p->client_name;\n          info.session_id = session_p->launch_session_id;\n\n          // Get client address\n          if (session_p->control.peer) {\n            try {\n              info.client_address = platf::from_sockaddr((sockaddr *) &session_p->control.peer->address.address);\n            }\n            catch (...) {\n              info.client_address = session_p->control.expected_peer_address;\n            }\n          }\n          else {\n            info.client_address = session_p->control.expected_peer_address;\n          }\n\n          // Get session state\n          auto state = session_p->state.load(std::memory_order_relaxed);\n          switch (state) {\n            case state_e::STOPPED:\n              info.state = \"STOPPED\";\n              break;\n            case state_e::STOPPING:\n              info.state = \"STOPPING\";\n              break;\n            case state_e::STARTING:\n              info.state = \"STARTING\";\n              break;\n            case state_e::RUNNING:\n              info.state = \"RUNNING\";\n              break;\n            default:\n              info.state = \"UNKNOWN\";\n              break;\n          }\n\n          // Get video configuration\n          info.width = session_p->config.monitor.width;\n          info.height = session_p->config.monitor.height;\n          info.fps = session_p->config.monitor.framerate;\n\n          // Get current total bitrate (including FEC) from session-specific field\n          // This is the user-configured bitrate, which may have been changed dynamically\n          info.bitrate = session_p->current_total_bitrate.load(std::memory_order_relaxed);\n\n          // Get audio and other settings\n          info.host_audio = session_p->config.audio.flags[audio::config_t::HOST_AUDIO];\n          info.enable_hdr = session_p->config.monitor.dynamicRange > 0;\n          info.enable_mic = session_p->audio.enable_mic;\n\n          // Get app information\n          try {\n            info.app_id = proc::proc.running();\n            info.app_name = (info.app_id > 0) ? proc::proc.get_last_run_app_name() : \"None\";\n          }\n          catch (...) {\n            info.app_id = 0;\n            info.app_name = \"None\";\n          }\n\n          sessions_info.push_back(info);\n        }\n        catch (const std::exception &e) {\n          BOOST_LOG(warning) << \"Error processing session: \" << e.what() << \" when getting all sessions info\";\n          continue;\n        }\n        catch (...) {\n          BOOST_LOG(warning) << \"Unknown error processing session when getting all sessions info\";\n          continue;\n        }\n      }\n\n      return sessions_info;\n    }\n  }  // namespace session\n}  // namespace stream\n"
  },
  {
    "path": "src/stream.h",
    "content": "/**\n * @file src/stream.h\n * @brief Declarations for the streaming protocols.\n */\n#pragma once\n#include <utility>\n#include <vector>\n#include <string>\n\n#include <boost/asio.hpp>\n\n#include \"audio.h\"\n#include \"crypto.h\"\n#include \"video.h\"\n\nnamespace stream {\n  constexpr auto VIDEO_STREAM_PORT = 9;\n  constexpr auto CONTROL_PORT = 10;\n  constexpr auto AUDIO_STREAM_PORT = 11;\n  constexpr auto MIC_STREAM_PORT = 12;  // Port for microphone streaming\n\n  struct session_t;\n  struct config_t {\n    audio::config_t audio;\n    video::config_t monitor;\n\n    int packetsize;\n    int minRequiredFecPackets;\n    int mlFeatureFlags;\n    int controlProtocolType;\n    int audioQosType;\n    int videoQosType;\n\n    uint32_t encryptionFlagsEnabled;\n\n    std::optional<int> gcmap;\n  };\n\n  // Session information structure for API responses\n  struct session_info_t {\n    std::string client_name;\n    std::string client_address;\n    std::string state;\n    uint32_t session_id;\n    int width;\n    int height;\n    int fps;\n    int bitrate;  // Current bitrate in Kbps\n    bool host_audio;\n    bool enable_hdr;\n    bool enable_mic;\n    std::string app_name;\n    int app_id;\n  };\n\n  namespace session {\n    enum class state_e : int {\n      STOPPED,  ///< The session is stopped\n      STOPPING,  ///< The session is stopping\n      STARTING,  ///< The session is starting\n      RUNNING,  ///< The session is running\n    };\n\n    std::shared_ptr<session_t>\n    alloc(config_t &config, rtsp_stream::launch_session_t &launch_session);\n    int\n    start(session_t &session, const std::string &addr_string);\n    void\n    stop(session_t &session);\n    void\n    join(session_t &session);\n    state_e\n    state(session_t &session);\n    \n\n\n    /**\n     * @brief Send dynamic parameter change event to a specific client session.\n     * @param client_name The name of the client to target.\n     * @param param The dynamic parameter to change.\n     * @return true if the event was sent successfully, false otherwise.\n     */\n    bool\n    change_dynamic_param_for_client(const std::string &client_name, const video::dynamic_param_t &param);\n\n    /**\n     * @brief Get information about all active sessions.\n     * @return Vector of session information.\n     */\n    std::vector<session_info_t>\n    get_all_sessions_info();\n  }  // namespace session\n}  // namespace stream\n"
  },
  {
    "path": "src/sync.h",
    "content": "/**\n * @file src/sync.h\n * @brief Declarations for synchronization utilities.\n */\n#pragma once\n\n#include <array>\n#include <mutex>\n#include <utility>\n\nnamespace sync_util {\n\n  template <class T, class M = std::mutex>\n  class sync_t {\n  public:\n    using value_t = T;\n    using mutex_t = M;\n\n    std::lock_guard<mutex_t>\n    lock() {\n      return std::lock_guard { _lock };\n    }\n\n    template <class... Args>\n    sync_t(Args &&...args):\n        raw { std::forward<Args>(args)... } {}\n\n    sync_t &\n    operator=(sync_t &&other) noexcept {\n      std::lock(_lock, other._lock);\n\n      raw = std::move(other.raw);\n\n      _lock.unlock();\n      other._lock.unlock();\n\n      return *this;\n    }\n\n    sync_t &\n    operator=(sync_t &other) noexcept {\n      std::lock(_lock, other._lock);\n\n      raw = other.raw;\n\n      _lock.unlock();\n      other._lock.unlock();\n\n      return *this;\n    }\n\n    template <class V>\n    sync_t &\n    operator=(V &&val) {\n      auto lg = lock();\n\n      raw = val;\n\n      return *this;\n    }\n\n    sync_t &\n    operator=(const value_t &val) noexcept {\n      auto lg = lock();\n\n      raw = val;\n\n      return *this;\n    }\n\n    sync_t &\n    operator=(value_t &&val) noexcept {\n      auto lg = lock();\n\n      raw = std::move(val);\n\n      return *this;\n    }\n\n    value_t *\n    operator->() {\n      return &raw;\n    }\n\n    value_t &\n    operator*() {\n      return raw;\n    }\n\n    const value_t &\n    operator*() const {\n      return raw;\n    }\n\n    value_t raw;\n\n  private:\n    mutex_t _lock;\n  };\n\n}  // namespace sync_util\n"
  },
  {
    "path": "src/system_tray.cpp",
    "content": "/**\n * @file src/system_tray.cpp\n * @brief Definitions for the system tray icon and notification system.\n */\n// macros\n#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1\n\n  #if defined(_WIN32)\n    #define WIN32_LEAN_AND_MEAN\n    #include <accctrl.h>\n    #include <aclapi.h>\n    #include <commdlg.h>  // 添加文件对话框支持\n    #include <shellapi.h>  // 添加 ShellExecuteW 函数声明\n    #include <shlobj.h>  // 添加 SHGetFolderPathW 函数声明\n    #include <shobjidl.h>  // 添加 IFileDialog COM接口声明\n    #include <tlhelp32.h>\n    #include <windows.h>\n    #define TRAY_ICON WEB_DIR \"images/sunshine.ico\"\n    #define TRAY_ICON_PLAYING WEB_DIR \"images/sunshine-playing.ico\"\n    #define TRAY_ICON_PAUSING WEB_DIR \"images/sunshine-pausing.ico\"\n    #define TRAY_ICON_LOCKED WEB_DIR \"images/sunshine-locked.ico\"\n  #elif defined(__linux__) || defined(linux) || defined(__linux)\n    #define TRAY_ICON \"sunshine-tray\"\n    #define TRAY_ICON_PLAYING \"sunshine-playing\"\n    #define TRAY_ICON_PAUSING \"sunshine-pausing\"\n    #define TRAY_ICON_LOCKED \"sunshine-locked\"\n  #elif defined(__APPLE__) || defined(__MACH__)\n    #define TRAY_ICON WEB_DIR \"images/logo-sunshine-16.png\"\n    #define TRAY_ICON_PLAYING WEB_DIR \"images/sunshine-playing-16.png\"\n    #define TRAY_ICON_PAUSING WEB_DIR \"images/sunshine-pausing-16.png\"\n    #define TRAY_ICON_LOCKED WEB_DIR \"images/sunshine-locked-16.png\"\n    #include <dispatch/dispatch.h>\n  #endif\n\n  // standard includes\n  #include <atomic>\n  #include <chrono>\n  #include <csignal>\n  #include <ctime>\n  #include <fstream>\n  #include <future>\n  #include <string>\n  #include <thread>\n\n  // lib includes\n  #include \"tray/src/tray.h\"\n  #include <boost/filesystem.hpp>\n  #include <boost/process/v1/environment.hpp>\n\n  // local includes\n  #include \"confighttp.h\"\n  #include \"display_device/session.h\"\n  #include \"file_handler.h\"\n  #include \"logging.h\"\n  #include \"platform/common.h\"\n  #include \"platform/windows/misc.h\"\n  #include \"process.h\"\n  #include \"src/display_device/display_device.h\"\n  #include \"src/entry_handler.h\"\n  #include \"src/globals.h\"\n  #include \"system_tray_i18n.h\"\n  #include \"version.h\"\n\nusing namespace std::literals;\n\n// system_tray namespace\nnamespace system_tray {\n  static std::atomic<bool> tray_initialized = false;\n\n  // Threading variables for all platforms\n  static std::thread tray_thread;\n  static std::atomic tray_thread_running = false;\n  static std::atomic tray_thread_should_exit = false;\n  static std::atomic<bool> end_tray_called = false;\n\n  // 前向声明全局变量\n  extern struct tray_menu tray_menus[];\n  extern struct tray tray;\n\n  // 静态字符串变量用于存储本地化的菜单文本\n  // 这些变量必须是静态的，以确保在 tray_menus 的生命周期内有效\n  static std::string s_open_sunshine;\n  static std::string s_vdd_base_display;\n  static std::string s_vdd_create;\n  static std::string s_vdd_close;\n  static std::string s_vdd_persistent;\n  static std::string s_vdd_headless_create;\n  static std::string s_import_config;\n  static std::string s_export_config;\n  static std::string s_reset_to_default;\n  static std::string s_language;\n  static std::string s_chinese;\n  static std::string s_english;\n  static std::string s_japanese;\n  static std::string s_star_project;\n  static std::string s_visit_project;\n  static std::string s_visit_project_sunshine;\n  static std::string s_visit_project_moonlight;\n  static std::string s_advanced_settings;\n  static std::string s_close_app;\n  static std::string s_reset_display_device_config;\n  static std::string s_restart;\n\n  static bool s_vdd_in_cooldown = false;\n  static std::string s_quit;\n\n  // 用于存储子菜单的静态数组\n  static struct tray_menu vdd_submenu[5];\n  static struct tray_menu advanced_settings_submenu[7];\n  static struct tray_menu visit_project_submenu[3];\n\n  // 更新高级设置菜单项的文本\n  static void update_advanced_settings_menu_text() {\n    advanced_settings_submenu[0].text = s_import_config.c_str();\n    advanced_settings_submenu[1].text = s_export_config.c_str();\n    advanced_settings_submenu[2].text = s_reset_to_default.c_str();\n    advanced_settings_submenu[3].text = \"-\";\n    advanced_settings_submenu[4].text = s_close_app.c_str();\n    advanced_settings_submenu[5].text = s_reset_display_device_config.c_str();\n  }\n\n  // 更新 VDD 子菜单项的文本\n  static void update_vdd_submenu_text() {\n    vdd_submenu[0].text = s_vdd_create.c_str();\n    vdd_submenu[1].text = s_vdd_close.c_str();\n    vdd_submenu[2].text = s_vdd_persistent.c_str();\n    vdd_submenu[3].text = s_vdd_headless_create.c_str();\n  }\n\n  static void clear_tray_notification() {\n    tray.notification_title = NULL;\n    tray.notification_text = NULL;\n    tray.notification_icon = NULL;\n    tray.notification_cb = NULL;\n  }\n\n  // 更新访问项目地址子菜单项的文本\n  static void tray_visit_project_submenu_text() {\n    visit_project_submenu[0].text = s_visit_project_sunshine.c_str();\n    visit_project_submenu[1].text = s_visit_project_moonlight.c_str();\n  }\n\n  // 初始化本地化字符串\n  void\n  init_localized_strings() {\n    s_open_sunshine = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_OPEN_SUNSHINE);\n    s_vdd_base_display = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_BASE_DISPLAY);\n    s_vdd_create = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CREATE);\n    s_vdd_close = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_CLOSE);\n    s_vdd_persistent = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_PERSISTENT);\n    s_vdd_headless_create = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_HEADLESS_CREATE);\n    s_import_config = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_CONFIG);\n    s_export_config = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_CONFIG);\n    s_reset_to_default = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_TO_DEFAULT);\n    s_language = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_LANGUAGE);\n    s_chinese = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_CHINESE);\n    s_english = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_ENGLISH);\n    s_japanese = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_JAPANESE);\n    s_star_project = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_STAR_PROJECT);\n    s_visit_project = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VISIT_PROJECT);\n    s_visit_project_sunshine = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VISIT_PROJECT_SUNSHINE);\n    s_visit_project_moonlight = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VISIT_PROJECT_MOONLIGHT);\n    s_advanced_settings = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_ADVANCED_SETTINGS);\n    s_close_app = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_CLOSE_APP);\n    s_reset_display_device_config = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_DISPLAY_DEVICE_CONFIG);\n    s_restart = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESTART);\n    s_quit = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_QUIT);\n  }\n\n  // 更新所有菜单项的文本\n  void\n  update_menu_texts() {\n    init_localized_strings();\n    tray_menus[0].text = s_open_sunshine.c_str();\n    tray_menus[2].text = s_vdd_base_display.c_str();\n    update_vdd_submenu_text();  // 更新 VDD 子菜单文本\n  #ifdef _WIN32\n    tray_menus[3].text = s_advanced_settings.c_str();\n    update_advanced_settings_menu_text();\n  #endif\n    tray_menus[5].text = s_language.c_str();\n    tray_menus[5].submenu[0].text = s_chinese.c_str();\n    tray_menus[5].submenu[1].text = s_english.c_str();\n    tray_menus[5].submenu[2].text = s_japanese.c_str();\n    tray_menus[7].text = s_star_project.c_str();\n    tray_menus[8].text = s_visit_project.c_str();\n    tray_visit_project_submenu_text();\n  #ifdef _WIN32\n    tray_menus[10].text = s_restart.c_str();\n    tray_menus[11].text = s_quit.c_str();\n  #else\n    tray_menus[9].text = s_restart.c_str();\n    tray_menus[10].text = s_quit.c_str();\n  #endif\n  }\n\n  auto tray_open_ui_cb = [](struct tray_menu *item) {\n    BOOST_LOG(debug) << \"Opening UI from system tray\"sv;\n    launch_ui();\n  };\n\n  // 检查 VDD 是否存在\n  static bool is_vdd_active() {\n    auto vdd_device_id = display_device::find_device_by_friendlyname(ZAKO_NAME);\n    return !vdd_device_id.empty();\n  }\n\n  // 更新 VDD 菜单项的文本和状态\n  static void update_vdd_menu_text() {\n    bool vdd_active = is_vdd_active();\n    bool keep_enabled = config::video.vdd_keep_enabled;\n    \n    // 1. 创建项：启用即勾选，启用后或冷却中禁止点击\n    vdd_submenu[0].checked = vdd_active ? 1 : 0;\n    vdd_submenu[0].disabled = (vdd_active || s_vdd_in_cooldown) ? 1 : 0;\n    \n    // 2. 关闭项：未启用即勾选，未启用、冷却中或保持启用模式下禁止点击\n    vdd_submenu[1].checked = vdd_active ? 0 : 1;\n    vdd_submenu[1].disabled = (!vdd_active || s_vdd_in_cooldown || keep_enabled) ? 1 : 0;\n    \n    // 3. 保持启用项\n    vdd_submenu[2].checked = keep_enabled ? 1 : 0;\n\n    // 4. 无显示器时自动创建\n    vdd_submenu[3].checked = config::video.vdd_headless_create_enabled ? 1 : 0;\n  }\n\n  // 启动统一的 10 秒冷却\n  static void start_vdd_cooldown() {\n    s_vdd_in_cooldown = true;\n    update_vdd_menu_text();\n    tray_update(&tray);\n\n    std::thread([&]() {\n      std::this_thread::sleep_for(10s);\n      s_vdd_in_cooldown = false;\n      update_vdd_menu_text();\n      tray_update(&tray);\n    }).detach();\n  }\n\n  // 创建虚拟显示器回调\n  auto tray_vdd_create_cb = [](struct tray_menu *item) {\n    if (!tray_initialized) return;\n    if (s_vdd_in_cooldown || is_vdd_active()) return;\n\n    BOOST_LOG(info) << \"Creating VDD from system tray (Separate Item)\"sv;\n    if (display_device::session_t::get().toggle_display_power()) {\n      start_vdd_cooldown();\n    }\n  };\n\n  // 关闭虚拟显示器回调\n  auto tray_vdd_destroy_cb = [](struct tray_menu *item) {\n    if (!tray_initialized) return;\n    if (s_vdd_in_cooldown || !is_vdd_active() || config::video.vdd_keep_enabled) return;\n\n    BOOST_LOG(info) << \"Closing VDD from system tray (Separate Item)\"sv;\n    display_device::session_t::get().destroy_vdd_monitor();\n    start_vdd_cooldown();\n  };\n\n  // 保持启用回调\n  auto tray_vdd_persistent_cb = [](struct tray_menu *item) {\n    BOOST_LOG(info) << \"Toggling persistent VDD from system tray\"sv;\n    \n    bool is_persistent = config::video.vdd_keep_enabled;\n    \n    if (!is_persistent) {\n      // 启用保持启用模式前弹出确认\n#ifdef _WIN32\n      std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_PERSISTENT_CONFIRM_TITLE));\n      std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_PERSISTENT_CONFIRM_MSG));\n\n      if (MessageBoxW(NULL, message.c_str(), title.c_str(), MB_YESNO | MB_ICONQUESTION) != IDYES) {\n        BOOST_LOG(info) << \"User cancelled enabling VDD keep-enabled mode\";\n        return;\n      }\n#endif\n      config::video.vdd_keep_enabled = true;\n      BOOST_LOG(info) << \"Enabled VDD keep-enabled mode (Auto-creation removed)\";\n    } else {\n      // 禁用保持启用模式，但不自动关闭 VDD\n      config::video.vdd_keep_enabled = false;\n      BOOST_LOG(info) << \"Disabled VDD keep-enabled mode (VDD remains if active)\";\n    }\n    \n    // 保存配置到文件\n    config::update_config({{\"vdd_keep_enabled\", config::video.vdd_keep_enabled ? \"true\" : \"false\"}});\n    \n    update_vdd_menu_text();\n    tray_update(&tray);\n  };\n\n  // 无显示器时自动创建\n  auto tray_vdd_headless_create_cb = [](struct tray_menu *item) {\n    BOOST_LOG(info) << \"Toggling headless VDD create from system tray\"sv;\n    if (!config::video.vdd_headless_create_enabled) {\n      // 启用前二次确认\n#ifdef _WIN32\n      std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_HEADLESS_CREATE_CONFIRM_TITLE));\n      std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_VDD_HEADLESS_CREATE_CONFIRM_MSG));\n      if (MessageBoxW(NULL, message.c_str(), title.c_str(), MB_YESNO | MB_ICONQUESTION) != IDYES) {\n        BOOST_LOG(info) << \"User cancelled enabling headless VDD create\";\n        return;\n      }\n#endif\n    }\n    config::video.vdd_headless_create_enabled = !config::video.vdd_headless_create_enabled;\n    config::update_config({{\"vdd_headless_create\", config::video.vdd_headless_create_enabled ? \"true\" : \"false\"}});\n    update_vdd_menu_text();\n    tray_update(&tray);\n  };\n\n  auto tray_close_app_cb = [](struct tray_menu *item) {\n    if (!tray_initialized) return;\n\n  #ifdef _WIN32\n    std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_CLOSE_APP_CONFIRM_TITLE));\n    std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_CLOSE_APP_CONFIRM_MSG));\n\n    int msgboxID = MessageBoxW(\n      NULL,\n      message.c_str(),\n      title.c_str(),\n      MB_ICONQUESTION | MB_YESNO);\n\n    if (msgboxID == IDYES) {\n      BOOST_LOG(info) << \"Clearing cache (terminating application) from system tray\"sv;\n      proc::proc.terminate();\n    }\n    else {\n      BOOST_LOG(info) << \"User cancelled clearing cache\"sv;\n    }\n  #else\n    // 非 Windows 平台，直接关闭\n    BOOST_LOG(info) << \"Closing application from system tray\"sv;\n    proc::proc.terminate();\n  #endif\n  };\n\n  auto tray_reset_display_device_config_cb = [](struct tray_menu *item) {\n    if (!tray_initialized) return;\n\n  #ifdef _WIN32\n    std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_DISPLAY_CONFIRM_TITLE));\n    std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_DISPLAY_CONFIRM_MSG));\n\n    int msgboxID = MessageBoxW(\n      NULL,\n      message.c_str(),\n      title.c_str(),\n      MB_ICONWARNING | MB_YESNO);\n\n    if (msgboxID == IDYES) {\n      BOOST_LOG(info) << \"Resetting display device config from system tray\"sv;\n      display_device::session_t::get().reset_persistence();\n    }\n    else {\n      BOOST_LOG(info) << \"User cancelled resetting display device config\"sv;\n    }\n  #else\n    // 非 Windows 平台，直接重置\n    BOOST_LOG(info) << \"Resetting display device config from system tray\"sv;\n    display_device::session_t::get().reset_persistence();\n  #endif\n  };\n\n  auto tray_restart_cb = [](struct tray_menu *item) {\n    BOOST_LOG(info) << \"Restarting from system tray\"sv;\n    platf::restart();\n  };\n\n  auto terminate_gui_processes = []() {\n  #ifdef _WIN32\n    BOOST_LOG(info) << \"Terminating sunshine-gui.exe processes...\"sv;\n\n    // Find and terminate sunshine-gui.exe processes\n    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);\n    if (snapshot != INVALID_HANDLE_VALUE) {\n      PROCESSENTRY32W pe32;\n      pe32.dwSize = sizeof(PROCESSENTRY32W);\n\n      if (Process32FirstW(snapshot, &pe32)) {\n        do {\n          // Check if this process is sunshine-gui.exe\n          if (wcscmp(pe32.szExeFile, L\"sunshine-gui.exe\") == 0) {\n            BOOST_LOG(info) << \"Found sunshine-gui.exe (PID: \" << pe32.th32ProcessID << \"), terminating...\"sv;\n\n            // Open process handle\n            HANDLE process_handle = OpenProcess(PROCESS_TERMINATE, FALSE, pe32.th32ProcessID);\n            if (process_handle != NULL) {\n              // Terminate the process\n              if (TerminateProcess(process_handle, 0)) {\n                BOOST_LOG(info) << \"Successfully terminated sunshine-gui.exe\"sv;\n              }\n              CloseHandle(process_handle);\n            }\n          }\n        } while (Process32NextW(snapshot, &pe32));\n      }\n      CloseHandle(snapshot);\n    }\n  #else\n    // For non-Windows platforms, this is a no-op\n    BOOST_LOG(debug) << \"GUI process termination not implemented for this platform\"sv;\n  #endif\n  };\n\n  auto tray_quit_cb = [](struct tray_menu *item) {\n    BOOST_LOG(info) << \"Quitting from system tray\"sv;\n\n  #ifdef _WIN32\n    // Get localized strings\n    std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_QUIT_TITLE));\n    std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_QUIT_MESSAGE));\n\n    int msgboxID = MessageBoxW(\n      NULL,\n      message.c_str(),\n      title.c_str(),\n      MB_ICONQUESTION | MB_YESNO);\n\n    if (msgboxID == IDYES) {\n      // First, terminate sunshine-gui.exe if it's running\n      terminate_gui_processes();\n\n      // If running as service (no console window), use ERROR_SHUTDOWN_IN_PROGRESS\n      // Otherwise, use exit code 0 to prevent service restart\n      if (GetConsoleWindow() == NULL) {\n        lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true);\n      }\n      else {\n        lifetime::exit_sunshine(0, false);\n      }\n      return;\n    }\n  #else\n    // For non-Windows platforms, just exit normally\n    lifetime::exit_sunshine(0, true);\n  #endif\n  };\n\n  auto tray_star_project_cb = [](struct tray_menu *item) {\n    platf::open_url_in_browser(\"https://www.alkaidlab.com/\");\n  };\n\n  auto tray_visit_project_sunshine_cb = [](struct tray_menu *item) {\n    platf::open_url_in_browser(\"https://github.com/AlkaidLab/foundation-sunshine\");\n  };\n\n  auto tray_visit_project_moonlight_cb = [](struct tray_menu *item) {\n    platf::open_url_in_browser(\"https://github.com/qiin2333/moonlight-vplus\");\n  };\n\n\n  // 文件对话框打开标志\n  static bool file_dialog_open = false;\n\n  // 安全验证：检查文件路径是否安全\n  auto is_safe_config_path = [](const std::string &path) -> bool {\n    try {\n      std::filesystem::path p(path);\n\n      // 检查文件是否存在\n      if (!std::filesystem::exists(p)) {\n        BOOST_LOG(warning) << \"[tray_check_config] File does not exist: \" << path;\n        return false;\n      }\n\n      // 规范化路径（解析符号链接和..）\n      std::filesystem::path canonical_path = std::filesystem::canonical(p);\n\n      // 检查文件扩展名\n      if (canonical_path.extension() != \".conf\") {\n        BOOST_LOG(warning) << \"[tray_check_config] Invalid file extension: \" << canonical_path.extension().string();\n        return false;\n      }\n\n      // 防止符号链接攻击\n      if (std::filesystem::is_symlink(p)) {\n        BOOST_LOG(warning) << \"[tray_check_config] Symlink not allowed: \" << path;\n        return false;\n      }\n\n      // 确保是常规文件\n      if (!std::filesystem::is_regular_file(canonical_path)) {\n        BOOST_LOG(warning) << \"[tray_check_config] Not a regular file: \" << path;\n        return false;\n      }\n\n      return true;\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(error) << \"[tray_check_config] Path validation error: \" << e.what();\n      return false;\n    }\n  };\n\n  // 安全验证：检查配置文件内容是否安全\n  auto is_safe_config_content = [](const std::string &content) -> bool {\n    // 检查文件大小（最大1MB）\n    const size_t MAX_CONFIG_SIZE = 1024 * 1024;\n    if (content.size() > MAX_CONFIG_SIZE) {\n      BOOST_LOG(warning) << \"[tray_check_config] Config file too large: \" << content.size() << \" bytes\";\n      return false;\n    }\n\n    // 检查是否为空\n    if (content.empty()) {\n      BOOST_LOG(warning) << \"[tray_check_config] Config file is empty\";\n      return false;\n    }\n\n    // 基本格式验证：尝试解析配置\n    try {\n      auto vars = config::parse_config(content);\n      // 如果解析成功，说明格式基本正确\n      BOOST_LOG(debug) << \"[tray_check_config] Config validation passed, \" << vars.size() << \" entries found\";\n      return true;\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(warning) << \"[tray_check_config] Config parsing failed: \" << e.what();\n      return false;\n    }\n  };\n\n\n  // 配置导入功能\n  auto tray_import_config_cb = [](struct tray_menu *item) {\n    if (file_dialog_open) {\n      BOOST_LOG(warning) << \"[tray_import_config] A file dialog is already open\";\n      return;\n    }\n    file_dialog_open = true;\n    auto clear_flag = util::fail_guard([&]() {\n      file_dialog_open = false;\n    });\n\n    BOOST_LOG(info) << \"[tray_import_config] Importing configuration from system tray\"sv;\n\n  #ifdef _WIN32\n    std::wstring file_path_wide;\n    bool file_selected = false;\n\n    // 直接显示文件对话框\n    auto show_file_dialog = [&]() {\n      // 初始化COM\n      HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);\n      bool com_initialized = SUCCEEDED(hr);\n      auto com_cleanup = util::fail_guard([com_initialized]() {\n        if (com_initialized) {\n          CoUninitialize();\n        }\n      });\n      \n      IFileOpenDialog *pFileOpen = nullptr;\n      hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen));\n      if (FAILED(hr)) {\n        BOOST_LOG(error) << \"[tray_import_config] Failed to create IFileOpenDialog: \" << hr;\n        return;\n      }\n      auto dialog_cleanup = util::fail_guard([pFileOpen]() {\n        pFileOpen->Release();\n      });\n      \n      // 设置对话框选项\n      // FOS_FORCEFILESYSTEM: 强制只使用文件系统\n      // FOS_DONTADDTORECENT: 不添加到最近文件列表\n      // FOS_NOCHANGEDIR: 不改变当前工作目录\n      // FOS_HIDEPINNEDPLACES: 隐藏固定的位置（导航面板中的快速访问等）\n      // FOS_NOVALIDATE: 不验证文件路径（避免访问不存在的系统路径）\n      DWORD dwFlags;\n      pFileOpen->GetOptions(&dwFlags);\n      pFileOpen->SetOptions(dwFlags | FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | \n                            FOS_FORCEFILESYSTEM | FOS_DONTADDTORECENT | \n                            FOS_NOCHANGEDIR | FOS_HIDEPINNEDPLACES | FOS_NOVALIDATE);\n      \n      // 设置文件类型过滤器\n      std::wstring config_files = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_FILE_DIALOG_CONFIG_FILES));\n      COMDLG_FILTERSPEC fileTypes[] = {\n        { config_files.c_str(), L\"*.conf\" },\n        { L\"All Files\", L\"*.*\" }\n      };\n      pFileOpen->SetFileTypes(2, fileTypes);\n      pFileOpen->SetFileTypeIndex(1);\n      \n      // 设置对话框标题\n      std::wstring dialog_title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_FILE_DIALOG_SELECT_IMPORT));\n      pFileOpen->SetTitle(dialog_title.c_str());\n      \n      // 设置默认打开路径为应用程序配置目录\n      IShellItem *psiDefault = NULL;\n      std::wstring default_path = platf::appdata().wstring();\n      hr = SHCreateItemFromParsingName(default_path.c_str(), NULL, IID_PPV_ARGS(&psiDefault));\n      if (SUCCEEDED(hr)) {\n        pFileOpen->SetFolder(psiDefault);\n        psiDefault->Release();\n      }\n      \n      // 手动添加驱动器到导航栏 (因为使用了 FOS_HIDEPINNEDPLACES)\n      DWORD dwSize = GetLogicalDriveStringsW(0, NULL);\n      if (dwSize > 0) {\n        std::vector<wchar_t> buffer(dwSize + 1);\n        if (GetLogicalDriveStringsW(dwSize, buffer.data())) {\n          wchar_t* pDrive = buffer.data();\n          while (*pDrive) {\n            IShellItem *psiDrive = NULL;\n            HRESULT hrDrive = SHCreateItemFromParsingName(pDrive, NULL, IID_PPV_ARGS(&psiDrive));\n            if (SUCCEEDED(hrDrive)) {\n              pFileOpen->AddPlace(psiDrive, FDAP_BOTTOM);\n              psiDrive->Release();\n            }\n            pDrive += wcslen(pDrive) + 1;\n          }\n        }\n      }\n      \n      // 添加\"此电脑\"到导航栏顶部\n      IShellItem *psiComputer = NULL;\n      hr = SHGetKnownFolderItem(FOLDERID_ComputerFolder, KF_FLAG_DEFAULT, NULL, IID_PPV_ARGS(&psiComputer));\n      if (SUCCEEDED(hr)) {\n        pFileOpen->AddPlace(psiComputer, FDAP_TOP);\n        psiComputer->Release();\n      }\n      \n      // 添加\"网络\"到导航栏\n      IShellItem *psiNetwork = NULL;\n      hr = SHGetKnownFolderItem(FOLDERID_NetworkFolder, KF_FLAG_DEFAULT, NULL, IID_PPV_ARGS(&psiNetwork));\n      if (SUCCEEDED(hr)) {\n        pFileOpen->AddPlace(psiNetwork, FDAP_BOTTOM);\n        psiNetwork->Release();\n      }\n      \n      // 显示对话框\n      hr = pFileOpen->Show(NULL);\n      if (SUCCEEDED(hr)) {\n        // 获取选择的文件\n        IShellItem *pItem = nullptr;\n        hr = pFileOpen->GetResult(&pItem);\n        if (SUCCEEDED(hr)) {\n          PWSTR pszFilePath = nullptr;\n          hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);\n          if (SUCCEEDED(hr)) {\n            file_path_wide = pszFilePath;\n            file_selected = true;\n            CoTaskMemFree(pszFilePath);\n          }\n          pItem->Release();\n        }\n      }\n      else if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {\n        BOOST_LOG(info) << \"[tray_import_config] User cancelled file dialog\"sv;\n      }\n      else {\n        BOOST_LOG(warning) << \"[tray_import_config] File dialog failed: 0x\" << std::hex << hr << std::dec;\n      }\n    };\n\n    // 直接显示文件对话框\n    show_file_dialog();\n\n    if (file_selected) {\n      std::string file_path = platf::to_utf8(file_path_wide);\n\n      // 安全验证：检查文件路径\n      if (!is_safe_config_path(file_path)) {\n        BOOST_LOG(error) << \"[tray_import_config] Config import rejected: unsafe file path: \" << file_path;\n        std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_ERROR_TITLE));\n        std::wstring message = L\"文件路径不安全或文件类型无效。\\n只允许 .conf 文件，不允许符号链接。\";\n        MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n        return;\n      }\n\n      try {\n        // 读取配置文件内容\n        std::string config_content = file_handler::read_file(file_path.c_str());\n        \n        // 安全验证：检查配置内容\n        if (!is_safe_config_content(config_content)) {\n          BOOST_LOG(error) << \"[tray_import_config] Config import rejected: unsafe content: \" << file_path;\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_ERROR_TITLE));\n          std::wstring message = L\"配置文件内容无效、太大或格式错误。\\n最大文件大小：1MB\";\n          MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n          return;\n        }\n\n        // 备份当前配置（检查是否成功）\n        std::string backup_path = config::sunshine.config_file + \".backup\";\n        std::string current_config = file_handler::read_file(config::sunshine.config_file.c_str());\n        int backup_result = file_handler::write_file(backup_path.c_str(), current_config);\n        \n        if (backup_result != 0) {\n          BOOST_LOG(error) << \"[tray_import_config] Failed to create backup, aborting import\";\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_ERROR_TITLE));\n          std::wstring message = L\"无法创建配置备份，导入操作已中止。\";\n          MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n          return;\n        }\n\n        BOOST_LOG(info) << \"[tray_import_config] Config backup created: \" << backup_path;\n\n        // 使用临时文件确保原子性写入\n        std::string temp_path = config::sunshine.config_file + \".tmp\";\n        int temp_result = file_handler::write_file(temp_path.c_str(), config_content);\n        \n        if (temp_result != 0) {\n          BOOST_LOG(error) << \"[tray_import_config] Failed to write temporary config file\";\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_ERROR_TITLE));\n          std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_ERROR_WRITE));\n          MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n          return;\n        }\n\n        // 原子性替换：重命名临时文件为实际配置文件\n        try {\n          std::filesystem::rename(temp_path, config::sunshine.config_file);\n          BOOST_LOG(info) << \"[tray_import_config] Configuration imported successfully from: \" << file_path;\n          \n          // 询问用户是否重启Sunshine以应用新配置\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_SUCCESS_TITLE));\n          std::wstring message = L\"配置导入成功！\\n\\n是否立即重启 Sunshine 以应用新配置？\";\n          int result = MessageBoxW(NULL, message.c_str(), title.c_str(), MB_YESNO | MB_ICONQUESTION);\n          \n          if (result == IDYES) {\n            BOOST_LOG(info) << \"[tray_import_config] User chose to restart Sunshine\"sv;\n            // 重启Sunshine\n            platf::restart();\n          }\n          else {\n            BOOST_LOG(info) << \"[tray_import_config] User chose not to restart Sunshine\"sv;\n          }\n        }\n        catch (const std::exception &e) {\n          BOOST_LOG(error) << \"[tray_import_config] Failed to rename temp file: \" << e.what();\n          // 清理临时文件\n          std::filesystem::remove(temp_path);\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_ERROR_TITLE));\n          std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_ERROR_WRITE));\n          MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n        }\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(error) << \"[tray_import_config] Exception during config import: \" << e.what();\n        std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_ERROR_TITLE));\n        std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_IMPORT_ERROR_EXCEPTION));\n        MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n      }\n    }\n  #else\n    // 非Windows平台的实现（可以后续添加）\n    BOOST_LOG(info) << \"[tray_import_config] Config import not implemented for this platform yet\";\n  #endif\n  };\n\n  // 配置导出功能\n  auto tray_export_config_cb = [](struct tray_menu *item) {\n    if (file_dialog_open) {\n      BOOST_LOG(warning) << \"[tray_export_config] A file dialog is already open\";\n      return;\n    }\n    file_dialog_open = true;\n    auto clear_flag = util::fail_guard([&]() {\n      file_dialog_open = false;\n    });\n\n    BOOST_LOG(info) << \"[tray_export_config] Exporting configuration from system tray\"sv;\n\n  #ifdef _WIN32\n    std::wstring file_path_wide;\n    bool file_selected = false;\n\n    // 直接显示文件对话框\n    auto show_file_dialog = [&]() {\n      // 初始化COM\n      HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);\n      bool com_initialized = SUCCEEDED(hr);\n      auto com_cleanup = util::fail_guard([com_initialized]() {\n        if (com_initialized) {\n          CoUninitialize();\n        }\n      });\n\n      IFileSaveDialog *pFileSave = nullptr;\n      hr = CoCreateInstance(CLSID_FileSaveDialog, NULL, CLSCTX_ALL, IID_IFileSaveDialog, reinterpret_cast<void**>(&pFileSave));\n      if (FAILED(hr)) {\n        BOOST_LOG(error) << \"[tray_export_config] Failed to create IFileSaveDialog: \" << hr;\n        return;\n      }\n      auto dialog_cleanup = util::fail_guard([pFileSave]() {\n        pFileSave->Release();\n      });\n\n      // 设置对话框选项\n      // FOS_FORCEFILESYSTEM: 强制只使用文件系统\n      // FOS_DONTADDTORECENT: 不添加到最近文件列表\n      // FOS_NOCHANGEDIR: 不改变当前工作目录\n      // FOS_HIDEPINNEDPLACES: 隐藏固定的位置（导航面板中的快速访问等）\n      // FOS_NOVALIDATE: 不验证文件路径（避免访问不存在的系统路径）\n      DWORD dwFlags;\n      pFileSave->GetOptions(&dwFlags);\n      pFileSave->SetOptions(dwFlags | FOS_PATHMUSTEXIST | FOS_OVERWRITEPROMPT | \n                            FOS_FORCEFILESYSTEM | FOS_DONTADDTORECENT | \n                            FOS_NOCHANGEDIR | FOS_HIDEPINNEDPLACES | FOS_NOVALIDATE);\n\n      // 设置文件类型过滤器\n      std::wstring config_files = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_FILE_DIALOG_CONFIG_FILES));\n      COMDLG_FILTERSPEC fileTypes[] = {\n        { config_files.c_str(), L\"*.conf\" },\n        { L\"All Files\", L\"*.*\" }\n      };\n      pFileSave->SetFileTypes(2, fileTypes);\n      pFileSave->SetFileTypeIndex(1);\n      pFileSave->SetDefaultExtension(L\"conf\");\n\n      // 设置对话框标题\n      std::wstring dialog_title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_FILE_DIALOG_SAVE_EXPORT));\n      pFileSave->SetTitle(dialog_title.c_str());\n\n      // 设置默认文件名\n      std::string default_name = \"sunshine_config_\" + std::to_string(std::time(nullptr)) + \".conf\";\n      std::wstring wdefault_name(default_name.begin(), default_name.end());\n      pFileSave->SetFileName(wdefault_name.c_str());\n\n      // 设置默认保存路径为应用程序配置目录\n      IShellItem *psiDefault = NULL;\n      std::wstring default_path = platf::appdata().wstring();\n      hr = SHCreateItemFromParsingName(default_path.c_str(), NULL, IID_PPV_ARGS(&psiDefault));\n      if (SUCCEEDED(hr)) {\n        pFileSave->SetFolder(psiDefault);\n        psiDefault->Release();\n      }\n\n      // 手动添加驱动器到导航栏 (因为使用了 FOS_HIDEPINNEDPLACES)\n      DWORD dwSize = GetLogicalDriveStringsW(0, NULL);\n      if (dwSize > 0) {\n        std::vector<wchar_t> buffer(dwSize + 1);\n        if (GetLogicalDriveStringsW(dwSize, buffer.data())) {\n          wchar_t* pDrive = buffer.data();\n          while (*pDrive) {\n            IShellItem *psiDrive = NULL;\n            HRESULT hrDrive = SHCreateItemFromParsingName(pDrive, NULL, IID_PPV_ARGS(&psiDrive));\n            if (SUCCEEDED(hrDrive)) {\n              pFileSave->AddPlace(psiDrive, FDAP_BOTTOM);\n              psiDrive->Release();\n            }\n            pDrive += wcslen(pDrive) + 1;\n          }\n        }\n      }\n      \n      // 添加\"此电脑\"到导航栏顶部\n      IShellItem *psiComputer = NULL;\n      hr = SHGetKnownFolderItem(FOLDERID_ComputerFolder, KF_FLAG_DEFAULT, NULL, IID_PPV_ARGS(&psiComputer));\n      if (SUCCEEDED(hr)) {\n        pFileSave->AddPlace(psiComputer, FDAP_TOP);\n        psiComputer->Release();\n      }\n\n      // 添加\"网络\"到导航栏\n      IShellItem *psiNetwork = NULL;\n      hr = SHGetKnownFolderItem(FOLDERID_NetworkFolder, KF_FLAG_DEFAULT, NULL, IID_PPV_ARGS(&psiNetwork));\n      if (SUCCEEDED(hr)) {\n        pFileSave->AddPlace(psiNetwork, FDAP_BOTTOM);\n        psiNetwork->Release();\n      }\n\n      // 显示对话框\n      hr = pFileSave->Show(NULL);\n      if (SUCCEEDED(hr)) {\n        // 获取选择的文件\n        IShellItem *pItem = nullptr;\n        hr = pFileSave->GetResult(&pItem);\n        if (SUCCEEDED(hr)) {\n          PWSTR pszFilePath = nullptr;\n          hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);\n          if (SUCCEEDED(hr)) {\n            file_path_wide = pszFilePath;\n            file_selected = true;\n            CoTaskMemFree(pszFilePath);\n          }\n          pItem->Release();\n        }\n      }\n      else if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {\n        BOOST_LOG(info) << \"[tray_export_config] User cancelled file dialog\"sv;\n      }\n      else {\n        BOOST_LOG(warning) << \"[tray_export_config] File dialog failed: 0x\" << std::hex << hr << std::dec;\n      }\n    };\n\n    // 直接显示文件对话框\n    show_file_dialog();\n\n    if (file_selected) {\n      std::string file_path = platf::to_utf8(file_path_wide);\n\n      // 安全验证：检查输出文件路径（基本检查）\n      try {\n        std::filesystem::path p(file_path);\n        \n        // 检查文件扩展名\n        if (p.extension() != \".conf\") {\n          BOOST_LOG(warning) << \"[tray_export_config] Config export rejected: invalid extension: \" << p.extension().string();\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_TITLE));\n          std::wstring message = L\"只允许导出为 .conf 文件。\";\n          MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n          return;\n        }\n\n        // 如果文件已存在，检查是否为符号链接\n        if (std::filesystem::exists(p) && std::filesystem::is_symlink(p)) {\n          BOOST_LOG(warning) << \"[tray_export_config] Config export rejected: target is symlink: \" << file_path;\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_TITLE));\n          std::wstring message = L\"不允许导出到符号链接。\";\n          MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n          return;\n        }\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(error) << \"[tray_export_config] Path validation error during export: \" << e.what();\n        std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_TITLE));\n        std::wstring message = L\"文件路径无效。\";\n        MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n        return;\n      }\n\n      try {\n        // 读取当前配置\n        std::string config_content = file_handler::read_file(config::sunshine.config_file.c_str());\n        if (config_content.empty()) {\n          BOOST_LOG(error) << \"[tray_export_config] No configuration to export\";\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_TITLE));\n          std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_NO_CONFIG));\n          MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n          return;\n        }\n\n        // 使用临时文件确保原子性写入\n        std::string temp_path = file_path + \".tmp\";\n        int temp_result = file_handler::write_file(temp_path.c_str(), config_content);\n        \n        if (temp_result != 0) {\n          BOOST_LOG(error) << \"[tray_export_config] Failed to write temporary export file\";\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_TITLE));\n          std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_WRITE));\n          MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n          return;\n        }\n\n        // 原子性替换\n        try {\n          std::filesystem::rename(temp_path, file_path);\n          BOOST_LOG(info) << \"[tray_export_config] Configuration exported successfully to: \" << file_path;\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_SUCCESS_TITLE));\n          std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_SUCCESS_MSG));\n          MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONINFORMATION);\n        }\n        catch (const std::exception &e) {\n          BOOST_LOG(error) << \"[tray_export_config] Failed to rename temp export file: \" << e.what();\n          // 清理临时文件\n          std::filesystem::remove(temp_path);\n          std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_TITLE));\n          std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_WRITE));\n          MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n        }\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(error) << \"[tray_export_config] Exception during config export: \" << e.what();\n        std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_TITLE));\n        std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_EXPORT_ERROR_EXCEPTION));\n        MessageBoxW(NULL, message.c_str(), title.c_str(), MB_OK | MB_ICONERROR);\n      }\n    }\n  #else\n    BOOST_LOG(info) << \"[tray_export_config] Config export not implemented for this platform yet\";\n  #endif\n  };\n\n  // 通用语言切换函数\n  static auto change_tray_language = [](const std::string &locale, const std::string &language_name) {\n    BOOST_LOG(info) << \"Changing tray language to \" << language_name << \" from system tray\"sv;\n    system_tray_i18n::set_tray_locale(locale);\n\n    // 保存到配置文件\n    config::update_config({{\"tray_locale\", locale}});\n\n    update_menu_texts();\n    tray_update(&tray);\n  };\n\n  auto tray_language_chinese_cb = [](struct tray_menu *item) {\n    change_tray_language(\"zh\", \"Chinese\");\n  };\n\n  auto tray_language_english_cb = [](struct tray_menu *item) {\n    change_tray_language(\"en\", \"English\");\n  };\n\n  auto tray_language_japanese_cb = [](struct tray_menu *item) {\n    change_tray_language(\"ja\", \"Japanese\");\n  };\n\n  auto tray_reset_config_cb = [](struct tray_menu *item) {\n    BOOST_LOG(info) << \"Resetting configuration from system tray\"sv;\n\n  #ifdef _WIN32\n    // 获取本地化字符串\n    std::wstring title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_CONFIRM_TITLE));\n    std::wstring message = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_CONFIRM_MSG));\n\n    int msgboxID = MessageBoxW(\n      NULL,\n      message.c_str(),\n      title.c_str(),\n      MB_ICONWARNING | MB_YESNO);\n\n    if (msgboxID == IDYES) {\n      try {\n        // 备份当前配置\n        std::string backup_path = config::sunshine.config_file + \".backup\";\n        std::string current_config = file_handler::read_file(config::sunshine.config_file.c_str());\n        if (!current_config.empty()) {\n          file_handler::write_file(backup_path.c_str(), current_config);\n        }\n\n        // 创建空的配置文件（重置为默认值）\n        std::ofstream config_file(config::sunshine.config_file);\n        if (config_file.is_open()) {\n          config_file.close();\n          BOOST_LOG(info) << \"Configuration reset successfully\";\n          std::wstring success_title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_SUCCESS_TITLE));\n          std::wstring success_msg = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_SUCCESS_MSG));\n          MessageBoxW(NULL, success_msg.c_str(), success_title.c_str(), MB_OK | MB_ICONINFORMATION);\n        }\n        else {\n          BOOST_LOG(error) << \"Failed to reset configuration file\";\n          std::wstring error_title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_ERROR_TITLE));\n          std::wstring error_msg = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_ERROR_MSG));\n          MessageBoxW(NULL, error_msg.c_str(), error_title.c_str(), MB_OK | MB_ICONERROR);\n        }\n      }\n      catch (const std::exception &e) {\n        BOOST_LOG(error) << \"Exception during config reset: \" << e.what();\n        std::wstring error_title = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_ERROR_TITLE));\n        std::wstring error_msg = system_tray_i18n::utf8_to_wstring(system_tray_i18n::get_localized_string(system_tray_i18n::KEY_RESET_ERROR_EXCEPTION));\n        MessageBoxW(NULL, error_msg.c_str(), error_title.c_str(), MB_OK | MB_ICONERROR);\n      }\n    }\n  #else\n    BOOST_LOG(info) << \"Config reset not implemented for this platform yet\";\n  #endif\n  };\n\n  // 菜单数组定义\n  struct tray_menu tray_menus[] = {\n    { .text = \"Open Sunshine\", .cb = tray_open_ui_cb },\n    { .text = \"-\" },\n  #ifdef _WIN32\n    { .text = \"Foundation Display\", .submenu = vdd_submenu },\n    { .text = \"Advanced Settings\", .submenu = advanced_settings_submenu },\n  #endif\n    { .text = \"-\" },\n    { .text = \"Language\",\n      .submenu =\n        (struct tray_menu[]) {\n          { .text = \"中文\", .cb = tray_language_chinese_cb },\n          { .text = \"English\", .cb = tray_language_english_cb },\n          { .text = \"日本語\", .cb = tray_language_japanese_cb },\n          { .text = nullptr } } },\n    { .text = \"-\" },\n    { .text = \"Star Project\", .cb = tray_star_project_cb },\n    { .text = \"Visit Project\", .submenu = visit_project_submenu },\n    { .text = \"-\" },\n    { .text = \"Restart\", .cb = tray_restart_cb },\n    { .text = \"Quit\", .cb = tray_quit_cb },\n    { .text = nullptr }\n  };\n\n  struct tray tray = {\n    .icon = TRAY_ICON,\n    .tooltip = PROJECT_NAME,\n    .menu = tray_menus,\n    .iconPathCount = 4,\n    .allIconPaths = { TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING },\n  };\n\n  int\n  init_tray() {\n    // 初始化本地化字符串并更新菜单文本\n    update_menu_texts();\n\n  #ifdef _WIN32\n    // If we're running as SYSTEM, Explorer.exe will not have permission to open our thread handle\n    // to monitor for thread termination. If Explorer fails to open our thread, our tray icon\n    // will persist forever if we terminate unexpectedly. To avoid this, we will modify our thread\n    // DACL to add an ACE that allows SYNCHRONIZE access to Everyone.\n    {\n      PACL old_dacl;\n      PSECURITY_DESCRIPTOR sd;\n      auto error = GetSecurityInfo(GetCurrentThread(),\n        SE_KERNEL_OBJECT,\n        DACL_SECURITY_INFORMATION,\n        nullptr,\n        nullptr,\n        &old_dacl,\n        nullptr,\n        &sd);\n      if (error != ERROR_SUCCESS) {\n        BOOST_LOG(warning) << \"GetSecurityInfo() failed: \"sv << error;\n        return 1;\n      }\n\n      auto free_sd = util::fail_guard([sd]() {\n        LocalFree(sd);\n      });\n\n      SID_IDENTIFIER_AUTHORITY sid_authority = SECURITY_WORLD_SID_AUTHORITY;\n      PSID world_sid;\n      if (!AllocateAndInitializeSid(&sid_authority, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &world_sid)) {\n        error = GetLastError();\n        BOOST_LOG(warning) << \"AllocateAndInitializeSid() failed: \"sv << error;\n        return 1;\n      }\n\n      auto free_sid = util::fail_guard([world_sid]() {\n        FreeSid(world_sid);\n      });\n\n      EXPLICIT_ACCESS ea {};\n      ea.grfAccessPermissions = SYNCHRONIZE;\n      ea.grfAccessMode = GRANT_ACCESS;\n      ea.grfInheritance = NO_INHERITANCE;\n      ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;\n      ea.Trustee.ptstrName = (LPSTR) world_sid;\n\n      PACL new_dacl;\n      error = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl);\n      if (error != ERROR_SUCCESS) {\n        BOOST_LOG(warning) << \"SetEntriesInAcl() failed: \"sv << error;\n        return 1;\n      }\n\n      auto free_new_dacl = util::fail_guard([new_dacl]() {\n        LocalFree(new_dacl);\n      });\n\n      error = SetSecurityInfo(GetCurrentThread(),\n        SE_KERNEL_OBJECT,\n        DACL_SECURITY_INFORMATION,\n        nullptr,\n        nullptr,\n        new_dacl,\n        nullptr);\n      if (error != ERROR_SUCCESS) {\n        BOOST_LOG(warning) << \"SetSecurityInfo() failed: \"sv << error;\n        return 1;\n      }\n    }\n\n    // Wait for the shell to be initialized before registering the tray icon.\n    // This ensures the tray icon works reliably after a logoff/logon cycle.\n    while (GetShellWindow() == nullptr) {\n      Sleep(1000);\n    }\n  #endif\n\n    // 初始化 VDD 子菜单 (创建, 关闭, 保持启用, 无显示器时自动创建)\n    vdd_submenu[0] = { .text = s_vdd_create.c_str(), .cb = tray_vdd_create_cb };\n    vdd_submenu[1] = { .text = s_vdd_close.c_str(), .cb = tray_vdd_destroy_cb };\n    vdd_submenu[2] = { .text = s_vdd_persistent.c_str(), .checked = 0, .cb = tray_vdd_persistent_cb };\n    vdd_submenu[3] = { .text = s_vdd_headless_create.c_str(), .checked = 0, .cb = tray_vdd_headless_create_cb };\n    vdd_submenu[4] = { .text = nullptr };\n\n  #ifdef _WIN32\n    advanced_settings_submenu[0] = { .text = s_import_config.c_str(), .cb = tray_import_config_cb };\n    advanced_settings_submenu[1] = { .text = s_export_config.c_str(), .cb = tray_export_config_cb };\n    advanced_settings_submenu[2] = { .text = s_reset_to_default.c_str(), .cb = tray_reset_config_cb };\n    advanced_settings_submenu[3] = { .text = \"-\" };\n    advanced_settings_submenu[4] = { .text = s_close_app.c_str(), .cb = tray_close_app_cb };\n    advanced_settings_submenu[5] = { .text = s_reset_display_device_config.c_str(), .cb = tray_reset_display_device_config_cb };\n    advanced_settings_submenu[6] = { .text = nullptr };\n  #endif\n\n    // 初始化访问项目地址子菜单\n    visit_project_submenu[0] = { .text = s_visit_project_sunshine.c_str(), .cb = tray_visit_project_sunshine_cb };\n    visit_project_submenu[1] = { .text = s_visit_project_moonlight.c_str(), .cb = tray_visit_project_moonlight_cb };\n    visit_project_submenu[2] = { .text = nullptr };\n\n    if (tray_init(&tray) < 0) {\n      BOOST_LOG(warning) << \"Failed to create system tray\"sv;\n      return 1;\n    }\n    else {\n      BOOST_LOG(info) << \"System tray created\"sv;\n    }\n\n    // 初始化时更新 VDD 菜单状态\n    update_vdd_menu_text();\n  #ifdef _WIN32\n    // 初始化时更新高级设置菜单文本\n    update_advanced_settings_menu_text();\n  #endif\n    // 初始化时更新访问项目地址子菜单文本\n    tray_visit_project_submenu_text();\n    tray_update(&tray);\n\n    tray_initialized = true;\n    return 0;\n  }\n\n  int\n  process_tray_events() {\n    if (!tray_initialized) {\n      BOOST_LOG(error) << \"System tray is not initialized\"sv;\n      return 1;\n    }\n\n    // Block until an event is processed or tray_quit() is called\n    return tray_loop(1);\n  }\n\n  int\n  end_tray() {\n    // Use atomic exchange to ensure only one call proceeds\n    if (end_tray_called.exchange(true)) {\n      return 0;\n    }\n\n    if (!tray_initialized) {\n      return 0;\n    }\n\n    tray_initialized = false;\n    tray_exit();\n    return 0;\n  }\n\n  void\n  update_tray_playing(std::string app_name) {\n    if (!tray_initialized) {\n      return;\n    }\n\n    clear_tray_notification();\n    tray.icon = TRAY_ICON_PLAYING;\n    tray_update(&tray);\n    tray.icon = TRAY_ICON_PLAYING;\n\n    // 使用本地化字符串（每次都重新获取以支持语言切换）\n    static std::string title;\n    static std::string msg;\n    title = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_STREAM_STARTED);\n    std::string msg_template = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_STREAMING_STARTED_FOR);\n\n    // 使用 std::string 格式化消息\n    char buffer[256];\n    snprintf(buffer, sizeof(buffer), msg_template.c_str(), app_name.c_str());\n    msg = buffer;\n\n    tray.notification_title = title.c_str();\n    tray.notification_text = msg.c_str();\n    tray.tooltip = msg.c_str();\n    tray.notification_icon = TRAY_ICON_PLAYING;\n    tray_update(&tray);\n\n    clear_tray_notification();\n  }\n\n  void\n  update_tray_pausing(std::string app_name) {\n    if (!tray_initialized) {\n      return;\n    }\n\n    clear_tray_notification();\n    tray.icon = TRAY_ICON_PAUSING;\n    tray_update(&tray);\n\n    // 使用本地化字符串（每次都重新获取以支持语言切换）\n    static std::string title;\n    static std::string msg;\n    title = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_STREAM_PAUSED);\n    std::string msg_template = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_STREAMING_PAUSED_FOR);\n\n    // 使用 std::string 格式化消息\n    char buffer[256];\n    snprintf(buffer, sizeof(buffer), msg_template.c_str(), app_name.c_str());\n    msg = buffer;\n\n    tray.icon = TRAY_ICON_PAUSING;\n    tray.notification_title = title.c_str();\n    tray.notification_text = msg.c_str();\n    tray.tooltip = msg.c_str();\n    tray.notification_icon = TRAY_ICON_PAUSING;\n    tray_update(&tray);\n\n    clear_tray_notification();\n  }\n\n  void\n  update_tray_stopped(std::string app_name) {\n    if (!tray_initialized) {\n      return;\n    }\n\n    clear_tray_notification();\n    tray.icon = TRAY_ICON;\n    tray_update(&tray);\n\n    // 使用本地化字符串（每次都重新获取以支持语言切换）\n    static std::string title;\n    static std::string msg;\n    title = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_APPLICATION_STOPPED);\n    std::string msg_template = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_APPLICATION_STOPPED_MSG);\n\n    // 使用 std::string 格式化消息\n    char buffer[256];\n    snprintf(buffer, sizeof(buffer), msg_template.c_str(), app_name.c_str());\n    msg = buffer;\n\n    tray.icon = TRAY_ICON;\n    tray.notification_icon = TRAY_ICON;\n    tray.notification_title = title.c_str();\n    tray.notification_text = msg.c_str();\n    tray.tooltip = PROJECT_NAME;\n    tray_update(&tray);\n\n    clear_tray_notification();\n  }\n\n  void\n  update_tray_require_pin(std::string pin_name) {\n    if (!tray_initialized) {\n      return;\n    }\n\n    clear_tray_notification();\n    tray.icon = TRAY_ICON;\n    tray_update(&tray);\n    tray.icon = TRAY_ICON;\n\n    // 使用本地化字符串（每次都重新获取以支持语言切换）\n    static std::string title;\n    static std::string notification_text;\n    std::string title_template = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_INCOMING_PAIRING_REQUEST);\n    notification_text = system_tray_i18n::get_localized_string(system_tray_i18n::KEY_CLICK_TO_COMPLETE_PAIRING);\n\n    // 使用 std::string 格式化标题\n    char buffer[256];\n    snprintf(buffer, sizeof(buffer), title_template.c_str(), pin_name.c_str());\n    title = buffer;\n\n    tray.notification_title = title.c_str();\n    tray.notification_text = notification_text.c_str();\n    tray.notification_icon = TRAY_ICON_LOCKED;\n    tray.tooltip = pin_name.c_str();\n    tray.notification_cb = []() {\n      launch_ui_with_path(\"/pin\");\n    };\n    tray_update(&tray);\n\n    clear_tray_notification();\n  }\n \n  void\n  update_vdd_menu() {\n    if (!tray_initialized) {\n      return;\n    }\n    update_vdd_menu_text();\n    tray_update(&tray);\n  }\n\n  // Threading functions available on all platforms\n  static void\n  tray_thread_worker() {\n    BOOST_LOG(info) << \"System tray thread started\"sv;\n\n    // Initialize the tray in this thread\n    if (init_tray() != 0) {\n      BOOST_LOG(error) << \"Failed to initialize tray in thread\"sv;\n      return;\n    }\n\n    // Main tray event loop\n    while (process_tray_events() == 0);\n\n    BOOST_LOG(info) << \"System tray thread ended\"sv;\n  }\n\n  int\n  init_tray_threaded() {\n    // Reset the end_tray flag for new tray instance\n    end_tray_called = false;\n\n    try {\n      auto tray_thread = std::thread(tray_thread_worker);\n\n      // The tray thread doesn't require strong lifetime management.\n      // It will exit asynchronously when tray_exit() is called.\n      tray_thread.detach();\n\n      BOOST_LOG(info) << \"System tray thread initialized successfully\"sv;\n      return 0;\n    }\n    catch (const std::exception &e) {\n      BOOST_LOG(error) << \"Failed to create tray thread: \" << e.what();\n      return 1;\n    }\n  }\n\n}  // namespace system_tray\n#endif\n"
  },
  {
    "path": "src/system_tray.h",
    "content": "/**\n * @file src/system_tray.h\n * @brief Declarations for the system tray icon and notification system.\n */\n#pragma once\n\n/**\n * @brief Handles the system tray icon and notification system.\n */\nnamespace system_tray {\n  /**\n   * @brief Callback for opening the UI from the system tray.\n   * @param item The tray menu item.\n   */\n  void\n  tray_open_ui_cb(struct tray_menu *item);\n\n  /**\n   * @brief Callback for opening GitHub Sponsors from the system tray.\n   * @param item The tray menu item.\n   */\n  void\n  tray_donate_github_cb(struct tray_menu *item);\n\n  /**\n   * @brief Callback for opening Patreon from the system tray.\n   * @param item The tray menu item.\n   */\n  void\n  tray_donate_patreon_cb(struct tray_menu *item);\n\n  /**\n   * @brief Callback for opening PayPal donation from the system tray.\n   * @param item The tray menu item.\n   */\n  void\n  tray_donate_paypal_cb(struct tray_menu *item);\n\n  /**\n   * @brief Callback for resetting display device configuration.\n   * @param item The tray menu item.\n   */\n  void\n  tray_reset_display_device_config_cb(struct tray_menu *item);\n\n  /**\n   * @brief Callback for restarting Sunshine from the system tray.\n   * @param item The tray menu item.\n   */\n  void\n  tray_restart_cb(struct tray_menu *item);\n\n  /**\n   * @brief Callback for exiting Sunshine from the system tray.\n   * @param item The tray menu item.\n   */\n  void\n  tray_quit_cb(struct tray_menu *item);\n\n  /**\n   * @brief Initializes the system tray without starting a loop.\n   * @return 0 if initialization was successful, non-zero otherwise.\n   */\n  int init_tray();\n\n  /**\n   * @brief Processes a single tray event iteration.\n   * @return 0 if processing was successful, non-zero otherwise.\n   */\n  int process_tray_events();\n\n  /**\n   * @brief Exit the system tray.\n   * @return 0 after exiting the system tray.\n   */\n  int\n  end_tray();\n\n  /**\n   * @brief Sets the tray icon in playing mode and spawns the appropriate notification\n   * @param app_name The started application name\n   */\n  void\n  update_tray_playing(std::string app_name);\n\n  /**\n   * @brief Sets the tray icon in pausing mode (stream stopped but app running) and spawns the appropriate notification\n   * @param app_name The paused application name\n   */\n  void\n  update_tray_pausing(std::string app_name);\n\n  /**\n   * @brief Sets the tray icon in stopped mode (app and stream stopped) and spawns the appropriate notification\n   * @param app_name The started application name\n   */\n  void\n  update_tray_stopped(std::string app_name);\n\n  /**\n   * @brief Spawns a notification for PIN Pairing. Clicking it opens the PIN Web UI Page\n   */\n  void\n  update_tray_require_pin(std::string pin_name);\n  \n  /**\n   * @brief Initializes and runs the system tray in a separate thread.\n   * @return 0 if initialization was successful, non-zero otherwise.\n   */\n  int init_tray_threaded();\n\n  // Internationalization support\n  std::string get_localized_string(const std::string& key);\n  std::wstring get_localized_wstring(const std::string& key);\n  \n  // GUI process management\n  void terminate_gui_processes();\n\n  // VDD menu management\n  void update_vdd_menu();\n}  // namespace system_tray\n"
  },
  {
    "path": "src/system_tray_i18n.cpp",
    "content": "#include \"system_tray_i18n.h\"\n#include \"config.h\"\n\n#ifdef _WIN32\n  #include <windows.h>\n#endif\n\nnamespace system_tray_i18n {\n  // String key constants\n  const std::string KEY_QUIT_TITLE = \"quit_title\";\n  const std::string KEY_QUIT_MESSAGE = \"quit_message\";\n  \n  // Menu item keys\n  const std::string KEY_OPEN_SUNSHINE = \"open_sunshine\";\n  const std::string KEY_VDD_BASE_DISPLAY = \"vdd_base_display\";\n  const std::string KEY_VDD_CREATE = \"vdd_create\";\n  const std::string KEY_VDD_CLOSE = \"vdd_close\";\n  const std::string KEY_VDD_PERSISTENT = \"vdd_persistent\";\n  const std::string KEY_VDD_HEADLESS_CREATE = \"vdd_headless_create\";\n  const std::string KEY_VDD_HEADLESS_CREATE_CONFIRM_TITLE = \"vdd_headless_create_confirm_title\";\n  const std::string KEY_VDD_HEADLESS_CREATE_CONFIRM_MSG = \"vdd_headless_create_confirm_msg\";\n  const std::string KEY_VDD_CONFIRM_CREATE_TITLE = \"vdd_confirm_create_title\";\n  const std::string KEY_VDD_CONFIRM_CREATE_MSG = \"vdd_confirm_create_msg\";\n  const std::string KEY_VDD_CONFIRM_KEEP_TITLE = \"vdd_confirm_keep_title\";\n  const std::string KEY_VDD_CONFIRM_KEEP_MSG = \"vdd_confirm_keep_msg\";\n  const std::string KEY_VDD_CANCEL_CREATE_LOG = \"vdd_cancel_create_log\";\n  const std::string KEY_VDD_PERSISTENT_CONFIRM_TITLE = \"vdd_persistent_confirm_title\";\n  const std::string KEY_VDD_PERSISTENT_CONFIRM_MSG = \"vdd_persistent_confirm_msg\";\n  const std::string KEY_CONFIGURATION = \"configuration\";\n  const std::string KEY_IMPORT_CONFIG = \"import_config\";\n  const std::string KEY_EXPORT_CONFIG = \"export_config\";\n  const std::string KEY_RESET_TO_DEFAULT = \"reset_to_default\";\n  const std::string KEY_LANGUAGE = \"language\";\n  const std::string KEY_CHINESE = \"chinese\";\n  const std::string KEY_ENGLISH = \"english\";\n  const std::string KEY_JAPANESE = \"japanese\";\n  const std::string KEY_STAR_PROJECT = \"star_project\";\n  const std::string KEY_VISIT_PROJECT = \"visit_project\";\n  const std::string KEY_VISIT_PROJECT_SUNSHINE = \"visit_project_sunshine\";\n  const std::string KEY_VISIT_PROJECT_MOONLIGHT = \"visit_project_moonlight\";\n  const std::string KEY_HELP_US = \"help_us\";\n  const std::string KEY_DEVELOPER_YUNDI339 = \"developer_yundi339\";\n  const std::string KEY_DEVELOPER_QIIN = \"developer_qiin\";\n  const std::string KEY_SPONSOR_ALKaidLab = \"sponsor_alkaidlab\";\n  const std::string KEY_ADVANCED_SETTINGS = \"advanced_settings\";\n  const std::string KEY_CLOSE_APP = \"clear_cache\";\n  const std::string KEY_CLOSE_APP_CONFIRM_TITLE = \"clear_cache_confirm_title\";\n  const std::string KEY_CLOSE_APP_CONFIRM_MSG = \"clear_cache_confirm_msg\";\n  const std::string KEY_RESET_DISPLAY_DEVICE_CONFIG = \"reset_display_device_config\";\n  const std::string KEY_RESET_DISPLAY_CONFIRM_TITLE = \"reset_display_confirm_title\";\n  const std::string KEY_RESET_DISPLAY_CONFIRM_MSG = \"reset_display_confirm_msg\";\n  const std::string KEY_RESTART = \"restart\";\n  const std::string KEY_QUIT = \"quit\";\n  \n  // Notification message keys\n  const std::string KEY_STREAM_STARTED = \"stream_started\";\n  const std::string KEY_STREAMING_STARTED_FOR = \"streaming_started_for\";\n  const std::string KEY_STREAM_PAUSED = \"stream_paused\";\n  const std::string KEY_STREAMING_PAUSED_FOR = \"streaming_paused_for\";\n  const std::string KEY_APPLICATION_STOPPED = \"application_stopped\";\n  const std::string KEY_APPLICATION_STOPPED_MSG = \"application_stopped_msg\";\n  const std::string KEY_INCOMING_PAIRING_REQUEST = \"incoming_pairing_request\";\n  const std::string KEY_CLICK_TO_COMPLETE_PAIRING = \"click_to_complete_pairing\";\n  \n  // MessageBox keys\n  const std::string KEY_ERROR_TITLE = \"error_title\";\n  const std::string KEY_ERROR_NO_USER_SESSION = \"error_no_user_session\";\n  const std::string KEY_IMPORT_SUCCESS_TITLE = \"import_success_title\";\n  const std::string KEY_IMPORT_SUCCESS_MSG = \"import_success_msg\";\n  const std::string KEY_IMPORT_ERROR_TITLE = \"import_error_title\";\n  const std::string KEY_IMPORT_ERROR_WRITE = \"import_error_write\";\n  const std::string KEY_IMPORT_ERROR_READ = \"import_error_read\";\n  const std::string KEY_IMPORT_ERROR_EXCEPTION = \"import_error_exception\";\n  const std::string KEY_EXPORT_SUCCESS_TITLE = \"export_success_title\";\n  const std::string KEY_EXPORT_SUCCESS_MSG = \"export_success_msg\";\n  const std::string KEY_EXPORT_ERROR_TITLE = \"export_error_title\";\n  const std::string KEY_EXPORT_ERROR_WRITE = \"export_error_write\";\n  const std::string KEY_EXPORT_ERROR_NO_CONFIG = \"export_error_no_config\";\n  const std::string KEY_EXPORT_ERROR_EXCEPTION = \"export_error_exception\";\n  const std::string KEY_RESET_CONFIRM_TITLE = \"reset_confirm_title\";\n  const std::string KEY_RESET_CONFIRM_MSG = \"reset_confirm_msg\";\n  const std::string KEY_RESET_SUCCESS_TITLE = \"reset_success_title\";\n  const std::string KEY_RESET_SUCCESS_MSG = \"reset_success_msg\";\n  const std::string KEY_RESET_ERROR_TITLE = \"reset_error_title\";\n  const std::string KEY_RESET_ERROR_MSG = \"reset_error_msg\";\n  const std::string KEY_RESET_ERROR_EXCEPTION = \"reset_error_exception\";\n  const std::string KEY_FILE_DIALOG_SELECT_IMPORT = \"file_dialog_select_import\";\n  const std::string KEY_FILE_DIALOG_SAVE_EXPORT = \"file_dialog_save_export\";\n  const std::string KEY_FILE_DIALOG_CONFIG_FILES = \"file_dialog_config_files\";\n  const std::string KEY_FILE_DIALOG_ALL_FILES = \"file_dialog_all_files\";\n\n  // Default English strings\n  const std::map<std::string, std::string> DEFAULT_STRINGS = {\n    { KEY_QUIT_TITLE, \"Wait! Don't Leave Me! T_T\" },\n    { KEY_QUIT_MESSAGE, \"Nooo! You can't just quit like that!\\nAre you really REALLY sure you want to leave?\\nI'll miss you... but okay, if you must...\\n\\n(This will also close the Sunshine GUI application.)\" },\n    { KEY_OPEN_SUNSHINE, \"Open GUI\" },\n    { KEY_VDD_BASE_DISPLAY, \"Foundation Display\" },\n    { KEY_VDD_CREATE, \"Create Virtual Display\" },\n    { KEY_VDD_CLOSE, \"Close Virtual Display\" },\n    { KEY_VDD_PERSISTENT, \"Keep Enabled\" },\n    { KEY_VDD_HEADLESS_CREATE, \"Server Mode (Beta)\" },\n    { KEY_VDD_HEADLESS_CREATE_CONFIRM_TITLE, \"[Beta] Enable: Server Mode\" },\n    { KEY_VDD_HEADLESS_CREATE_CONFIRM_MSG, \"This is an internal beta feature. It works differently from \\\"Keep Enabled\\\".\\n\\nThis feature runs only when Sunshine starts or when a stream ends; if a headless host has no display after stream end, it will auto-create the base display to avoid app issues.\\n\\nExplanation:\\nHeadless host: a computer with no physical display connected (or no available display).\\nBase display: the built-in screen used by this software, with streaming second screen, privacy screen, custom resolution and refresh rate.\\n\\nAfter reconnecting a physical display, the base display may stay on. If you get a black screen, try:\\n1. Shortcut: Ctrl+Alt+Win+B\\n2. Win+P twice then Enter\\n3. Restart the app\\n4. Start a stream then end it\\nIf none works, check HDMI cable, display and keyboard.\\n\\nEnable this feature?\" },\n    { KEY_VDD_CONFIRM_CREATE_TITLE, \"Create Virtual Display\" },\n    { KEY_VDD_CONFIRM_CREATE_MSG, \"Are you sure you want to manually create a base display?\\n\\nCreating it may cause a brief black screen, which is normal. If that happens, press Win+P twice to recover.\\n\\nNote: Prefer creating when not streaming; creating during a stream won't switch to the base display automatically.\" },\n    { KEY_VDD_CONFIRM_KEEP_TITLE, \"Confirm Virtual Display\" },\n    { KEY_VDD_CONFIRM_KEEP_MSG, \"Virtual display created, do you want to keep using it?\\n\\nIf not confirmed, it will be automatically closed in 20 seconds.\" },\n    { KEY_VDD_CANCEL_CREATE_LOG, \"User cancelled creating virtual display\" },\n    { KEY_VDD_PERSISTENT_CONFIRM_TITLE, \"Keep Virtual Display Enabled\" },\n    { KEY_VDD_PERSISTENT_CONFIRM_MSG, \"By enabling this option, the virtual display will NOT be closed after you stop streaming.\\n\\nDo you want to enable this feature?\" },\n    { KEY_CONFIGURATION, \"Configuration\" },\n    { KEY_IMPORT_CONFIG, \"Import Config\" },\n    { KEY_EXPORT_CONFIG, \"Export Config\" },\n    { KEY_RESET_TO_DEFAULT, \"Reset Config\" },\n    { KEY_LANGUAGE, \"Language\" },\n    { KEY_CHINESE, \"中文\" },\n    { KEY_ENGLISH, \"English\" },\n    { KEY_JAPANESE, \"日本語\" },\n    { KEY_STAR_PROJECT, \"Visit Website\" },\n    { KEY_VISIT_PROJECT, \"Visit Project\" },\n    { KEY_VISIT_PROJECT_SUNSHINE, \"Sunshine\" },\n    { KEY_VISIT_PROJECT_MOONLIGHT, \"Moonlight\" },\n    { KEY_HELP_US, \"Sponsor Us\" },\n    { KEY_DEVELOPER_YUNDI339, \"Developer: Yundi339\" },\n    { KEY_DEVELOPER_QIIN, \"Developer: qiin2333\" },\n    { KEY_SPONSOR_ALKaidLab, \"Sponsor AlkaidLab\" },\n    { KEY_ADVANCED_SETTINGS, \"Advanced Settings\" },\n    { KEY_CLOSE_APP, \"Clear Cache\" },\n    { KEY_CLOSE_APP_CONFIRM_TITLE, \"Clear Cache\" },\n    { KEY_CLOSE_APP_CONFIRM_MSG, \"This operation will clear streaming state, may terminate the streaming application, and clean up related processes and state. Do you want to continue?\" },\n    { KEY_RESET_DISPLAY_DEVICE_CONFIG, \"Reset Display\" },\n    { KEY_RESET_DISPLAY_CONFIRM_TITLE, \"Reset Display\" },\n    { KEY_RESET_DISPLAY_CONFIRM_MSG, \"Are you sure you want to reset display device memory? This action cannot be undone.\" },\n    { KEY_RESTART, \"Restart\" },\n    { KEY_QUIT, \"Quit\" },\n    { KEY_STREAM_STARTED, \"Stream Started\" },\n    { KEY_STREAMING_STARTED_FOR, \"Streaming started for %s\" },\n    { KEY_STREAM_PAUSED, \"Stream Paused\" },\n    { KEY_STREAMING_PAUSED_FOR, \"Streaming paused for %s\" },\n    { KEY_APPLICATION_STOPPED, \"Application Stopped\" },\n    { KEY_APPLICATION_STOPPED_MSG, \"Application %s successfully stopped\" },\n    { KEY_INCOMING_PAIRING_REQUEST, \"Incoming PIN Request From: %s\" },\n    { KEY_CLICK_TO_COMPLETE_PAIRING, \"Click here to enter PIN\" },\n    { KEY_ERROR_TITLE, \"Error\" },\n    { KEY_ERROR_NO_USER_SESSION, \"Cannot open file dialog: No active user session found.\" },\n    { KEY_IMPORT_SUCCESS_TITLE, \"Import Success\" },\n    { KEY_IMPORT_SUCCESS_MSG, \"Configuration imported successfully!\\nPlease restart Sunshine to apply changes.\" },\n    { KEY_IMPORT_ERROR_TITLE, \"Import Error\" },\n    { KEY_IMPORT_ERROR_WRITE, \"Failed to import configuration file.\" },\n    { KEY_IMPORT_ERROR_READ, \"Failed to read the selected configuration file.\" },\n    { KEY_IMPORT_ERROR_EXCEPTION, \"An error occurred while importing configuration.\" },\n    { KEY_EXPORT_SUCCESS_TITLE, \"Export Success\" },\n    { KEY_EXPORT_SUCCESS_MSG, \"Configuration exported successfully!\" },\n    { KEY_EXPORT_ERROR_TITLE, \"Export Error\" },\n    { KEY_EXPORT_ERROR_WRITE, \"Failed to export configuration file.\" },\n    { KEY_EXPORT_ERROR_NO_CONFIG, \"No configuration found to export.\" },\n    { KEY_EXPORT_ERROR_EXCEPTION, \"An error occurred while exporting configuration.\" },\n    { KEY_RESET_CONFIRM_TITLE, \"Reset Configuration\" },\n    { KEY_RESET_CONFIRM_MSG, \"This will reset all configuration to default values.\\nThis action cannot be undone.\\n\\nDo you want to continue?\" },\n    { KEY_RESET_SUCCESS_TITLE, \"Reset Success\" },\n    { KEY_RESET_SUCCESS_MSG, \"Configuration has been reset to default values.\\nPlease restart Sunshine to apply changes.\" },\n    { KEY_RESET_ERROR_TITLE, \"Reset Error\" },\n    { KEY_RESET_ERROR_MSG, \"Failed to reset configuration file.\" },\n    { KEY_RESET_ERROR_EXCEPTION, \"An error occurred while resetting configuration.\" },\n    { KEY_FILE_DIALOG_SELECT_IMPORT, \"Select Configuration File to Import\" },\n    { KEY_FILE_DIALOG_SAVE_EXPORT, \"Save Configuration File As\" },\n    { KEY_FILE_DIALOG_CONFIG_FILES, \"Configuration Files\" },\n    { KEY_FILE_DIALOG_ALL_FILES, \"All Files\" }\n  };\n\n  // Chinese strings\n  const std::map<std::string, std::string> CHINESE_STRINGS = {\n    { KEY_QUIT_TITLE, \"真的要退出吗\" },\n    { KEY_QUIT_MESSAGE, \"你不能退出!\\n那么想退吗? 真拿你没办法呢, 继续点一下吧~\\n\\n这将同时关闭Sunshine GUI应用程序。\" },\n    { KEY_OPEN_SUNSHINE, \"打开基地面板\" },\n    { KEY_VDD_BASE_DISPLAY, \"基地显示器\" },\n    { KEY_VDD_CREATE, \"创建显示器\" },\n    { KEY_VDD_CLOSE, \"关闭显示器\" },\n    { KEY_VDD_PERSISTENT, \"保持启用\" },\n    { KEY_VDD_HEADLESS_CREATE, \"服务器模式(测试)\" },\n    { KEY_VDD_HEADLESS_CREATE_CONFIRM_TITLE, \"【内测功能】启用「服务器模式(测试)」\" },\n    { KEY_VDD_HEADLESS_CREATE_CONFIRM_MSG, \"此为内测功能，请知悉。与「保持启用」原理不同。\\n\\n此功能仅在 Sunshine 启动或串流结束时检测；无头主机退出串流后若无显示器，将自动创建基地显示器，避免部分应用异常。\\n\\n说明：\\n无头主机：未连接物理显示器（或当前无可用显示设备）的电脑。\\n基地显示器：此软件使用的内置屏幕，具有串流副屏、隐私屏、任意分辨率、任意帧率的功能。\\n\\n接回物理显示器后，基地显示器也仍然会保持开启状态。如果出现黑屏，请尝试：\\n1、快捷键 Ctrl+Alt+Win+B\\n2、快捷键 Win+P 按 2 次后回车\\n3、重启程序\\n4、串流后退出\\n如果都没用，请检查 HDMI 线、显示器和键盘是否损坏。\\n\\n确定启用？\" },\n    { KEY_VDD_CONFIRM_CREATE_TITLE, \"创建基地显示器\" },\n    { KEY_VDD_CONFIRM_CREATE_MSG, \"确定要手动创建基地显示器吗？\\n\\n创建后可能会短暂黑屏，属正常现象。如遇黑屏，请按两次 Win+P 恢复。\\n\\n注意：建议在非串流时创建；串流中手动创建，画面不会自动切换到基地显示器。\" },\n    { KEY_VDD_CONFIRM_KEEP_TITLE, \"显示器确认\" },\n    { KEY_VDD_CONFIRM_KEEP_MSG, \"已创建基地显示器，是否继续使用？\\n\\n如不确认，20秒后将自动关闭显示器\" },\n    { KEY_VDD_CANCEL_CREATE_LOG, \"用户取消创建基地显示器\" },\n    { KEY_VDD_PERSISTENT_CONFIRM_TITLE, \"保持开启虚拟显示器\" },\n    { KEY_VDD_PERSISTENT_CONFIRM_MSG, \"启用此选项后，在串流结束后基地显示器将不会被自动关闭。\\n\\n确定要开启此功能吗？\" },\n    { KEY_CONFIGURATION, \"配置\" },\n    { KEY_IMPORT_CONFIG, \"导入配置\" },\n    { KEY_EXPORT_CONFIG, \"导出配置\" },\n    { KEY_RESET_TO_DEFAULT, \"重置配置\" },\n    { KEY_LANGUAGE, \"语言 / Langue\" },\n    { KEY_CHINESE, \"中文\" },\n    { KEY_ENGLISH, \"English\" },\n    { KEY_JAPANESE, \"日本語\" },\n    { KEY_STAR_PROJECT, \"访问官网\" },\n    { KEY_VISIT_PROJECT, \"访问项目地址\" },\n    { KEY_VISIT_PROJECT_SUNSHINE, \"Sunshine\" },\n    { KEY_VISIT_PROJECT_MOONLIGHT, \"Moonlight\" },\n    { KEY_HELP_US, \"赞助我们\" },\n    { KEY_DEVELOPER_YUNDI339, \"开发者：Yundi339\" },\n    { KEY_DEVELOPER_QIIN, \"开发者：qiin2333\" },\n    { KEY_SPONSOR_ALKaidLab, \"赞助 AlkaidLab\" },\n    { KEY_ADVANCED_SETTINGS, \"高级设置\" },\n    { KEY_CLOSE_APP, \"清理缓存\" },\n    { KEY_CLOSE_APP_CONFIRM_TITLE, \"清理缓存\" },\n    { KEY_CLOSE_APP_CONFIRM_MSG, \"此操作将会清理串流状态，可能会终止串流应用，并清理相关进程和状态。是否继续？\" },\n    { KEY_RESET_DISPLAY_DEVICE_CONFIG, \"重置显示器\" },\n    { KEY_RESET_DISPLAY_CONFIRM_TITLE, \"重置显示器\" },\n    { KEY_RESET_DISPLAY_CONFIRM_MSG, \"确定要重置显示器设备记忆吗？此操作无法撤销。\" },\n    { KEY_RESTART, \"重新启动\" },\n    { KEY_QUIT, \"退出\" },\n    { KEY_STREAM_STARTED, \"串流已开始\" },\n    { KEY_STREAMING_STARTED_FOR, \"已开始串流：%s\" },\n    { KEY_STREAM_PAUSED, \"串流已暂停\" },\n    { KEY_STREAMING_PAUSED_FOR, \"已暂停串流：%s\" },\n    { KEY_APPLICATION_STOPPED, \"应用已停止\" },\n    { KEY_APPLICATION_STOPPED_MSG, \"应用 %s 已成功停止\" },\n    { KEY_INCOMING_PAIRING_REQUEST, \"来自 %s 的PIN请求\" },\n    { KEY_CLICK_TO_COMPLETE_PAIRING, \"点击此处完成PIN验证\" },\n    { KEY_ERROR_TITLE, \"错误\" },\n    { KEY_ERROR_NO_USER_SESSION, \"无法打开文件对话框：未找到活动的用户会话。\" },\n    { KEY_IMPORT_SUCCESS_TITLE, \"导入成功\" },\n    { KEY_IMPORT_SUCCESS_MSG, \"配置已成功导入！\\n请重新启动 Sunshine 以应用更改。\" },\n    { KEY_IMPORT_ERROR_TITLE, \"导入失败\" },\n    { KEY_IMPORT_ERROR_WRITE, \"无法写入配置文件。\" },\n    { KEY_IMPORT_ERROR_READ, \"无法读取所选的配置文件。\" },\n    { KEY_IMPORT_ERROR_EXCEPTION, \"导入配置时发生错误。\" },\n    { KEY_EXPORT_SUCCESS_TITLE, \"导出成功\" },\n    { KEY_EXPORT_SUCCESS_MSG, \"配置已成功导出！\" },\n    { KEY_EXPORT_ERROR_TITLE, \"导出失败\" },\n    { KEY_EXPORT_ERROR_WRITE, \"无法导出配置文件。\" },\n    { KEY_EXPORT_ERROR_NO_CONFIG, \"未找到可导出的配置。\" },\n    { KEY_EXPORT_ERROR_EXCEPTION, \"导出配置时发生错误。\" },\n    { KEY_RESET_CONFIRM_TITLE, \"重置配置\" },\n    { KEY_RESET_CONFIRM_MSG, \"这将把所有配置重置为默认值。\\n此操作无法撤销。\\n\\n确定要继续吗？\" },\n    { KEY_RESET_SUCCESS_TITLE, \"重置成功\" },\n    { KEY_RESET_SUCCESS_MSG, \"配置已重置为默认值。\\n请重新启动 Sunshine 以应用更改。\" },\n    { KEY_RESET_ERROR_TITLE, \"重置失败\" },\n    { KEY_RESET_ERROR_MSG, \"无法重置配置文件。\" },\n    { KEY_RESET_ERROR_EXCEPTION, \"重置配置时发生错误。\" },\n    { KEY_FILE_DIALOG_SELECT_IMPORT, \"选择要导入的配置文件\" },\n    { KEY_FILE_DIALOG_SAVE_EXPORT, \"配置文件另存为\" },\n    { KEY_FILE_DIALOG_CONFIG_FILES, \"配置文件\" },\n    { KEY_FILE_DIALOG_ALL_FILES, \"所有文件\" }\n  };\n\n  const std::map<std::string, std::string> JAPANESE_STRINGS = {\n    { KEY_QUIT_TITLE, \"本当に終了しますか？\" },\n    { KEY_QUIT_MESSAGE, \"終了できません！\\n本当に終了したいですか？\\n\\nこれによりSunshine GUIアプリケーションも閉じられます。\" },\n    { KEY_OPEN_SUNSHINE, \"GUIを開く\" },\n    { KEY_VDD_BASE_DISPLAY, \"基地ディスプレイ\" },\n    { KEY_VDD_CREATE, \"仮想ディスプレイを作成\" },\n    { KEY_VDD_CLOSE, \"仮想ディスプレイを閉じる\" },\n    { KEY_VDD_PERSISTENT, \"常駐仮想ディスプレイを\" },\n    { KEY_VDD_HEADLESS_CREATE, \"サーバーモード(テスト)\" },\n    { KEY_VDD_HEADLESS_CREATE_CONFIRM_TITLE, \"【β版】「サーバーモード(テスト)」を有効にする\" },\n    { KEY_VDD_HEADLESS_CREATE_CONFIRM_MSG, \"内側テスト用のβ機能です。「常駐」とは仕様が異なります。\\n\\nこの機能は Sunshine 起動時またはストリーム終了時のみ検出。ヘッドレスでストリーム終了後にディスプレイが無い場合、基地ディスプレイを自動作成し、一部アプリの不具合を防ぎます。\\n\\n説明：\\nヘッドレス：物理ディスプレイが接続されていない（または利用可能なディスプレイがない）PC。\\n基地ディスプレイ：本ソフトが使う内蔵画面。串流副画面・プライバシー画面・任意解像度・任意リフレッシュレートの機能を持つ。\\n\\n物理ディスプレイを接続した後も基地ディスプレイはオンのままです。黒画面の場合は次を試してください：\\n1. ショートカット Ctrl+Alt+Win+B\\n2. Win+P を2回押して Enter\\n3. プログラムを再起動\\n4. ストリーム開始後に終了\\nそれでも治らない場合は、HDMIケーブル・ディスプレイ・キーボードの故障を確認してください。\\n\\n有効にしますか？\" },\n    { KEY_VDD_CONFIRM_CREATE_TITLE, \"仮想ディスプレイを作成\" },\n    { KEY_VDD_CONFIRM_CREATE_MSG, \"手動で基地ディスプレイを作成しますか？\\n\\n作成後に一時的な黒画面が発生する場合がありますが、正常です。黒画面の場合は Win+P を2回押して回復してください。\\n\\n注意：ストリーム中ではなく、ストリーム外で作成することを推奨します。ストリーム中に作成しても基地ディスプレイへは自動切り替えされません。\" },\n    { KEY_VDD_CONFIRM_KEEP_TITLE, \"ディスプレイの確認\" },\n    { KEY_VDD_CONFIRM_KEEP_MSG, \"仮想ディスプレイが作成されました。継続して使用しますか？\\n\\n確認がない場合、20秒後に自動的に閉じられます。\" },\n    { KEY_VDD_CANCEL_CREATE_LOG, \"ユーザーが仮想ディスプレイの作成をキャンセルしました\" },\n    { KEY_VDD_PERSISTENT_CONFIRM_TITLE, \"仮想ディスプレイを有効に保つ\" },\n    { KEY_VDD_PERSISTENT_CONFIRM_MSG, \"このオプションを有効にすると、ストリーミング終了後に仮想ディスプレイは**自動的に閉じられません**。\\n\\nこの機能を有効にしますか？\" },\n    { KEY_CONFIGURATION, \"設定\" },\n    { KEY_IMPORT_CONFIG, \"設定をインポート\" },\n    { KEY_EXPORT_CONFIG, \"設定をエクスポート\" },\n    { KEY_RESET_TO_DEFAULT, \"設定をリセット\" },\n    { KEY_LANGUAGE, \"言語\" },\n    { KEY_CHINESE, \"中文\" },\n    { KEY_ENGLISH, \"English\" },\n    { KEY_JAPANESE, \"日本語\" },\n    { KEY_STAR_PROJECT, \"公式サイトを訪問\" },\n    { KEY_VISIT_PROJECT, \"プロジェクトアドレスを訪問\" },\n    { KEY_VISIT_PROJECT_SUNSHINE, \"Sunshine\" },\n    { KEY_VISIT_PROJECT_MOONLIGHT, \"Moonlight\" },\n    { KEY_HELP_US, \"スポンサー\" },\n    { KEY_DEVELOPER_YUNDI339, \"開発者：Yundi339\" },\n    { KEY_DEVELOPER_QIIN, \"開発者：qiin2333\" },\n    { KEY_SPONSOR_ALKaidLab, \"AlkaidLabをスポンサー\" },\n    { KEY_ADVANCED_SETTINGS, \"詳細設定\" },\n    { KEY_CLOSE_APP, \"キャッシュをクリア\" },\n    { KEY_CLOSE_APP_CONFIRM_TITLE, \"キャッシュをクリア\" },\n    { KEY_CLOSE_APP_CONFIRM_MSG, \"この操作はストリーミング状態をクリアし、ストリーミングアプリケーションを終了する可能性があり、関連するプロセスと状態をクリーンアップします。続行しますか？\" },\n    { KEY_RESET_DISPLAY_DEVICE_CONFIG, \"ディスプレイをリセット\" },\n    { KEY_RESET_DISPLAY_CONFIRM_TITLE, \"ディスプレイをリセット\" },\n    { KEY_RESET_DISPLAY_CONFIRM_MSG, \"ディスプレイデバイスのメモリをリセットしてもよろしいですか？この操作は元に戻せません。\" },\n    { KEY_RESTART, \"再起動\" },\n    { KEY_QUIT, \"終了\" },\n    { KEY_STREAM_STARTED, \"ストリーム開始\" },\n    { KEY_STREAMING_STARTED_FOR, \"%s のストリーミングを開始しました\" },\n    { KEY_STREAM_PAUSED, \"ストリーム一時停止\" },\n    { KEY_STREAMING_PAUSED_FOR, \"%s のストリーミングを一時停止しました\" },\n    { KEY_APPLICATION_STOPPED, \"アプリケーション停止\" },\n    { KEY_APPLICATION_STOPPED_MSG, \"アプリケーション %s が正常に停止しました\" },\n    { KEY_INCOMING_PAIRING_REQUEST, \"%s からのPIN要求\" },\n    { KEY_CLICK_TO_COMPLETE_PAIRING, \"クリックしてPIN認証を完了\" },\n    { KEY_ERROR_TITLE, \"エラー\" },\n    { KEY_ERROR_NO_USER_SESSION, \"ファイルダイアログを開けません：アクティブなユーザーセッションが見つかりません。\" },\n    { KEY_IMPORT_SUCCESS_TITLE, \"インポート成功\" },\n    { KEY_IMPORT_SUCCESS_MSG, \"設定のインポートに成功しました！\\n変更を適用するにはSunshineを再起動してください。\" },\n    { KEY_IMPORT_ERROR_TITLE, \"インポート失敗\" },\n    { KEY_IMPORT_ERROR_WRITE, \"設定ファイルを書き込めませんでした。\" },\n    { KEY_IMPORT_ERROR_READ, \"選択した設定ファイルを読み取れませんでした。\" },\n    { KEY_IMPORT_ERROR_EXCEPTION, \"設定のインポート中にエラーが発生しました。\" },\n    { KEY_EXPORT_SUCCESS_TITLE, \"エクスポート成功\" },\n    { KEY_EXPORT_SUCCESS_MSG, \"設定のエクスポートに成功しました！\" },\n    { KEY_EXPORT_ERROR_TITLE, \"エクスポート失敗\" },\n    { KEY_EXPORT_ERROR_WRITE, \"設定ファイルをエクスポートできませんでした。\" },\n    { KEY_EXPORT_ERROR_NO_CONFIG, \"エクスポートする設定が見つかりません。\" },\n    { KEY_EXPORT_ERROR_EXCEPTION, \"設定のエクスポート中にエラーが発生しました。\" },\n    { KEY_RESET_CONFIRM_TITLE, \"設定のリセット\" },\n    { KEY_RESET_CONFIRM_MSG, \"すべての設定をデフォルト値にリセットします。\\nこの操作は元に戻せません。\\n\\n続行しますか？\" },\n    { KEY_RESET_SUCCESS_TITLE, \"リセット成功\" },\n    { KEY_RESET_SUCCESS_MSG, \"設定をデフォルト値にリセットしました。\\n変更を適用するにはSunshineを再起動してください。\" },\n    { KEY_RESET_ERROR_TITLE, \"リセット失敗\" },\n    { KEY_RESET_ERROR_MSG, \"設定ファイルをリセットできませんでした。\" },\n    { KEY_RESET_ERROR_EXCEPTION, \"設定のリセット中にエラーが発生しました。\" },\n    { KEY_FILE_DIALOG_SELECT_IMPORT, \"インポートする設定ファイルを選択\" },\n    { KEY_FILE_DIALOG_SAVE_EXPORT, \"設定ファイルに名前を付けて保存\" },\n    { KEY_FILE_DIALOG_CONFIG_FILES, \"設定ファイル\" },\n    { KEY_FILE_DIALOG_ALL_FILES, \"すべてのファイル\" }\n  };\n\n  // Get current locale from config\n  std::string\n  get_current_locale() {\n    // Try to get from config::sunshine.tray_locale\n    try {\n      // Check if config is available\n      if (!config::sunshine.tray_locale.empty()) {\n        return config::sunshine.tray_locale;\n      }\n    }\n    catch (...) {\n      // If config is not available, fall back to default\n    }\n\n    // Default to English\n    return \"en\";\n  }\n\n  // Set tray locale\n  void\n  set_tray_locale(const std::string &locale) {\n    // Update config\n    config::sunshine.tray_locale = locale;\n  }\n\n  // Get localized string\n  std::string\n  get_localized_string(const std::string &key) {\n    std::string locale = get_current_locale();\n\n    if (locale == \"zh\" || locale == \"zh_CN\" || locale == \"zh_TW\") {\n      auto it = CHINESE_STRINGS.find(key);\n      if (it != CHINESE_STRINGS.end()) {\n        return it->second;\n      }\n    }\n\n    if (locale == \"ja\" || locale == \"ja_JP\") {\n      auto it = JAPANESE_STRINGS.find(key);\n      if (it != JAPANESE_STRINGS.end()) {\n        return it->second;\n      }\n    }\n\n    // Fallback to English\n    auto it = DEFAULT_STRINGS.find(key);\n    if (it != DEFAULT_STRINGS.end()) {\n      return it->second;\n    }\n\n    return key;  // Return key if not found\n  }\n\n  // Convert UTF-8 string to wide string\n  std::wstring\n  utf8_to_wstring(const std::string &utf8_str) {\n    // Modern C++ approach: use Windows API on Windows, simple conversion on other platforms\n  #ifdef _WIN32\n    if (utf8_str.empty()) {\n      return L\"\";\n    }\n    \n    // Get required buffer size\n    int wide_size = MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), -1, nullptr, 0);\n    if (wide_size == 0) {\n      // Fallback: simple char-by-char conversion\n      std::wstring result;\n      result.reserve(utf8_str.length());\n      for (char c : utf8_str) {\n        result += static_cast<wchar_t>(c);\n      }\n      return result;\n    }\n    \n    // Convert to wide string\n    std::wstring result(wide_size - 1, L'\\0');\n    if (MultiByteToWideChar(CP_UTF8, 0, utf8_str.c_str(), -1, &result[0], wide_size) == 0) {\n      // Fallback: simple char-by-char conversion\n      result.clear();\n      result.reserve(utf8_str.length());\n      for (char c : utf8_str) {\n        result += static_cast<wchar_t>(c);\n      }\n    }\n    return result;\n  #else\n    // On non-Windows platforms, use simple char-by-char conversion\n    // This is not perfect for UTF-8, but it's a reasonable fallback\n    std::wstring result;\n    result.reserve(utf8_str.length());\n    for (char c : utf8_str) {\n      result += static_cast<wchar_t>(c);\n    }\n    return result;\n  #endif\n  }\n}  // namespace system_tray_i18n\n"
  },
  {
    "path": "src/system_tray_i18n.h",
    "content": "#pragma once\n\n#include <string>\n#include <map>\n\nnamespace system_tray_i18n {\n  // String key constants\n  extern const std::string KEY_QUIT_TITLE;\n  extern const std::string KEY_QUIT_MESSAGE;\n  \n  // Menu item keys\n  extern const std::string KEY_OPEN_SUNSHINE;\n  extern const std::string KEY_VDD_BASE_DISPLAY;\n  extern const std::string KEY_VDD_CREATE;\n  extern const std::string KEY_VDD_CLOSE;\n  extern const std::string KEY_VDD_PERSISTENT;\n  extern const std::string KEY_VDD_HEADLESS_CREATE;\n  extern const std::string KEY_VDD_HEADLESS_CREATE_CONFIRM_TITLE;\n  extern const std::string KEY_VDD_HEADLESS_CREATE_CONFIRM_MSG;\n  extern const std::string KEY_VDD_CONFIRM_CREATE_TITLE;\n  extern const std::string KEY_VDD_CONFIRM_CREATE_MSG;\n  extern const std::string KEY_VDD_CONFIRM_KEEP_TITLE;\n  extern const std::string KEY_VDD_CONFIRM_KEEP_MSG;\n  extern const std::string KEY_VDD_CANCEL_CREATE_LOG;\n  extern const std::string KEY_VDD_PERSISTENT_CONFIRM_TITLE;\n  extern const std::string KEY_VDD_PERSISTENT_CONFIRM_MSG;\n  extern const std::string KEY_CONFIGURATION;\n  extern const std::string KEY_IMPORT_CONFIG;\n  extern const std::string KEY_EXPORT_CONFIG;\n  extern const std::string KEY_RESET_TO_DEFAULT;\n  extern const std::string KEY_LANGUAGE;\n  extern const std::string KEY_CHINESE;\n  extern const std::string KEY_ENGLISH;\n  extern const std::string KEY_JAPANESE;\n  extern const std::string KEY_STAR_PROJECT;\n  extern const std::string KEY_VISIT_PROJECT;\n  extern const std::string KEY_VISIT_PROJECT_SUNSHINE;\n  extern const std::string KEY_VISIT_PROJECT_MOONLIGHT;\n  extern const std::string KEY_HELP_US;\n  extern const std::string KEY_DEVELOPER_YUNDI339;\n  extern const std::string KEY_DEVELOPER_QIIN;\n  extern const std::string KEY_SPONSOR_ALKaidLab;\n  extern const std::string KEY_ADVANCED_SETTINGS;\n  extern const std::string KEY_CLOSE_APP;\n  extern const std::string KEY_CLOSE_APP_CONFIRM_TITLE;\n  extern const std::string KEY_CLOSE_APP_CONFIRM_MSG;\n  extern const std::string KEY_RESET_DISPLAY_DEVICE_CONFIG;\n  extern const std::string KEY_RESET_DISPLAY_CONFIRM_TITLE;\n  extern const std::string KEY_RESET_DISPLAY_CONFIRM_MSG;\n  extern const std::string KEY_RESTART;\n  extern const std::string KEY_QUIT;\n  \n  // Notification message keys\n  extern const std::string KEY_STREAM_STARTED;\n  extern const std::string KEY_STREAMING_STARTED_FOR;\n  extern const std::string KEY_STREAM_PAUSED;\n  extern const std::string KEY_STREAMING_PAUSED_FOR;\n  extern const std::string KEY_APPLICATION_STOPPED;\n  extern const std::string KEY_APPLICATION_STOPPED_MSG;\n  extern const std::string KEY_INCOMING_PAIRING_REQUEST;\n  extern const std::string KEY_CLICK_TO_COMPLETE_PAIRING;\n  \n  // MessageBox keys\n  extern const std::string KEY_ERROR_TITLE;\n  extern const std::string KEY_ERROR_NO_USER_SESSION;\n  extern const std::string KEY_IMPORT_SUCCESS_TITLE;\n  extern const std::string KEY_IMPORT_SUCCESS_MSG;\n  extern const std::string KEY_IMPORT_ERROR_TITLE;\n  extern const std::string KEY_IMPORT_ERROR_WRITE;\n  extern const std::string KEY_IMPORT_ERROR_READ;\n  extern const std::string KEY_IMPORT_ERROR_EXCEPTION;\n  extern const std::string KEY_EXPORT_SUCCESS_TITLE;\n  extern const std::string KEY_EXPORT_SUCCESS_MSG;\n  extern const std::string KEY_EXPORT_ERROR_TITLE;\n  extern const std::string KEY_EXPORT_ERROR_WRITE;\n  extern const std::string KEY_EXPORT_ERROR_NO_CONFIG;\n  extern const std::string KEY_EXPORT_ERROR_EXCEPTION;\n  extern const std::string KEY_RESET_CONFIRM_TITLE;\n  extern const std::string KEY_RESET_CONFIRM_MSG;\n  extern const std::string KEY_RESET_SUCCESS_TITLE;\n  extern const std::string KEY_RESET_SUCCESS_MSG;\n  extern const std::string KEY_RESET_ERROR_TITLE;\n  extern const std::string KEY_RESET_ERROR_MSG;\n  extern const std::string KEY_RESET_ERROR_EXCEPTION;\n  extern const std::string KEY_FILE_DIALOG_SELECT_IMPORT;\n  extern const std::string KEY_FILE_DIALOG_SAVE_EXPORT;\n  extern const std::string KEY_FILE_DIALOG_CONFIG_FILES;\n  extern const std::string KEY_FILE_DIALOG_ALL_FILES;\n  \n  // Get localized string\n  std::string get_localized_string(const std::string& key);\n  \n  // Set tray locale\n  void set_tray_locale(const std::string& locale);\n  \n  // Convert UTF-8 string to wide string\n  std::wstring utf8_to_wstring(const std::string& utf8_str);\n}\n"
  },
  {
    "path": "src/task_pool.h",
    "content": "/**\n * @file src/task_pool.h\n * @brief Declarations for the task pool system.\n */\n#pragma once\n\n#include <chrono>\n#include <deque>\n#include <functional>\n#include <future>\n#include <mutex>\n#include <optional>\n#include <type_traits>\n#include <utility>\n#include <vector>\n\n#include \"move_by_copy.h\"\n#include \"utility.h\"\nnamespace task_pool_util {\n\n  class _ImplBase {\n  public:\n    // _unique_base_type _this_ptr;\n\n    inline virtual ~_ImplBase() = default;\n\n    virtual void\n    run() = 0;\n  };\n\n  template <class Function>\n  class _Impl: public _ImplBase {\n    Function _func;\n\n  public:\n    _Impl(Function &&f):\n        _func(std::forward<Function>(f)) {}\n\n    void\n    run() override {\n      _func();\n    }\n  };\n\n  class TaskPool {\n  public:\n    typedef std::unique_ptr<_ImplBase> __task;\n    typedef _ImplBase *task_id_t;\n\n    typedef std::chrono::steady_clock::time_point __time_point;\n\n    template <class R>\n    class timer_task_t {\n    public:\n      task_id_t task_id;\n      std::future<R> future;\n\n      timer_task_t(task_id_t task_id, std::future<R> &future):\n          task_id { task_id }, future { std::move(future) } {}\n    };\n\n  protected:\n    std::deque<__task> _tasks;\n    std::vector<std::pair<__time_point, __task>> _timer_tasks;\n    std::mutex _task_mutex;\n\n  public:\n    TaskPool() = default;\n    TaskPool(TaskPool &&other) noexcept:\n        _tasks { std::move(other._tasks) }, _timer_tasks { std::move(other._timer_tasks) } {}\n\n    TaskPool &\n    operator=(TaskPool &&other) noexcept {\n      std::swap(_tasks, other._tasks);\n      std::swap(_timer_tasks, other._timer_tasks);\n\n      return *this;\n    }\n\n    template <class Function, class... Args>\n    auto\n    push(Function &&newTask, Args &&...args) {\n      static_assert(std::is_invocable_v<Function, Args &&...>, \"arguments don't match the function\");\n\n      using __return = std::invoke_result_t<Function, Args &&...>;\n      using task_t = std::packaged_task<__return()>;\n\n      auto bind = [task = std::forward<Function>(newTask), tuple_args = std::make_tuple(std::forward<Args>(args)...)]() mutable {\n        return std::apply(task, std::move(tuple_args));\n      };\n\n      task_t task(std::move(bind));\n\n      auto future = task.get_future();\n\n      std::lock_guard<std::mutex> lg(_task_mutex);\n      _tasks.emplace_back(toRunnable(std::move(task)));\n\n      return future;\n    }\n\n    void\n    pushDelayed(std::pair<__time_point, __task> &&task) {\n      std::lock_guard lg(_task_mutex);\n\n      auto it = _timer_tasks.cbegin();\n      for (; it < _timer_tasks.cend(); ++it) {\n        if (std::get<0>(*it) < task.first) {\n          break;\n        }\n      }\n\n      _timer_tasks.emplace(it, task.first, std::move(task.second));\n    }\n\n    /**\n     * @return An id to potentially delay the task.\n     */\n    template <class Function, class X, class Y, class... Args>\n    auto\n    pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&...args) {\n      static_assert(std::is_invocable_v<Function, Args &&...>, \"arguments don't match the function\");\n\n      using __return = std::invoke_result_t<Function, Args &&...>;\n      using task_t = std::packaged_task<__return()>;\n\n      __time_point time_point;\n      if constexpr (std::is_floating_point_v<X>) {\n        time_point = std::chrono::steady_clock::now() + std::chrono::duration_cast<std::chrono::nanoseconds>(duration);\n      }\n      else {\n        time_point = std::chrono::steady_clock::now() + duration;\n      }\n\n      auto bind = [task = std::forward<Function>(newTask), tuple_args = std::make_tuple(std::forward<Args>(args)...)]() mutable {\n        return std::apply(task, std::move(tuple_args));\n      };\n\n      task_t task(std::move(bind));\n\n      auto future = task.get_future();\n      auto runnable = toRunnable(std::move(task));\n\n      task_id_t task_id = &*runnable;\n\n      pushDelayed(std::pair { time_point, std::move(runnable) });\n\n      return timer_task_t<__return> { task_id, future };\n    }\n\n    /**\n     * @param task_id The id of the task to delay.\n     * @param duration The delay before executing the task.\n     */\n    template <class X, class Y>\n    void\n    delay(task_id_t task_id, std::chrono::duration<X, Y> duration) {\n      std::lock_guard<std::mutex> lg(_task_mutex);\n\n      auto it = _timer_tasks.begin();\n      for (; it < _timer_tasks.cend(); ++it) {\n        const __task &task = std::get<1>(*it);\n\n        if (&*task == task_id) {\n          std::get<0>(*it) = std::chrono::steady_clock::now() + duration;\n\n          break;\n        }\n      }\n\n      if (it == _timer_tasks.cend()) {\n        return;\n      }\n\n      // smaller time goes to the back\n      auto prev = it - 1;\n      while (it > _timer_tasks.cbegin()) {\n        if (std::get<0>(*it) > std::get<0>(*prev)) {\n          std::swap(*it, *prev);\n        }\n\n        --prev;\n        --it;\n      }\n    }\n\n    bool\n    cancel(task_id_t task_id) {\n      std::lock_guard lg(_task_mutex);\n\n      auto it = _timer_tasks.begin();\n      for (; it < _timer_tasks.cend(); ++it) {\n        const __task &task = std::get<1>(*it);\n\n        if (&*task == task_id) {\n          _timer_tasks.erase(it);\n\n          return true;\n        }\n      }\n\n      return false;\n    }\n\n    std::optional<std::pair<__time_point, __task>>\n    pop(task_id_t task_id) {\n      std::lock_guard lg(_task_mutex);\n\n      auto pos = std::find_if(std::begin(_timer_tasks), std::end(_timer_tasks), [&task_id](const auto &t) { return t.second.get() == task_id; });\n\n      if (pos == std::end(_timer_tasks)) {\n        return std::nullopt;\n      }\n\n      return std::move(*pos);\n    }\n\n    std::optional<__task>\n    pop() {\n      std::lock_guard lg(_task_mutex);\n\n      if (!_tasks.empty()) {\n        __task task = std::move(_tasks.front());\n        _tasks.pop_front();\n        return task;\n      }\n\n      if (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now()) {\n        __task task = std::move(std::get<1>(_timer_tasks.back()));\n        _timer_tasks.pop_back();\n        return task;\n      }\n\n      return std::nullopt;\n    }\n\n    bool\n    ready() {\n      std::lock_guard<std::mutex> lg(_task_mutex);\n\n      return !_tasks.empty() || (!_timer_tasks.empty() && std::get<0>(_timer_tasks.back()) <= std::chrono::steady_clock::now());\n    }\n\n    std::optional<__time_point>\n    next() {\n      std::lock_guard<std::mutex> lg(_task_mutex);\n\n      if (_timer_tasks.empty()) {\n        return std::nullopt;\n      }\n\n      return std::get<0>(_timer_tasks.back());\n    }\n\n  private:\n    template <class Function>\n    std::unique_ptr<_ImplBase>\n    toRunnable(Function &&f) {\n      return std::make_unique<_Impl<Function>>(std::forward<Function &&>(f));\n    }\n  };\n}  // namespace task_pool_util\n"
  },
  {
    "path": "src/thread_pool.h",
    "content": "/**\n * @file src/thread_pool.h\n * @brief Declarations for the thread pool system.\n */\n#pragma once\n\n#include \"task_pool.h\"\n#include <thread>\n\nnamespace thread_pool_util {\n  /**\n   * Allow threads to execute unhindered while keeping full control over the threads.\n   */\n  class ThreadPool: public task_pool_util::TaskPool {\n  public:\n    typedef TaskPool::__task __task;\n\n  private:\n    std::vector<std::thread> _thread;\n\n    std::condition_variable _cv;\n    std::mutex _lock;\n\n    bool _continue;\n\n  public:\n    ThreadPool():\n        _continue { false } {}\n\n    explicit ThreadPool(int threads):\n        _thread(threads), _continue { true } {\n      for (auto &t : _thread) {\n        t = std::thread(&ThreadPool::_main, this);\n      }\n    }\n\n    ~ThreadPool() noexcept {\n      if (!_continue) return;\n\n      stop();\n      join();\n    }\n\n    template <class Function, class... Args>\n    auto\n    push(Function &&newTask, Args &&...args) {\n      std::lock_guard lg(_lock);\n      auto future = TaskPool::push(std::forward<Function>(newTask), std::forward<Args>(args)...);\n\n      _cv.notify_one();\n      return future;\n    }\n\n    void\n    pushDelayed(std::pair<__time_point, __task> &&task) {\n      std::lock_guard lg(_lock);\n\n      TaskPool::pushDelayed(std::move(task));\n    }\n\n    template <class Function, class X, class Y, class... Args>\n    auto\n    pushDelayed(Function &&newTask, std::chrono::duration<X, Y> duration, Args &&...args) {\n      std::lock_guard lg(_lock);\n      auto future = TaskPool::pushDelayed(std::forward<Function>(newTask), duration, std::forward<Args>(args)...);\n\n      // Update all timers for wait_until\n      _cv.notify_all();\n      return future;\n    }\n\n    void\n    start(int threads) {\n      _continue = true;\n\n      _thread.resize(threads);\n\n      for (auto &t : _thread) {\n        t = std::thread(&ThreadPool::_main, this);\n      }\n    }\n\n    void\n    stop() {\n      std::lock_guard lg(_lock);\n\n      _continue = false;\n      _cv.notify_all();\n    }\n\n    void\n    join() {\n      for (auto &t : _thread) {\n        t.join();\n      }\n    }\n\n  public:\n    void\n    _main() {\n      while (_continue) {\n        if (auto task = this->pop()) {\n          (*task)->run();\n        }\n        else {\n          std::unique_lock uniq_lock(_lock);\n\n          if (ready()) {\n            continue;\n          }\n\n          if (!_continue) {\n            break;\n          }\n\n          if (auto tp = next()) {\n            _cv.wait_until(uniq_lock, *tp);\n          }\n          else {\n            _cv.wait(uniq_lock);\n          }\n        }\n      }\n\n      // Execute remaining tasks\n      while (auto task = this->pop()) {\n        (*task)->run();\n      }\n    }\n  };\n}  // namespace thread_pool_util\n"
  },
  {
    "path": "src/thread_safe.h",
    "content": "/**\n * @file src/thread_safe.h\n * @brief Declarations for thread-safe data structures.\n */\n#pragma once\n\n#include <array>\n#include <atomic>\n#include <condition_variable>\n#include <functional>\n#include <map>\n#include <mutex>\n#include <vector>\n\n#include \"utility.h\"\n\nnamespace safe {\n  template <class T>\n  class event_t {\n  public:\n    using status_t = util::optional_t<T>;\n\n    template <class... Args>\n    void\n    raise(Args &&...args) {\n      std::lock_guard lg { _lock };\n      if (!_continue) {\n        return;\n      }\n\n      if constexpr (std::is_same_v<std::optional<T>, status_t>) {\n        _status = std::make_optional<T>(std::forward<Args>(args)...);\n      }\n      else {\n        _status = status_t { std::forward<Args>(args)... };\n      }\n\n      _cv.notify_all();\n    }\n\n    // pop and view should not be used interchangeably\n    status_t\n    pop() {\n      std::unique_lock ul { _lock };\n\n      if (!_continue) {\n        return util::false_v<status_t>;\n      }\n\n      while (!_status) {\n        _cv.wait(ul);\n\n        if (!_continue) {\n          return util::false_v<status_t>;\n        }\n      }\n\n      auto val = std::move(_status);\n      _status = util::false_v<status_t>;\n      return val;\n    }\n\n    // pop and view should not be used interchangeably\n    template <class Rep, class Period>\n    status_t\n    pop(std::chrono::duration<Rep, Period> delay) {\n      std::unique_lock ul { _lock };\n\n      if (!_continue) {\n        return util::false_v<status_t>;\n      }\n\n      while (!_status) {\n        if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {\n          return util::false_v<status_t>;\n        }\n      }\n\n      auto val = std::move(_status);\n      _status = util::false_v<status_t>;\n      return val;\n    }\n\n    // pop and view should not be used interchangeably\n    status_t\n    view() {\n      std::unique_lock ul { _lock };\n\n      if (!_continue) {\n        return util::false_v<status_t>;\n      }\n\n      while (!_status) {\n        _cv.wait(ul);\n\n        if (!_continue) {\n          return util::false_v<status_t>;\n        }\n      }\n\n      return _status;\n    }\n\n    // pop and view should not be used interchangeably\n    template <class Rep, class Period>\n    status_t\n    view(std::chrono::duration<Rep, Period> delay) {\n      std::unique_lock ul { _lock };\n\n      if (!_continue) {\n        return util::false_v<status_t>;\n      }\n\n      while (!_status) {\n        if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {\n          return util::false_v<status_t>;\n        }\n      }\n\n      return _status;\n    }\n\n    bool\n    peek() {\n      return _continue && (bool) _status;\n    }\n\n    void\n    stop() {\n      std::lock_guard lg { _lock };\n\n      _continue = false;\n\n      _cv.notify_all();\n    }\n\n    void\n    reset() {\n      std::lock_guard lg { _lock };\n\n      _continue = true;\n\n      _status = util::false_v<status_t>;\n    }\n\n    [[nodiscard]] bool\n    running() const {\n      return _continue;\n    }\n\n  private:\n    bool _continue { true };\n    status_t _status { util::false_v<status_t> };\n\n    std::condition_variable _cv;\n    std::mutex _lock;\n  };\n\n  template <class T>\n  class alarm_raw_t {\n  public:\n    using status_t = util::optional_t<T>;\n\n    void\n    ring(const status_t &status) {\n      std::lock_guard lg(_lock);\n\n      _status = status;\n      _rang = true;\n      _cv.notify_one();\n    }\n\n    void\n    ring(status_t &&status) {\n      std::lock_guard lg(_lock);\n\n      _status = std::move(status);\n      _rang = true;\n      _cv.notify_one();\n    }\n\n    template <class Rep, class Period>\n    auto\n    wait_for(const std::chrono::duration<Rep, Period> &rel_time) {\n      std::unique_lock ul(_lock);\n\n      return _cv.wait_for(ul, rel_time, [this]() { return _rang; });\n    }\n\n    template <class Rep, class Period, class Pred>\n    auto\n    wait_for(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {\n      std::unique_lock ul(_lock);\n\n      return _cv.wait_for(ul, rel_time, [this, &pred]() { return _rang || pred(); });\n    }\n\n    template <class Rep, class Period>\n    auto\n    wait_until(const std::chrono::duration<Rep, Period> &rel_time) {\n      std::unique_lock ul(_lock);\n\n      return _cv.wait_until(ul, rel_time, [this]() { return _rang; });\n    }\n\n    template <class Rep, class Period, class Pred>\n    auto\n    wait_until(const std::chrono::duration<Rep, Period> &rel_time, Pred &&pred) {\n      std::unique_lock ul(_lock);\n\n      return _cv.wait_until(ul, rel_time, [this, &pred]() { return _rang || pred(); });\n    }\n\n    auto\n    wait() {\n      std::unique_lock ul(_lock);\n      _cv.wait(ul, [this]() { return _rang; });\n    }\n\n    template <class Pred>\n    auto\n    wait(Pred &&pred) {\n      std::unique_lock ul(_lock);\n      _cv.wait(ul, [this, &pred]() { return _rang || pred(); });\n    }\n\n    const status_t &\n    status() const {\n      return _status;\n    }\n\n    status_t &\n    status() {\n      return _status;\n    }\n\n    void\n    reset() {\n      _status = status_t {};\n      _rang = false;\n    }\n\n  private:\n    std::mutex _lock;\n    std::condition_variable _cv;\n\n    status_t _status { util::false_v<status_t> };\n    bool _rang { false };\n  };\n\n  template <class T>\n  using alarm_t = std::shared_ptr<alarm_raw_t<T>>;\n\n  template <class T>\n  alarm_t<T>\n  make_alarm() {\n    return std::make_shared<alarm_raw_t<T>>();\n  }\n\n  template <class T>\n  class queue_t {\n  public:\n    using status_t = util::optional_t<T>;\n\n    queue_t(std::uint32_t max_elements = 32):\n        _max_elements { max_elements } {}\n\n    template <class... Args>\n    void\n    raise(Args &&...args) {\n      std::lock_guard ul { _lock };\n\n      if (!_continue) {\n        return;\n      }\n\n      if (_queue.size() == _max_elements) {\n        _queue.clear();\n      }\n\n      _queue.emplace_back(std::forward<Args>(args)...);\n\n      _cv.notify_all();\n    }\n\n    bool\n    peek() {\n      return _continue && !_queue.empty();\n    }\n\n    template <class Rep, class Period>\n    status_t\n    pop(std::chrono::duration<Rep, Period> delay) {\n      std::unique_lock ul { _lock };\n\n      if (!_continue) {\n        return util::false_v<status_t>;\n      }\n\n      while (_queue.empty()) {\n        if (!_continue || _cv.wait_for(ul, delay) == std::cv_status::timeout) {\n          return util::false_v<status_t>;\n        }\n      }\n\n      auto val = std::move(_queue.front());\n      _queue.erase(std::begin(_queue));\n\n      return val;\n    }\n\n    status_t\n    pop() {\n      std::unique_lock ul { _lock };\n\n      if (!_continue) {\n        return util::false_v<status_t>;\n      }\n\n      while (_queue.empty()) {\n        _cv.wait(ul);\n\n        if (!_continue) {\n          return util::false_v<status_t>;\n        }\n      }\n\n      auto val = std::move(_queue.front());\n      _queue.erase(std::begin(_queue));\n\n      return val;\n    }\n\n    std::vector<T> &\n    unsafe() {\n      return _queue;\n    }\n\n    void\n    stop() {\n      std::lock_guard lg { _lock };\n\n      _continue = false;\n\n      _cv.notify_all();\n    }\n\n    [[nodiscard]] bool\n    running() const {\n      return _continue;\n    }\n\n  private:\n    bool _continue { true };\n    std::uint32_t _max_elements;\n\n    std::mutex _lock;\n    std::condition_variable _cv;\n\n    std::vector<T> _queue;\n  };\n\n  template <class T>\n  class shared_t {\n  public:\n    using element_type = T;\n\n    using construct_f = std::function<int(element_type &)>;\n    using destruct_f = std::function<void(element_type &)>;\n\n    struct ptr_t {\n      shared_t *owner;\n\n      ptr_t():\n          owner { nullptr } {}\n      explicit ptr_t(shared_t *owner):\n          owner { owner } {}\n\n      ptr_t(ptr_t &&ptr) noexcept:\n          owner { ptr.owner } {\n        ptr.owner = nullptr;\n      }\n\n      ptr_t(const ptr_t &ptr) noexcept:\n          owner { ptr.owner } {\n        if (!owner) {\n          return;\n        }\n\n        auto tmp = ptr.owner->ref();\n        tmp.owner = nullptr;\n      }\n\n      ptr_t &\n      operator=(const ptr_t &ptr) noexcept {\n        if (!ptr.owner) {\n          release();\n\n          return *this;\n        }\n\n        return *this = std::move(*ptr.owner->ref());\n      }\n\n      ptr_t &\n      operator=(ptr_t &&ptr) noexcept {\n        if (owner) {\n          release();\n        }\n\n        std::swap(owner, ptr.owner);\n\n        return *this;\n      }\n\n      ~ptr_t() {\n        if (owner) {\n          release();\n        }\n      }\n\n      operator bool() const {\n        return owner != nullptr;\n      }\n\n      void\n      release() {\n        std::lock_guard lg { owner->_lock };\n\n        if (!--owner->_count) {\n          owner->_destruct(*get());\n          (*this)->~element_type();\n        }\n\n        owner = nullptr;\n      }\n\n      element_type *\n      get() const {\n        return reinterpret_cast<element_type *>(owner->_object_buf.data());\n      }\n\n      element_type *\n      operator->() {\n        return reinterpret_cast<element_type *>(owner->_object_buf.data());\n      }\n    };\n\n    template <class FC, class FD>\n    shared_t(FC &&fc, FD &&fd):\n        _construct { std::forward<FC>(fc) }, _destruct { std::forward<FD>(fd) } {}\n    [[nodiscard]] ptr_t\n    ref() {\n      std::lock_guard lg { _lock };\n\n      if (!_count) {\n        new (_object_buf.data()) element_type;\n        if (_construct(*reinterpret_cast<element_type *>(_object_buf.data()))) {\n          return ptr_t { nullptr };\n        }\n      }\n\n      ++_count;\n\n      return ptr_t { this };\n    }\n\n    /**\n     * @brief Check if there are any active references without creating a new one.\n     * @return true if there are active references, false otherwise.\n     */\n    [[nodiscard]] bool\n    has_ref() {\n      std::lock_guard lg { _lock };\n      return _count > 0;\n    }\n\n  private:\n    construct_f _construct;\n    destruct_f _destruct;\n\n    std::array<std::uint8_t, sizeof(element_type)> _object_buf;\n\n    std::uint32_t _count;\n    std::mutex _lock;\n  };\n\n  template <class T, class F_Construct, class F_Destruct>\n  auto\n  make_shared(F_Construct &&fc, F_Destruct &&fd) {\n    return shared_t<T> {\n      std::forward<F_Construct>(fc), std::forward<F_Destruct>(fd)\n    };\n  }\n\n  using signal_t = event_t<bool>;\n\n  class mail_raw_t;\n  using mail_t = std::shared_ptr<mail_raw_t>;\n\n  void\n  cleanup(mail_raw_t *);\n  template <class T>\n  class post_t: public T {\n  public:\n    template <class... Args>\n    post_t(mail_t mail, Args &&...args):\n        T(std::forward<Args>(args)...), mail { std::move(mail) } {}\n\n    mail_t mail;\n\n    ~post_t() {\n      cleanup(mail.get());\n    }\n  };\n\n  template <class T>\n  inline auto\n  lock(const std::weak_ptr<void> &wp) {\n    return std::reinterpret_pointer_cast<typename T::element_type>(wp.lock());\n  }\n\n  class mail_raw_t: public std::enable_shared_from_this<mail_raw_t> {\n  public:\n    template <class T>\n    using event_t = std::shared_ptr<post_t<event_t<T>>>;\n\n    template <class T>\n    using queue_t = std::shared_ptr<post_t<queue_t<T>>>;\n\n    template <class T>\n    event_t<T>\n    event(const std::string_view &id) {\n      std::lock_guard lg { mutex };\n\n      auto it = id_to_post.find(id);\n      if (it != std::end(id_to_post)) {\n        return lock<event_t<T>>(it->second);\n      }\n\n      auto post = std::make_shared<typename event_t<T>::element_type>(shared_from_this());\n      id_to_post.emplace(std::pair<std::string, std::weak_ptr<void>> { std::string { id }, post });\n\n      return post;\n    }\n\n    template <class T>\n    queue_t<T>\n    queue(const std::string_view &id) {\n      std::lock_guard lg { mutex };\n\n      auto it = id_to_post.find(id);\n      if (it != std::end(id_to_post)) {\n        return lock<queue_t<T>>(it->second);\n      }\n\n      auto post = std::make_shared<typename queue_t<T>::element_type>(shared_from_this(), 32);\n      id_to_post.emplace(std::pair<std::string, std::weak_ptr<void>> { std::string { id }, post });\n\n      return post;\n    }\n\n    void\n    cleanup() {\n      std::lock_guard lg { mutex };\n\n      for (auto it = std::begin(id_to_post); it != std::end(id_to_post); ++it) {\n        auto &weak = it->second;\n\n        if (weak.expired()) {\n          id_to_post.erase(it);\n\n          return;\n        }\n      }\n    }\n\n    std::mutex mutex;\n\n    std::map<std::string, std::weak_ptr<void>, std::less<>> id_to_post;\n  };\n\n  inline void\n  cleanup(mail_raw_t *mail) {\n    mail->cleanup();\n  }\n}  // namespace safe\n"
  },
  {
    "path": "src/upnp.cpp",
    "content": "/**\n * @file src/upnp.cpp\n * @brief Definitions for UPnP port mapping.\n */\n#include <cstddef>\n#include <miniupnpc/miniupnpc.h>\n#include <miniupnpc/upnpcommands.h>\n\n#include \"config.h\"\n#include \"confighttp.h\"\n#include \"globals.h\"\n#include \"logging.h\"\n#include \"network.h\"\n#include \"nvhttp.h\"\n#include \"rtsp.h\"\n#include \"stream.h\"\n#include \"upnp.h\"\n#include \"utility.h\"\n\nusing namespace std::literals;\n\nnamespace upnp {\n\n  struct mapping_t {\n    struct {\n      std::string wan;\n      std::string lan;\n      std::string proto;\n    } port;\n\n    std::string description;\n  };\n\n  static std::string_view\n  status_string(int status) {\n    switch (status) {\n      case 0:\n        return \"No IGD device found\"sv;\n      case 1:\n        return \"Valid IGD device found\"sv;\n      case 2:\n        return \"Valid IGD device found,  but it isn't connected\"sv;\n      case 3:\n        return \"A UPnP device has been found,  but it wasn't recognized as an IGD\"sv;\n    }\n\n    return \"Unknown status\"sv;\n  }\n\n  /**\n   * This function is a wrapper around UPNP_GetValidIGD() that returns the status code. There is a pre-processor\n   * check to determine which version of the function to call based on the version of the MiniUPnPc library.\n   */\n  int\n  UPNP_GetValidIGDStatus(device_t &device, urls_t *urls, IGDdatas *data, std::array<char, INET6_ADDRESS_STRLEN> &lan_addr) {\n#if (MINIUPNPC_API_VERSION >= 18)\n    return UPNP_GetValidIGD(device.get(), &urls->el, data, lan_addr.data(), lan_addr.size(), nullptr, 0);\n#else\n    return UPNP_GetValidIGD(device.get(), &urls->el, data, lan_addr.data(), lan_addr.size());\n#endif\n  }\n\n  class deinit_t: public platf::deinit_t {\n  public:\n    deinit_t() {\n      auto rtsp = std::to_string(net::map_port(rtsp_stream::RTSP_SETUP_PORT));\n      auto video = std::to_string(net::map_port(stream::VIDEO_STREAM_PORT));\n      auto audio = std::to_string(net::map_port(stream::AUDIO_STREAM_PORT));\n      auto control = std::to_string(net::map_port(stream::CONTROL_PORT));\n      auto mic_stream = std::to_string(net::map_port(stream::MIC_STREAM_PORT));\n      auto gs_http = std::to_string(net::map_port(nvhttp::PORT_HTTP));\n      auto gs_https = std::to_string(net::map_port(nvhttp::PORT_HTTPS));\n      auto wm_http = std::to_string(net::map_port(confighttp::PORT_HTTPS));\n\n      mappings.assign({\n        { { rtsp, rtsp, \"TCP\"s }, \"Sunshine - RTSP\"s },\n        { { video, video, \"UDP\"s }, \"Sunshine - Video\"s },\n        { { audio, audio, \"UDP\"s }, \"Sunshine - Audio\"s },\n        { { control, control, \"UDP\"s }, \"Sunshine - Control\"s },\n        { { mic_stream, mic_stream, \"UDP\"s }, \"Sunshine - Microphone\"s },\n        { { gs_http, gs_http, \"TCP\"s }, \"Sunshine - Client HTTP\"s },\n        { { gs_https, gs_https, \"TCP\"s }, \"Sunshine - Client HTTPS\"s },\n      });\n\n      // Only map port for the Web Manager if it is configured to accept connection from WAN\n      if (net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) {\n        mappings.emplace_back(mapping_t { { wm_http, wm_http, \"TCP\"s }, \"Sunshine - Web UI\"s });\n      }\n\n      // Start the mapping thread\n      upnp_thread = std::thread { &deinit_t::upnp_thread_proc, this };\n    }\n\n    ~deinit_t() {\n      upnp_thread.join();\n    }\n\n    /**\n     * @brief Opens pinholes for IPv6 traffic if the IGD is capable.\n     * @details Not many IGDs support this feature, so we perform error logging with debug level.\n     * @return `true` if the pinholes were opened successfully.\n     */\n    bool\n    create_ipv6_pinholes() {\n      int err;\n      device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv6, 2, &err) };\n      if (!device || err) {\n        BOOST_LOG(debug) << \"Couldn't discover any IPv6 UPNP devices\"sv;\n        return false;\n      }\n\n      IGDdatas data;\n      urls_t urls;\n      std::array<char, INET6_ADDRESS_STRLEN> lan_addr;\n      auto status = upnp::UPNP_GetValidIGDStatus(device, &urls, &data, lan_addr);\n      if (status != 1 && status != 2) {\n        BOOST_LOG(debug) << \"No valid IPv6 IGD: \"sv << status_string(status);\n        return false;\n      }\n\n      if (data.IPv6FC.controlurl[0] != 0) {\n        int firewallEnabled, pinholeAllowed;\n\n        // Check if this firewall supports IPv6 pinholes\n        err = UPNP_GetFirewallStatus(urls->controlURL_6FC, data.IPv6FC.servicetype, &firewallEnabled, &pinholeAllowed);\n        if (err == UPNPCOMMAND_SUCCESS) {\n          BOOST_LOG(debug) << \"UPnP IPv6 firewall control available. Firewall is \"sv\n                           << (firewallEnabled ? \"enabled\"sv : \"disabled\"sv)\n                           << \", pinhole is \"sv\n                           << (pinholeAllowed ? \"allowed\"sv : \"disallowed\"sv);\n\n          if (pinholeAllowed) {\n            // Create pinholes for each port\n            auto mapping_period = std::to_string(PORT_MAPPING_LIFETIME.count());\n            auto shutdown_event = mail::man->event<bool>(mail::shutdown);\n\n            for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) {\n              auto mapping = *it;\n              char uniqueId[8];\n\n              // Open a pinhole for the LAN port, since there will be no WAN->LAN port mapping on IPv6\n              err = UPNP_AddPinhole(urls->controlURL_6FC,\n                data.IPv6FC.servicetype,\n                \"\", \"0\",\n                lan_addr.data(),\n                mapping.port.lan.c_str(),\n                mapping.port.proto.c_str(),\n                mapping_period.c_str(),\n                uniqueId);\n              if (err == UPNPCOMMAND_SUCCESS) {\n                BOOST_LOG(debug) << \"Successfully created pinhole for \"sv << mapping.port.proto << ' ' << mapping.port.lan;\n              }\n              else {\n                BOOST_LOG(debug) << \"Failed to create pinhole for \"sv << mapping.port.proto << ' ' << mapping.port.lan << \": \"sv << err;\n              }\n            }\n\n            return err == 0;\n          }\n          else {\n            BOOST_LOG(debug) << \"IPv6 pinholes are not allowed by the IGD\"sv;\n            return false;\n          }\n        }\n        else {\n          BOOST_LOG(debug) << \"Failed to get IPv6 firewall status: \"sv << err;\n          return false;\n        }\n      }\n      else {\n        BOOST_LOG(debug) << \"IPv6 Firewall Control is not supported by the IGD\"sv;\n        return false;\n      }\n    }\n\n    /**\n     * @brief Maps a port via UPnP.\n     * @param data IGDdatas from UPNP_GetValidIGD()\n     * @param urls urls_t from UPNP_GetValidIGD()\n     * @param lan_addr Local IP address to map to\n     * @param mapping Information about port to map\n     * @return `true` on success.\n     */\n    bool\n    map_upnp_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) {\n      char intClient[16];\n      char intPort[6];\n      char desc[80];\n      char enabled[4];\n      char leaseDuration[16];\n      bool indefinite = false;\n\n      // First check if this port is already mapped successfully\n      BOOST_LOG(debug) << \"Checking for existing UPnP port mapping for \"sv << mapping.port.wan;\n      auto err = UPNP_GetSpecificPortMappingEntry(\n        urls->controlURL,\n        data.first.servicetype,\n        // In params\n        mapping.port.wan.c_str(),\n        mapping.port.proto.c_str(),\n        nullptr,\n        // Out params\n        intClient, intPort, desc, enabled, leaseDuration);\n      if (err == 714) {  // NoSuchEntryInArray\n        BOOST_LOG(debug) << \"Mapping entry not found for \"sv << mapping.port.wan;\n      }\n      else if (err == UPNPCOMMAND_SUCCESS) {\n        // Some routers change the description, so we can't check that here\n        if (!std::strcmp(intClient, lan_addr.c_str())) {\n          if (std::atoi(leaseDuration) == 0) {\n            BOOST_LOG(debug) << \"Static mapping entry found for \"sv << mapping.port.wan;\n\n            // It's a static mapping, so we're done here\n            return true;\n          }\n          else {\n            BOOST_LOG(debug) << \"Mapping entry found for \"sv << mapping.port.wan << \" (\"sv << leaseDuration << \" seconds remaining)\"sv;\n          }\n        }\n        else {\n          BOOST_LOG(warning) << \"UPnP conflict detected with: \"sv << intClient;\n\n          // Some UPnP IGDs won't let unauthenticated clients delete other conflicting port mappings\n          // for security reasons, but we will give it a try anyway.\n          err = UPNP_DeletePortMapping(\n            urls->controlURL,\n            data.first.servicetype,\n            mapping.port.wan.c_str(),\n            mapping.port.proto.c_str(),\n            nullptr);\n          if (err) {\n            BOOST_LOG(error) << \"Unable to delete conflicting UPnP port mapping: \"sv << err;\n            return false;\n          }\n        }\n      }\n      else {\n        BOOST_LOG(error) << \"UPNP_GetSpecificPortMappingEntry() failed: \"sv << err;\n\n        // If we get a strange error from the router, we'll assume it's some old broken IGDv1\n        // device and only use indefinite lease durations to hopefully avoid confusing it.\n        if (err != 606) {  // Unauthorized\n          indefinite = true;\n        }\n      }\n\n      // Add/update the port mapping\n      auto mapping_period = std::to_string(indefinite ? 0 : PORT_MAPPING_LIFETIME.count());\n      err = UPNP_AddPortMapping(\n        urls->controlURL,\n        data.first.servicetype,\n        mapping.port.wan.c_str(),\n        mapping.port.lan.c_str(),\n        lan_addr.data(),\n        mapping.description.c_str(),\n        mapping.port.proto.c_str(),\n        nullptr,\n        mapping_period.c_str());\n\n      if (err != UPNPCOMMAND_SUCCESS && !indefinite) {\n        // This may be an old/broken IGD that doesn't like non-static mappings.\n        BOOST_LOG(debug) << \"Trying static mapping after failure: \"sv << err;\n        err = UPNP_AddPortMapping(\n          urls->controlURL,\n          data.first.servicetype,\n          mapping.port.wan.c_str(),\n          mapping.port.lan.c_str(),\n          lan_addr.data(),\n          mapping.description.c_str(),\n          mapping.port.proto.c_str(),\n          nullptr,\n          \"0\");\n      }\n\n      if (err) {\n        BOOST_LOG(error) << \"Failed to map \"sv << mapping.port.proto << ' ' << mapping.port.lan << \": \"sv << err;\n        return false;\n      }\n\n      BOOST_LOG(debug) << \"Successfully mapped \"sv << mapping.port.proto << ' ' << mapping.port.lan;\n      return true;\n    }\n\n    /**\n     * @brief Unmaps all ports.\n     * @param urls urls_t from UPNP_GetValidIGD()\n     * @param data IGDdatas from UPNP_GetValidIGD()\n     */\n    void\n    unmap_all_upnp_ports(const urls_t &urls, const IGDdatas &data) {\n      for (auto it = std::begin(mappings); it != std::end(mappings); ++it) {\n        auto status = UPNP_DeletePortMapping(\n          urls->controlURL,\n          data.first.servicetype,\n          it->port.wan.c_str(),\n          it->port.proto.c_str(),\n          nullptr);\n\n        if (status && status != 714) {  // NoSuchEntryInArray\n          BOOST_LOG(warning) << \"Failed to unmap \"sv << it->port.proto << ' ' << it->port.lan << \": \"sv << status;\n        }\n        else {\n          BOOST_LOG(debug) << \"Successfully unmapped \"sv << it->port.proto << ' ' << it->port.lan;\n        }\n      }\n    }\n\n    /**\n     * @brief Maintains UPnP port forwarding rules\n     */\n    void\n    upnp_thread_proc() {\n      auto shutdown_event = mail::man->event<bool>(mail::shutdown);\n      bool mapped = false;\n      IGDdatas data;\n      urls_t mapped_urls;\n      auto address_family = net::af_from_enum_string(config::sunshine.address_family);\n\n      // Refresh UPnP rules every few minutes. They can be lost if the router reboots,\n      // WAN IP address changes, or various other conditions.\n      do {\n        int err = 0;\n        device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) };\n        if (!device || err) {\n          BOOST_LOG(warning) << \"Couldn't discover any IPv4 UPNP devices\"sv;\n          mapped = false;\n          continue;\n        }\n\n        for (auto dev = device.get(); dev != nullptr; dev = dev->pNext) {\n          BOOST_LOG(debug) << \"Found device: \"sv << dev->descURL;\n        }\n\n        std::array<char, INET6_ADDRESS_STRLEN> lan_addr;\n\n        urls_t urls;\n        auto status = upnp::UPNP_GetValidIGDStatus(device, &urls, &data, lan_addr);\n        if (status != 1 && status != 2) {\n          BOOST_LOG(error) << status_string(status);\n          mapped = false;\n          continue;\n        }\n\n        std::string lan_addr_str { lan_addr.data() };\n\n        BOOST_LOG(debug) << \"Found valid IGD device: \"sv << urls->rootdescURL;\n\n        for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) {\n          map_upnp_port(data, urls, lan_addr_str, *it);\n        }\n\n        if (!mapped) {\n          BOOST_LOG(info) << \"Completed UPnP port mappings to \"sv << lan_addr_str << \" via \"sv << urls->rootdescURL;\n        }\n\n        // If we are listening on IPv6 and the IGD has an IPv6 firewall enabled, try to create IPv6 firewall pinholes\n        if (address_family == net::af_e::BOTH) {\n          if (create_ipv6_pinholes() && !mapped) {\n            // Only log the first time through\n            BOOST_LOG(info) << \"Successfully opened IPv6 pinholes on the IGD\"sv;\n          }\n        }\n\n        mapped = true;\n        mapped_urls = std::move(urls);\n      } while (!shutdown_event->view(REFRESH_INTERVAL));\n\n      if (mapped) {\n        // Unmap ports upon termination\n        BOOST_LOG(info) << \"Unmapping UPNP ports...\"sv;\n        unmap_all_upnp_ports(mapped_urls, data);\n      }\n    }\n\n    std::vector<mapping_t> mappings;\n    std::thread upnp_thread;\n  };\n\n  std::unique_ptr<platf::deinit_t>\n  start() {\n    if (!config::sunshine.flags[config::flag::UPNP]) {\n      return nullptr;\n    }\n\n    return std::make_unique<deinit_t>();\n  }\n}  // namespace upnp\n"
  },
  {
    "path": "src/upnp.h",
    "content": "/**\n * @file src/upnp.h\n * @brief Declarations for UPnP port mapping.\n */\n#pragma once\n\n#include <miniupnpc/miniupnpc.h>\n\n#include \"platform/common.h\"\n\n/**\n * @brief UPnP port mapping.\n */\nnamespace upnp {\n  constexpr auto INET6_ADDRESS_STRLEN = 46;\n  constexpr auto IPv4 = 0;\n  constexpr auto IPv6 = 1;\n  constexpr auto PORT_MAPPING_LIFETIME = 3600s;\n  constexpr auto REFRESH_INTERVAL = 120s;\n\n  using device_t = util::safe_ptr<UPNPDev, freeUPNPDevlist>;\n\n  KITTY_USING_MOVE_T(urls_t, UPNPUrls, , {\n    FreeUPNPUrls(&el);\n  });\n\n  /**\n   * @brief Get the valid IGD status.\n   * @param device The device.\n   * @param urls The URLs.\n   * @param data The IGD data.\n   * @param lan_addr The LAN address.\n   * @return The UPnP Status.\n   * @retval 0 No IGD found.\n   * @retval 1 A valid connected IGD has been found.\n   * @retval 2 A valid IGD has been found but it reported as not connected.\n   * @retval 3 An UPnP device has been found but was not recognized as an IGD.\n   */\n  int\n  UPNP_GetValidIGDStatus(device_t &device, urls_t *urls, IGDdatas *data, std::array<char, INET6_ADDRESS_STRLEN> &lan_addr);\n\n  [[nodiscard]] std::unique_ptr<platf::deinit_t>\n  start();\n}  // namespace upnp\n"
  },
  {
    "path": "src/utility.h",
    "content": "/**\n * @file src/utility.h\n * @brief Declarations for utility functions.\n */\n#pragma once\n\n#include <algorithm>\n#include <condition_variable>\n#include <memory>\n#include <mutex>\n#include <optional>\n#include <ostream>\n#include <string>\n#include <string_view>\n#include <type_traits>\n#include <variant>\n#include <vector>\n\n#define KITTY_WHILE_LOOP(x, y, z) \\\n  {                               \\\n    x;                            \\\n    while (y) z                   \\\n  }\n\ntemplate <typename T>\nstruct argument_type;\n\ntemplate <typename T, typename U>\nstruct argument_type<T(U)> {\n  typedef U type;\n};\n\n#define KITTY_USING_MOVE_T(move_t, t, init_val, z)                \\\n  class move_t {                                                  \\\n  public:                                                         \\\n    using element_type = typename argument_type<void(t)>::type;   \\\n                                                                  \\\n    move_t(): el { init_val } {}                                  \\\n    template <class... Args>                                      \\\n    move_t(Args &&...args): el { std::forward<Args>(args)... } {} \\\n    move_t(const move_t &) = delete;                              \\\n                                                                  \\\n    move_t(move_t &&other) noexcept: el { std::move(other.el) } { \\\n      other.el = element_type { init_val };                       \\\n    }                                                             \\\n                                                                  \\\n    move_t &                                                      \\\n    operator=(const move_t &) = delete;                           \\\n                                                                  \\\n    move_t &                                                      \\\n    operator=(move_t &&other) {                                   \\\n      std::swap(el, other.el);                                    \\\n      return *this;                                               \\\n    }                                                             \\\n    element_type *                                                \\\n    operator->() { return &el; }                                  \\\n    const element_type *                                          \\\n    operator->() const { return &el; }                            \\\n                                                                  \\\n    inline element_type                                           \\\n    release() {                                                   \\\n      element_type val = std::move(el);                           \\\n      el = element_type { init_val };                             \\\n      return val;                                                 \\\n    }                                                             \\\n                                                                  \\\n    ~move_t() z                                                   \\\n                                                                  \\\n      element_type el;                                            \\\n  }\n\n#define KITTY_DECL_CONSTR(x)             \\\n  x(x &&) noexcept = default;            \\\n  x &operator=(x &&) noexcept = default; \\\n  x();\n\n#define KITTY_DEFAULT_CONSTR_MOVE(x) \\\n  x(x &&) noexcept = default;        \\\n  x &operator=(x &&) noexcept = default;\n\n#define KITTY_DEFAULT_CONSTR_MOVE_THROW(x) \\\n  x(x &&) = default;                       \\\n  x &operator=(x &&) = default;            \\\n  x() = default;\n\n#define KITTY_DEFAULT_CONSTR(x)    \\\n  KITTY_DEFAULT_CONSTR_MOVE(x)     \\\n  x(const x &) noexcept = default; \\\n  x &operator=(const x &) = default;\n\n#define TUPLE_2D(a, b, expr)      \\\n  decltype(expr) a##_##b = expr;  \\\n  auto &a = std::get<0>(a##_##b); \\\n  auto &b = std::get<1>(a##_##b)\n\n#define TUPLE_2D_REF(a, b, expr)  \\\n  auto &a##_##b = expr;           \\\n  auto &a = std::get<0>(a##_##b); \\\n  auto &b = std::get<1>(a##_##b)\n\n#define TUPLE_3D(a, b, c, expr)         \\\n  decltype(expr) a##_##b##_##c = expr;  \\\n  auto &a = std::get<0>(a##_##b##_##c); \\\n  auto &b = std::get<1>(a##_##b##_##c); \\\n  auto &c = std::get<2>(a##_##b##_##c)\n\n#define TUPLE_3D_REF(a, b, c, expr)     \\\n  auto &a##_##b##_##c = expr;           \\\n  auto &a = std::get<0>(a##_##b##_##c); \\\n  auto &b = std::get<1>(a##_##b##_##c); \\\n  auto &c = std::get<2>(a##_##b##_##c)\n\n#define TUPLE_EL(a, b, expr)  \\\n  decltype(expr) a##_ = expr; \\\n  auto &a = std::get<b>(a##_)\n\n#define TUPLE_EL_REF(a, b, expr) \\\n  auto &a = std::get<b>(expr)\n\nnamespace util {\n\n  template <template <typename...> class X, class... Y>\n  struct __instantiation_of: public std::false_type {};\n\n  template <template <typename...> class X, class... Y>\n  struct __instantiation_of<X, X<Y...>>: public std::true_type {};\n\n  template <template <typename...> class X, class T, class... Y>\n  static constexpr auto instantiation_of_v = __instantiation_of<X, T, Y...>::value;\n\n  template <bool V, class X, class Y>\n  struct __either;\n\n  template <class X, class Y>\n  struct __either<true, X, Y> {\n    using type = X;\n  };\n\n  template <class X, class Y>\n  struct __either<false, X, Y> {\n    using type = Y;\n  };\n\n  template <bool V, class X, class Y>\n  using either_t = typename __either<V, X, Y>::type;\n\n  template <class... Ts>\n  struct overloaded: Ts... {\n    using Ts::operator()...;\n  };\n  template <class... Ts>\n  overloaded(Ts...) -> overloaded<Ts...>;\n\n  template <class T>\n  class FailGuard {\n  public:\n    FailGuard() = delete;\n    FailGuard(T &&f) noexcept:\n        _func { std::forward<T>(f) } {}\n    FailGuard(FailGuard &&other) noexcept:\n        _func { std::move(other._func) } {\n      this->failure = other.failure;\n\n      other.failure = false;\n    }\n\n    FailGuard(const FailGuard &) = delete;\n\n    FailGuard &\n    operator=(const FailGuard &) = delete;\n    FailGuard &\n    operator=(FailGuard &&other) = delete;\n\n    ~FailGuard() noexcept {\n      if (failure) {\n        _func();\n      }\n    }\n\n    void\n    disable() { failure = false; }\n    bool failure { true };\n\n  private:\n    T _func;\n  };\n\n  template <class T>\n  [[nodiscard]] auto\n  fail_guard(T &&f) {\n    return FailGuard<T> { std::forward<T>(f) };\n  }\n\n  template <class T>\n  void\n  append_struct(std::vector<uint8_t> &buf, const T &_struct) {\n    constexpr size_t data_len = sizeof(_struct);\n\n    buf.reserve(buf.size() + data_len);\n\n    auto *data = (uint8_t *) &_struct;\n\n    buf.insert(buf.end(), data, data + data_len);\n  }\n\n  template <class T>\n  class Hex {\n  public:\n    typedef T elem_type;\n\n  private:\n    const char _bits[16] {\n      '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'\n    };\n\n    char _hex[sizeof(elem_type) * 2];\n\n  public:\n    Hex(const elem_type &elem, bool rev) {\n      if (!rev) {\n        const uint8_t *data = reinterpret_cast<const uint8_t *>(&elem) + sizeof(elem_type) - 1;\n        for (auto it = begin(); it < cend();) {\n          *it++ = _bits[*data / 16];\n          *it++ = _bits[*data-- % 16];\n        }\n      }\n      else {\n        const uint8_t *data = reinterpret_cast<const uint8_t *>(&elem);\n        for (auto it = begin(); it < cend();) {\n          *it++ = _bits[*data / 16];\n          *it++ = _bits[*data++ % 16];\n        }\n      }\n    }\n\n    char *\n    begin() { return _hex; }\n    char *\n    end() { return _hex + sizeof(elem_type) * 2; }\n\n    const char *\n    begin() const { return _hex; }\n    const char *\n    end() const { return _hex + sizeof(elem_type) * 2; }\n\n    const char *\n    cbegin() const { return _hex; }\n    const char *\n    cend() const { return _hex + sizeof(elem_type) * 2; }\n\n    std::string\n    to_string() const {\n      return { begin(), end() };\n    }\n\n    std::string_view\n    to_string_view() const {\n      return { begin(), sizeof(elem_type) * 2 };\n    }\n  };\n\n  template <class T>\n  Hex<T>\n  hex(const T &elem, bool rev = false) {\n    return Hex<T>(elem, rev);\n  }\n\n  template <typename T>\n  std::string\n  log_hex(const T &value) {\n    return \"0x\" + Hex<T>(value, false).to_string();\n  }\n\n  template <class It>\n  std::string\n  hex_vec(It begin, It end, bool rev = false) {\n    auto str_size = 2 * std::distance(begin, end);\n\n    std::string hex;\n    hex.resize(str_size);\n\n    const char _bits[16] {\n      '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'\n    };\n\n    if (rev) {\n      for (auto it = std::begin(hex); it < std::end(hex);) {\n        *it++ = _bits[((uint8_t) *begin) / 16];\n        *it++ = _bits[((uint8_t) *begin++) % 16];\n      }\n    }\n    else {\n      --end;\n      for (auto it = std::begin(hex); it < std::end(hex);) {\n        *it++ = _bits[((uint8_t) *end) / 16];\n        *it++ = _bits[((uint8_t) *end--) % 16];\n      }\n    }\n\n    return hex;\n  }\n\n  template <class C>\n  std::string\n  hex_vec(C &&c, bool rev = false) {\n    return hex_vec(std::begin(c), std::end(c), rev);\n  }\n\n  template <class T>\n  T\n  from_hex(const std::string_view &hex, bool rev = false) {\n    std::uint8_t buf[sizeof(T)];\n\n    static char constexpr shift_bit = 'a' - 'A';\n\n    auto is_convertable = [](char ch) -> bool {\n      if (isdigit(ch)) {\n        return true;\n      }\n\n      ch |= shift_bit;\n\n      if ('a' > ch || ch > 'z') {\n        return false;\n      }\n\n      return true;\n    };\n\n    auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2;\n    auto padding = sizeof(T) - buf_size;\n\n    const char *data = hex.data() + hex.size() - 1;\n\n    auto convert = [](char ch) -> std::uint8_t {\n      if (ch >= '0' && ch <= '9') {\n        return (std::uint8_t) ch - '0';\n      }\n\n      return (std::uint8_t)(ch | (char) 32) - 'a' + (char) 10;\n    };\n\n    std::fill_n(buf + buf_size, padding, 0);\n\n    std::for_each_n(buf, buf_size, [&](auto &el) {\n      while (!is_convertable(*data)) { --data; }\n      std::uint8_t ch_r = convert(*data--);\n\n      while (!is_convertable(*data)) { --data; }\n      std::uint8_t ch_l = convert(*data--);\n\n      el = (ch_l << 4) | ch_r;\n    });\n\n    if (rev) {\n      std::reverse(std::begin(buf), std::end(buf));\n    }\n\n    return *reinterpret_cast<T *>(buf);\n  }\n\n  inline std::string\n  from_hex_vec(const std::string &hex, bool rev = false) {\n    std::string buf;\n\n    static char constexpr shift_bit = 'a' - 'A';\n    auto is_convertable = [](char ch) -> bool {\n      if (isdigit(ch)) {\n        return true;\n      }\n\n      ch |= shift_bit;\n\n      if ('a' > ch || ch > 'z') {\n        return false;\n      }\n\n      return true;\n    };\n\n    auto buf_size = std::count_if(std::begin(hex), std::end(hex), is_convertable) / 2;\n    buf.resize(buf_size);\n\n    const char *data = hex.data() + hex.size() - 1;\n\n    auto convert = [](char ch) -> std::uint8_t {\n      if (ch >= '0' && ch <= '9') {\n        return (std::uint8_t) ch - '0';\n      }\n\n      return (std::uint8_t)(ch | (char) 32) - 'a' + (char) 10;\n    };\n\n    for (auto &el : buf) {\n      while (!is_convertable(*data)) { --data; }\n      std::uint8_t ch_r = convert(*data--);\n\n      while (!is_convertable(*data)) { --data; }\n      std::uint8_t ch_l = convert(*data--);\n\n      el = (ch_l << 4) | ch_r;\n    }\n\n    if (rev) {\n      std::reverse(std::begin(buf), std::end(buf));\n    }\n\n    return buf;\n  }\n\n  template <class T>\n  class hash {\n  public:\n    using value_type = T;\n    std::size_t\n    operator()(const value_type &value) const {\n      const auto *p = reinterpret_cast<const char *>(&value);\n\n      return std::hash<std::string_view> {}(std::string_view { p, sizeof(value_type) });\n    }\n  };\n\n  template <class T>\n  auto\n  enm(const T &val) -> const std::underlying_type_t<T> & {\n    return *reinterpret_cast<const std::underlying_type_t<T> *>(&val);\n  }\n\n  template <class T>\n  auto\n  enm(T &val) -> std::underlying_type_t<T> & {\n    return *reinterpret_cast<std::underlying_type_t<T> *>(&val);\n  }\n\n  inline std::int64_t\n  from_chars(const char *begin, const char *end) {\n    if (begin == end) {\n      return 0;\n    }\n\n    std::int64_t res {};\n    std::int64_t mul = 1;\n    while (begin != --end) {\n      res += (std::int64_t)(*end - '0') * mul;\n\n      mul *= 10;\n    }\n\n    return *begin != '-' ? res + (std::int64_t)(*begin - '0') * mul : -res;\n  }\n\n  inline std::int64_t\n  from_view(const std::string_view &number) {\n    return from_chars(std::begin(number), std::end(number));\n  }\n\n  template <class X, class Y>\n  class Either: public std::variant<std::monostate, X, Y> {\n  public:\n    using std::variant<std::monostate, X, Y>::variant;\n\n    constexpr bool\n    has_left() const {\n      return std::holds_alternative<X>(*this);\n    }\n    constexpr bool\n    has_right() const {\n      return std::holds_alternative<Y>(*this);\n    }\n\n    X &\n    left() {\n      return std::get<X>(*this);\n    }\n\n    Y &\n    right() {\n      return std::get<Y>(*this);\n    }\n\n    const X &\n    left() const {\n      return std::get<X>(*this);\n    }\n\n    const Y &\n    right() const {\n      return std::get<Y>(*this);\n    }\n  };\n\n  // Compared to std::unique_ptr, it adds the ability to get the address of the pointer itself\n  template <typename T, typename D = std::default_delete<T>>\n  class uniq_ptr {\n  public:\n    using element_type = T;\n    using pointer = element_type *;\n    using const_pointer = element_type const *;\n    using deleter_type = D;\n\n    constexpr uniq_ptr() noexcept:\n        _p { nullptr } {}\n    constexpr uniq_ptr(std::nullptr_t) noexcept:\n        _p { nullptr } {}\n\n    uniq_ptr(const uniq_ptr &other) noexcept = delete;\n    uniq_ptr &\n    operator=(const uniq_ptr &other) noexcept = delete;\n\n    template <class V>\n    uniq_ptr(V *p) noexcept:\n        _p { p } {\n      static_assert(std::is_same_v<element_type, void> || std::is_same_v<element_type, V> || std::is_base_of_v<element_type, V>, \"element_type must be base class of V\");\n    }\n\n    template <class V>\n    uniq_ptr(std::unique_ptr<V, deleter_type> &&uniq) noexcept:\n        _p { uniq.release() } {\n      static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, \"element_type must be base class of V\");\n    }\n\n    template <class V>\n    uniq_ptr(uniq_ptr<V, deleter_type> &&other) noexcept:\n        _p { other.release() } {\n      static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, \"element_type must be base class of V\");\n    }\n\n    template <class V>\n    uniq_ptr &\n    operator=(uniq_ptr<V, deleter_type> &&other) noexcept {\n      static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, \"element_type must be base class of V\");\n      reset(other.release());\n\n      return *this;\n    }\n\n    template <class V>\n    uniq_ptr &\n    operator=(std::unique_ptr<V, deleter_type> &&uniq) noexcept {\n      static_assert(std::is_same_v<element_type, void> || std::is_same_v<T, V> || std::is_base_of_v<element_type, V>, \"element_type must be base class of V\");\n\n      reset(uniq.release());\n\n      return *this;\n    }\n\n    ~uniq_ptr() {\n      reset();\n    }\n\n    void\n    reset(pointer p = pointer()) {\n      if (_p) {\n        _deleter(_p);\n      }\n\n      _p = p;\n    }\n\n    pointer\n    release() {\n      auto tmp = _p;\n      _p = nullptr;\n      return tmp;\n    }\n\n    pointer\n    get() {\n      return _p;\n    }\n\n    const_pointer\n    get() const {\n      return _p;\n    }\n\n    std::add_lvalue_reference_t<element_type const>\n    operator*() const {\n      return *_p;\n    }\n    std::add_lvalue_reference_t<element_type>\n    operator*() {\n      return *_p;\n    }\n    const_pointer\n    operator->() const {\n      return _p;\n    }\n    pointer\n    operator->() {\n      return _p;\n    }\n    pointer *\n    operator&() const {\n      return &_p;\n    }\n\n    pointer *\n    operator&() {\n      return &_p;\n    }\n\n    deleter_type &\n    get_deleter() {\n      return _deleter;\n    }\n\n    const deleter_type &\n    get_deleter() const {\n      return _deleter;\n    }\n\n    explicit\n    operator bool() const {\n      return _p != nullptr;\n    }\n\n  protected:\n    pointer _p;\n    deleter_type _deleter;\n  };\n\n  template <class T1, class D1, class T2, class D2>\n  bool\n  operator==(const uniq_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {\n    return x.get() == y.get();\n  }\n\n  template <class T1, class D1, class T2, class D2>\n  bool\n  operator!=(const uniq_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {\n    return x.get() != y.get();\n  }\n\n  template <class T1, class D1, class T2, class D2>\n  bool\n  operator==(const std::unique_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {\n    return x.get() == y.get();\n  }\n\n  template <class T1, class D1, class T2, class D2>\n  bool\n  operator!=(const std::unique_ptr<T1, D1> &x, const uniq_ptr<T2, D2> &y) {\n    return x.get() != y.get();\n  }\n\n  template <class T1, class D1, class T2, class D2>\n  bool\n  operator==(const uniq_ptr<T1, D1> &x, const std::unique_ptr<T1, D1> &y) {\n    return x.get() == y.get();\n  }\n\n  template <class T1, class D1, class T2, class D2>\n  bool\n  operator!=(const uniq_ptr<T1, D1> &x, const std::unique_ptr<T1, D1> &y) {\n    return x.get() != y.get();\n  }\n\n  template <class T, class D>\n  bool\n  operator==(const uniq_ptr<T, D> &x, std::nullptr_t) {\n    return !(bool) x;\n  }\n\n  template <class T, class D>\n  bool\n  operator!=(const uniq_ptr<T, D> &x, std::nullptr_t) {\n    return (bool) x;\n  }\n\n  template <class T, class D>\n  bool\n  operator==(std::nullptr_t, const uniq_ptr<T, D> &y) {\n    return !(bool) y;\n  }\n\n  template <class T, class D>\n  bool\n  operator!=(std::nullptr_t, const uniq_ptr<T, D> &y) {\n    return (bool) y;\n  }\n\n  template <class P>\n  using shared_t = std::shared_ptr<typename P::element_type>;\n\n  template <class P, class T>\n  shared_t<P>\n  make_shared(T *pointer) {\n    return shared_t<P>(reinterpret_cast<typename P::pointer>(pointer), typename P::deleter_type());\n  }\n\n  template <class T>\n  class wrap_ptr {\n  public:\n    using element_type = T;\n    using pointer = element_type *;\n    using const_pointer = element_type const *;\n    using reference = element_type &;\n    using const_reference = element_type const &;\n\n    wrap_ptr():\n        _own_ptr { false }, _p { nullptr } {}\n    wrap_ptr(pointer p):\n        _own_ptr { false }, _p { p } {}\n    wrap_ptr(std::unique_ptr<element_type> &&uniq_p):\n        _own_ptr { true }, _p { uniq_p.release() } {}\n    wrap_ptr(wrap_ptr &&other):\n        _own_ptr { other._own_ptr }, _p { other._p } {\n      other._own_ptr = false;\n    }\n\n    wrap_ptr &\n    operator=(wrap_ptr &&other) noexcept {\n      if (_own_ptr) {\n        delete _p;\n      }\n\n      _p = other._p;\n\n      _own_ptr = other._own_ptr;\n      other._own_ptr = false;\n\n      return *this;\n    }\n\n    template <class V>\n    wrap_ptr &\n    operator=(std::unique_ptr<V> &&uniq_ptr) {\n      static_assert(std::is_base_of_v<element_type, V>, \"element_type must be base class of V\");\n      _own_ptr = true;\n      _p = uniq_ptr.release();\n\n      return *this;\n    }\n\n    wrap_ptr &\n    operator=(pointer p) {\n      if (_own_ptr) {\n        delete _p;\n      }\n\n      _p = p;\n      _own_ptr = false;\n\n      return *this;\n    }\n\n    ~wrap_ptr() {\n      if (_own_ptr) {\n        delete _p;\n      }\n\n      _own_ptr = false;\n    }\n\n    const_reference\n    operator*() const {\n      return *_p;\n    }\n    reference\n    operator*() {\n      return *_p;\n    }\n    const_pointer\n    operator->() const {\n      return _p;\n    }\n    pointer\n    operator->() {\n      return _p;\n    }\n\n  private:\n    bool _own_ptr;\n    pointer _p;\n  };\n\n  template <class T>\n  constexpr bool is_pointer_v =\n    instantiation_of_v<std::unique_ptr, T> ||\n    instantiation_of_v<std::shared_ptr, T> ||\n    instantiation_of_v<uniq_ptr, T> ||\n    std::is_pointer_v<T>;\n\n  template <class T, class V = void>\n  struct __false_v;\n\n  template <class T>\n  struct __false_v<T, std::enable_if_t<instantiation_of_v<std::optional, T>>> {\n    static constexpr std::nullopt_t value = std::nullopt;\n  };\n\n  template <class T>\n  struct __false_v<T, std::enable_if_t<is_pointer_v<T>>> {\n    static constexpr std::nullptr_t value = nullptr;\n  };\n\n  template <class T>\n  struct __false_v<T, std::enable_if_t<std::is_same_v<T, bool>>> {\n    static constexpr bool value = false;\n  };\n\n  template <class T>\n  static constexpr auto false_v = __false_v<T>::value;\n\n  template <class T>\n  using optional_t = either_t<\n    (std::is_same_v<T, bool> || is_pointer_v<T>),\n    T, std::optional<T>>;\n\n  template <class T>\n  class buffer_t {\n  public:\n    buffer_t():\n        _els { 0 } {};\n    buffer_t(buffer_t &&o) noexcept:\n        _els { o._els }, _buf { std::move(o._buf) } {\n      o._els = 0;\n    }\n    buffer_t(const buffer_t &o):\n        _els { o._els }, _buf { std::make_unique<T[]>(_els) } {\n      std::copy(o.begin(), o.end(), begin());\n    }\n    buffer_t &\n    operator=(buffer_t &&o) noexcept {\n      std::swap(_els, o._els);\n      std::swap(_buf, o._buf);\n\n      return *this;\n    };\n\n    explicit buffer_t(size_t elements):\n        _els { elements }, _buf { std::make_unique<T[]>(elements) } {}\n    explicit buffer_t(size_t elements, const T &t):\n        _els { elements }, _buf { std::make_unique<T[]>(elements) } {\n      std::fill_n(_buf.get(), elements, t);\n    }\n\n    T &\n    operator[](size_t el) {\n      return _buf[el];\n    }\n\n    const T &\n    operator[](size_t el) const {\n      return _buf[el];\n    }\n\n    size_t\n    size() const {\n      return _els;\n    }\n\n    void\n    fake_resize(std::size_t els) {\n      _els = els;\n    }\n\n    T *\n    begin() {\n      return _buf.get();\n    }\n\n    const T *\n    begin() const {\n      return _buf.get();\n    }\n\n    T *\n    end() {\n      return _buf.get() + _els;\n    }\n\n    const T *\n    end() const {\n      return _buf.get() + _els;\n    }\n\n  private:\n    size_t _els;\n    std::unique_ptr<T[]> _buf;\n  };\n\n  template <class T>\n  T\n  either(std::optional<T> &&l, T &&r) {\n    if (l) {\n      return std::move(*l);\n    }\n\n    return std::forward<T>(r);\n  }\n\n  template <class ReturnType, class... Args>\n  struct Function {\n    typedef ReturnType (*type)(Args...);\n  };\n\n  template <class T, class ReturnType, typename Function<ReturnType, T>::type function>\n  struct Destroy {\n    typedef T pointer;\n\n    void\n    operator()(pointer p) {\n      function(p);\n    }\n  };\n\n  template <class T, typename Function<void, T *>::type function>\n  using safe_ptr = uniq_ptr<T, Destroy<T *, void, function>>;\n\n  // You cannot specialize an alias\n  template <class T, class ReturnType, typename Function<ReturnType, T *>::type function>\n  using safe_ptr_v2 = uniq_ptr<T, Destroy<T *, ReturnType, function>>;\n\n  template <class T>\n  void\n  c_free(T *p) {\n    free(p);\n  }\n\n  template <class T, class ReturnType, ReturnType (**function)(T *)>\n  void\n  dynamic(T *p) {\n    (*function)(p);\n  }\n\n  template <class T, void (**function)(T *)>\n  using dyn_safe_ptr = safe_ptr<T, dynamic<T, void, function>>;\n\n  template <class T, class ReturnType, ReturnType (**function)(T *)>\n  using dyn_safe_ptr_v2 = safe_ptr<T, dynamic<T, ReturnType, function>>;\n\n  template <class T>\n  using c_ptr = safe_ptr<T, c_free<T>>;\n\n  template <class It>\n  std::string_view\n  view(It begin, It end) {\n    return std::string_view { (const char *) begin, (std::size_t)(end - begin) };\n  }\n\n  template <class T>\n  std::string_view\n  view(const T &data) {\n    return std::string_view((const char *) &data, sizeof(T));\n  }\n\n  struct point_t {\n    double x;\n    double y;\n\n    friend std::ostream &\n    operator<<(std::ostream &os, const point_t &p) {\n      return (os << \"Point(x: \" << p.x << \", y: \" << p.y << \")\");\n    }\n  };\n\n  namespace endian {\n    template <class T = void>\n    struct endianness {\n      enum : bool {\n#if defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN || \\\n  defined(__BIG_ENDIAN__) ||                                 \\\n  defined(__ARMEB__) ||                                      \\\n  defined(__THUMBEB__) ||                                    \\\n  defined(__AARCH64EB__) ||                                  \\\n  defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__)\n        // It's a big-endian target architecture\n        little = false,\n#elif defined(__BYTE_ORDER) && __BYTE_ORDER == __LITTLE_ENDIAN || \\\n  defined(__LITTLE_ENDIAN__) ||                                   \\\n  defined(__ARMEL__) ||                                           \\\n  defined(__THUMBEL__) ||                                         \\\n  defined(__AARCH64EL__) ||                                       \\\n  defined(_MIPSEL) || defined(__MIPSEL) || defined(__MIPSEL__) || \\\n  defined(_WIN32)\n        little = true,  ///< little-endian target architecture\n#else\n  #error \"Unknown Endianness\"\n#endif\n        big = !little  ///< big-endian target architecture\n      };\n    };\n\n    template <class T, class S = void>\n    struct endian_helper {};\n\n    template <class T>\n    struct endian_helper<T, std::enable_if_t<\n                              !(instantiation_of_v<std::optional, T>)>> {\n      static inline T\n      big(T x) {\n        if constexpr (endianness<T>::little) {\n          uint8_t *data = reinterpret_cast<uint8_t *>(&x);\n\n          std::reverse(data, data + sizeof(x));\n        }\n\n        return x;\n      }\n\n      static inline T\n      little(T x) {\n        if constexpr (endianness<T>::big) {\n          uint8_t *data = reinterpret_cast<uint8_t *>(&x);\n\n          std::reverse(data, data + sizeof(x));\n        }\n\n        return x;\n      }\n    };\n\n    template <class T>\n    struct endian_helper<T, std::enable_if_t<\n                              instantiation_of_v<std::optional, T>>> {\n      static inline T\n      little(T x) {\n        if (!x) return x;\n\n        if constexpr (endianness<T>::big) {\n          auto *data = reinterpret_cast<uint8_t *>(&*x);\n\n          std::reverse(data, data + sizeof(*x));\n        }\n\n        return x;\n      }\n\n      static inline T\n      big(T x) {\n        if (!x) return x;\n\n        if constexpr (endianness<T>::little) {\n          auto *data = reinterpret_cast<uint8_t *>(&*x);\n\n          std::reverse(data, data + sizeof(*x));\n        }\n\n        return x;\n      }\n    };\n\n    template <class T>\n    inline auto\n    little(T x) { return endian_helper<T>::little(x); }\n\n    template <class T>\n    inline auto\n    big(T x) { return endian_helper<T>::big(x); }\n  }  // namespace endian\n}  // namespace util\n"
  },
  {
    "path": "src/uuid.h",
    "content": "/**\n * @file src/uuid.h\n * @brief Declarations for UUID generation.\n */\n#pragma once\n\n#include <random>\n\n/**\n * @brief UUID utilities.\n */\nnamespace uuid_util {\n  union uuid_t {\n    std::uint8_t b8[16];\n    std::uint16_t b16[8];\n    std::uint32_t b32[4];\n    std::uint64_t b64[2];\n\n    static uuid_t\n    generate(std::default_random_engine &engine) {\n      std::uniform_int_distribution<std::uint8_t> dist(0, std::numeric_limits<std::uint8_t>::max());\n\n      uuid_t buf;\n      for (auto &el : buf.b8) {\n        el = dist(engine);\n      }\n\n      buf.b8[7] &= (std::uint8_t) 0b00101111;\n      buf.b8[9] &= (std::uint8_t) 0b10011111;\n\n      return buf;\n    }\n\n    static uuid_t\n    generate() {\n      std::random_device r;\n\n      std::default_random_engine engine { r() };\n\n      return generate(engine);\n    }\n\n    [[nodiscard]] std::string\n    string() const {\n      std::string result;\n\n      result.reserve(sizeof(uuid_t) * 2 + 4);\n\n      auto hex = util::hex(*this, true);\n      auto hex_view = hex.to_string_view();\n\n      std::string_view slices[] = {\n        hex_view.substr(0, 8),\n        hex_view.substr(8, 4),\n        hex_view.substr(12, 4),\n        hex_view.substr(16, 4)\n      };\n      auto last_slice = hex_view.substr(20, 12);\n\n      for (auto &slice : slices) {\n        std::copy(std::begin(slice), std::end(slice), std::back_inserter(result));\n\n        result.push_back('-');\n      }\n\n      std::copy(std::begin(last_slice), std::end(last_slice), std::back_inserter(result));\n\n      return result;\n    }\n\n    constexpr bool\n    operator==(const uuid_t &other) const {\n      return b64[0] == other.b64[0] && b64[1] == other.b64[1];\n    }\n\n    constexpr bool\n    operator<(const uuid_t &other) const {\n      return (b64[0] < other.b64[0] || (b64[0] == other.b64[0] && b64[1] < other.b64[1]));\n    }\n\n    constexpr bool\n    operator>(const uuid_t &other) const {\n      return (b64[0] > other.b64[0] || (b64[0] == other.b64[0] && b64[1] > other.b64[1]));\n    }\n  };\n}  // namespace uuid_util\n"
  },
  {
    "path": "src/version.h.in",
    "content": "/**\n * @file src/version.h.in\n * @brief Version definitions for Sunshine.\n * @note The final `version.h` is generated from this file during the CMake build.\n * @todo Use CMake definitions directly, instead of configuring this file.\n */\n#pragma once\n\n#define PROJECT_NAME \"@PROJECT_NAME@\"\n#define PROJECT_VER  \"@PROJECT_VERSION@\"\n#define PROJECT_VER_MAJOR \"@PROJECT_VERSION_MAJOR@\"\n#define PROJECT_VER_MINOR \"@PROJECT_VERSION_MINOR@\"\n#define PROJECT_VER_PATCH \"@PROJECT_VERSION_PATCH@\"\n"
  },
  {
    "path": "src/video.cpp",
    "content": "/**\n * @file src/video.cpp\n * @brief Definitions for video.\n */\n// standard includes\n#include <algorithm>\n#include <atomic>\n#include <bitset>\n#include <functional>\n#include <list>\n#include <thread>\n\n#include <boost/pointer_cast.hpp>\n\nextern \"C\" {\n#include <libavutil/hdr_dynamic_metadata.h>\n#include <libavutil/hdr_dynamic_vivid_metadata.h>\n#include <libavutil/imgutils.h>\n#include <libavutil/mastering_display_metadata.h>\n#include <libavutil/opt.h>\n#include <libavutil/pixdesc.h>\n}\n\n// AMF SDK headers for direct encoder access (Windows only)\n#ifdef _WIN32\n  #include <AMF/components/Component.h>\n  #include <AMF/components/VideoEncoderAV1.h>\n  #include <AMF/components/VideoEncoderHEVC.h>\n  #include <AMF/components/VideoEncoderVCE.h>\n  #include <AMF/core/Interface.h>\n  #include <AMF/core/PropertyStorage.h>\n  #include <cstring>  // for strstr\n\n// Forward declaration of FFmpeg's internal AMFEncoderContext structure\n// This structure layout must match FFmpeg's libavcodec/amfenc.h\n// We only need the first few fields to access the encoder pointer\nstruct AMFEncoderContext_Partial {\n  void *avclass;  // AVClass pointer\n  void *device_ctx_ref;  // AVBufferRef pointer\n  amf::AMFComponent *encoder;  // AMF encoder object\n};\n#endif\n\n// lib includes\n#include \"cbs.h\"\n#include \"config.h\"\n#include \"display_device/display_device.h\"\n#include \"globals.h\"\n#include \"input.h\"\n#include \"logging.h\"\n#include \"nvenc/nvenc_encoder.h\"\n#include \"amf/amf_encoder.h\"\n#include \"platform/common.h\"\n#include \"sync.h\"\n#include \"video.h\"\n\n#ifdef _WIN32\nextern \"C\" {\n  #include <libavutil/hwcontext_d3d11va.h>\n}\n  #include \"platform/windows/display_device/windows_utils.h\"\n#endif\n\nusing namespace std::literals;\nnamespace video {\n\n  namespace {\n    /**\n     * @brief Check if we can allow probing for the encoders.\n     * @return True if there should be no issues with the probing, false if we should prevent it.\n     */\n    bool\n    allow_encoder_probing() {\n      const auto devices { display_device::enum_available_devices() };\n\n      // If there are no devices, then either the API is not working correctly or OS does not support the lib.\n      // Either way we should not block the probing in this case as we can't tell what's wrong.\n      if (devices.empty()) {\n        return true;\n      }\n\n      // Since Windows 11 24H2, it is possible that there will be no active devices present\n      // for some reason (probably a bug). Trying to probe encoders in such a state locks/breaks the DXGI\n      // and also the display device for Windows. So we must have at least 1 active device.\n      const bool at_least_one_device_is_active = std::any_of(std::begin(devices), std::end(devices), [](const auto &device) {\n        // If device has additional info, it is active.\n        return device.second.device_state == display_device::device_state_e::active ||\n               device.second.device_state == display_device::device_state_e::primary;\n      });\n\n      if (at_least_one_device_is_active) {\n        return true;\n      }\n\n      BOOST_LOG(error) << \"No display devices are active at the moment! Cannot probe the encoders.\";\n      return false;\n    }\n  }  // namespace\n\n  void\n  free_ctx(AVCodecContext *ctx) {\n    avcodec_free_context(&ctx);\n  }\n\n  void\n  free_frame(AVFrame *frame) {\n    av_frame_free(&frame);\n  }\n\n  void\n  free_buffer(AVBufferRef *ref) {\n    av_buffer_unref(&ref);\n  }\n\n  namespace nv {\n\n    enum class profile_h264_e : int {\n      high = 2,  ///< High profile\n      high_444p = 3,  ///< High 4:4:4 Predictive profile\n    };\n\n    enum class profile_hevc_e : int {\n      main = 0,  ///< Main profile\n      main_10 = 1,  ///< Main 10 profile\n      rext = 2,  ///< Rext profile\n    };\n\n  }  // namespace nv\n\n  namespace qsv {\n\n    enum class profile_h264_e : int {\n      high = 100,  ///< High profile\n      high_444p = 244,  ///< High 4:4:4 Predictive profile\n    };\n\n    enum class profile_hevc_e : int {\n      main = 1,  ///< Main profile\n      main_10 = 2,  ///< Main 10 profile\n      rext = 4,  ///< RExt profile\n    };\n\n    enum class profile_av1_e : int {\n      main = 1,  ///< Main profile\n      high = 2,  ///< High profile\n    };\n\n  }  // namespace qsv\n\n  util::Either<avcodec_buffer_t, int>\n  dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);\n  util::Either<avcodec_buffer_t, int>\n  vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);\n  util::Either<avcodec_buffer_t, int>\n  cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);\n  util::Either<avcodec_buffer_t, int>\n  vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);\n  util::Either<avcodec_buffer_t, int>\n  vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);\n\n  class avcodec_software_encode_device_t: public platf::avcodec_encode_device_t {\n  public:\n    int\n    convert(platf::img_t &img) override {\n      // If we need to add aspect ratio padding, we need to scale into an intermediate output buffer\n      bool requires_padding = (sw_frame->width != sws_output_frame->width || sw_frame->height != sws_output_frame->height);\n\n      // Setup the input frame using the caller's img_t\n      sws_input_frame->data[0] = img.data;\n      sws_input_frame->linesize[0] = img.row_pitch;\n\n      // Perform color conversion and scaling to the final size\n      auto status = sws_scale_frame(sws.get(), requires_padding ? sws_output_frame.get() : sw_frame.get(), sws_input_frame.get());\n      if (status < 0) {\n        char string[AV_ERROR_MAX_STRING_SIZE];\n        BOOST_LOG(error) << \"Couldn't scale frame: \"sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);\n        return -1;\n      }\n\n      // If we require aspect ratio padding, copy the output frame into the final padded frame\n      if (requires_padding) {\n        auto fmt_desc = av_pix_fmt_desc_get((AVPixelFormat) sws_output_frame->format);\n        auto planes = av_pix_fmt_count_planes((AVPixelFormat) sws_output_frame->format);\n        for (int plane = 0; plane < planes; plane++) {\n          auto shift_h = plane == 0 ? 0 : fmt_desc->log2_chroma_h;\n          auto shift_w = plane == 0 ? 0 : fmt_desc->log2_chroma_w;\n          auto offset = ((offsetW >> shift_w) * fmt_desc->comp[plane].step) + (offsetH >> shift_h) * sw_frame->linesize[plane];\n\n          // Copy line-by-line to preserve leading padding for each row\n          for (int line = 0; line < sws_output_frame->height >> shift_h; line++) {\n            memcpy(sw_frame->data[plane] + offset + (line * sw_frame->linesize[plane]),\n              sws_output_frame->data[plane] + (line * sws_output_frame->linesize[plane]),\n              (size_t) (sws_output_frame->width >> shift_w) * fmt_desc->comp[plane].step);\n          }\n        }\n      }\n\n      // If frame is not a software frame, it means we still need to transfer from main memory\n      // to vram memory\n      if (frame->hw_frames_ctx) {\n        auto status = av_hwframe_transfer_data(frame, sw_frame.get(), 0);\n        if (status < 0) {\n          char string[AV_ERROR_MAX_STRING_SIZE];\n          BOOST_LOG(error) << \"Failed to transfer image data to hardware frame: \"sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);\n          return -1;\n        }\n      }\n\n      return 0;\n    }\n\n    int\n    set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {\n      this->frame = frame;\n\n      // If it's a hwframe, allocate buffers for hardware\n      if (hw_frames_ctx) {\n        hw_frame.reset(frame);\n\n        if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) return -1;\n      }\n      else {\n        sw_frame.reset(frame);\n      }\n\n      return 0;\n    }\n\n    void\n    apply_colorspace() override {\n      auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace);\n      sws_setColorspaceDetails(sws.get(),\n        sws_getCoefficients(SWS_CS_DEFAULT), 0,\n        sws_getCoefficients(avcodec_colorspace.software_format), avcodec_colorspace.range - 1,\n        0, 1 << 16, 1 << 16);\n    }\n\n    /**\n     * When preserving aspect ratio, ensure that padding is black\n     */\n    void\n    prefill() {\n      auto frame = sw_frame ? sw_frame.get() : this->frame;\n      av_frame_get_buffer(frame, 0);\n      av_frame_make_writable(frame);\n      ptrdiff_t linesize[4] = { frame->linesize[0], frame->linesize[1], frame->linesize[2], frame->linesize[3] };\n      av_image_fill_black(frame->data, linesize, (AVPixelFormat) frame->format, frame->color_range, frame->width, frame->height);\n    }\n\n    int\n    init(int in_width, int in_height, AVFrame *frame, AVPixelFormat format, bool hardware) {\n      // If the device used is hardware, yet the image resides on main memory\n      if (hardware) {\n        sw_frame.reset(av_frame_alloc());\n\n        sw_frame->width = frame->width;\n        sw_frame->height = frame->height;\n        sw_frame->format = format;\n      }\n      else {\n        this->frame = frame;\n      }\n\n      // Fill aspect ratio padding in the destination frame\n      prefill();\n\n      auto out_width = frame->width;\n      auto out_height = frame->height;\n\n      // Ensure aspect ratio is maintained\n      auto scalar = std::fminf((float) out_width / in_width, (float) out_height / in_height);\n      out_width = in_width * scalar;\n      out_height = in_height * scalar;\n\n      sws_input_frame.reset(av_frame_alloc());\n      sws_input_frame->width = in_width;\n      sws_input_frame->height = in_height;\n      sws_input_frame->format = AV_PIX_FMT_BGR0;\n\n      sws_output_frame.reset(av_frame_alloc());\n      sws_output_frame->width = out_width;\n      sws_output_frame->height = out_height;\n      sws_output_frame->format = format;\n\n      // Result is always positive\n      offsetW = (frame->width - out_width) / 2;\n      offsetH = (frame->height - out_height) / 2;\n\n      sws.reset(sws_alloc_context());\n      if (!sws) {\n        return -1;\n      }\n\n      AVDictionary *options { nullptr };\n      av_dict_set_int(&options, \"srcw\", sws_input_frame->width, 0);\n      av_dict_set_int(&options, \"srch\", sws_input_frame->height, 0);\n      av_dict_set_int(&options, \"src_format\", sws_input_frame->format, 0);\n      av_dict_set_int(&options, \"dstw\", sws_output_frame->width, 0);\n      av_dict_set_int(&options, \"dsth\", sws_output_frame->height, 0);\n      av_dict_set_int(&options, \"dst_format\", sws_output_frame->format, 0);\n      av_dict_set_int(&options, \"sws_flags\", SWS_LANCZOS | SWS_ACCURATE_RND, 0);\n      av_dict_set_int(&options, \"threads\", config::video.min_threads, 0);\n\n      auto status = av_opt_set_dict(sws.get(), &options);\n      av_dict_free(&options);\n      if (status < 0) {\n        char string[AV_ERROR_MAX_STRING_SIZE];\n        BOOST_LOG(error) << \"Failed to set SWS options: \"sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);\n        return -1;\n      }\n\n      status = sws_init_context(sws.get(), nullptr, nullptr);\n      if (status < 0) {\n        char string[AV_ERROR_MAX_STRING_SIZE];\n        BOOST_LOG(error) << \"Failed to initialize SWS: \"sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);\n        return -1;\n      }\n\n      return 0;\n    }\n\n    // Store ownership when frame is hw_frame\n    avcodec_frame_t hw_frame;\n\n    avcodec_frame_t sw_frame;\n    avcodec_frame_t sws_input_frame;\n    avcodec_frame_t sws_output_frame;\n    sws_t sws;\n\n    // Offset of input image to output frame in pixels\n    int offsetW;\n    int offsetH;\n  };\n\n  enum flag_e : uint32_t {\n    DEFAULT = 0,  ///< Default flags\n    PARALLEL_ENCODING = 1 << 1,  ///< Capture and encoding can run concurrently on separate threads\n    H264_ONLY = 1 << 2,  ///< When HEVC is too heavy\n    LIMITED_GOP_SIZE = 1 << 3,  ///< Some encoders don't like it when you have an infinite GOP_SIZE. e.g. VAAPI\n    SINGLE_SLICE_ONLY = 1 << 4,  ///< Never use multiple slices. Older intel iGPU's ruin it for everyone else\n    CBR_WITH_VBR = 1 << 5,  ///< Use a VBR rate control mode to simulate CBR\n    RELAXED_COMPLIANCE = 1 << 6,  ///< Use FF_COMPLIANCE_UNOFFICIAL compliance mode\n    NO_RC_BUF_LIMIT = 1 << 7,  ///< Don't set rc_buffer_size\n    REF_FRAMES_INVALIDATION = 1 << 8,  ///< Support reference frames invalidation\n    ALWAYS_REPROBE = 1 << 9,  ///< This is an encoder of last resort and we want to aggressively probe for a better one\n    YUV444_SUPPORT = 1 << 10,  ///< Encoder may support 4:4:4 chroma sampling depending on hardware\n    ASYNC_TEARDOWN = 1 << 11,  ///< Encoder supports async teardown on a different thread\n  };\n\n  class avcodec_encode_session_t: public encode_session_t {\n  public:\n    avcodec_encode_session_t() = default;\n    avcodec_encode_session_t(avcodec_ctx_t &&avcodec_ctx, std::unique_ptr<platf::avcodec_encode_device_t> encode_device, int inject):\n        avcodec_ctx { std::move(avcodec_ctx) }, device { std::move(encode_device) }, inject { inject } {}\n\n    avcodec_encode_session_t(avcodec_encode_session_t &&other) noexcept = default;\n    ~avcodec_encode_session_t() {\n      // Flush any remaining frames in the encoder\n      if (avcodec_send_frame(avcodec_ctx.get(), nullptr) == 0) {\n        packet_raw_avcodec pkt;\n        while (avcodec_receive_packet(avcodec_ctx.get(), pkt.av_packet) == 0);\n      }\n\n      // Order matters here because the context relies on the hwdevice still being valid\n      avcodec_ctx.reset();\n      device.reset();\n    }\n\n    // Ensure objects are destroyed in the correct order\n    avcodec_encode_session_t &\n    operator=(avcodec_encode_session_t &&other) {\n      device = std::move(other.device);\n      avcodec_ctx = std::move(other.avcodec_ctx);\n      replacements = std::move(other.replacements);\n      sps = std::move(other.sps);\n      vps = std::move(other.vps);\n\n      inject = other.inject;\n\n      return *this;\n    }\n\n    int\n    convert(platf::img_t &img) override {\n      if (!device) return -1;\n      return device->convert(img);\n    }\n\n    void\n    request_idr_frame() override {\n      if (device && device->frame) {\n        auto &frame = device->frame;\n        frame->pict_type = AV_PICTURE_TYPE_I;\n        frame->flags |= AV_FRAME_FLAG_KEY;\n      }\n    }\n\n    void\n    request_normal_frame() override {\n      if (device && device->frame) {\n        auto &frame = device->frame;\n        frame->pict_type = AV_PICTURE_TYPE_NONE;\n        frame->flags &= ~AV_FRAME_FLAG_KEY;\n      }\n    }\n\n    void\n    invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override {\n      BOOST_LOG(error) << \"Encoder doesn't support reference frame invalidation\";\n      request_idr_frame();\n    }\n\n    void\n    set_bitrate(int bitrate_kbps) override {\n      if (!avcodec_ctx) return;\n\n      // Adjust encoding bitrate considering FEC overhead\n      // When FEC percentage is X%, actual encoding bitrate should be (100-X)% of requested\n      auto adjusted_bitrate_kbps = bitrate_kbps;\n      if (config::stream.fec_percentage > 0 && config::stream.fec_percentage <= 80) {\n        adjusted_bitrate_kbps = bitrate_kbps * (100 - config::stream.fec_percentage) / 100;\n      }\n\n      auto bitrate = static_cast<int64_t>(adjusted_bitrate_kbps) * 1000;  // Convert to bps\n\n      // Update AVCodecContext fields (for software encoders and as fallback)\n      avcodec_ctx->bit_rate = bitrate;\n      avcodec_ctx->rc_max_rate = bitrate;\n      avcodec_ctx->rc_min_rate = bitrate;\n\n#ifdef _WIN32\n      // For AMF encoders, directly call AMF SDK to change bitrate dynamically\n      // AMF_VIDEO_ENCODER_TARGET_BITRATE, AMF_VIDEO_ENCODER_PEAK_BITRATE, and\n      // AMF_VIDEO_ENCODER_VBV_BUFFER_SIZE are documented as \"Dynamic properties -\n      // can be set at any time\" in AMF SDK\n      const AVCodec *codec = avcodec_ctx->codec;\n      if (codec && codec->name && avcodec_ctx->priv_data && strstr(codec->name, \"_amf\")) {\n        auto *amf_ctx = reinterpret_cast<AMFEncoderContext_Partial *>(avcodec_ctx->priv_data);\n        if (amf_ctx && amf_ctx->encoder) {\n          // VBV buffer size: 1 second worth of data at the target bitrate\n          int64_t vbv_buffer_size = bitrate;\n          AMF_RESULT res = AMF_OK;\n\n          // Set properties based on codec type\n          if (strstr(codec->name, \"h264_amf\")) {\n            res = amf_ctx->encoder->SetProperty(AMF_VIDEO_ENCODER_VBV_BUFFER_SIZE, vbv_buffer_size);\n            if (res == AMF_OK) res = amf_ctx->encoder->SetProperty(AMF_VIDEO_ENCODER_TARGET_BITRATE, bitrate);\n            if (res == AMF_OK) res = amf_ctx->encoder->SetProperty(AMF_VIDEO_ENCODER_PEAK_BITRATE, bitrate);\n          }\n          else if (strstr(codec->name, \"hevc_amf\")) {\n            res = amf_ctx->encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_VBV_BUFFER_SIZE, vbv_buffer_size);\n            if (res == AMF_OK) res = amf_ctx->encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_TARGET_BITRATE, bitrate);\n            if (res == AMF_OK) res = amf_ctx->encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_PEAK_BITRATE, bitrate);\n          }\n          else if (strstr(codec->name, \"av1_amf\")) {\n            res = amf_ctx->encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_VBV_BUFFER_SIZE, vbv_buffer_size);\n            if (res == AMF_OK) res = amf_ctx->encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_TARGET_BITRATE, bitrate);\n            if (res == AMF_OK) res = amf_ctx->encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_PEAK_BITRATE, bitrate);\n          }\n\n          if (res == AMF_OK) {\n            BOOST_LOG(info) << \"AMF encoder bitrate dynamically changed to: \" << adjusted_bitrate_kbps\n                            << \" Kbps (requested: \" << bitrate_kbps << \" Kbps, FEC: \"\n                            << config::stream.fec_percentage << \"%)\";\n            return;\n          }\n          BOOST_LOG(warning) << \"AMF SetProperty for bitrate failed with error: \" << res;\n        }\n      }\n#endif\n\n      BOOST_LOG(info) << \"AVCodec encoder bitrate set to: \" << adjusted_bitrate_kbps\n                      << \" Kbps (requested: \" << bitrate_kbps << \" Kbps, FEC: \"\n                      << config::stream.fec_percentage << \"%)\";\n    }\n\n    void\n    set_dynamic_param(const dynamic_param_t &param) override {\n      if (!avcodec_ctx) return;\n\n      switch (param.type) {\n        case dynamic_param_type_e::RESOLUTION:\n          // 分辨率变更需要重新初始化编码器\n          BOOST_LOG(info) << \"AVCodec encoder: Resolution change requested (requires encoder reinitialization)\";\n          break;\n        case dynamic_param_type_e::FPS:\n          // FPS变更需要重新配置编码器\n          BOOST_LOG(info) << \"AVCodec encoder: FPS change requested: \" << param.value.float_value\n                          << \" fps (requires encoder reconfiguration)\";\n          break;\n        case dynamic_param_type_e::BITRATE: {\n          // 码率调整通过set_bitrate处理\n          set_bitrate(param.value.int_value);\n          break;\n        }\n        case dynamic_param_type_e::QP: {\n          // 设置量化参数\n          if (param.value.int_value >= 0 && param.value.int_value <= 51) {\n            avcodec_ctx->qmin = param.value.int_value;\n            avcodec_ctx->qmax = param.value.int_value;\n            BOOST_LOG(info) << \"AVCodec encoder QP changed to: \" << param.value.int_value;\n          }\n          else {\n            BOOST_LOG(warning) << \"Invalid QP value: \" << param.value.int_value << \" (must be 0-51)\";\n          }\n          break;\n        }\n        case dynamic_param_type_e::VBV_BUFFER_SIZE: {\n          // 设置VBV缓冲区大小\n          if (param.value.int_value > 0) {\n            avcodec_ctx->rc_buffer_size = param.value.int_value * 1000;  // 转换为bps\n            BOOST_LOG(info) << \"AVCodec encoder VBV buffer size changed to: \" << param.value.int_value << \" Kbps\";\n          }\n          break;\n        }\n        default:\n          BOOST_LOG(warning) << \"AVCodec encoder: Unsupported dynamic parameter type: \" << (int) param.type;\n          break;\n      }\n    }\n\n    avcodec_ctx_t avcodec_ctx;\n    std::unique_ptr<platf::avcodec_encode_device_t> device;\n\n    std::vector<packet_raw_t::replace_t> replacements;\n\n    cbs::nal_t sps;\n    cbs::nal_t vps;\n\n    // inject sps/vps data into idr pictures\n    int inject;\n  };\n\n  class nvenc_encode_session_t: public encode_session_t {\n  public:\n    nvenc_encode_session_t(std::unique_ptr<platf::nvenc_encode_device_t> encode_device):\n        device(std::move(encode_device)) {\n    }\n\n    int\n    convert(platf::img_t &img) override {\n      if (!device) return -1;\n      return device->convert(img);\n    }\n\n    void\n    request_idr_frame() override {\n      force_idr = true;\n    }\n\n    void\n    request_normal_frame() override {\n      force_idr = false;\n    }\n\n    void\n    invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override {\n      if (!device || !device->nvenc) return;\n\n      if (!device->nvenc->invalidate_ref_frames(first_frame, last_frame)) {\n        force_idr = true;\n      }\n    }\n\n    void\n    set_bitrate(int bitrate_kbps) override {\n      if (device && device->nvenc) {\n        // 考虑FEC影响，调整编码码率\n        // 当FEC百分比为X%时，实际编码码率需要调整为原始码率的(100-X)%\n        auto adjusted_bitrate_kbps = bitrate_kbps;\n        if (config::stream.fec_percentage <= 80) {\n          adjusted_bitrate_kbps = (int) (bitrate_kbps * (100 - config::stream.fec_percentage) / 100.0f);\n        }\n\n        device->nvenc->set_bitrate(adjusted_bitrate_kbps);\n        BOOST_LOG(info) << \"NVENC encoder bitrate changed to: \" << adjusted_bitrate_kbps\n                        << \" Kbps (requested: \" << bitrate_kbps << \" Kbps, FEC: \"\n                        << config::stream.fec_percentage << \"%)\";\n      }\n    }\n\n    void\n    set_dynamic_param(const dynamic_param_t &param) override {\n      if (!device || !device->nvenc) return;\n\n      switch (param.type) {\n        case dynamic_param_type_e::RESOLUTION:\n          // 分辨率变更需要重新初始化编码器，这里只记录日志\n          BOOST_LOG(info) << \"NVENC encoder: Resolution change requested (requires encoder reinitialization)\";\n          break;\n        case dynamic_param_type_e::FPS:\n          // FPS变更需要重新配置编码器\n          BOOST_LOG(info) << \"NVENC encoder: FPS change requested: \" << param.value.float_value\n                          << \" fps (requires encoder reconfiguration)\";\n          break;\n        case dynamic_param_type_e::BITRATE: {\n          // 码率调整通过set_bitrate处理\n          set_bitrate(param.value.int_value);\n          break;\n        }\n        case dynamic_param_type_e::QP: {\n          // NVENC的QP调整需要通过重新配置编码器\n          BOOST_LOG(info) << \"NVENC encoder QP change requested: \" << param.value.int_value\n                          << \" (requires encoder reconfiguration)\";\n          break;\n        }\n        case dynamic_param_type_e::ADAPTIVE_QUANTIZATION: {\n          // 自适应量化开关\n          BOOST_LOG(info) << \"NVENC encoder adaptive quantization change requested: \" << param.value.bool_value;\n          break;\n        }\n        case dynamic_param_type_e::MULTI_PASS: {\n          // 多遍编码设置\n          BOOST_LOG(info) << \"NVENC encoder multi-pass change requested: \" << param.value.int_value;\n          break;\n        }\n        case dynamic_param_type_e::VBV_BUFFER_SIZE: {\n          // VBV缓冲区大小\n          BOOST_LOG(info) << \"NVENC encoder VBV buffer size change requested: \" << param.value.int_value << \" Kbps\";\n          break;\n        }\n        default:\n          BOOST_LOG(warning) << \"NVENC encoder: Unsupported dynamic parameter type: \" << (int) param.type;\n          break;\n      }\n    }\n\n    nvenc::nvenc_encoded_frame\n    encode_frame(uint64_t frame_index) {\n      if (!device || !device->nvenc) return {};\n\n      // Pass per-frame HDR luminance stats to NVENC for dynamic metadata injection\n      if (device->hdr_luminance_stats.valid) {\n        device->nvenc->set_luminance_stats(device->hdr_luminance_stats);\n      }\n\n      auto result = device->nvenc->encode_frame(frame_index, force_idr);\n      force_idr = false;\n      return result;\n    }\n\n  private:\n    std::unique_ptr<platf::nvenc_encode_device_t> device;\n    bool force_idr = false;\n  };\n\n  class amf_encode_session_t: public encode_session_t {\n  public:\n    amf_encode_session_t(std::unique_ptr<platf::amf_encode_device_t> encode_device):\n        device(std::move(encode_device)) {\n    }\n\n    int\n    convert(platf::img_t &img) override {\n      if (!device) return -1;\n      return device->convert(img);\n    }\n\n    void\n    request_idr_frame() override {\n      force_idr = true;\n    }\n\n    void\n    request_normal_frame() override {\n      force_idr = false;\n    }\n\n    void\n    invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override {\n      if (!device || !device->amf) return;\n\n      if (!device->amf->invalidate_ref_frames(first_frame, last_frame)) {\n        force_idr = true;\n      }\n    }\n\n    void\n    set_bitrate(int bitrate_kbps) override {\n      if (device && device->amf) {\n        auto adjusted_bitrate_kbps = bitrate_kbps;\n        if (config::stream.fec_percentage <= 80) {\n          adjusted_bitrate_kbps = (int) (bitrate_kbps * (100 - config::stream.fec_percentage) / 100.0f);\n        }\n\n        device->amf->set_bitrate(adjusted_bitrate_kbps);\n        BOOST_LOG(info) << \"AMF standalone encoder bitrate changed to: \" << adjusted_bitrate_kbps\n                        << \" Kbps (requested: \" << bitrate_kbps << \" Kbps, FEC: \"\n                        << config::stream.fec_percentage << \"%)\";\n      }\n    }\n\n    void\n    set_dynamic_param(const dynamic_param_t &param) override {\n      if (!device || !device->amf) return;\n\n      switch (param.type) {\n        case dynamic_param_type_e::BITRATE:\n          set_bitrate(param.value.int_value);\n          break;\n        default:\n          break;\n      }\n    }\n\n    amf::amf_encoded_frame\n    encode_frame(uint64_t frame_index) {\n      if (!device || !device->amf) return {};\n\n      auto result = device->amf->encode_frame(frame_index, force_idr);\n      force_idr = false;\n      return result;\n    }\n\n  private:\n    std::unique_ptr<platf::amf_encode_device_t> device;\n    bool force_idr = false;\n  };\n\n  struct sync_session_ctx_t {\n    safe::signal_t *join_event;\n    safe::mail_raw_t::event_t<bool> shutdown_event;\n    safe::mail_raw_t::queue_t<packet_t> packets;\n    safe::mail_raw_t::event_t<bool> idr_events;\n    safe::mail_raw_t::event_t<hdr_info_t> hdr_events;\n    safe::mail_raw_t::event_t<input::touch_port_t> touch_port_events;\n\n    config_t config;\n    int frame_nr;\n    void *channel_data;\n  };\n\n  struct sync_session_t {\n    sync_session_ctx_t *ctx;\n    std::unique_ptr<encode_session_t> session;\n  };\n\n  using encode_session_ctx_queue_t = safe::queue_t<sync_session_ctx_t>;\n  using encode_e = platf::capture_e;\n\n  struct capture_ctx_t {\n    img_event_t images;\n    config_t config;\n  };\n\n  struct capture_thread_async_ctx_t {\n    std::shared_ptr<safe::queue_t<capture_ctx_t>> capture_ctx_queue;\n    std::thread capture_thread;\n\n    safe::signal_t reinit_event;\n    const encoder_t *encoder_p;\n    sync_util::sync_t<std::weak_ptr<platf::display_t>> display_wp;\n  };\n\n  struct capture_thread_sync_ctx_t {\n    encode_session_ctx_queue_t encode_session_ctx_queue { 30 };\n  };\n\n  int\n  start_capture_sync(capture_thread_sync_ctx_t &ctx);\n  void\n  end_capture_sync(capture_thread_sync_ctx_t &ctx);\n  int\n  start_capture_async(capture_thread_async_ctx_t &ctx);\n  void\n  end_capture_async(capture_thread_async_ctx_t &ctx);\n\n  // Keep a reference counter to ensure the capture thread only runs when other threads have a reference to the capture thread\n  auto capture_thread_async = safe::make_shared<capture_thread_async_ctx_t>(start_capture_async, end_capture_async);\n  auto capture_thread_sync = safe::make_shared<capture_thread_sync_ctx_t>(start_capture_sync, end_capture_sync);\n\n#ifdef _WIN32\n  encoder_t nvenc {\n    \"nvenc\"sv,\n    std::make_unique<encoder_platform_formats_nvenc>(\n      platf::mem_type_e::dxgi,\n      platf::pix_fmt_e::nv12, platf::pix_fmt_e::p010,\n      platf::pix_fmt_e::ayuv, platf::pix_fmt_e::yuv444p16),\n    {\n      {},  // Common options\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"av1_nvenc\"s,\n    },\n    {\n      {},  // Common options\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"hevc_nvenc\"s,\n    },\n    {\n      {},  // Common options\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"h264_nvenc\"s,\n    },\n    PARALLEL_ENCODING | REF_FRAMES_INVALIDATION | YUV444_SUPPORT | ASYNC_TEARDOWN  // flags\n  };\n#elif !defined(__APPLE__)\n  encoder_t nvenc {\n    \"nvenc\"sv,\n    std::make_unique<encoder_platform_formats_avcodec>(\n  #ifdef _WIN32\n      AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE,\n      AV_PIX_FMT_D3D11,\n  #else\n      AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_NONE,\n      AV_PIX_FMT_CUDA,\n  #endif\n      AV_PIX_FMT_NV12, AV_PIX_FMT_P010,\n      AV_PIX_FMT_NONE, AV_PIX_FMT_NONE,\n  #ifdef _WIN32\n      dxgi_init_avcodec_hardware_input_buffer\n  #else\n      cuda_init_avcodec_hardware_input_buffer\n  #endif\n      ),\n    {\n      // Common options\n      {\n        { \"delay\"s, 0 },\n        { \"forced-idr\"s, 1 },\n        { \"zerolatency\"s, 1 },\n        { \"surfaces\"s, 1 },\n        { \"cbr_padding\"s, false },\n        { \"preset\"s, &config::video.nv_legacy.preset },\n        { \"tune\"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY },\n        { \"rc\"s, NV_ENC_PARAMS_RC_CBR },\n        { \"multipass\"s, &config::video.nv_legacy.multipass },\n        { \"aq\"s, &config::video.nv_legacy.aq },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"av1_nvenc\"s,\n    },\n    {\n      // Common options\n      {\n        { \"delay\"s, 0 },\n        { \"forced-idr\"s, 1 },\n        { \"zerolatency\"s, 1 },\n        { \"surfaces\"s, 1 },\n        { \"cbr_padding\"s, false },\n        { \"preset\"s, &config::video.nv_legacy.preset },\n        { \"tune\"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY },\n        { \"rc\"s, NV_ENC_PARAMS_RC_CBR },\n        { \"multipass\"s, &config::video.nv_legacy.multipass },\n        { \"aq\"s, &config::video.nv_legacy.aq },\n      },\n      {\n        // SDR-specific options\n        { \"profile\"s, (int) nv::profile_hevc_e::main },\n      },\n      {\n        // HDR-specific options\n        { \"profile\"s, (int) nv::profile_hevc_e::main_10 },\n      },\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"hevc_nvenc\"s,\n    },\n    {\n      {\n        { \"delay\"s, 0 },\n        { \"forced-idr\"s, 1 },\n        { \"zerolatency\"s, 1 },\n        { \"surfaces\"s, 1 },\n        { \"cbr_padding\"s, false },\n        { \"preset\"s, &config::video.nv_legacy.preset },\n        { \"tune\"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY },\n        { \"rc\"s, NV_ENC_PARAMS_RC_CBR },\n        { \"coder\"s, &config::video.nv_legacy.h264_coder },\n        { \"multipass\"s, &config::video.nv_legacy.multipass },\n        { \"aq\"s, &config::video.nv_legacy.aq },\n      },\n      {\n        // SDR-specific options\n        { \"profile\"s, (int) nv::profile_h264_e::high },\n      },\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"h264_nvenc\"s,\n    },\n    PARALLEL_ENCODING\n  };\n#endif\n\n#ifdef _WIN32\n  encoder_t quicksync {\n    \"quicksync\"sv,\n    std::make_unique<encoder_platform_formats_avcodec>(\n      AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_QSV,\n      AV_PIX_FMT_QSV,\n      AV_PIX_FMT_NV12, AV_PIX_FMT_P010,\n      AV_PIX_FMT_VUYX, AV_PIX_FMT_XV30,\n      dxgi_init_avcodec_hardware_input_buffer),\n    {\n      // Common options\n      {\n        { \"preset\"s, &config::video.qsv.qsv_preset },\n        { \"forced_idr\"s, 1 },\n        { \"async_depth\"s, 1 },\n        { \"low_delay_brc\"s, 1 },\n        { \"low_power\"s, 1 },\n      },\n      {\n        // SDR-specific options\n        { \"profile\"s, (int) qsv::profile_av1_e::main },\n      },\n      {\n        // HDR-specific options\n        { \"profile\"s, (int) qsv::profile_av1_e::main },\n      },\n      {\n        // YUV444 SDR-specific options\n        { \"profile\"s, (int) qsv::profile_av1_e::high },\n      },\n      {\n        // YUV444 HDR-specific options\n        { \"profile\"s, (int) qsv::profile_av1_e::high },\n      },\n      {},  // Fallback options\n      \"av1_qsv\"s,\n    },\n    {\n      // Common options\n      {\n        { \"preset\"s, &config::video.qsv.qsv_preset },\n        { \"forced_idr\"s, 1 },\n        { \"async_depth\"s, 1 },\n        { \"low_delay_brc\"s, 1 },\n        { \"low_power\"s, 1 },\n        { \"recovery_point_sei\"s, 0 },\n        { \"pic_timing_sei\"s, 0 },\n      },\n      {\n        // SDR-specific options\n        { \"profile\"s, (int) qsv::profile_hevc_e::main },\n      },\n      {\n        // HDR-specific options\n        { \"profile\"s, (int) qsv::profile_hevc_e::main_10 },\n      },\n      {\n        // YUV444 SDR-specific options\n        { \"profile\"s, (int) qsv::profile_hevc_e::rext },\n      },\n      {\n        // YUV444 HDR-specific options\n        { \"profile\"s, (int) qsv::profile_hevc_e::rext },\n      },\n      {\n        // Fallback options\n        { \"low_power\"s, []() { return config::video.qsv.qsv_slow_hevc ? 0 : 1; } },\n      },\n      \"hevc_qsv\"s,\n    },\n    {\n      // Common options\n      {\n        { \"preset\"s, &config::video.qsv.qsv_preset },\n        { \"cavlc\"s, &config::video.qsv.qsv_cavlc },\n        { \"forced_idr\"s, 1 },\n        { \"async_depth\"s, 1 },\n        { \"low_delay_brc\"s, 1 },\n        { \"low_power\"s, 1 },\n        { \"recovery_point_sei\"s, 0 },\n        { \"vcm\"s, 1 },\n        { \"pic_timing_sei\"s, 0 },\n        { \"max_dec_frame_buffering\"s, 1 },\n      },\n      {\n        // SDR-specific options\n        { \"profile\"s, (int) qsv::profile_h264_e::high },\n      },\n      {},  // HDR-specific options\n      {\n        // YUV444 SDR-specific options\n        { \"profile\"s, (int) qsv::profile_h264_e::high_444p },\n      },\n      {},  // YUV444 HDR-specific options\n      {\n        // Fallback options\n        { \"low_power\"s, 0 },  // Some old/low-end Intel GPUs don't support low power encoding\n      },\n      \"h264_qsv\"s,\n    },\n    PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT | YUV444_SUPPORT\n  };\n\n  encoder_t amdvce {\n    \"amdvce\"sv,\n    std::make_unique<encoder_platform_formats_amf>(\n      platf::mem_type_e::dxgi,\n      platf::pix_fmt_e::nv12, platf::pix_fmt_e::p010,\n      platf::pix_fmt_e::unknown, platf::pix_fmt_e::unknown),\n    {\n      {},  // Common options (handled by AMF directly)\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"av1_amf\"s,\n    },\n    {\n      {},\n      {},\n      {},\n      {},\n      {},\n      {},\n      \"hevc_amf\"s,\n    },\n    {\n      {},\n      {},\n      {},\n      {},\n      {},\n      {},\n      \"h264_amf\"s,\n    },\n    PARALLEL_ENCODING | REF_FRAMES_INVALIDATION\n  };\n\n  // Legacy FFmpeg-based AMF encoder (fallback)\n  encoder_t amdvce_legacy {\n    \"amdvce_legacy\"sv,\n    std::make_unique<encoder_platform_formats_avcodec>(\n      AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE,\n      AV_PIX_FMT_D3D11,\n      AV_PIX_FMT_NV12, AV_PIX_FMT_P010,\n      AV_PIX_FMT_NONE, AV_PIX_FMT_NONE,\n      dxgi_init_avcodec_hardware_input_buffer),\n    {\n      // Common options\n      {\n        { \"filler_data\"s, false },\n        { \"forced_idr\"s, 1 },\n        { \"latency\"s, \"lowest_latency\"s },\n        { \"async_depth\"s, 1 },\n        { \"skip_frame\"s, 0 },\n        { \"log_to_dbg\"s, []() {\n           return config::sunshine.min_log_level < 2 ? 1 : 0;\n         } },\n        { \"preencode\"s, &config::video.amd.amd_preanalysis },\n        { \"quality\"s, &config::video.amd.amd_quality_av1 },\n        { \"rc\"s, &config::video.amd.amd_rc_av1 },\n        { \"usage\"s, &config::video.amd.amd_usage_av1 },\n        { \"enforce_hrd\"s, &config::video.amd.amd_enforce_hrd },\n        // AV1 optimization options (no latency impact)\n        { \"high_motion_quality_boost_enable\"s, true },\n        { \"pa_paq_mode\"s, \"caq\"s },\n        { \"pa_taq_mode\"s, 2 },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"av1_amf\"s,\n    },\n    {\n      // Common options\n      {\n        { \"filler_data\"s, false },\n        { \"forced_idr\"s, 1 },\n        { \"latency\"s, 1 },\n        { \"async_depth\"s, 1 },\n        { \"skip_frame\"s, 0 },\n        { \"log_to_dbg\"s, []() {\n           return config::sunshine.min_log_level < 2 ? 1 : 0;\n         } },\n        { \"gops_per_idr\"s, 1 },\n        { \"header_insertion_mode\"s, \"idr\"s },\n        { \"preencode\"s, &config::video.amd.amd_preanalysis },\n        { \"quality\"s, &config::video.amd.amd_quality_hevc },\n        { \"rc\"s, &config::video.amd.amd_rc_hevc },\n        { \"usage\"s, &config::video.amd.amd_usage_hevc },\n        { \"vbaq\"s, &config::video.amd.amd_vbaq },\n        { \"enforce_hrd\"s, &config::video.amd.amd_enforce_hrd },\n        { \"level\"s, [](const config_t &cfg) {\n           auto size = cfg.width * cfg.height;\n           // For 4K and below, try to use level 5.1 or 5.2 if possible\n           if (size <= 8912896) {\n             if (size * cfg.framerate <= 534773760) {\n               return \"5.1\"s;\n             }\n             else if (size * cfg.framerate <= 1069547520) {\n               return \"5.2\"s;\n             }\n           }\n           return \"auto\"s;\n         } },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"hevc_amf\"s,\n    },\n    {\n      // Common options\n      {\n        { \"filler_data\"s, false },\n        { \"forced_idr\"s, 1 },\n        { \"latency\"s, 1 },\n        { \"async_depth\"s, 1 },\n        { \"frame_skipping\"s, 0 },\n        { \"log_to_dbg\"s, []() {\n           return config::sunshine.min_log_level < 2 ? 1 : 0;\n         } },\n        { \"preencode\"s, &config::video.amd.amd_preanalysis },\n        { \"quality\"s, &config::video.amd.amd_quality_h264 },\n        { \"rc\"s, &config::video.amd.amd_rc_h264 },\n        { \"usage\"s, &config::video.amd.amd_usage_h264 },\n        { \"vbaq\"s, &config::video.amd.amd_vbaq },\n        { \"enforce_hrd\"s, &config::video.amd.amd_enforce_hrd },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {\n        // Fallback options\n        { \"usage\"s, 2 /* AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY */ },  // Workaround for https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/410\n      },\n      \"h264_amf\"s,\n    },\n    PARALLEL_ENCODING\n  };\n#endif\n\n  encoder_t software {\n    \"software\"sv,\n    std::make_unique<encoder_platform_formats_avcodec>(\n      AV_HWDEVICE_TYPE_NONE, AV_HWDEVICE_TYPE_NONE,\n      AV_PIX_FMT_NONE,\n      AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P10,\n      AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV444P10,\n      nullptr),\n    {\n      // libsvtav1 takes different presets than libx264/libx265.\n      // We set an infinite GOP length, use a low delay prediction structure,\n      // force I frames to be key frames, and set max bitrate to default to work\n      // around a FFmpeg bug with CBR mode.\n      {\n        { \"svtav1-params\"s, \"keyint=-1:pred-struct=1:force-key-frames=1:mbr=0\"s },\n        { \"preset\"s, &config::video.sw.svtav1_preset },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n\n#ifdef ENABLE_BROKEN_AV1_ENCODER\n           // Due to bugs preventing on-demand IDR frames from working and very poor\n           // real-time encoding performance, we do not enable libsvtav1 by default.\n           // It is only suitable for testing AV1 until the IDR frame issue is fixed.\n      \"libsvtav1\"s,\n#else\n      {},\n#endif\n    },\n    {\n      // x265's Info SEI is so long that it causes the IDR picture data to be\n      // kicked to the 2nd packet in the frame, breaking Moonlight's parsing logic.\n      // It also looks like gop_size isn't passed on to x265, so we have to set\n      // 'keyint=-1' in the parameters ourselves.\n      {\n        { \"forced-idr\"s, 1 },\n        { \"x265-params\"s, \"info=0:keyint=-1\"s },\n        { \"preset\"s, &config::video.sw.sw_preset },\n        { \"tune\"s, &config::video.sw.sw_tune },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"libx265\"s,\n    },\n    {\n      // Common options\n      {\n        { \"preset\"s, &config::video.sw.sw_preset },\n        { \"tune\"s, &config::video.sw.sw_tune },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"libx264\"s,\n    },\n    H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE | YUV444_SUPPORT\n  };\n\n#ifdef __linux__\n  encoder_t vaapi {\n    \"vaapi\"sv,\n    std::make_unique<encoder_platform_formats_avcodec>(\n      AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_NONE,\n      AV_PIX_FMT_VAAPI,\n      AV_PIX_FMT_NV12, AV_PIX_FMT_P010,\n      AV_PIX_FMT_NONE, AV_PIX_FMT_NONE,\n      vaapi_init_avcodec_hardware_input_buffer),\n    {\n      // Common options\n      {\n        { \"async_depth\"s, 1 },\n        { \"idr_interval\"s, std::numeric_limits<int>::max() },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"av1_vaapi\"s,\n    },\n    {\n      // Common options\n      {\n        { \"async_depth\"s, 1 },\n        { \"sei\"s, 0 },\n        { \"idr_interval\"s, std::numeric_limits<int>::max() },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"hevc_vaapi\"s,\n    },\n    {\n      // Common options\n      {\n        { \"async_depth\"s, 1 },\n        { \"sei\"s, 0 },\n        { \"idr_interval\"s, std::numeric_limits<int>::max() },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"h264_vaapi\"s,\n    },\n    // RC buffer size will be set in platform code if supported\n    LIMITED_GOP_SIZE | PARALLEL_ENCODING | NO_RC_BUF_LIMIT\n  };\n#endif\n\n#ifdef __APPLE__\n  encoder_t videotoolbox {\n    \"videotoolbox\"sv,\n    std::make_unique<encoder_platform_formats_avcodec>(\n      AV_HWDEVICE_TYPE_VIDEOTOOLBOX, AV_HWDEVICE_TYPE_NONE,\n      AV_PIX_FMT_VIDEOTOOLBOX,\n      AV_PIX_FMT_NV12, AV_PIX_FMT_P010,\n      AV_PIX_FMT_NONE, AV_PIX_FMT_NONE,\n      vt_init_avcodec_hardware_input_buffer),\n    {\n      // Common options\n      {\n        { \"allow_sw\"s, &config::video.vt.vt_allow_sw },\n        { \"require_sw\"s, &config::video.vt.vt_require_sw },\n        { \"realtime\"s, &config::video.vt.vt_realtime },\n        { \"prio_speed\"s, 1 },\n        { \"max_ref_frames\"s, 1 },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"av1_videotoolbox\"s,\n    },\n    {\n      // Common options\n      {\n        { \"allow_sw\"s, &config::video.vt.vt_allow_sw },\n        { \"require_sw\"s, &config::video.vt.vt_require_sw },\n        { \"realtime\"s, &config::video.vt.vt_realtime },\n        { \"prio_speed\"s, 1 },\n        { \"max_ref_frames\"s, 1 },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"hevc_videotoolbox\"s,\n    },\n    {\n      // Common options\n      {\n        { \"allow_sw\"s, &config::video.vt.vt_allow_sw },\n        { \"require_sw\"s, &config::video.vt.vt_require_sw },\n        { \"realtime\"s, &config::video.vt.vt_realtime },\n        { \"prio_speed\"s, 1 },\n        { \"max_ref_frames\"s, 1 },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {\n        // Fallback options\n        { \"flags\"s, \"-low_delay\" },\n      },\n      \"h264_videotoolbox\"s,\n    },\n    DEFAULT\n  };\n#endif\n\n  // Vulkan encoder - cross-platform (Windows and Linux)\n#if !defined(__APPLE__)\n  encoder_t vulkan {\n    \"vulkan\"sv,\n    std::make_unique<encoder_platform_formats_avcodec>(\n      AV_HWDEVICE_TYPE_VULKAN, AV_HWDEVICE_TYPE_NONE,\n      AV_PIX_FMT_VULKAN,\n      AV_PIX_FMT_NV12, AV_PIX_FMT_P010,\n      AV_PIX_FMT_NONE, AV_PIX_FMT_NONE,\n      vulkan_init_avcodec_hardware_input_buffer),\n    {\n      // Common options for AV1\n      {\n        { \"forced_idr\"s, 1 },\n        { \"async_depth\"s, 1 },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"av1_vulkan\"s,\n    },\n    {\n      // Common options for HEVC (if supported)\n      {\n        { \"forced_idr\"s, 1 },\n        { \"async_depth\"s, 1 },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"hevc_vulkan\"s,\n    },\n    {\n      // Common options for H.264 (if supported)\n      {\n        { \"forced_idr\"s, 1 },\n        { \"async_depth\"s, 1 },\n      },\n      {},  // SDR-specific options\n      {},  // HDR-specific options\n      {},  // YUV444 SDR-specific options\n      {},  // YUV444 HDR-specific options\n      {},  // Fallback options\n      \"h264_vulkan\"s,\n    },\n    PARALLEL_ENCODING\n  };\n#endif\n\n  static const std::vector<encoder_t *> encoders {\n#ifndef __APPLE__\n    &nvenc,\n    &vulkan,  // Vulkan encoder (cross-platform)\n#endif\n#ifdef _WIN32\n    &quicksync,\n    &amdvce,\n    &amdvce_legacy,\n#endif\n#ifdef __linux__\n    &vaapi,\n#endif\n#ifdef __APPLE__\n    &videotoolbox,\n#endif\n    &software\n  };\n\n  static encoder_t *chosen_encoder;\n  int active_hevc_mode;\n  int active_av1_mode;\n  bool last_encoder_probe_supported_ref_frames_invalidation = false;\n  std::array<bool, 3> last_encoder_probe_supported_yuv444_for_codec = {};\n\n  void\n  reset_display(std::shared_ptr<platf::display_t> &disp, const platf::mem_type_e &type, const std::string &display_name, const config_t &config) {\n    // We try this twice, in case we still get an error on reinitialization\n    for (int x = 0; x < 2; ++x) {\n      disp.reset();\n      disp = platf::display(type, display_name, config);\n      if (disp) {\n        BOOST_LOG(debug) << \"[reset_display] 成功重置显示器: \" << display_name;\n        break;\n      }\n      BOOST_LOG(debug) << \"[reset_display] 显示器创建失败 (尝试 \" << (x + 1) << \"/2): \" << display_name;\n      // The capture code depends on us to sleep between failures\n      std::this_thread::sleep_for(200ms);\n    }\n  }\n\n  /**\n   * @brief Update the list of display names before or during a stream.\n   * @details This will attempt to keep `current_display_index` pointing at the same display.\n   * @param dev_type The encoder device type used for display lookup.\n   * @param display_names The list of display names to repopulate.\n   * @param current_display_index The current display index or -1 if not yet known.\n   */\n  void\n  refresh_displays(platf::mem_type_e dev_type, std::vector<std::string> &display_names, int &current_display_index) {\n    // It is possible that the output display name may be empty even if it wasn't before (device disconnected)\n    const auto output_name { display_device::get_display_name(config::video.output_name) };\n    std::string current_display_name;\n\n    // If we have a current display index, let's start with that\n    if (current_display_index >= 0 && current_display_index < display_names.size()) {\n      current_display_name = display_names.at(current_display_index);\n    }\n\n    // Refresh the display names\n    auto old_display_names = std::move(display_names);\n    display_names = platf::display_names(dev_type);\n\n    // If we now have no displays, let's put the old display array back and fail\n    if (display_names.empty() && !old_display_names.empty()) {\n      BOOST_LOG(error) << \"No displays were found after reenumeration!\"sv;\n      display_names = std::move(old_display_names);\n      return;\n    }\n    else if (display_names.empty()) {\n      display_names.emplace_back(output_name);\n    }\n\n    // We now have a new display name list, so reset the index back to 0\n    current_display_index = 0;\n\n    // If we had a name previously, let's try to find it in the new list\n    if (!current_display_name.empty()) {\n      for (int x = 0; x < display_names.size(); ++x) {\n        if (display_names[x] == current_display_name) {\n          current_display_index = x;\n          return;\n        }\n      }\n\n      // The old display was removed, so we'll start back at the first display again\n      BOOST_LOG(warning) << \"Previous active display [\"sv << current_display_name << \"] is no longer present\"sv;\n    }\n    else {\n      for (int x = 0; x < display_names.size(); ++x) {\n        if (display_names[x] == output_name) {\n          current_display_index = x;\n          return;\n        }\n      }\n    }\n  }\n\n  void\n  captureThread(\n    std::shared_ptr<safe::queue_t<capture_ctx_t>> capture_ctx_queue,\n    sync_util::sync_t<std::weak_ptr<platf::display_t>> &display_wp,\n    safe::signal_t &reinit_event,\n    const encoder_t &encoder) {\n    std::vector<capture_ctx_t> capture_ctxs;\n\n    auto fg = util::fail_guard([&]() {\n      capture_ctx_queue->stop();\n\n      // Stop all sessions listening to this thread\n      for (auto &capture_ctx : capture_ctxs) {\n        capture_ctx.images->stop();\n      }\n      for (auto &capture_ctx : capture_ctx_queue->unsafe()) {\n        capture_ctx.images->stop();\n      }\n    });\n\n    auto switch_display_event = mail::man->event<int>(mail::switch_display);\n\n    // Wait for the initial capture context or a request to stop the queue\n    auto initial_capture_ctx = capture_ctx_queue->pop();\n    if (!initial_capture_ctx) {\n      return;\n    }\n    capture_ctxs.emplace_back(std::move(*initial_capture_ctx));\n\n    // Get all the monitor names now, rather than at boot, to\n    // get the most up-to-date list available monitors\n    std::vector<std::string> display_names;\n    int display_p = -1;\n    refresh_displays(encoder.platform_formats->dev_type, display_names, display_p);\n\n    // Use client-specified display_name if provided, otherwise use the selected display\n    std::string target_display_name;\n    const auto &config = capture_ctxs.front().config;\n    if (!config.display_name.empty()) {\n      // config.display_name may be a device ID (e.g., {xxx-xxx-xxx}) rather than display name (e.g., \\\\.\\DISPLAY1)\n      // Try to convert device ID to display name first\n      std::string resolved_display_name = display_device::get_display_name(config.display_name);\n      if (resolved_display_name.empty()) {\n        // If conversion failed, use the original value (might already be a display name)\n        resolved_display_name = config.display_name;\n      }\n\n      // Try to find the display in the list\n      bool found = false;\n      for (int x = 0; x < display_names.size(); ++x) {\n        if (display_names[x] == resolved_display_name) {\n          display_p = x;\n          target_display_name = resolved_display_name;\n          found = true;\n          BOOST_LOG(info) << \"Using client-specified display: \" << target_display_name;\n          break;\n        }\n      }\n      if (!found) {\n        BOOST_LOG(warning) << \"Client-specified display [\" << config.display_name << \"] (resolved: \" << resolved_display_name << \") not found, using default display\";\n        target_display_name = display_names[display_p];\n      }\n    }\n    else {\n      target_display_name = display_names[display_p];\n    }\n\n    auto disp = platf::display(encoder.platform_formats->dev_type, target_display_name, config);\n    if (!disp) {\n      return;\n    }\n    display_wp = disp;\n\n    constexpr auto capture_buffer_size = 12;\n    std::list<std::shared_ptr<platf::img_t>> imgs(capture_buffer_size);\n\n    std::vector<std::optional<std::chrono::steady_clock::time_point>> imgs_used_timestamps;\n    const std::chrono::seconds trim_timeot = 3s;\n    auto trim_imgs = [&]() {\n      // count allocated and used within current pool\n      size_t allocated_count = 0;\n      size_t used_count = 0;\n      for (const auto &img : imgs) {\n        if (img) {\n          allocated_count += 1;\n          if (img.use_count() > 1) {\n            used_count += 1;\n          }\n        }\n      }\n\n      // remember the timestamp of currently used count\n      const auto now = std::chrono::steady_clock::now();\n      if (imgs_used_timestamps.size() <= used_count) {\n        imgs_used_timestamps.resize(used_count + 1);\n      }\n      imgs_used_timestamps[used_count] = now;\n\n      // decide whether to trim allocated unused above the currently used count\n      // based on last used timestamp and universal timeout\n      size_t trim_target = used_count;\n      for (size_t i = used_count; i < imgs_used_timestamps.size(); i++) {\n        if (imgs_used_timestamps[i] && now - *imgs_used_timestamps[i] < trim_timeot) {\n          trim_target = i;\n        }\n      }\n\n      // trim allocated unused above the newly decided trim target\n      if (allocated_count > trim_target) {\n        size_t to_trim = allocated_count - trim_target;\n        // prioritize trimming least recently used\n        for (auto it = imgs.rbegin(); it != imgs.rend(); it++) {\n          auto &img = *it;\n          if (img && img.use_count() == 1) {\n            img.reset();\n            to_trim -= 1;\n            if (to_trim == 0) break;\n          }\n        }\n        // forget timestamps that no longer relevant\n        imgs_used_timestamps.resize(trim_target + 1);\n      }\n    };\n\n    auto pull_free_image_callback = [&](std::shared_ptr<platf::img_t> &img_out) -> bool {\n      img_out.reset();\n      while (capture_ctx_queue->running()) {\n        // pick first allocated but unused\n        for (auto it = imgs.begin(); it != imgs.end(); it++) {\n          if (*it && it->use_count() == 1) {\n            img_out = *it;\n            if (it != imgs.begin()) {\n              // move image to the front of the list to prioritize its reusal\n              imgs.erase(it);\n              imgs.push_front(img_out);\n            }\n            break;\n          }\n        }\n        // otherwise pick first unallocated\n        if (!img_out) {\n          for (auto it = imgs.begin(); it != imgs.end(); it++) {\n            if (!*it) {\n              // allocate image\n              *it = disp->alloc_img();\n              img_out = *it;\n              if (it != imgs.begin()) {\n                // move image to the front of the list to prioritize its reusal\n                imgs.erase(it);\n                imgs.push_front(img_out);\n              }\n              break;\n            }\n          }\n        }\n        if (img_out) {\n          // trim allocated but unused portion of the pool based on timeouts\n          trim_imgs();\n          img_out->frame_timestamp.reset();\n          return true;\n        }\n        else {\n          // sleep and retry if image pool is full\n          std::this_thread::sleep_for(1ms);\n        }\n      }\n      return false;\n    };\n\n    // Capture takes place on this thread\n    platf::adjust_thread_priority(platf::thread_priority_e::critical);\n\n    while (capture_ctx_queue->running()) {\n      bool artificial_reinit = false;\n\n      auto push_captured_image_callback = [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) -> bool {\n        KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), {\n          if (!capture_ctx->images->running()) {\n            capture_ctx = capture_ctxs.erase(capture_ctx);\n\n            continue;\n          }\n\n          if (frame_captured) {\n            capture_ctx->images->raise(img);\n          }\n\n          ++capture_ctx;\n        })\n\n        if (!capture_ctx_queue->running()) {\n          return false;\n        }\n\n        while (capture_ctx_queue->peek()) {\n          capture_ctxs.emplace_back(std::move(*capture_ctx_queue->pop()));\n        }\n\n        if (switch_display_event->peek()) {\n          artificial_reinit = true;\n          return false;\n        }\n\n        return true;\n      };\n\n      auto status = disp->capture(push_captured_image_callback, pull_free_image_callback, &display_cursor);\n\n      if (artificial_reinit && status != platf::capture_e::error) {\n        status = platf::capture_e::reinit;\n\n        artificial_reinit = false;\n      }\n\n      switch (status) {\n        case platf::capture_e::reinit: {\n          reinit_event.raise(true);\n\n          // Some classes of images contain references to the display --> display won't delete unless img is deleted\n          for (auto &img : imgs) {\n            img.reset();\n          }\n\n          // display_wp is modified in this thread only\n          // Wait for the other shared_ptr's of display to be destroyed.\n          // New displays will only be created in this thread.\n          while (display_wp->use_count() != 1) {\n            // Free images that weren't consumed by the encoders. These can reference the display and prevent\n            // the ref count from reaching 1. We do this here rather than on the encoder thread to avoid race\n            // conditions where the encoding loop might free a good frame after reinitializing if we capture\n            // a new frame here before the encoder has finished reinitializing.\n            KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), {\n              if (!capture_ctx->images->running()) {\n                capture_ctx = capture_ctxs.erase(capture_ctx);\n                continue;\n              }\n\n              while (capture_ctx->images->peek()) {\n                capture_ctx->images->pop();\n              }\n\n              ++capture_ctx;\n            });\n\n            std::this_thread::sleep_for(20ms);\n          }\n\n          while (capture_ctx_queue->running()) {\n            // Release the display before reenumerating displays, since some capture backends\n            // only support a single display session per device/application.\n            disp.reset();\n\n            // Refresh display names since a display removal might have caused the reinitialization\n            refresh_displays(encoder.platform_formats->dev_type, display_names, display_p);\n\n            // Process any pending display switch with the new list of displays\n            bool user_switched = false;\n            if (switch_display_event->peek()) {\n              display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1);\n              user_switched = true;\n            }\n\n            // Use client-specified display_name if provided (only for auto-reinit, not manual switch)\n            const auto &config = capture_ctxs.front().config;\n            std::string target_display_name = display_names[display_p];\n            if (!user_switched && !config.display_name.empty()) {\n              // config.display_name may be a device ID - convert to display name\n              std::string resolved_display_name = display_device::get_display_name(config.display_name);\n              if (resolved_display_name.empty()) {\n                resolved_display_name = config.display_name;\n              }\n\n              // Try to find the display in the list\n              bool found = false;\n              for (int x = 0; x < display_names.size(); ++x) {\n                if (display_names[x] == resolved_display_name) {\n                  display_p = x;\n                  target_display_name = resolved_display_name;\n                  found = true;\n                  break;\n                }\n              }\n              if (!found) {\n                BOOST_LOG(warning) << \"Client-specified display [\" << config.display_name << \"] (resolved: \" << resolved_display_name << \") not found, using default display\";\n              }\n            }\n\n            // reset_display() will sleep between retries\n            reset_display(disp, encoder.platform_formats->dev_type, target_display_name, config);\n            if (disp) {\n              break;\n            }\n          }\n          if (!disp) {\n            return;\n          }\n\n          display_wp = disp;\n\n          reinit_event.reset();\n          continue;\n        }\n        case platf::capture_e::error:\n        case platf::capture_e::ok:\n        case platf::capture_e::timeout:\n        case platf::capture_e::interrupted:\n          return;\n        default:\n          BOOST_LOG(error) << \"Unrecognized capture status [\"sv << (int) status << ']';\n          return;\n      }\n    }\n  }\n\n  /**\n   * @brief Temporal EMA (Exponential Moving Average) state for HDR luminance stats.\n   * Prevents frame-to-frame brightness jitter/flicker in tone mapping by smoothing\n   * the raw per-frame GPU statistics over time.\n   */\n  struct hdr_luminance_ema_t {\n    float min_maxrgb = 0.0f;\n    float max_maxrgb = 0.0f;\n    float avg_maxrgb = 0.0f;\n    float percentile_95 = 0.0f;\n    float percentile_99 = 0.0f;\n    bool initialized = false;\n\n    /// EMA smoothing factor: 0.15 = responsive to changes while avoiding flicker.\n    /// Lower α = more smoothing (less flicker, slower adaptation).\n    /// Scene cuts are handled by fast-tracking when the change exceeds a threshold.\n    static constexpr float ALPHA = 0.15f;\n    static constexpr float SCENE_CUT_THRESHOLD = 3.0f;  // Ratio threshold for scene cut detection\n\n    /**\n     * @brief Apply EMA smoothing to raw per-frame stats.\n     * On first frame or scene cuts (>3x luminance change), snaps to current value.\n     * Otherwise applies exponential smoothing: smoothed = α·current + (1-α)·previous.\n     */\n    void\n    update(const platf::hdr_frame_luminance_stats_t &raw) {\n      if (!raw.valid) return;\n\n      if (!initialized) {\n        // First frame: snap to current values\n        min_maxrgb = raw.min_maxrgb;\n        max_maxrgb = raw.max_maxrgb;\n        avg_maxrgb = raw.avg_maxrgb;\n        percentile_95 = raw.percentile_95;\n        percentile_99 = raw.percentile_99;\n        initialized = true;\n        return;\n      }\n\n      // Scene cut detection: if peak luminance changes dramatically, snap immediately\n      float ratio = (max_maxrgb > 1.0f) ? raw.max_maxrgb / max_maxrgb : SCENE_CUT_THRESHOLD + 1.0f;\n      float alpha = (ratio > SCENE_CUT_THRESHOLD || ratio < 1.0f / SCENE_CUT_THRESHOLD)\n                    ? 1.0f  // Scene cut: snap to new values\n                    : ALPHA; // Normal: smooth transition\n\n      min_maxrgb = alpha * raw.min_maxrgb + (1.0f - alpha) * min_maxrgb;\n      max_maxrgb = alpha * raw.max_maxrgb + (1.0f - alpha) * max_maxrgb;\n      avg_maxrgb = alpha * raw.avg_maxrgb + (1.0f - alpha) * avg_maxrgb;\n      percentile_95 = alpha * raw.percentile_95 + (1.0f - alpha) * percentile_95;\n      percentile_99 = alpha * raw.percentile_99 + (1.0f - alpha) * percentile_99;\n    }\n  };\n\n  /**\n   * @brief Update per-frame HDR dynamic metadata with smoothed GPU-computed luminance stats.\n   *\n   * Called before each avcodec_send_frame() to inject accurate per-frame\n   * maxRGB statistics into HDR Vivid and HDR10+ side data.\n   * Uses P95 percentile for peak luminance (more stable than raw max) and\n   * EMA-smoothed values to prevent frame-to-frame flicker.\n   *\n   * @param frame The AVFrame with pre-allocated dynamic HDR side data\n   * @param ema Temporally-smoothed luminance statistics\n   * @param max_display_luminance Display peak luminance in nits (from EDID)\n   */\n  void\n  update_hdr_dynamic_metadata(AVFrame *frame, const hdr_luminance_ema_t &ema, uint16_t max_display_luminance) {\n    if (!ema.initialized || !frame) return;\n\n    float peak_nits = max_display_luminance > 0 ? static_cast<float>(max_display_luminance) : 1000.0f;\n\n    // Use P95 as the \"effective peak\" for metadata — more stable than raw max,\n    // avoids single-pixel HDR highlights distorting global tone mapping\n    float effective_max = ema.percentile_95;\n\n    // Update HDR Vivid (CUVA) dynamic metadata\n    auto vivid_sd = av_frame_get_side_data(frame, AV_FRAME_DATA_DYNAMIC_HDR_VIVID);\n    if (vivid_sd) {\n      auto *vivid = reinterpret_cast<AVDynamicHDRVivid *>(vivid_sd->data);\n      if (vivid && vivid->num_windows > 0) {\n        auto &params = vivid->params[0];\n\n        // Normalize to [0, 1] range relative to peak luminance\n        // CUVA spec uses Q4.12 representation via AVRational with denominator 4095\n        float min_norm = std::clamp(ema.min_maxrgb / peak_nits, 0.0f, 1.0f);\n        float avg_norm = std::clamp(ema.avg_maxrgb / peak_nits, 0.0f, 1.0f);\n        float max_norm = std::clamp(effective_max / peak_nits, 0.0f, 1.0f);\n\n        // Variance: spread between P99 and min, normalized\n        float variance_norm = std::clamp((ema.percentile_99 - ema.min_maxrgb) / peak_nits, 0.0f, 1.0f);\n\n        params.minimum_maxrgb = av_make_q(static_cast<int>(min_norm * 4095), 4095);\n        params.average_maxrgb = av_make_q(static_cast<int>(avg_norm * 4095), 4095);\n        params.variance_maxrgb = av_make_q(static_cast<int>(variance_norm * 4095), 4095);\n        params.maximum_maxrgb = av_make_q(static_cast<int>(max_norm * 4095), 4095);\n\n        // Update targeted display luminance in tone mapping params\n        for (int i = 0; i < 2; i++) {\n          params.tm_params[i].targeted_system_display_maximum_luminance = av_make_q(max_display_luminance, 1);\n        }\n      }\n    }\n\n    // Update HDR10+ dynamic metadata\n    auto hdr10plus_sd = av_frame_get_side_data(frame, AV_FRAME_DATA_DYNAMIC_HDR_PLUS);\n    if (hdr10plus_sd) {\n      auto *hdr10plus = reinterpret_cast<AVDynamicHDRPlus *>(hdr10plus_sd->data);\n      if (hdr10plus && hdr10plus->num_windows > 0) {\n        auto &params = hdr10plus->params[0];\n\n        // HDR10+ maxscl: use P95 for stability\n        float max_norm = std::clamp(effective_max / peak_nits, 0.0f, 1.0f);\n        float avg_norm = std::clamp(ema.avg_maxrgb / peak_nits, 0.0f, 1.0f);\n\n        params.maxscl[0] = av_make_q(static_cast<int>(max_norm * 100000), 100000);\n        params.maxscl[1] = av_make_q(static_cast<int>(max_norm * 100000), 100000);\n        params.maxscl[2] = av_make_q(static_cast<int>(max_norm * 100000), 100000);\n        params.average_maxrgb = av_make_q(static_cast<int>(avg_norm * 100000), 100000);\n\n        hdr10plus->targeted_system_display_maximum_luminance = av_make_q(max_display_luminance, 1);\n      }\n    }\n  }\n\n  // Per-session EMA state for temporal smoothing of HDR luminance stats\n  static thread_local hdr_luminance_ema_t hdr_ema_state;\n\n  int\n  encode_avcodec(int64_t frame_nr, avcodec_encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {\n    auto &frame = session.device->frame;\n    frame->pts = frame_nr;\n\n    auto &ctx = session.avcodec_ctx;\n\n    auto &sps = session.sps;\n    auto &vps = session.vps;\n\n    // Update per-frame HDR dynamic metadata with GPU-computed luminance stats\n    // Apply temporal EMA smoothing to prevent brightness jitter between frames\n    {\n      auto &raw_stats = session.device->hdr_luminance_stats;\n      if (raw_stats.valid) {\n        hdr_ema_state.update(raw_stats);\n\n        uint16_t max_lum = 1000;\n        auto mdm_sd = av_frame_get_side_data(frame, AV_FRAME_DATA_MASTERING_DISPLAY_METADATA);\n        if (mdm_sd) {\n          auto *mdm = reinterpret_cast<AVMasteringDisplayMetadata *>(mdm_sd->data);\n          if (mdm && mdm->has_luminance) {\n            max_lum = static_cast<uint16_t>(av_q2d(mdm->max_luminance));\n          }\n        }\n        update_hdr_dynamic_metadata(frame, hdr_ema_state, max_lum);\n      }\n    }\n\n    // send the frame to the encoder\n    auto ret = avcodec_send_frame(ctx.get(), frame);\n    if (ret < 0) {\n      char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n      BOOST_LOG(error) << \"Could not send a frame for encoding: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, ret);\n\n      return -1;\n    }\n\n    while (ret >= 0) {\n      auto packet = std::make_unique<packet_raw_avcodec>();\n      auto av_packet = packet.get()->av_packet;\n\n      ret = avcodec_receive_packet(ctx.get(), av_packet);\n      if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {\n        return 0;\n      }\n      else if (ret < 0) {\n        return ret;\n      }\n\n      if (av_packet->flags & AV_PKT_FLAG_KEY) {\n        BOOST_LOG(debug) << \"Frame \"sv << frame_nr << \": IDR Keyframe (AV_FRAME_FLAG_KEY)\"sv;\n      }\n\n      if ((frame->flags & AV_FRAME_FLAG_KEY) && !(av_packet->flags & AV_PKT_FLAG_KEY)) {\n        BOOST_LOG(error) << \"Encoder did not produce IDR frame when requested!\"sv;\n      }\n\n      if (session.inject) {\n        if (session.inject == 1) {\n          auto h264 = cbs::make_sps_h264(ctx.get(), av_packet);\n\n          sps = std::move(h264.sps);\n        }\n        else {\n          auto hevc = cbs::make_sps_hevc(ctx.get(), av_packet);\n\n          sps = std::move(hevc.sps);\n          vps = std::move(hevc.vps);\n\n          session.replacements.emplace_back(\n            std::string_view((char *) std::begin(vps.old), vps.old.size()),\n            std::string_view((char *) std::begin(vps._new), vps._new.size()));\n        }\n\n        session.inject = 0;\n\n        session.replacements.emplace_back(\n          std::string_view((char *) std::begin(sps.old), sps.old.size()),\n          std::string_view((char *) std::begin(sps._new), sps._new.size()));\n      }\n\n      if (av_packet && av_packet->pts == frame_nr) {\n        packet->frame_timestamp = frame_timestamp;\n      }\n\n      packet->replacements = &session.replacements;\n      packet->channel_data = channel_data;\n      packets->raise(std::move(packet));\n    }\n\n    return 0;\n  }\n\n  int\n  encode_nvenc(int64_t frame_nr, nvenc_encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {\n    auto encoded_frame = session.encode_frame(frame_nr);\n    if (encoded_frame.data.empty()) {\n      // Empty data with valid frame_index means encoder needs more input (NV_ENC_ERR_NEED_MORE_INPUT).\n      // This is not an error - just return success and continue with next frame.\n      if (encoded_frame.frame_index == static_cast<uint64_t>(frame_nr)) {\n        BOOST_LOG(debug) << \"NvENC: frame \" << frame_nr << \" buffered, waiting for more input\";\n        return 0;\n      }\n      BOOST_LOG(error) << \"NvENC returned empty packet\";\n      return -1;\n    }\n\n    if (frame_nr != encoded_frame.frame_index) {\n      BOOST_LOG(error) << \"NvENC frame index mismatch \" << frame_nr << \" \" << encoded_frame.frame_index;\n    }\n\n    auto packet = std::make_unique<packet_raw_generic>(std::move(encoded_frame.data), encoded_frame.frame_index, encoded_frame.idr);\n    packet->channel_data = channel_data;\n    packet->after_ref_frame_invalidation = encoded_frame.after_ref_frame_invalidation;\n    packet->frame_timestamp = frame_timestamp;\n    packets->raise(std::move(packet));\n\n    return 0;\n  }\n\n  int\n  encode_amf(int64_t frame_nr, amf_encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {\n    auto encoded_frame = session.encode_frame(frame_nr);\n    if (encoded_frame.data.empty()) {\n      if (encoded_frame.frame_index == static_cast<uint64_t>(frame_nr)) {\n        BOOST_LOG(debug) << \"AMF: frame \" << frame_nr << \" buffered, waiting for more input\";\n        return 0;\n      }\n      BOOST_LOG(error) << \"AMF returned empty packet\";\n      return -1;\n    }\n\n    if (frame_nr != encoded_frame.frame_index) {\n      BOOST_LOG(error) << \"AMF frame index mismatch \" << frame_nr << \" \" << encoded_frame.frame_index;\n    }\n\n    auto packet = std::make_unique<packet_raw_generic>(std::move(encoded_frame.data), encoded_frame.frame_index, encoded_frame.idr);\n    packet->channel_data = channel_data;\n    packet->after_ref_frame_invalidation = encoded_frame.after_ref_frame_invalidation;\n    packet->frame_timestamp = frame_timestamp;\n    packets->raise(std::move(packet));\n\n    return 0;\n  }\n\n  int\n  encode(int64_t frame_nr, encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {\n    if (auto avcodec_session = dynamic_cast<avcodec_encode_session_t *>(&session)) {\n      return encode_avcodec(frame_nr, *avcodec_session, packets, channel_data, frame_timestamp);\n    }\n    else if (auto nvenc_session = dynamic_cast<nvenc_encode_session_t *>(&session)) {\n      return encode_nvenc(frame_nr, *nvenc_session, packets, channel_data, frame_timestamp);\n    }\n    else if (auto amf_session = dynamic_cast<amf_encode_session_t *>(&session)) {\n      return encode_amf(frame_nr, *amf_session, packets, channel_data, frame_timestamp);\n    }\n\n    return -1;\n  }\n\n  std::unique_ptr<avcodec_encode_session_t>\n  make_avcodec_encode_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr<platf::avcodec_encode_device_t> encode_device) {\n    auto platform_formats = dynamic_cast<const encoder_platform_formats_avcodec *>(encoder.platform_formats.get());\n    if (!platform_formats) {\n      return nullptr;\n    }\n\n    bool hardware = platform_formats->avcodec_base_dev_type != AV_HWDEVICE_TYPE_NONE;\n\n    auto &video_format = encoder.codec_from_config(config);\n    if (!video_format[encoder_t::PASSED] || !disp->is_codec_supported(video_format.name, config)) {\n      BOOST_LOG(error) << encoder.name << \": \"sv << video_format.name << \" mode not supported\"sv;\n      return nullptr;\n    }\n\n    if (config.dynamicRange && !video_format[encoder_t::DYNAMIC_RANGE]) {\n      BOOST_LOG(error) << video_format.name << \": dynamic range not supported\"sv;\n      return nullptr;\n    }\n\n    if (config.chromaSamplingType == 1 && !video_format[encoder_t::YUV444]) {\n      BOOST_LOG(error) << video_format.name << \": YUV 4:4:4 not supported\"sv;\n      return nullptr;\n    }\n\n    auto codec = avcodec_find_encoder_by_name(video_format.name.c_str());\n    if (!codec) {\n      BOOST_LOG(error) << \"Couldn't open [\"sv << video_format.name << ']';\n\n      return nullptr;\n    }\n\n    auto colorspace = encode_device->colorspace;\n    auto sw_fmt = (colorspace.bit_depth == 8 && config.chromaSamplingType == 0)  ? platform_formats->avcodec_pix_fmt_8bit :\n                  (colorspace.bit_depth == 8 && config.chromaSamplingType == 1)  ? platform_formats->avcodec_pix_fmt_yuv444_8bit :\n                  (colorspace.bit_depth == 10 && config.chromaSamplingType == 0) ? platform_formats->avcodec_pix_fmt_10bit :\n                  (colorspace.bit_depth == 10 && config.chromaSamplingType == 1) ? platform_formats->avcodec_pix_fmt_yuv444_10bit :\n                                                                                   AV_PIX_FMT_NONE;\n\n    // Allow up to 1 retry to apply the set of fallback options.\n    //\n    // Note: If we later end up needing multiple sets of\n    // fallback options, we may need to allow more retries\n    // to try applying each set.\n    avcodec_ctx_t ctx;\n    for (int retries = 0; retries < 2; retries++) {\n      ctx.reset(avcodec_alloc_context3(codec));\n      ctx->width = config.width;\n      ctx->height = config.height;\n\n      // Use fractional framerate if available (for NTSC support)\n      if (config.frameRateNum > 0 && config.frameRateDen > 0) {\n        ctx->time_base = AVRational { config.frameRateDen, config.frameRateNum };\n        ctx->framerate = AVRational { config.frameRateNum, config.frameRateDen };\n        BOOST_LOG(debug) << \"Using fractional framerate: \" << config.frameRateNum << \"/\" << config.frameRateDen\n                         << \" (\" << config.get_effective_framerate() << \"fps)\";\n      }\n      else {\n        ctx->time_base = AVRational { 1, config.framerate };\n        ctx->framerate = AVRational { config.framerate, 1 };\n      }\n\n      switch (config.videoFormat) {\n        case 0:\n          // 10-bit h264 encoding is not supported by our streaming protocol\n          assert(!config.dynamicRange);\n          ctx->profile = (config.chromaSamplingType == 1) ? AV_PROFILE_H264_HIGH_444_PREDICTIVE : AV_PROFILE_H264_HIGH;\n          break;\n\n        case 1:\n          if (config.chromaSamplingType == 1) {\n            // HEVC uses the same RExt profile for both 8 and 10 bit YUV 4:4:4 encoding\n            ctx->profile = AV_PROFILE_HEVC_REXT;\n          }\n          else {\n            ctx->profile = config.dynamicRange ? AV_PROFILE_HEVC_MAIN_10 : AV_PROFILE_HEVC_MAIN;\n          }\n          break;\n\n        case 2:\n          // AV1 supports both 8 and 10 bit encoding with the same Main profile\n          // but YUV 4:4:4 sampling requires High profile\n          ctx->profile = (config.chromaSamplingType == 1) ? AV_PROFILE_AV1_HIGH : AV_PROFILE_AV1_MAIN;\n          break;\n      }\n\n      // B-frames delay decoder output, so never use them\n      ctx->max_b_frames = 0;\n\n      // Use an infinite GOP length since I-frames are generated on demand\n      ctx->gop_size = encoder.flags & LIMITED_GOP_SIZE ?\n                        std::numeric_limits<std::int16_t>::max() :\n                        std::numeric_limits<int>::max();\n\n      ctx->keyint_min = std::numeric_limits<int>::max();\n\n      // Some client decoders have limits on the number of reference frames\n      if (config.numRefFrames) {\n        if (video_format[encoder_t::REF_FRAMES_RESTRICT]) {\n          ctx->refs = config.numRefFrames;\n        }\n        else {\n          BOOST_LOG(warning) << \"Client requested reference frame limit, but encoder doesn't support it!\"sv;\n        }\n      }\n\n      // We forcefully reset the flags to avoid clash on reuse of AVCodecContext\n      ctx->flags = 0;\n      ctx->flags |= AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY;\n\n      ctx->flags2 |= AV_CODEC_FLAG2_FAST;\n\n      auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace);\n\n      ctx->color_range = avcodec_colorspace.range;\n      ctx->color_primaries = avcodec_colorspace.primaries;\n      ctx->color_trc = avcodec_colorspace.transfer_function;\n      ctx->colorspace = avcodec_colorspace.matrix;\n\n      // Used by cbs::make_sps_hevc\n      ctx->sw_pix_fmt = sw_fmt;\n\n      if (hardware) {\n        avcodec_buffer_t encoding_stream_context;\n\n        ctx->pix_fmt = platform_formats->avcodec_dev_pix_fmt;\n\n        // Create the base hwdevice context\n        auto buf_or_error = platform_formats->init_avcodec_hardware_input_buffer(encode_device.get());\n        if (buf_or_error.has_right()) {\n          return nullptr;\n        }\n        encoding_stream_context = std::move(buf_or_error.left());\n\n        // If this encoder requires derivation from the base, derive the desired type\n        if (platform_formats->avcodec_derived_dev_type != AV_HWDEVICE_TYPE_NONE) {\n          avcodec_buffer_t derived_context;\n\n          // Allow the hwdevice to prepare for this type of context to be derived\n          if (encode_device->prepare_to_derive_context(platform_formats->avcodec_derived_dev_type)) {\n            return nullptr;\n          }\n\n          auto err = av_hwdevice_ctx_create_derived(&derived_context, platform_formats->avcodec_derived_dev_type, encoding_stream_context.get(), 0);\n          if (err) {\n            char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n            BOOST_LOG(error) << \"Failed to derive device context: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);\n\n            return nullptr;\n          }\n\n          encoding_stream_context = std::move(derived_context);\n        }\n\n        // Initialize avcodec hardware frames\n        {\n          avcodec_buffer_t frame_ref { av_hwframe_ctx_alloc(encoding_stream_context.get()) };\n\n          auto frame_ctx = (AVHWFramesContext *) frame_ref->data;\n          frame_ctx->format = ctx->pix_fmt;\n          frame_ctx->sw_format = sw_fmt;\n          frame_ctx->height = ctx->height;\n          frame_ctx->width = ctx->width;\n          frame_ctx->initial_pool_size = 0;\n\n          // Allow the hwdevice to modify hwframe context parameters\n          encode_device->init_hwframes(frame_ctx);\n\n          if (auto err = av_hwframe_ctx_init(frame_ref.get()); err < 0) {\n            return nullptr;\n          }\n\n          ctx->hw_frames_ctx = av_buffer_ref(frame_ref.get());\n        }\n\n        ctx->slices = config.slicesPerFrame;\n      }\n      else /* software */ {\n        ctx->pix_fmt = sw_fmt;\n\n        // Clients will request for the fewest slices per frame to get the\n        // most efficient encode, but we may want to provide more slices than\n        // requested to ensure we have enough parallelism for good performance.\n        ctx->slices = std::max(config.slicesPerFrame, config::video.min_threads);\n      }\n\n      if (encoder.flags & SINGLE_SLICE_ONLY) {\n        ctx->slices = 1;\n      }\n\n      ctx->thread_type = FF_THREAD_SLICE;\n      ctx->thread_count = ctx->slices;\n\n      AVDictionary *options { nullptr };\n      auto handle_option = [&options, &config](const encoder_t::option_t &option) {\n        std::visit(\n          util::overloaded {\n            [&](int v) {\n              av_dict_set_int(&options, option.name.c_str(), v, 0);\n            },\n            [&](int *v) {\n              av_dict_set_int(&options, option.name.c_str(), *v, 0);\n            },\n            [&](std::optional<int> *v) {\n              if (*v) {\n                av_dict_set_int(&options, option.name.c_str(), **v, 0);\n              }\n            },\n            [&](const std::function<int()> &v) {\n              av_dict_set_int(&options, option.name.c_str(), v(), 0);\n            },\n            [&](const std::string &v) {\n              av_dict_set(&options, option.name.c_str(), v.c_str(), 0);\n            },\n            [&](std::string *v) {\n              if (!v->empty()) {\n                av_dict_set(&options, option.name.c_str(), v->c_str(), 0);\n              }\n            },\n            [&](const std::function<const std::string(const config_t &cfg)> &v) {\n              av_dict_set(&options, option.name.c_str(), v(config).c_str(), 0);\n            } },\n          option.value);\n      };\n\n      // Apply common options, then format-specific overrides\n      for (auto &option : video_format.common_options) {\n        handle_option(option);\n      }\n      for (auto &option : (config.dynamicRange ? video_format.hdr_options : video_format.sdr_options)) {\n        handle_option(option);\n      }\n      if (config.chromaSamplingType == 1) {\n        for (auto &option : (config.dynamicRange ? video_format.hdr444_options : video_format.sdr444_options)) {\n          handle_option(option);\n        }\n      }\n      if (retries > 0) {\n        for (auto &option : video_format.fallback_options) {\n          handle_option(option);\n        }\n      }\n\n      auto bitrate = ((config::video.max_bitrate > 0) ? std::min(config.bitrate, config::video.max_bitrate) : config.bitrate) * 1000;\n      BOOST_LOG(info) << \"Streaming bitrate is \" << bitrate;\n      ctx->rc_max_rate = bitrate;\n      ctx->bit_rate = bitrate;\n\n      if (encoder.flags & CBR_WITH_VBR) {\n        // Ensure rc_max_bitrate != bit_rate to force VBR mode\n        ctx->bit_rate--;\n      }\n      else {\n        ctx->rc_min_rate = bitrate;\n      }\n\n      if (encoder.flags & RELAXED_COMPLIANCE) {\n        ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;\n      }\n\n      if (!(encoder.flags & NO_RC_BUF_LIMIT)) {\n        // Use effective framerate for VBV buffer calculation (supports NTSC fractional framerates)\n        double effective_fps = config.get_effective_framerate();\n\n        if (!hardware && (ctx->slices > 1 || config.videoFormat == 1)) {\n          // Use a larger rc_buffer_size for software encoding when slices are enabled,\n          // because libx264 can severely degrade quality if the buffer is too small.\n          // libx265 encounters this issue more frequently, so always scale the\n          // buffer by 1.5x for software HEVC encoding.\n          ctx->rc_buffer_size = static_cast<int>(bitrate / (effective_fps * 10 / 15));\n        }\n        else {\n          ctx->rc_buffer_size = static_cast<int>(bitrate / effective_fps);\n\n#ifndef __APPLE__\n          if (encoder.name == \"nvenc\" && config::video.nv_legacy.vbv_percentage_increase > 0) {\n            ctx->rc_buffer_size += ctx->rc_buffer_size * config::video.nv_legacy.vbv_percentage_increase / 100;\n          }\n#endif\n        }\n      }\n\n      // Allow the encoding device a final opportunity to set/unset or override any options\n      encode_device->init_codec_options(ctx.get(), &options);\n\n      if (auto status = avcodec_open2(ctx.get(), codec, &options)) {\n        char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n\n        if (!video_format.fallback_options.empty() && retries == 0) {\n          BOOST_LOG(info)\n            << \"Retrying with fallback configuration options for [\"sv << video_format.name << \"] after error: \"sv\n            << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status);\n\n          continue;\n        }\n        else {\n          BOOST_LOG(error)\n            << \"Could not open codec [\"sv\n            << video_format.name << \"]: \"sv\n            << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status);\n\n          return nullptr;\n        }\n      }\n\n      // Successfully opened the codec\n      break;\n    }\n\n    avcodec_frame_t frame { av_frame_alloc() };\n    frame->format = ctx->pix_fmt;\n    frame->width = ctx->width;\n    frame->height = ctx->height;\n    frame->color_range = ctx->color_range;\n    frame->color_primaries = ctx->color_primaries;\n    frame->color_trc = ctx->color_trc;\n    frame->colorspace = ctx->colorspace;\n    frame->chroma_location = ctx->chroma_sample_location;\n\n    // Attach HDR metadata to the AVFrame\n    // Both PQ (ST 2084) and HLG (ARIB STD-B67) can carry HDR metadata.\n    // PQ uses absolute luminance and requires static metadata (MDCV, CLL).\n    // HLG uses scene-referred relative luminance but benefits from HDR Vivid (CUVA)\n    // dynamic metadata for enhanced tone mapping on capable displays.\n    if (colorspace_is_hdr(colorspace)) {\n      SS_HDR_METADATA hdr_metadata;\n      bool has_metadata = disp->get_hdr_metadata(hdr_metadata);\n\n      if (has_metadata) {\n        // Attach static HDR metadata (Mastering Display Color Volume + Content Light Level)\n        // Required for PQ, optional but beneficial for HLG with HDR Vivid\n        auto mdm = av_mastering_display_metadata_create_side_data(frame.get());\n\n        mdm->display_primaries[0][0] = av_make_q(hdr_metadata.displayPrimaries[0].x, 50000);\n        mdm->display_primaries[0][1] = av_make_q(hdr_metadata.displayPrimaries[0].y, 50000);\n        mdm->display_primaries[1][0] = av_make_q(hdr_metadata.displayPrimaries[1].x, 50000);\n        mdm->display_primaries[1][1] = av_make_q(hdr_metadata.displayPrimaries[1].y, 50000);\n        mdm->display_primaries[2][0] = av_make_q(hdr_metadata.displayPrimaries[2].x, 50000);\n        mdm->display_primaries[2][1] = av_make_q(hdr_metadata.displayPrimaries[2].y, 50000);\n\n        mdm->white_point[0] = av_make_q(hdr_metadata.whitePoint.x, 50000);\n        mdm->white_point[1] = av_make_q(hdr_metadata.whitePoint.y, 50000);\n\n        mdm->min_luminance = av_make_q(hdr_metadata.minDisplayLuminance, 10000);\n        mdm->max_luminance = av_make_q(hdr_metadata.maxDisplayLuminance, 1);\n\n        mdm->has_luminance = hdr_metadata.maxDisplayLuminance != 0 ? 1 : 0;\n        mdm->has_primaries = hdr_metadata.displayPrimaries[0].x != 0 ? 1 : 0;\n\n        if (hdr_metadata.maxContentLightLevel != 0 || hdr_metadata.maxFrameAverageLightLevel != 0) {\n          auto clm = av_content_light_metadata_create_side_data(frame.get());\n\n          clm->MaxCLL = hdr_metadata.maxContentLightLevel;\n          clm->MaxFALL = hdr_metadata.maxFrameAverageLightLevel;\n        }\n\n        // HDR10+ dynamic metadata - PQ only (Samsung ST 2094-40, uses absolute luminance)\n        if (colorspace_is_pq(colorspace)) {\n          auto hdr10plus = av_dynamic_hdr_plus_create_side_data(frame.get());\n          if (hdr10plus) {\n            // Set default values for HDR10+\n            hdr10plus->itu_t_t35_country_code = 0xB5;  // USA\n            hdr10plus->application_version = 0;\n            hdr10plus->num_windows = 1;  // Single processing window covering entire frame\n\n            // Initialize the first (and only) processing window\n            auto &params = hdr10plus->params[0];\n            params.window_upper_left_corner_x = av_make_q(0, 1);\n            params.window_upper_left_corner_y = av_make_q(0, 1);\n            params.window_lower_right_corner_x = av_make_q(1, 1);\n            params.window_lower_right_corner_y = av_make_q(1, 1);\n\n            // Set center of elliptical pixel selector to center of frame\n            params.center_of_ellipse_x = static_cast<uint16_t>(config.width / 2);\n            params.center_of_ellipse_y = static_cast<uint16_t>(config.height / 2);\n            params.rotation_angle = 0;  // 0 degrees\n            params.semimajor_axis_internal_ellipse = static_cast<uint16_t>(config.width / 2);\n            params.semimajor_axis_external_ellipse = static_cast<uint16_t>(config.width / 2);\n            params.semiminor_axis_external_ellipse = static_cast<uint16_t>(config.height / 2);\n            params.overlap_process_option = AV_HDR_PLUS_OVERLAP_PROCESS_WEIGHTED_AVERAGING;\n\n            // Set maxscl (maximum of R, G, B) to 1.0 (full brightness)\n            params.maxscl[0] = av_make_q(1, 1);\n            params.maxscl[1] = av_make_q(1, 1);\n            params.maxscl[2] = av_make_q(1, 1);\n            params.maxscl[3] = av_make_q(0, 1);  // Unused\n\n            // Set average maxRGB to 1.0\n            params.average_maxrgb = av_make_q(1, 1);\n\n            // Initialize percentile distribution (simplified)\n            params.num_distribution_maxrgb_percentiles = 0;  // No percentiles for simplified metadata\n\n            // Set fraction brightness to 0 (no bright pixels)\n            params.fraction_bright_pixels = av_make_q(0, 1);\n\n            // Set tone mapping curve to linear (no adjustment)\n            params.tone_mapping_flag = 0;\n            params.knee_point_x = av_make_q(0, 1);\n            params.knee_point_y = av_make_q(0, 1);\n            params.num_bezier_curve_anchors = 0;\n\n            // Set targeted system display maximum luminance from static metadata\n            hdr10plus->targeted_system_display_maximum_luminance = av_make_q(hdr_metadata.maxDisplayLuminance, 1);\n            hdr10plus->targeted_system_display_actual_peak_luminance_flag = 0;\n            hdr10plus->mastering_display_actual_peak_luminance_flag = 0;\n\n            BOOST_LOG(debug) << \"Added HDR10+ dynamic metadata to frame\";\n          }\n        }\n\n        // HDR Vivid (CUVA HDR / T/UWA 3.137) dynamic metadata - both PQ and HLG\n        // HDR Vivid supports both transfer functions:\n        //   - PQ mode: absolute luminance tone mapping\n        //   - HLG mode: scene-referred relative luminance tone mapping\n        // The CUVA metadata is carried as ITU-T T.35 registered SEI/OBU, independent\n        // of the underlying transfer function.\n        auto vivid = av_dynamic_hdr_vivid_create_side_data(frame.get());\n        if (vivid) {\n          // Set default values for HDR Vivid\n          vivid->system_start_code = 0x01;\n          vivid->num_windows = 0x01;  // Single processing window\n\n          // Initialize the first (and only) processing window\n          auto &params = vivid->params[0];\n\n          // Initialize maxrgb values (simplified - use full range)\n          params.minimum_maxrgb = av_make_q(0, 4095);\n          params.average_maxrgb = av_make_q(2047, 4095);  // 0.5\n          params.variance_maxrgb = av_make_q(0, 4095);\n          params.maximum_maxrgb = av_make_q(4095, 4095);  // 1.0\n\n          // Initialize tone mapping parameters (simplified - no tone mapping)\n          params.tone_mapping_mode_flag = 0;\n          params.tone_mapping_param_num = 0;\n\n          // Initialize color saturation mapping (disabled)\n          params.color_saturation_mapping_flag = 0;\n          params.color_saturation_num = 0;\n          for (int j = 0; j < 8; j++) {\n            params.color_saturation_gain[j] = av_make_q(128, 128);  // 1.0 (no adjustment)\n          }\n\n          // Initialize tone mapping params structure (even if not used)\n          for (int i = 0; i < 2; i++) {\n            auto &tm_params = params.tm_params[i];\n            tm_params.targeted_system_display_maximum_luminance = av_make_q(hdr_metadata.maxDisplayLuminance, 1);\n            tm_params.base_enable_flag = 0;\n            tm_params.base_param_m_p = av_make_q(0, 16383);\n            tm_params.base_param_m_m = av_make_q(0, 10);\n            tm_params.base_param_m_a = av_make_q(0, 1023);\n            tm_params.base_param_m_b = av_make_q(0, 1023);\n            tm_params.base_param_m_n = av_make_q(0, 10);\n            tm_params.base_param_k1 = 0;\n            tm_params.base_param_k2 = 0;\n            tm_params.base_param_k3 = 0;\n            tm_params.base_param_Delta_enable_mode = 0;\n            tm_params.base_param_Delta = av_make_q(0, 127);\n            tm_params.three_Spline_enable_flag = 0;\n            tm_params.three_Spline_num = 0;\n            // Initialize three spline parameters\n            for (int j = 0; j < 2; j++) {\n              auto &spline = tm_params.three_spline[j];\n              spline.th_mode = 0;\n              spline.th_enable_mb = av_make_q(0, 255);\n              spline.th_enable = av_make_q(0, 4095);\n              spline.th_delta1 = av_make_q(0, 1023);\n              spline.th_delta2 = av_make_q(0, 1023);\n              spline.enable_strength = av_make_q(0, 255);\n            }\n          }\n\n          BOOST_LOG(debug) << \"Added HDR Vivid dynamic metadata to frame\"\n                           << (colorspace_is_hlg(colorspace) ? \" (HLG mode)\" : \" (PQ mode)\");\n        }\n      }\n      else {\n        BOOST_LOG(error) << \"Couldn't get display hdr metadata when colorspace selection indicates it should have one\";\n      }\n    }\n\n    std::unique_ptr<platf::avcodec_encode_device_t> encode_device_final;\n\n    if (!encode_device->data) {\n      auto software_encode_device = std::make_unique<avcodec_software_encode_device_t>();\n\n      if (software_encode_device->init(width, height, frame.get(), sw_fmt, hardware)) {\n        return nullptr;\n      }\n      software_encode_device->colorspace = colorspace;\n\n      encode_device_final = std::move(software_encode_device);\n    }\n    else {\n      encode_device_final = std::move(encode_device);\n    }\n\n    if (encode_device_final->set_frame(frame.release(), ctx->hw_frames_ctx)) {\n      return nullptr;\n    }\n\n    encode_device_final->apply_colorspace();\n\n    auto session = std::make_unique<avcodec_encode_session_t>(\n      std::move(ctx),\n      std::move(encode_device_final),\n\n      // 0 ==> don't inject, 1 ==> inject for h264, 2 ==> inject for hevc\n      config.videoFormat <= 1 ? (1 - (int) video_format[encoder_t::VUI_PARAMETERS]) * (1 + config.videoFormat) : 0);\n\n    return session;\n  }\n\n  std::unique_ptr<nvenc_encode_session_t>\n  make_nvenc_encode_session(platf::display_t *disp, const config_t &client_config, std::unique_ptr<platf::nvenc_encode_device_t> encode_device, bool is_probe = false) {\n    if (!encode_device->init_encoder(client_config, encode_device->colorspace, is_probe)) {\n      return nullptr;\n    }\n\n    // Set HDR metadata for NVENC encoder if HDR is enabled (both PQ and HLG)\n    // PQ needs mastering display + content light level SEI for proper absolute luminance mapping.\n    // HLG benefits from these SEI for HDR Vivid tone mapping on the decoder side.\n    if (colorspace_is_hdr(encode_device->colorspace) && encode_device->nvenc) {\n      SS_HDR_METADATA hdr_metadata;\n      if (disp->get_hdr_metadata(hdr_metadata)) {\n        nvenc::nvenc_hdr_metadata nvenc_metadata;\n        // Copy display primaries (RGB order)\n        for (int i = 0; i < 3; i++) {\n          nvenc_metadata.displayPrimaries[i].x = hdr_metadata.displayPrimaries[i].x;\n          nvenc_metadata.displayPrimaries[i].y = hdr_metadata.displayPrimaries[i].y;\n        }\n        nvenc_metadata.whitePoint.x = hdr_metadata.whitePoint.x;\n        nvenc_metadata.whitePoint.y = hdr_metadata.whitePoint.y;\n        nvenc_metadata.maxDisplayLuminance = hdr_metadata.maxDisplayLuminance;\n        nvenc_metadata.minDisplayLuminance = hdr_metadata.minDisplayLuminance;\n        nvenc_metadata.maxContentLightLevel = hdr_metadata.maxContentLightLevel;\n        nvenc_metadata.maxFrameAverageLightLevel = hdr_metadata.maxFrameAverageLightLevel;\n        encode_device->nvenc->set_hdr_metadata(nvenc_metadata);\n        BOOST_LOG(info) << \"NVENC: HDR metadata set - max luminance: \" << nvenc_metadata.maxDisplayLuminance\n                        << \" nits, mode: \" << (colorspace_is_hlg(encode_device->colorspace) ? \"HLG\" : \"PQ\");\n      }\n    }\n\n    return std::make_unique<nvenc_encode_session_t>(std::move(encode_device));\n  }\n\n  std::unique_ptr<amf_encode_session_t>\n  make_amf_encode_session(platf::display_t *disp, const config_t &client_config, std::unique_ptr<platf::amf_encode_device_t> encode_device, bool is_probe = false) {\n    if (!encode_device->init_encoder(client_config, encode_device->colorspace, is_probe)) {\n      return nullptr;\n    }\n\n    // Set HDR metadata for AMF encoder if HDR is enabled\n    if (colorspace_is_hdr(encode_device->colorspace) && encode_device->amf) {\n      SS_HDR_METADATA hdr_metadata;\n      if (disp->get_hdr_metadata(hdr_metadata)) {\n        amf::amf_hdr_metadata amf_metadata;\n        for (int i = 0; i < 3; i++) {\n          amf_metadata.displayPrimaries[i].x = hdr_metadata.displayPrimaries[i].x;\n          amf_metadata.displayPrimaries[i].y = hdr_metadata.displayPrimaries[i].y;\n        }\n        amf_metadata.whitePoint.x = hdr_metadata.whitePoint.x;\n        amf_metadata.whitePoint.y = hdr_metadata.whitePoint.y;\n        amf_metadata.maxDisplayLuminance = hdr_metadata.maxDisplayLuminance;\n        amf_metadata.minDisplayLuminance = hdr_metadata.minDisplayLuminance;\n        amf_metadata.maxContentLightLevel = hdr_metadata.maxContentLightLevel;\n        amf_metadata.maxFrameAverageLightLevel = hdr_metadata.maxFrameAverageLightLevel;\n        encode_device->amf->set_hdr_metadata(amf_metadata);\n        BOOST_LOG(info) << \"AMF: HDR metadata set - max luminance: \" << amf_metadata.maxDisplayLuminance\n                        << \" nits, mode: \" << (colorspace_is_hlg(encode_device->colorspace) ? \"HLG\" : \"PQ\");\n      }\n    }\n\n    return std::make_unique<amf_encode_session_t>(std::move(encode_device));\n  }\n\n  std::unique_ptr<encode_session_t>\n  make_encode_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr<platf::encode_device_t> encode_device, bool is_probe = false) {\n    if (dynamic_cast<platf::avcodec_encode_device_t *>(encode_device.get())) {\n      auto avcodec_encode_device = boost::dynamic_pointer_cast<platf::avcodec_encode_device_t>(std::move(encode_device));\n      return make_avcodec_encode_session(disp, encoder, config, width, height, std::move(avcodec_encode_device));\n    }\n    else if (dynamic_cast<platf::nvenc_encode_device_t *>(encode_device.get())) {\n      auto nvenc_encode_device = boost::dynamic_pointer_cast<platf::nvenc_encode_device_t>(std::move(encode_device));\n      return make_nvenc_encode_session(disp, config, std::move(nvenc_encode_device), is_probe);\n    }\n    else if (dynamic_cast<platf::amf_encode_device_t *>(encode_device.get())) {\n      auto amf_encode_device = boost::dynamic_pointer_cast<platf::amf_encode_device_t>(std::move(encode_device));\n      return make_amf_encode_session(disp, config, std::move(amf_encode_device), is_probe);\n    }\n\n    return nullptr;\n  }\n\n  /**\n   * @brief Get NTSC framerate for a given integer framerate.\n   * @details NTSC framerates are slightly lower than integer framerates:\n   *          120 -> 119.88 (120000/1001)\n   *          60 -> 59.94 (60000/1001)\n   *          30 -> 29.97 (30000/1001)\n   *          24 -> 23.976 (24000/1001)\n   * @param fps Integer framerate\n   * @param num Output numerator\n   * @param den Output denominator\n   * @return true if NTSC framerate is available for this fps\n   */\n  bool\n  get_ntsc_framerate(int fps, int &num, int &den) {\n    // NTSC framerate pattern: fps * 1000 / 1001\n    // Only support common framerates that have NTSC equivalents\n    static const int supported_fps[] = { 24, 30, 48, 60, 120, 144, 240 };\n    for (int supported : supported_fps) {\n      if (fps == supported) {\n        num = fps * 1000;\n        den = 1001;\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * @brief Create encode session with NTSC framerate fallback.\n   * @details If the initial framerate fails, try NTSC framerate (e.g., 120 -> 119.88fps).\n   * @param disp Display device\n   * @param encoder Encoder to use\n   * @param config Configuration (may be modified if NTSC fallback is used)\n   * @param width Frame width\n   * @param height Frame height\n   * @param make_encode_device_func Function to create encode device\n   * @return Encode session or nullptr on failure\n   */\n  std::unique_ptr<encode_session_t>\n  make_encode_session_with_ntsc_fallback(\n    platf::display_t *disp,\n    const encoder_t &encoder,\n    config_t &config,\n    int width,\n    int height,\n    std::function<std::unique_ptr<platf::encode_device_t>()> make_encode_device_func) {\n    // First try with original framerate\n    auto encode_device = make_encode_device_func();\n    if (!encode_device) {\n      return nullptr;\n    }\n\n    auto session = make_encode_session(disp, encoder, config, width, height, std::move(encode_device));\n    if (session) {\n      return session;\n    }\n\n    // If failed, try NTSC framerate fallback\n    int ntsc_num, ntsc_den;\n    if (get_ntsc_framerate(config.framerate, ntsc_num, ntsc_den)) {\n      BOOST_LOG(info) << \"Encoder initialization failed at \" << config.framerate << \"fps, \"\n                      << \"trying NTSC framerate \" << ntsc_num << \"/\" << ntsc_den\n                      << \" (\" << (double) ntsc_num / ntsc_den << \"fps)\";\n\n      config.frameRateNum = ntsc_num;\n      config.frameRateDen = ntsc_den;\n\n      // Create new encode device with NTSC framerate\n      encode_device = make_encode_device_func();\n      if (!encode_device) {\n        BOOST_LOG(warning) << \"Failed to create encode device with NTSC framerate\";\n        // Reset to integer framerate\n        config.frameRateNum = 0;\n        config.frameRateDen = 1;\n        return nullptr;\n      }\n\n      session = make_encode_session(disp, encoder, config, width, height, std::move(encode_device));\n      if (session) {\n        BOOST_LOG(info) << \"Successfully initialized encoder with NTSC framerate \"\n                        << (double) ntsc_num / ntsc_den << \"fps\";\n        return session;\n      }\n\n      // Reset to integer framerate if NTSC also failed\n      config.frameRateNum = 0;\n      config.frameRateDen = 1;\n      BOOST_LOG(warning) << \"NTSC framerate fallback also failed\";\n    }\n\n    return nullptr;\n  }\n\n  void\n  encode_run(\n    int &frame_nr,  // Store progress of the frame number\n    safe::mail_t mail,\n    img_event_t images,\n    config_t config,\n    std::shared_ptr<platf::display_t> disp,\n    std::unique_ptr<platf::encode_device_t> encode_device,\n    safe::signal_t &reinit_event,\n    const encoder_t &encoder,\n    void *channel_data,\n    std::optional<safe::mail_raw_t::event_t<dynamic_param_t>> dynamic_param_events) {\n    auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device));\n    if (!session) {\n      return;\n    }\n\n    // As a workaround for NVENC hangs and to generally speed up encoder reinit,\n    // we will complete the encoder teardown in a separate thread if supported.\n    // This will move expensive processing off the encoder thread to allow us\n    // to restart encoding as soon as possible. For cases where the NVENC driver\n    // hang occurs, this thread may probably never exit, but it will allow\n    // streaming to continue without requiring a full restart of Sunshine.\n    auto fail_guard = util::fail_guard([&encoder, &session] {\n      if (encoder.flags & ASYNC_TEARDOWN) {\n        std::thread encoder_teardown_thread { [session = std::move(session)]() mutable {\n          BOOST_LOG(info) << \"Starting async encoder teardown\";\n          session.reset();\n          BOOST_LOG(info) << \"Async encoder teardown complete\";\n        } };\n        encoder_teardown_thread.detach();\n      }\n    });\n\n    // set minimum frame time based on client-requested target framerate or minimum_fps_target\n    std::chrono::duration<double, std::milli> minimum_frame_time;\n    if (config::video.minimum_fps_target > 0) {\n      // Use minimum_fps_target if specified\n      minimum_frame_time = std::chrono::duration<double, std::milli> { 1000.0 / config::video.minimum_fps_target };\n      BOOST_LOG(info) << \"Minimum frame time set to \"sv << minimum_frame_time.count() << \"ms, based on minimum_fps_target \"sv << config::video.minimum_fps_target << \" fps.\"sv;\n    }\n    else {\n      // Default behavior: about half the stream FPS\n      minimum_frame_time = std::chrono::duration<double, std::milli> { 2000.0 / config.framerate };\n      BOOST_LOG(info) << \"Minimum frame time set to \"sv << minimum_frame_time.count() << \"ms, based on client-requested target framerate \"sv << config.framerate << \".\"sv;\n    }\n\n    auto shutdown_event = mail->event<bool>(mail::shutdown);\n    auto packets = mail::man->queue<packet_t>(mail::video_packets);\n    auto idr_events = mail->event<bool>(mail::idr);\n    auto invalidate_ref_frames_events = mail->event<std::pair<int64_t, int64_t>>(mail::invalidate_ref_frames);\n    auto dynamic_param_events_ptr = dynamic_param_events.value_or(mail::man->event<dynamic_param_t>(mail::dynamic_param_change));\n\n    {\n      // Load a dummy image into the AVFrame to ensure we have something to encode\n      // even if we timeout waiting on the first frame. This is a relatively large\n      // allocation which can be freed immediately after convert(), so we do this\n      // in a separate scope.\n      auto dummy_img = disp->alloc_img();\n      if (!dummy_img || disp->dummy_img(dummy_img.get()) || session->convert(*dummy_img)) {\n        return;\n      }\n    }\n\n    while (true) {\n      // Break out of the encoding loop if any of the following are true:\n      // a) The stream is ending\n      // b) Sunshine is quitting\n      // c) The capture side is waiting to reinit and we've encoded at least one frame\n      //\n      // If we have to reinit before we have received any captured frames, we will encode\n      // the blank dummy frame just to let Moonlight know that we're alive.\n      if (shutdown_event->peek() || !images->running() || (reinit_event.peek() && frame_nr > 1)) {\n        break;\n      }\n\n      bool requested_idr_frame = false;\n\n      while (invalidate_ref_frames_events->peek()) {\n        if (auto frames = invalidate_ref_frames_events->pop(0ms)) {\n          session->invalidate_ref_frames(frames->first, frames->second);\n        }\n      }\n\n      if (idr_events->peek()) {\n        requested_idr_frame = true;\n        idr_events->pop();\n      }\n\n      // 处理动态参数调整\n      while (dynamic_param_events_ptr->peek()) {\n        if (auto param = dynamic_param_events_ptr->pop(0ms)) {\n          BOOST_LOG(info) << \"Applying dynamic parameter change: type=\" << (int) param->type;\n          session->set_dynamic_param(*param);\n        }\n      }\n\n      if (requested_idr_frame) {\n        session->request_idr_frame();\n      }\n\n      std::optional<std::chrono::steady_clock::time_point> frame_timestamp;\n      bool has_new_frame = false;\n\n      // Encode at a minimum FPS to avoid image quality issues with static content\n      // When variable_refresh_rate is enabled, only encode when we have a new frame\n      if (!requested_idr_frame || images->peek()) {\n        if (auto img = images->pop(minimum_frame_time)) {\n          frame_timestamp = img->frame_timestamp;\n          if (session->convert(*img)) {\n            BOOST_LOG(error) << \"Could not convert image\"sv;\n            // Don't exit permanently — break to let the outer reinit loop handle recovery\n            break;\n          }\n          has_new_frame = true;\n        }\n        else if (!images->running()) {\n          break;\n        }\n      }\n\n      // If variable refresh rate is enabled, skip encoding when no new frame is available\n      // This allows the stream framerate to match the render framerate for VRR support\n      // However, if minimum_fps_target is set, we still encode to maintain minimum FPS\n      if (config::video.variable_refresh_rate && !has_new_frame && !requested_idr_frame) {\n        // Only skip if minimum_fps_target is 0 (disabled) or we've already met the minimum\n        if (config::video.minimum_fps_target == 0) {\n          continue;\n        }\n        // If minimum_fps_target is set, we'll encode anyway to maintain minimum FPS\n      }\n\n      if (encode(frame_nr++, *session, packets, channel_data, frame_timestamp)) {\n        BOOST_LOG(error) << \"Could not encode video packet\"sv;\n        // Don't exit permanently — break to let the outer reinit loop handle recovery\n        break;\n      }\n\n      session->request_normal_frame();\n    }\n  }\n\n  input::touch_port_t\n  make_port(platf::display_t *display, const config_t &config) {\n    float wd = display->width;\n    float hd = display->height;\n\n    float wt = config.width;\n    float ht = config.height;\n\n    auto scalar = std::fminf(wt / wd, ht / hd);\n\n    auto w2 = scalar * wd;\n    auto h2 = scalar * hd;\n\n    auto offsetX = (config.width - w2) * 0.5f;\n    auto offsetY = (config.height - h2) * 0.5f;\n\n    return input::touch_port_t {\n      {\n        display->offset_x,\n        display->offset_y,\n        config.width,\n        config.height,\n      },\n      display->env_width,\n      display->env_height,\n      offsetX,\n      offsetY,\n      1.0f / scalar,\n    };\n  }\n\n  std::unique_ptr<platf::encode_device_t>\n  make_encode_device(platf::display_t &disp, const encoder_t &encoder, const config_t &config) {\n    std::unique_ptr<platf::encode_device_t> result;\n\n    auto colorspace = colorspace_from_client_config(config, disp.is_hdr());\n\n    platf::pix_fmt_e pix_fmt;\n    if (config.chromaSamplingType == 1) {\n      // YUV 4:4:4\n      if (!(encoder.flags & YUV444_SUPPORT)) {\n        // Encoder can't support YUV 4:4:4 regardless of hardware capabilities\n        return {};\n      }\n      pix_fmt = (colorspace.bit_depth == 10) ?\n                  encoder.platform_formats->pix_fmt_yuv444_10bit :\n                  encoder.platform_formats->pix_fmt_yuv444_8bit;\n    }\n    else {\n      // YUV 4:2:0\n      pix_fmt = (colorspace.bit_depth == 10) ?\n                  encoder.platform_formats->pix_fmt_10bit :\n                  encoder.platform_formats->pix_fmt_8bit;\n    }\n\n    {\n      auto encoder_name = encoder.codec_from_config(config).name;\n\n      auto color_coding = colorspace.colorspace == colorspace_e::bt2020    ? \"HDR (Rec. 2020 + SMPTE 2084 PQ)\" :\n                          colorspace.colorspace == colorspace_e::bt2020hlg ? \"HDR (Rec. 2020 + HLG)\" :\n                          colorspace.colorspace == colorspace_e::rec601    ? \"SDR (Rec. 601)\" :\n                          colorspace.colorspace == colorspace_e::rec709    ? \"SDR (Rec. 709)\" :\n                          colorspace.colorspace == colorspace_e::bt2020sdr ? \"SDR (Rec. 2020)\" :\n                                                                             \"unknown\";\n\n      BOOST_LOG(info) << \"Creating encoder \" << logging::bracket(encoder_name)\n                      << \", Color coding: \" << color_coding\n                      << \", Color depth: \" << colorspace.bit_depth << \"-bit\"\n                      << \", Color range: \" << (colorspace.full_range ? \"JPEG\" : \"MPEG\");\n    }\n\n    if (dynamic_cast<const encoder_platform_formats_avcodec *>(encoder.platform_formats.get())) {\n      result = disp.make_avcodec_encode_device(pix_fmt);\n    }\n    else if (dynamic_cast<const encoder_platform_formats_nvenc *>(encoder.platform_formats.get())) {\n      result = disp.make_nvenc_encode_device(pix_fmt);\n    }\n    else if (dynamic_cast<const encoder_platform_formats_amf *>(encoder.platform_formats.get())) {\n      result = disp.make_amf_encode_device(pix_fmt);\n    }\n\n    if (result) {\n      result->colorspace = colorspace;\n    }\n\n    return result;\n  }\n\n  std::optional<sync_session_t>\n  make_synced_session(platf::display_t *disp, const encoder_t &encoder, platf::img_t &img, sync_session_ctx_t &ctx) {\n    sync_session_t encode_session;\n\n    encode_session.ctx = &ctx;\n\n    // absolute mouse coordinates require that the dimensions of the screen are known\n    ctx.touch_port_events->raise(make_port(disp, ctx.config));\n\n    // Create encode device with NTSC framerate fallback support\n    auto make_encode_device_func = [&]() {\n      return make_encode_device(*disp, encoder, ctx.config);\n    };\n\n    auto session = make_encode_session_with_ntsc_fallback(\n      disp, encoder, ctx.config, img.width, img.height, make_encode_device_func);\n    if (!session) {\n      return std::nullopt;\n    }\n\n    // Get encode device colorspace for HDR metadata (need to create a temporary device)\n    auto encode_device = make_encode_device(*disp, encoder, ctx.config);\n    if (!encode_device) {\n      return std::nullopt;\n    }\n\n    // Update client with our current HDR display state\n    hdr_info_t hdr_info = std::make_unique<hdr_info_raw_t>(false);\n    if (colorspace_is_hdr(encode_device->colorspace)) {\n      if (disp->get_hdr_metadata(hdr_info->metadata)) {\n        hdr_info->enabled = true;\n      }\n      else {\n        BOOST_LOG(error) << \"Couldn't get display hdr metadata when colorspace selection indicates it should have one\";\n      }\n    }\n    ctx.hdr_events->raise(std::move(hdr_info));\n\n    // Load the initial image to prepare for encoding\n    if (session->convert(img)) {\n      BOOST_LOG(error) << \"Could not convert initial image\"sv;\n      return std::nullopt;\n    }\n\n    encode_session.session = std::move(session);\n\n    return encode_session;\n  }\n\n  encode_e\n  encode_run_sync(\n    std::vector<std::unique_ptr<sync_session_ctx_t>> &synced_session_ctxs,\n    encode_session_ctx_queue_t &encode_session_ctx_queue,\n    std::vector<std::string> &display_names,\n    int &display_p) {\n    const auto &encoder = *chosen_encoder;\n\n    std::shared_ptr<platf::display_t> disp;\n\n    auto switch_display_event = mail::man->event<int>(mail::switch_display);\n\n    if (synced_session_ctxs.empty()) {\n      auto ctx = encode_session_ctx_queue.pop();\n      if (!ctx) {\n        return encode_e::ok;\n      }\n\n      synced_session_ctxs.emplace_back(std::make_unique<sync_session_ctx_t>(std::move(*ctx)));\n    }\n\n    while (encode_session_ctx_queue.running()) {\n      // Refresh display names since a display removal might have caused the reinitialization\n      refresh_displays(encoder.platform_formats->dev_type, display_names, display_p);\n\n      // Process any pending display switch with the new list of displays\n      bool user_switched = false;\n      if (switch_display_event->peek()) {\n        display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1);\n        user_switched = true;\n      }\n\n      // Use client-specified display_name if provided (only for auto-reinit, not manual switch)\n      const auto &config = synced_session_ctxs.front()->config;\n      std::string target_display_name = display_names[display_p];\n      if (!user_switched && !config.display_name.empty()) {\n        // config.display_name may be a device ID - convert to display name\n        std::string resolved_display_name = display_device::get_display_name(config.display_name);\n        if (resolved_display_name.empty()) {\n          resolved_display_name = config.display_name;\n        }\n\n        // Try to find the display in the list\n        bool found = false;\n        for (int x = 0; x < display_names.size(); ++x) {\n          if (display_names[x] == resolved_display_name) {\n            display_p = x;\n            target_display_name = resolved_display_name;\n            found = true;\n            break;\n          }\n        }\n        if (!found) {\n          BOOST_LOG(warning) << \"Client-specified display [\" << config.display_name << \"] (resolved: \" << resolved_display_name << \") not found, using default display\";\n        }\n      }\n\n      // reset_display() will sleep between retries\n      reset_display(disp, encoder.platform_formats->dev_type, target_display_name, config);\n      if (disp) {\n        break;\n      }\n    }\n\n    if (!disp) {\n      return encode_e::error;\n    }\n\n    auto img = disp->alloc_img();\n    if (!img || disp->dummy_img(img.get())) {\n      return encode_e::error;\n    }\n\n    std::vector<sync_session_t> synced_sessions;\n    for (auto &ctx : synced_session_ctxs) {\n      auto synced_session = make_synced_session(disp.get(), encoder, *img, *ctx);\n      if (!synced_session) {\n        return encode_e::error;\n      }\n\n      synced_sessions.emplace_back(std::move(*synced_session));\n    }\n\n    auto ec = platf::capture_e::ok;\n    while (encode_session_ctx_queue.running()) {\n      auto push_captured_image_callback = [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) -> bool {\n        while (encode_session_ctx_queue.peek()) {\n          auto encode_session_ctx = encode_session_ctx_queue.pop();\n          if (!encode_session_ctx) {\n            return false;\n          }\n\n          synced_session_ctxs.emplace_back(std::make_unique<sync_session_ctx_t>(std::move(*encode_session_ctx)));\n\n          auto encode_session = make_synced_session(disp.get(), encoder, *img, *synced_session_ctxs.back());\n          if (!encode_session) {\n            ec = platf::capture_e::error;\n            return false;\n          }\n\n          synced_sessions.emplace_back(std::move(*encode_session));\n        }\n\n        KITTY_WHILE_LOOP(auto pos = std::begin(synced_sessions), pos != std::end(synced_sessions), {\n          auto ctx = pos->ctx;\n          if (ctx->shutdown_event->peek()) {\n            // Let waiting thread know it can delete shutdown_event\n            ctx->join_event->raise(true);\n\n            pos = synced_sessions.erase(pos);\n            synced_session_ctxs.erase(std::find_if(std::begin(synced_session_ctxs), std::end(synced_session_ctxs), [&ctx_p = ctx](auto &ctx) {\n              return ctx.get() == ctx_p;\n            }));\n\n            if (synced_sessions.empty()) {\n              return false;\n            }\n\n            continue;\n          }\n\n          if (ctx->idr_events->peek()) {\n            pos->session->request_idr_frame();\n            ctx->idr_events->pop();\n          }\n\n          if (frame_captured && pos->session->convert(*img)) {\n            BOOST_LOG(error) << \"Could not convert image\"sv;\n            ctx->shutdown_event->raise(true);\n\n            continue;\n          }\n\n          std::optional<std::chrono::steady_clock::time_point> frame_timestamp;\n          if (img) {\n            frame_timestamp = img->frame_timestamp;\n          }\n\n          if (encode(ctx->frame_nr++, *pos->session, ctx->packets, ctx->channel_data, frame_timestamp)) {\n            BOOST_LOG(error) << \"Could not encode video packet\"sv;\n            ctx->shutdown_event->raise(true);\n\n            continue;\n          }\n\n          pos->session->request_normal_frame();\n\n          ++pos;\n        })\n\n        if (switch_display_event->peek()) {\n          ec = platf::capture_e::reinit;\n          return false;\n        }\n\n        return true;\n      };\n\n      auto pull_free_image_callback = [&img](std::shared_ptr<platf::img_t> &img_out) -> bool {\n        img_out = img;\n        img_out->frame_timestamp.reset();\n        return true;\n      };\n\n      auto status = disp->capture(push_captured_image_callback, pull_free_image_callback, &display_cursor);\n      switch (status) {\n        case platf::capture_e::reinit:\n        case platf::capture_e::error:\n        case platf::capture_e::ok:\n        case platf::capture_e::timeout:\n        case platf::capture_e::interrupted:\n          return ec != platf::capture_e::ok ? ec : status;\n      }\n    }\n\n    return encode_e::ok;\n  }\n\n  void\n  captureThreadSync() {\n    auto ref = capture_thread_sync.ref();\n\n    std::vector<std::unique_ptr<sync_session_ctx_t>> synced_session_ctxs;\n\n    auto &ctx = ref->encode_session_ctx_queue;\n    auto lg = util::fail_guard([&]() {\n      ctx.stop();\n\n      for (auto &ctx : synced_session_ctxs) {\n        ctx->shutdown_event->raise(true);\n        ctx->join_event->raise(true);\n      }\n\n      for (auto &ctx : ctx.unsafe()) {\n        ctx.shutdown_event->raise(true);\n        ctx.join_event->raise(true);\n      }\n    });\n\n    // Encoding and capture takes place on this thread\n    platf::adjust_thread_priority(platf::thread_priority_e::high);\n\n    std::vector<std::string> display_names;\n    int display_p = -1;\n    while (encode_run_sync(synced_session_ctxs, ctx, display_names, display_p) == encode_e::reinit) {}\n  }\n\n  void\n  capture_async(\n    safe::mail_t mail,\n    config_t &config,\n    void *channel_data,\n    std::optional<safe::mail_raw_t::event_t<dynamic_param_t>> dynamic_param_events) {\n    auto shutdown_event = mail->event<bool>(mail::shutdown);\n\n    auto images = std::make_shared<img_event_t::element_type>();\n    auto lg = util::fail_guard([&]() {\n      images->stop();\n      shutdown_event->raise(true);\n    });\n\n    auto ref = capture_thread_async.ref();\n    if (!ref) {\n      return;\n    }\n\n    ref->capture_ctx_queue->raise(capture_ctx_t { images, config });\n\n    if (!ref->capture_ctx_queue->running()) {\n      return;\n    }\n\n    int frame_nr = 1;\n\n    auto touch_port_event = mail->event<input::touch_port_t>(mail::touch_port);\n    auto hdr_event = mail->event<hdr_info_t>(mail::hdr);\n    auto idr_events = mail->event<bool>(mail::idr);\n    auto resolution_change_event = mail->event<std::pair<std::uint32_t, std::uint32_t>>(mail::resolution_change);\n\n    // Encoding takes place on this thread\n    platf::adjust_thread_priority(platf::thread_priority_e::high);\n\n    // Cache window capture mode check outside the loop\n    const bool is_window_capture = (config::video.capture_target == \"window\");\n\n    // Track display dimensions for resolution change detection\n    int last_display_width = 0;\n    int last_display_height = 0;\n\n    // Track initial scale ratio (encoding resolution / display resolution)\n    // Used to maintain consistent scaling when display resolution changes\n    float initial_scale_x = 1.0f;\n    float initial_scale_y = 1.0f;\n\n    while (!shutdown_event->peek() && images->running()) {\n      // Wait for the main capture event when the display is being reinitialized\n      if (ref->reinit_event.peek()) {\n        BOOST_LOG(debug) << \"[Display] Reinit event detected, waiting for display ready...\";\n        std::this_thread::sleep_for(20ms);\n        continue;\n      }\n\n      // Wait for the display to be ready\n      std::shared_ptr<platf::display_t> display;\n      {\n        auto lg = ref->display_wp.lock();\n        if (ref->display_wp->expired()) {\n          BOOST_LOG(verbose) << \"[Display] Display object expired, waiting for reinit...\";\n          // std::this_thread::sleep_for(20ms);\n          continue;\n        }\n        display = ref->display_wp->lock();\n      }\n\n      // Detect display resolution changes (e.g., rotation causing width/height swap)\n      // For WGC window capture, display->width/height is monitor resolution, not window size\n      const int current_width = display->width;\n      const int current_height = display->height;\n\n      // Helper lambda to compute even-aligned resolution with minimum 64\n      auto compute_aligned_resolution = [](int dimension, float scale) {\n        return std::max(64, (static_cast<int>(dimension * scale) + 1) & ~1);\n      };\n\n      // Initialize cached display dimensions on first iteration\n      if (last_display_width == 0 && last_display_height == 0) {\n        last_display_width = current_width;\n        last_display_height = current_height;\n\n        // Check if display orientation matches client request\n        const bool display_is_portrait = (current_height > current_width);\n        const bool client_wants_landscape = (config.width > config.height);\n        const bool orientation_mismatch = !is_window_capture &&\n                                          (display_is_portrait == client_wants_landscape);\n\n        if (orientation_mismatch) {\n          // When orientation mismatches, client width maps to display height and vice versa\n          initial_scale_x = static_cast<float>(config.width) / current_height;\n          initial_scale_y = static_cast<float>(config.height) / current_width;\n          BOOST_LOG(info) << \"Display orientation mismatch: display=\"\n                          << current_width << \"x\" << current_height\n                          << \", client=\" << config.width << \"x\" << config.height\n                          << \" -> using display resolution\";\n        }\n        else {\n          initial_scale_x = static_cast<float>(config.width) / current_width;\n          initial_scale_y = static_cast<float>(config.height) / current_height;\n          BOOST_LOG(info) << \"Initial display: \" << current_width << \"x\" << current_height\n                          << \", encoding: \" << config.width << \"x\" << config.height\n                          << \", scale: \" << initial_scale_x << \"x\" << initial_scale_y;\n        }\n\n        config.width = compute_aligned_resolution(current_width, initial_scale_x);\n        config.height = compute_aligned_resolution(current_height, initial_scale_y);\n\n        resolution_change_event->raise(std::make_pair(\n          static_cast<std::uint32_t>(current_width),\n          static_cast<std::uint32_t>(current_height)));\n\n        if (orientation_mismatch) {\n          idr_events->raise(true);\n        }\n      }\n      else if (!is_window_capture &&\n               (current_width != last_display_width || current_height != last_display_height)) {\n        const bool is_rotation = (last_display_width == current_height && last_display_height == current_width);\n\n        BOOST_LOG(info) << \"Display resolution changed: \"\n                        << last_display_width << \"x\" << last_display_height << \" -> \"\n                        << current_width << \"x\" << current_height\n                        << (is_rotation ? \" (rotation)\" : \"\");\n\n        last_display_width = current_width;\n        last_display_height = current_height;\n\n        if (is_rotation) {\n          std::swap(initial_scale_x, initial_scale_y);\n        }\n\n        config.width = compute_aligned_resolution(current_width, initial_scale_x);\n        config.height = compute_aligned_resolution(current_height, initial_scale_y);\n\n        BOOST_LOG(info) << \"New encoding resolution: \" << config.width << \"x\" << config.height\n                        << \" (scale: \" << initial_scale_x << \"x\" << initial_scale_y << \")\";\n\n        resolution_change_event->raise(std::make_pair(\n          static_cast<std::uint32_t>(current_width),\n          static_cast<std::uint32_t>(current_height)));\n\n        idr_events->raise(true);\n        std::this_thread::sleep_for(100ms);\n      }\n\n      auto &encoder = *chosen_encoder;\n\n      auto encode_device = make_encode_device(*display, encoder, config);\n      if (!encode_device) {\n        return;\n      }\n\n      // Absolute mouse coordinates require that the dimensions of the screen are known\n      touch_port_event->raise(make_port(display.get(), config));\n\n      // Update client with our current HDR display state\n      hdr_info_t hdr_info = std::make_unique<hdr_info_raw_t>(false);\n      if (colorspace_is_hdr(encode_device->colorspace)) {\n        if (display->get_hdr_metadata(hdr_info->metadata)) {\n          hdr_info->enabled = true;\n        }\n        else {\n          BOOST_LOG(error) << \"Couldn't get display HDR metadata when colorspace indicates it should have one\";\n        }\n      }\n      hdr_event->raise(std::move(hdr_info));\n\n      encode_run(\n        frame_nr,\n        mail, images,\n        config, display,\n        std::move(encode_device),\n        ref->reinit_event, *ref->encoder_p,\n        channel_data, dynamic_param_events);\n    }\n  }\n\n  void\n  capture(\n    safe::mail_t mail,\n    config_t config,\n    void *channel_data,\n    std::optional<safe::mail_raw_t::event_t<dynamic_param_t>> dynamic_param_events) {\n    auto idr_events = mail->event<bool>(mail::idr);\n\n    idr_events->raise(true);\n    if (chosen_encoder->flags & PARALLEL_ENCODING) {\n      capture_async(std::move(mail), config, channel_data, dynamic_param_events);\n    }\n    else {\n      safe::signal_t join_event;\n      auto ref = capture_thread_sync.ref();\n      ref->encode_session_ctx_queue.raise(sync_session_ctx_t {\n        &join_event,\n        mail->event<bool>(mail::shutdown),\n        mail::man->queue<packet_t>(mail::video_packets),\n        std::move(idr_events),\n        mail->event<hdr_info_t>(mail::hdr),\n        mail->event<input::touch_port_t>(mail::touch_port),\n        config,\n        1,\n        channel_data,\n      });\n\n      // Wait for join signal\n      join_event.view();\n    }\n  }\n\n  enum validate_flag_e {\n    VUI_PARAMS = 0x01,  ///< VUI parameters\n  };\n\n  int\n  validate_config(std::shared_ptr<platf::display_t> disp, const encoder_t &encoder, const config_t &config) {\n    auto encode_device = make_encode_device(*disp, encoder, config);\n    if (!encode_device) {\n      return -1;\n    }\n\n    auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device), true);\n    if (!session) {\n      return -1;\n    }\n\n    {\n      // Image buffers are large, so we use a separate scope to free it immediately after convert()\n      auto img = disp->alloc_img();\n      if (!img || disp->dummy_img(img.get()) || session->convert(*img)) {\n        return -1;\n      }\n    }\n\n    session->request_idr_frame();\n\n    auto packets = mail::man->queue<packet_t>(mail::video_packets);\n    auto encode_start = std::chrono::steady_clock::now();\n    while (!packets->peek()) {\n      if (encode(1, *session, packets, nullptr, {})) {\n        return -1;\n      }\n      // Timeout protection: if encoding takes more than 5 seconds, it's likely hung\n      if (std::chrono::steady_clock::now() - encode_start > std::chrono::seconds(5)) {\n        BOOST_LOG(error) << \"validate_config: encode timed out (5s), encoder may be incompatible with current settings\";\n        return -1;\n      }\n    }\n\n    auto packet = packets->pop();\n    if (!packet->is_idr()) {\n      BOOST_LOG(error) << \"First packet type is not an IDR frame\"sv;\n\n      return -1;\n    }\n\n    int flag = 0;\n\n    // This check only applies for H.264 and HEVC\n    if (config.videoFormat <= 1) {\n      if (auto packet_avcodec = dynamic_cast<packet_raw_avcodec *>(packet.get())) {\n        if (cbs::validate_sps(packet_avcodec->av_packet, config.videoFormat ? AV_CODEC_ID_H265 : AV_CODEC_ID_H264)) {\n          flag |= VUI_PARAMS;\n        }\n      }\n      else {\n        // Don't check it for non-avcodec encoders.\n        flag |= VUI_PARAMS;\n      }\n    }\n\n    return flag;\n  }\n\n  /**\n   * @brief Validate encoder configuration, with optional NTSC framerate fallback.\n   * @details If the integer framerate fails, try NTSC framerate (e.g., 120 -> 119.88fps).\n   * @param disp Display device\n   * @param encoder Encoder to test\n   * @param config Configuration to test\n   * @param try_ntsc_fallback Whether to try NTSC framerate if integer framerate fails\n   * @return Validation flags on success, -1 on failure\n   */\n  int\n  validate_config_with_fallback(std::shared_ptr<platf::display_t> disp, const encoder_t &encoder, config_t &config, bool try_ntsc_fallback = true) {\n    // First try with the original framerate\n    auto result = validate_config(disp, encoder, config);\n    if (result >= 0) {\n      return result;\n    }\n\n    // If failed and NTSC fallback is enabled, try NTSC framerate\n    if (try_ntsc_fallback) {\n      int ntsc_num, ntsc_den;\n      if (get_ntsc_framerate(config.framerate, ntsc_num, ntsc_den)) {\n        BOOST_LOG(info) << \"Integer framerate \" << config.framerate << \"fps failed, trying NTSC framerate \"\n                        << ntsc_num << \"/\" << ntsc_den << \" (\" << (double) ntsc_num / ntsc_den << \"fps)\";\n\n        config.frameRateNum = ntsc_num;\n        config.frameRateDen = ntsc_den;\n\n        result = validate_config(disp, encoder, config);\n        if (result >= 0) {\n          BOOST_LOG(info) << \"NTSC framerate \" << (double) ntsc_num / ntsc_den << \"fps succeeded\";\n          return result;\n        }\n\n        // Reset to integer framerate if NTSC also failed\n        config.frameRateNum = 0;\n        config.frameRateDen = 1;\n        BOOST_LOG(warning) << \"NTSC framerate fallback also failed\";\n      }\n    }\n\n    return -1;\n  }\n\n  bool\n  validate_encoder(encoder_t &encoder, bool expect_failure) {\n    std::shared_ptr<platf::display_t> disp;\n\n    BOOST_LOG(info) << \"Trying encoder [\"sv << encoder.name << ']';\n    auto fg = util::fail_guard([&]() {\n      BOOST_LOG(info) << \"Encoder [\"sv << encoder.name << \"] failed\"sv;\n    });\n\n    // Quick GPU compatibility check: skip encoders that definitely won't work on this GPU\n    // This optimization prevents testing encoders on incompatible hardware (e.g., NVIDIA NVENC on AMD GPU)\n    // Extract GPU vendor from encoder name or check against known incompatible combinations\n    if (encoder.name.find(\"nvenc\") != std::string::npos || encoder.name.find(\"cuda\") != std::string::npos) {\n      // NVIDIA encoders - would need NVIDIA GPU\n      // We'll let the actual validation fail naturally, but at a fast level\n    }\n    else if (encoder.name.find(\"quicksync\") != std::string::npos || encoder.name.find(\"qsv\") != std::string::npos) {\n      // Intel QuickSync - would need Intel GPU\n      // We'll let the actual validation fail naturally\n    }\n\n    auto test_hevc = active_hevc_mode >= 2 || (active_hevc_mode == 0 && !(encoder.flags & H264_ONLY));\n    auto test_av1 = active_av1_mode >= 2 || (active_av1_mode == 0 && !(encoder.flags & H264_ONLY));\n\n    encoder.h264.capabilities.set();\n    encoder.hevc.capabilities.set();\n    encoder.av1.capabilities.set();\n\n    // First, test encoder viability\n    // Note: videoFormat starts at 0 (H.264), will be changed to 1 (HEVC) or 2 (AV1) later if needed\n    config_t config_max_ref_frames { 1920, 1080, 60, 1000, 1, 1, 1, 0, 0, 0, 0 };\n    config_t config_autoselect { 1920, 1080, 60, 1000, 1, 1, 0, 0, 0, 0, 0 };\n\n    // If the encoder isn't supported at all (not even H.264), bail early\n    const auto output_display_name { display_device::get_display_name(config::video.output_name) };\n    reset_display(disp, encoder.platform_formats->dev_type, output_display_name, config_autoselect);\n    if (!disp) {\n      return false;\n    }\n    if (!disp->is_codec_supported(encoder.h264.name, config_autoselect)) {\n      fg.disable();\n      BOOST_LOG(info) << \"Encoder [\"sv << encoder.name << \"] is not supported on this GPU\"sv;\n      return false;\n    }\n\n    // If we're expecting failure, use the autoselect ref config first since that will always succeed\n    // if the encoder is available.\n    auto max_ref_frames_h264 = expect_failure ? -1 : validate_config(disp, encoder, config_max_ref_frames);\n    auto autoselect_h264 = max_ref_frames_h264 >= 0 ? max_ref_frames_h264 : validate_config(disp, encoder, config_autoselect);\n    if (autoselect_h264 < 0) {\n      return false;\n    }\n    else if (expect_failure) {\n      // We expected failure, but actually succeeded. Do the max_ref_frames probe we skipped.\n      max_ref_frames_h264 = validate_config(disp, encoder, config_max_ref_frames);\n    }\n\n    std::vector<std::pair<validate_flag_e, encoder_t::flag_e>> packet_deficiencies {\n      { VUI_PARAMS, encoder_t::VUI_PARAMETERS },\n    };\n\n    for (auto [validate_flag, encoder_flag] : packet_deficiencies) {\n      encoder.h264[encoder_flag] = (max_ref_frames_h264 & validate_flag && autoselect_h264 & validate_flag);\n    }\n\n    encoder.h264[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_h264 >= 0;\n    encoder.h264[encoder_t::PASSED] = true;\n\n    if (test_hevc) {\n      config_max_ref_frames.videoFormat = 1;\n      config_autoselect.videoFormat = 1;\n\n      if (disp->is_codec_supported(encoder.hevc.name, config_autoselect)) {\n        auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames);\n\n        // If H.264 succeeded with max ref frames specified, assume that we can count on\n        // HEVC to also succeed with max ref frames specified if HEVC is supported.\n        auto autoselect_hevc = (max_ref_frames_hevc >= 0 || max_ref_frames_h264 >= 0) ?\n                                 max_ref_frames_hevc :\n                                 validate_config(disp, encoder, config_autoselect);\n\n        for (auto [validate_flag, encoder_flag] : packet_deficiencies) {\n          encoder.hevc[encoder_flag] = (max_ref_frames_hevc & validate_flag && autoselect_hevc & validate_flag);\n        }\n\n        encoder.hevc[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_hevc >= 0;\n        encoder.hevc[encoder_t::PASSED] = max_ref_frames_hevc >= 0 || autoselect_hevc >= 0;\n      }\n      else {\n        BOOST_LOG(info) << \"Encoder [\"sv << encoder.hevc.name << \"] is not supported on this GPU\"sv;\n        encoder.hevc.capabilities.reset();\n      }\n    }\n    else {\n      // Clear all cap bits for HEVC if we didn't probe it\n      encoder.hevc.capabilities.reset();\n    }\n\n    if (test_av1) {\n      config_max_ref_frames.videoFormat = 2;\n      config_autoselect.videoFormat = 2;\n\n      if (disp->is_codec_supported(encoder.av1.name, config_autoselect)) {\n        auto max_ref_frames_av1 = validate_config(disp, encoder, config_max_ref_frames);\n\n        // If H.264 succeeded with max ref frames specified, assume that we can count on\n        // AV1 to also succeed with max ref frames specified if AV1 is supported.\n        auto autoselect_av1 = (max_ref_frames_av1 >= 0 || max_ref_frames_h264 >= 0) ?\n                                max_ref_frames_av1 :\n                                validate_config(disp, encoder, config_autoselect);\n\n        for (auto [validate_flag, encoder_flag] : packet_deficiencies) {\n          encoder.av1[encoder_flag] = (max_ref_frames_av1 & validate_flag && autoselect_av1 & validate_flag);\n        }\n\n        encoder.av1[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_av1 >= 0;\n        encoder.av1[encoder_t::PASSED] = max_ref_frames_av1 >= 0 || autoselect_av1 >= 0;\n      }\n      else {\n        BOOST_LOG(info) << \"Encoder [\"sv << encoder.av1.name << \"] is not supported on this GPU\"sv;\n        encoder.av1.capabilities.reset();\n      }\n    }\n    else {\n      // Clear all cap bits for AV1 if we didn't probe it\n      encoder.av1.capabilities.reset();\n    }\n\n    // Test HDR and YUV444 support\n    {\n#ifdef _WIN32\n      const bool is_rdp_session = !is_running_as_system_user && display_device::w_utils::is_any_rdp_session_active();\n#else\n      const bool is_rdp_session = false;\n#endif\n\n      // H.264 is special because encoders may support YUV 4:4:4 without supporting 10-bit color depth\n      if (encoder.flags & YUV444_SUPPORT) {\n        config_t config_h264_yuv444 { 1920, 1080, 60, 1000, 1, 1, 0, 0, 0, 0, 1 };\n        encoder.h264[encoder_t::YUV444] = disp->is_codec_supported(encoder.h264.name, config_h264_yuv444) &&\n                                          validate_config(disp, encoder, config_h264_yuv444) >= 0;\n      }\n      else {\n        encoder.h264[encoder_t::YUV444] = false;\n      }\n\n      // HDR is not supported with H.264\n      encoder.h264[encoder_t::DYNAMIC_RANGE] = false;\n\n      // Skip HDR testing in RDP/virtual display environments\n      if (is_rdp_session) {\n        BOOST_LOG(info) << \"Skipping HDR testing in RDP environment\";\n        encoder.hevc[encoder_t::DYNAMIC_RANGE] = false;\n        encoder.av1[encoder_t::DYNAMIC_RANGE] = false;\n      }\n      else {\n        const config_t generic_hdr_config = { 1920, 1080, 60, 1000, 1, 1, 0, 3, 1, 1, 0 };\n\n        // Reset the display since we're switching from SDR to HDR\n        reset_display(disp, encoder.platform_formats->dev_type, output_display_name, generic_hdr_config);\n        if (!disp) {\n          return false;\n        }\n\n        auto test_hdr_and_yuv444 = [&](auto &flag_map, int video_format) {\n          if (!flag_map[encoder_t::PASSED]) {\n            flag_map[encoder_t::DYNAMIC_RANGE] = false;\n            flag_map[encoder_t::YUV444] = false;\n            return;\n          }\n\n          auto config = generic_hdr_config;\n          config.videoFormat = video_format;\n          auto encoder_codec_name = encoder.codec_from_config(config).name;\n\n          // Test 4:4:4 HDR first. If 4:4:4 is supported, 4:2:0 should also be supported.\n          if (encoder.flags & YUV444_SUPPORT) {\n            config.chromaSamplingType = 1;\n            if (disp->is_codec_supported(encoder_codec_name, config) &&\n                validate_config(disp, encoder, config) >= 0) {\n              flag_map[encoder_t::DYNAMIC_RANGE] = true;\n              flag_map[encoder_t::YUV444] = true;\n              return;\n            }\n          }\n          flag_map[encoder_t::YUV444] = false;\n\n          // Test 4:2:0 HDR\n          config.chromaSamplingType = 0;\n          flag_map[encoder_t::DYNAMIC_RANGE] = disp->is_codec_supported(encoder_codec_name, config) &&\n                                               validate_config(disp, encoder, config) >= 0;\n        };\n\n        test_hdr_and_yuv444(encoder.hevc, 1);\n        test_hdr_and_yuv444(encoder.av1, 2);\n      }\n    }\n\n    encoder.h264[encoder_t::VUI_PARAMETERS] = encoder.h264[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE];\n    encoder.hevc[encoder_t::VUI_PARAMETERS] = encoder.hevc[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE];\n\n    if (!encoder.h264[encoder_t::VUI_PARAMETERS]) {\n      BOOST_LOG(warning) << encoder.name << \": h264 missing sps->vui parameters\"sv;\n    }\n    if (encoder.hevc[encoder_t::PASSED] && !encoder.hevc[encoder_t::VUI_PARAMETERS]) {\n      BOOST_LOG(warning) << encoder.name << \": hevc missing sps->vui parameters\"sv;\n    }\n\n    fg.disable();\n    return true;\n  }\n\n  int\n  probe_encoders() {\n    if (!allow_encoder_probing()) {\n      // Error already logged\n      return -1;\n    }\n    auto encoder_list = encoders;\n\n    // If we already have a good encoder, check to see if another probe is required\n    if (chosen_encoder && !(chosen_encoder->flags & ALWAYS_REPROBE) && !platf::needs_encoder_reenumeration()) {\n      BOOST_LOG(info) << \"Using cached encoder validation results\";\n      return 0;\n    }\n\n    // Restart encoder selection\n    auto previous_encoder = chosen_encoder;\n    chosen_encoder = nullptr;\n    active_hevc_mode = config::video.hevc_mode;\n    active_av1_mode = config::video.av1_mode;\n    last_encoder_probe_supported_ref_frames_invalidation = false;\n\n    auto adjust_encoder_constraints = [&](encoder_t *encoder) {\n      // If we can't satisfy both the encoder and codec requirement, prefer the encoder over codec support\n      if (active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) {\n        BOOST_LOG(warning) << \"Encoder [\"sv << encoder->name << \"] does not support HEVC Main10 on this system\"sv;\n        active_hevc_mode = 0;\n      }\n      else if (active_hevc_mode == 2 && !encoder->hevc[encoder_t::PASSED]) {\n        BOOST_LOG(warning) << \"Encoder [\"sv << encoder->name << \"] does not support HEVC on this system\"sv;\n        active_hevc_mode = 0;\n      }\n\n      if (active_av1_mode == 3 && !encoder->av1[encoder_t::DYNAMIC_RANGE]) {\n        BOOST_LOG(warning) << \"Encoder [\"sv << encoder->name << \"] does not support AV1 Main10 on this system\"sv;\n        active_av1_mode = 0;\n      }\n      else if (active_av1_mode == 2 && !encoder->av1[encoder_t::PASSED]) {\n        BOOST_LOG(warning) << \"Encoder [\"sv << encoder->name << \"] does not support AV1 on this system\"sv;\n        active_av1_mode = 0;\n      }\n    };\n\n    if (!config::video.encoder.empty()) {\n      // If there is a specific encoder specified, use it if it passes validation\n      KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), {\n        auto encoder = *pos;\n\n        if (encoder->name == config::video.encoder) {\n          // Remove the encoder from the list entirely if it fails validation\n          if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) {\n            pos = encoder_list.erase(pos);\n            break;\n          }\n\n          // We will return an encoder here even if it fails one of the codec requirements specified by the user\n          adjust_encoder_constraints(encoder);\n\n          chosen_encoder = encoder;\n          break;\n        }\n\n        pos++;\n      });\n\n      if (chosen_encoder == nullptr) {\n        BOOST_LOG(error) << \"Couldn't find any working encoder matching [\"sv << config::video.encoder << ']';\n      }\n    }\n\n    BOOST_LOG(info) << \"Testing for available encoders - Errors during this phase can be ignored (测试可用编码器 - 此阶段的错误可以忽略)\";\n\n    // If we haven't found an encoder yet, but we want one with specific codec support, search for that now.\n    if (chosen_encoder == nullptr && (active_hevc_mode >= 2 || active_av1_mode >= 2)) {\n      KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), {\n        auto encoder = *pos;\n\n        // Remove the encoder from the list entirely if it fails validation\n        if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) {\n          pos = encoder_list.erase(pos);\n          continue;\n        }\n\n        // Skip it if it doesn't support the specified codec at all\n        if ((active_hevc_mode >= 2 && !encoder->hevc[encoder_t::PASSED]) ||\n            (active_av1_mode >= 2 && !encoder->av1[encoder_t::PASSED])) {\n          pos++;\n          continue;\n        }\n\n        // Skip it if it doesn't support HDR on the specified codec\n        if ((active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) ||\n            (active_av1_mode == 3 && !encoder->av1[encoder_t::DYNAMIC_RANGE])) {\n          pos++;\n          continue;\n        }\n\n        chosen_encoder = encoder;\n        break;\n      });\n\n      if (chosen_encoder == nullptr) {\n        BOOST_LOG(error) << \"Couldn't find any working encoder that meets HEVC/AV1 requirements\"sv;\n      }\n    }\n\n    // If no encoder was specified or the specified encoder was unusable, keep trying\n    // the remaining encoders until we find one that passes validation.\n    if (chosen_encoder == nullptr) {\n      KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), {\n        auto encoder = *pos;\n\n        // If we've used a previous encoder and it's not this one, we expect this encoder to\n        // fail to validate. It will use a slightly different order of checks to more quickly\n        // eliminate failing encoders.\n        if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) {\n          pos = encoder_list.erase(pos);\n          continue;\n        }\n\n        // We will return an encoder here even if it fails one of the codec requirements specified by the user\n        adjust_encoder_constraints(encoder);\n\n        chosen_encoder = encoder;\n        break;\n      });\n    }\n\n    if (chosen_encoder == nullptr) {\n      const auto output_display_name { display_device::get_display_name(config::video.output_name) };\n      BOOST_LOG(error) << \"Unable to find display or encoder during startup.\"sv;\n      if (!config::video.adapter_name.empty() || !output_display_name.empty()) {\n        BOOST_LOG(error) << \"Please ensure your manually chosen GPU and monitor are connected and powered on.\"sv;\n      }\n      else {\n        BOOST_LOG(fatal) << \"Please check that a display is connected and powered on.\"sv;\n      }\n      return -1;\n    }\n\n    BOOST_LOG(info) << \"Ignore any errors, Encoder testing completed (忽略任何错误，编码器测试完成)\";\n\n    auto &encoder = *chosen_encoder;\n\n    last_encoder_probe_supported_ref_frames_invalidation = (encoder.flags & REF_FRAMES_INVALIDATION);\n    last_encoder_probe_supported_yuv444_for_codec[0] = encoder.h264[encoder_t::PASSED] &&\n                                                       encoder.h264[encoder_t::YUV444];\n    last_encoder_probe_supported_yuv444_for_codec[1] = encoder.hevc[encoder_t::PASSED] &&\n                                                       encoder.hevc[encoder_t::YUV444];\n    last_encoder_probe_supported_yuv444_for_codec[2] = encoder.av1[encoder_t::PASSED] &&\n                                                       encoder.av1[encoder_t::YUV444];\n\n    BOOST_LOG(debug) << \"------  h264 ------\"sv;\n    for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {\n      auto flag = (encoder_t::flag_e) x;\n      BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.h264[flag] ? \": supported\"sv : \": unsupported\"sv);\n    }\n    BOOST_LOG(debug) << \"-------------------\"sv;\n    BOOST_LOG(info) << \"Found H.264 encoder: \"sv << encoder.h264.name << \" [\"sv << encoder.name << ']';\n\n    if (encoder.hevc[encoder_t::PASSED]) {\n      BOOST_LOG(debug) << \"------  hevc ------\"sv;\n      for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {\n        auto flag = (encoder_t::flag_e) x;\n        BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.hevc[flag] ? \": supported\"sv : \": unsupported\"sv);\n      }\n      BOOST_LOG(debug) << \"-------------------\"sv;\n\n      BOOST_LOG(info) << \"Found HEVC encoder: \"sv << encoder.hevc.name << \" [\"sv << encoder.name << ']';\n    }\n\n    if (encoder.av1[encoder_t::PASSED]) {\n      BOOST_LOG(debug) << \"------  av1 ------\"sv;\n      for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {\n        auto flag = (encoder_t::flag_e) x;\n        BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.av1[flag] ? \": supported\"sv : \": unsupported\"sv);\n      }\n      BOOST_LOG(debug) << \"-------------------\"sv;\n\n      BOOST_LOG(info) << \"Found AV1 encoder: \"sv << encoder.av1.name << \" [\"sv << encoder.name << ']';\n    }\n\n    if (active_hevc_mode == 0) {\n      active_hevc_mode = encoder.hevc[encoder_t::PASSED] ? (encoder.hevc[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1;\n    }\n\n    if (active_av1_mode == 0) {\n      active_av1_mode = encoder.av1[encoder_t::PASSED] ? (encoder.av1[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1;\n    }\n\n    return 0;\n  }\n\n  // Linux only declaration\n  typedef int (*vaapi_init_avcodec_hardware_input_buffer_fn)(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf);\n\n  util::Either<avcodec_buffer_t, int>\n  vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {\n    avcodec_buffer_t hw_device_buf;\n\n    // If an egl hwdevice\n    if (encode_device->data) {\n      if (((vaapi_init_avcodec_hardware_input_buffer_fn) encode_device->data)(encode_device, &hw_device_buf)) {\n        return -1;\n      }\n\n      return hw_device_buf;\n    }\n\n    auto render_device = config::video.adapter_name.empty() ? nullptr : config::video.adapter_name.c_str();\n\n    auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_VAAPI, render_device, nullptr, 0);\n    if (status < 0) {\n      char string[AV_ERROR_MAX_STRING_SIZE];\n      BOOST_LOG(error) << \"Failed to create a VAAPI device: \"sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);\n      return -1;\n    }\n\n    return hw_device_buf;\n  }\n\n  util::Either<avcodec_buffer_t, int>\n  cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {\n    avcodec_buffer_t hw_device_buf;\n\n    auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 1 /* AV_CUDA_USE_PRIMARY_CONTEXT */);\n    if (status < 0) {\n      char string[AV_ERROR_MAX_STRING_SIZE];\n      BOOST_LOG(error) << \"Failed to create a CUDA device: \"sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);\n      return -1;\n    }\n\n    return hw_device_buf;\n  }\n\n  util::Either<avcodec_buffer_t, int>\n  vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {\n    avcodec_buffer_t hw_device_buf;\n\n    auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_VIDEOTOOLBOX, nullptr, nullptr, 0);\n    if (status < 0) {\n      char string[AV_ERROR_MAX_STRING_SIZE];\n      BOOST_LOG(error) << \"Failed to create a VideoToolbox device: \"sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);\n      return -1;\n    }\n\n    return hw_device_buf;\n  }\n\n  util::Either<avcodec_buffer_t, int>\n  vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {\n    avcodec_buffer_t hw_device_buf;\n\n    auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_VULKAN, nullptr, nullptr, 0);\n    if (status < 0) {\n      char string[AV_ERROR_MAX_STRING_SIZE];\n      BOOST_LOG(error) << \"Failed to create a Vulkan device: \"sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);\n      return -1;\n    }\n\n    return hw_device_buf;\n  }\n\n#ifdef _WIN32\n}\n\nvoid\ndo_nothing(void *) {}\n\nnamespace video {\n  util::Either<avcodec_buffer_t, int>\n  dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {\n    avcodec_buffer_t ctx_buf { av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA) };\n    auto ctx = (AVD3D11VADeviceContext *) ((AVHWDeviceContext *) ctx_buf->data)->hwctx;\n\n    std::fill_n((std::uint8_t *) ctx, sizeof(AVD3D11VADeviceContext), 0);\n\n    auto device = (ID3D11Device *) encode_device->data;\n\n    device->AddRef();\n    ctx->device = device;\n\n    ctx->lock_ctx = (void *) 1;\n    ctx->lock = do_nothing;\n    ctx->unlock = do_nothing;\n\n    auto err = av_hwdevice_ctx_init(ctx_buf.get());\n    if (err) {\n      char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };\n      BOOST_LOG(error) << \"Failed to create FFMpeg hardware device context: \"sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);\n\n      return err;\n    }\n\n    return ctx_buf;\n  }\n#endif\n\n  int\n  start_capture_async(capture_thread_async_ctx_t &capture_thread_ctx) {\n    capture_thread_ctx.encoder_p = chosen_encoder;\n    capture_thread_ctx.reinit_event.reset();\n\n    capture_thread_ctx.capture_ctx_queue = std::make_shared<safe::queue_t<capture_ctx_t>>(30);\n\n    capture_thread_ctx.capture_thread = std::thread {\n      captureThread,\n      capture_thread_ctx.capture_ctx_queue,\n      std::ref(capture_thread_ctx.display_wp),\n      std::ref(capture_thread_ctx.reinit_event),\n      std::ref(*capture_thread_ctx.encoder_p)\n    };\n\n    return 0;\n  }\n  void\n  end_capture_async(capture_thread_async_ctx_t &capture_thread_ctx) {\n    capture_thread_ctx.capture_ctx_queue->stop();\n\n    capture_thread_ctx.capture_thread.join();\n  }\n\n  int\n  start_capture_sync(capture_thread_sync_ctx_t &ctx) {\n    std::thread { &captureThreadSync }.detach();\n    return 0;\n  }\n  void\n  end_capture_sync(capture_thread_sync_ctx_t &ctx) {}\n\n  platf::mem_type_e\n  map_base_dev_type(AVHWDeviceType type) {\n    switch (type) {\n      case AV_HWDEVICE_TYPE_D3D11VA:\n        return platf::mem_type_e::dxgi;\n      case AV_HWDEVICE_TYPE_VAAPI:\n        return platf::mem_type_e::vaapi;\n      case AV_HWDEVICE_TYPE_CUDA:\n        return platf::mem_type_e::cuda;\n      case AV_HWDEVICE_TYPE_NONE:\n        return platf::mem_type_e::system;\n      case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:\n        return platf::mem_type_e::videotoolbox;\n      case AV_HWDEVICE_TYPE_VULKAN:\n        return platf::mem_type_e::vulkan;\n      default:\n        return platf::mem_type_e::unknown;\n    }\n\n    return platf::mem_type_e::unknown;\n  }\n\n  platf::pix_fmt_e\n  map_pix_fmt(AVPixelFormat fmt) {\n    switch (fmt) {\n      case AV_PIX_FMT_VUYX:\n        return platf::pix_fmt_e::ayuv;\n      case AV_PIX_FMT_XV30:\n        return platf::pix_fmt_e::y410;\n      case AV_PIX_FMT_YUV420P10:\n        return platf::pix_fmt_e::yuv420p10;\n      case AV_PIX_FMT_YUV420P:\n        return platf::pix_fmt_e::yuv420p;\n      case AV_PIX_FMT_NV12:\n        return platf::pix_fmt_e::nv12;\n      case AV_PIX_FMT_P010:\n        return platf::pix_fmt_e::p010;\n      default:\n        return platf::pix_fmt_e::unknown;\n    }\n\n    return platf::pix_fmt_e::unknown;\n  }\n\n}  // namespace video\n"
  },
  {
    "path": "src/video.h",
    "content": "/**\n * @file src/video.h\n * @brief Declarations for video.\n */\n#pragma once\n\n#include \"input.h\"\n#include \"platform/common.h\"\n#include \"thread_safe.h\"\n#include \"video_colorspace.h\"\n\nextern \"C\" {\n#include <libavcodec/avcodec.h>\n#include <libswscale/swscale.h>\n}\n\nstruct AVPacket;\nnamespace video {\n\n  // 动态参数调节类型\n  enum class dynamic_param_type_e : int {\n    RESOLUTION,        // 分辨率 - 值：2个int (width, height)\n    FPS,               // 帧率 - 值：1个float\n    BITRATE,           // 码率 (Kbps) - 值：1个int\n    QP,                // 量化参数 - 值：1个int\n    FEC_PERCENTAGE,    // FEC百分比 - 值：1个int\n    PRESET,            // 编码预设 - 值：1个int\n    ADAPTIVE_QUANTIZATION, // 自适应量化 - 值：1个bool\n    MULTI_PASS,        // 多遍编码 - 值：1个int\n    VBV_BUFFER_SIZE,   // VBV缓冲区大小 - 值：1个int\n    MAX_PARAM_TYPE\n  };\n\n  // 动态参数值联合体\n  union dynamic_param_value_t {\n    int int_value;\n    int int_array_value[2];\n    bool bool_value;\n    float float_value;\n  };\n\n  // 动态参数结构\n  struct dynamic_param_t {\n    dynamic_param_type_e type;\n    dynamic_param_value_t value;\n    bool valid;\n  };\n\n  // 动态参数调节事件类型\n  using dynamic_param_change_event_t = safe::mail_raw_t::event_t<dynamic_param_t>;\n\n  /* Encoding configuration requested by remote client */\n  struct config_t {\n    int width;  // Video width in pixels\n    int height;  // Video height in pixels\n    int framerate;  // Requested framerate, used in individual frame bitrate budget calculation\n    int bitrate;  // Video bitrate in kilobits (1000 bits) for requested framerate\n    int slicesPerFrame;  // Number of slices per frame\n    int numRefFrames;  // Max number of reference frames\n\n    /* Requested color range and SDR encoding colorspace, HDR encoding colorspace is always BT.2020+ST2084\n       Color range (encoderCscMode & 0x1) : 0 - limited, 1 - full\n       SDR encoding colorspace (encoderCscMode >> 1) : 0 - BT.601, 1 - BT.709, 2 - BT.2020 */\n    int encoderCscMode;\n\n    int videoFormat;  // 0 - H.264, 1 - HEVC, 2 - AV1\n\n    /* Encoding color depth and HDR transfer function:\n       0 - SDR 8-bit\n       1 - HDR 10-bit with PQ (SMPTE ST 2084)\n       2 - HDR 10-bit with HLG (ARIB STD-B67)\n       HDR encoding activates when dynamicRange > 0 and the display is operating in HDR mode */\n    int dynamicRange;\n\n    int chromaSamplingType;  // 0 - 4:2:0, 1 - 4:4:4\n\n    int enableIntraRefresh;  // 0 - disabled, 1 - enabled\n\n    // NTSC framerate support: use frameRateNum/frameRateDen for precise framerate\n    // e.g., 120000/1001 = 119.88fps (NTSC), 60000/1001 = 59.94fps\n    // When frameRateDen is 0 or 1, use integer framerate\n    int frameRateNum = 0;  // Framerate numerator (0 = use integer framerate)\n    int frameRateDen = 1;  // Framerate denominator\n\n    // Display name for screen capture (specified by client)\n    // If empty, use the default display from global configuration\n    std::string display_name;\n\n    // Helper to get effective framerate as double\n    double get_effective_framerate() const {\n      if (frameRateNum > 0 && frameRateDen > 0) {\n        return static_cast<double>(frameRateNum) / frameRateDen;\n      }\n      return static_cast<double>(framerate);\n    }\n  };\n\n  platf::mem_type_e\n  map_base_dev_type(AVHWDeviceType type);\n  platf::pix_fmt_e\n  map_pix_fmt(AVPixelFormat fmt);\n\n  void\n  free_ctx(AVCodecContext *ctx);\n  void\n  free_frame(AVFrame *frame);\n  void\n  free_buffer(AVBufferRef *ref);\n\n  using avcodec_ctx_t = util::safe_ptr<AVCodecContext, free_ctx>;\n  using avcodec_frame_t = util::safe_ptr<AVFrame, free_frame>;\n  using avcodec_buffer_t = util::safe_ptr<AVBufferRef, free_buffer>;\n  using sws_t = util::safe_ptr<SwsContext, sws_freeContext>;\n  using img_event_t = std::shared_ptr<safe::event_t<std::shared_ptr<platf::img_t>>>;\n\n  struct encoder_platform_formats_t {\n    virtual ~encoder_platform_formats_t() = default;\n    platf::mem_type_e dev_type;\n    platf::pix_fmt_e pix_fmt_8bit, pix_fmt_10bit;\n    platf::pix_fmt_e pix_fmt_yuv444_8bit, pix_fmt_yuv444_10bit;\n  };\n\n  struct encoder_platform_formats_avcodec: encoder_platform_formats_t {\n    using init_buffer_function_t = std::function<util::Either<avcodec_buffer_t, int>(platf::avcodec_encode_device_t *)>;\n\n    encoder_platform_formats_avcodec(\n      const AVHWDeviceType &avcodec_base_dev_type,\n      const AVHWDeviceType &avcodec_derived_dev_type,\n      const AVPixelFormat &avcodec_dev_pix_fmt,\n      const AVPixelFormat &avcodec_pix_fmt_8bit,\n      const AVPixelFormat &avcodec_pix_fmt_10bit,\n      const AVPixelFormat &avcodec_pix_fmt_yuv444_8bit,\n      const AVPixelFormat &avcodec_pix_fmt_yuv444_10bit,\n      const init_buffer_function_t &init_avcodec_hardware_input_buffer_function):\n        avcodec_base_dev_type { avcodec_base_dev_type },\n        avcodec_derived_dev_type { avcodec_derived_dev_type },\n        avcodec_dev_pix_fmt { avcodec_dev_pix_fmt },\n        avcodec_pix_fmt_8bit { avcodec_pix_fmt_8bit },\n        avcodec_pix_fmt_10bit { avcodec_pix_fmt_10bit },\n        avcodec_pix_fmt_yuv444_8bit { avcodec_pix_fmt_yuv444_8bit },\n        avcodec_pix_fmt_yuv444_10bit { avcodec_pix_fmt_yuv444_10bit },\n        init_avcodec_hardware_input_buffer { init_avcodec_hardware_input_buffer_function } {\n      dev_type = map_base_dev_type(avcodec_base_dev_type);\n      pix_fmt_8bit = map_pix_fmt(avcodec_pix_fmt_8bit);\n      pix_fmt_10bit = map_pix_fmt(avcodec_pix_fmt_10bit);\n      pix_fmt_yuv444_8bit = map_pix_fmt(avcodec_pix_fmt_yuv444_8bit);\n      pix_fmt_yuv444_10bit = map_pix_fmt(avcodec_pix_fmt_yuv444_10bit);\n    }\n\n    AVHWDeviceType avcodec_base_dev_type, avcodec_derived_dev_type;\n    AVPixelFormat avcodec_dev_pix_fmt;\n    AVPixelFormat avcodec_pix_fmt_8bit, avcodec_pix_fmt_10bit;\n    AVPixelFormat avcodec_pix_fmt_yuv444_8bit, avcodec_pix_fmt_yuv444_10bit;\n\n    init_buffer_function_t init_avcodec_hardware_input_buffer;\n  };\n\n  struct encoder_platform_formats_nvenc: encoder_platform_formats_t {\n    encoder_platform_formats_nvenc(\n      const platf::mem_type_e &dev_type,\n      const platf::pix_fmt_e &pix_fmt_8bit,\n      const platf::pix_fmt_e &pix_fmt_10bit,\n      const platf::pix_fmt_e &pix_fmt_yuv444_8bit,\n      const platf::pix_fmt_e &pix_fmt_yuv444_10bit) {\n      encoder_platform_formats_t::dev_type = dev_type;\n      encoder_platform_formats_t::pix_fmt_8bit = pix_fmt_8bit;\n      encoder_platform_formats_t::pix_fmt_10bit = pix_fmt_10bit;\n      encoder_platform_formats_t::pix_fmt_yuv444_8bit = pix_fmt_yuv444_8bit;\n      encoder_platform_formats_t::pix_fmt_yuv444_10bit = pix_fmt_yuv444_10bit;\n    }\n  };\n\n  struct encoder_platform_formats_amf: encoder_platform_formats_t {\n    encoder_platform_formats_amf(\n      const platf::mem_type_e &dev_type,\n      const platf::pix_fmt_e &pix_fmt_8bit,\n      const platf::pix_fmt_e &pix_fmt_10bit,\n      const platf::pix_fmt_e &pix_fmt_yuv444_8bit,\n      const platf::pix_fmt_e &pix_fmt_yuv444_10bit) {\n      encoder_platform_formats_t::dev_type = dev_type;\n      encoder_platform_formats_t::pix_fmt_8bit = pix_fmt_8bit;\n      encoder_platform_formats_t::pix_fmt_10bit = pix_fmt_10bit;\n      encoder_platform_formats_t::pix_fmt_yuv444_8bit = pix_fmt_yuv444_8bit;\n      encoder_platform_formats_t::pix_fmt_yuv444_10bit = pix_fmt_yuv444_10bit;\n    }\n  };\n\n  struct encoder_t {\n    std::string_view name;\n    enum flag_e {\n      PASSED,  ///< Indicates the encoder is supported.\n      REF_FRAMES_RESTRICT,  ///< Set maximum reference frames.\n      DYNAMIC_RANGE,  ///< HDR support.\n      YUV444,  ///< YUV 4:4:4 support.\n      VUI_PARAMETERS,  ///< AMD encoder with VAAPI doesn't add VUI parameters to SPS.\n      MAX_FLAGS  ///< Maximum number of flags.\n    };\n\n    static std::string_view\n    from_flag(flag_e flag) {\n#define _CONVERT(x) \\\n  case flag_e::x:   \\\n    return std::string_view(#x)\n      switch (flag) {\n        _CONVERT(PASSED);\n        _CONVERT(REF_FRAMES_RESTRICT);\n        _CONVERT(DYNAMIC_RANGE);\n        _CONVERT(YUV444);\n        _CONVERT(VUI_PARAMETERS);\n        _CONVERT(MAX_FLAGS);\n      }\n#undef _CONVERT\n\n      return { \"unknown\" };\n    }\n\n    struct option_t {\n      KITTY_DEFAULT_CONSTR_MOVE(option_t)\n      option_t(const option_t &) = default;\n\n      std::string name;\n      std::variant<int, int *, std::optional<int> *, std::function<int()>, std::string, std::string *, std::function<const std::string(const config_t &)>> value;\n\n      option_t(std::string &&name, decltype(value) &&value):\n          name { std::move(name) }, value { std::move(value) } {}\n    };\n\n    const std::unique_ptr<const encoder_platform_formats_t> platform_formats;\n\n    struct codec_t {\n      std::vector<option_t> common_options;\n      std::vector<option_t> sdr_options;\n      std::vector<option_t> hdr_options;\n      std::vector<option_t> sdr444_options;\n      std::vector<option_t> hdr444_options;\n      std::vector<option_t> fallback_options;\n\n      std::string name;\n      std::bitset<MAX_FLAGS> capabilities;\n\n      bool\n      operator[](flag_e flag) const {\n        return capabilities[(std::size_t) flag];\n      }\n\n      std::bitset<MAX_FLAGS>::reference\n      operator[](flag_e flag) {\n        return capabilities[(std::size_t) flag];\n      }\n    } av1, hevc, h264;\n\n    const codec_t &\n    codec_from_config(const config_t &config) const {\n      switch (config.videoFormat) {\n        default:\n          BOOST_LOG(error) << \"Unknown video format \" << config.videoFormat << \", falling back to H.264\";\n          // fallthrough\n        case 0:\n          return h264;\n        case 1:\n          return hevc;\n        case 2:\n          return av1;\n      }\n    }\n\n    uint32_t flags;\n  };\n\n  struct encode_session_t {\n    virtual ~encode_session_t() = default;\n\n    virtual int\n    convert(platf::img_t &img) = 0;\n\n    virtual void\n    request_idr_frame() = 0;\n\n    virtual void\n    request_normal_frame() = 0;\n\n    virtual void\n    invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0;\n\n    virtual void\n    set_bitrate(int bitrate_kbps) = 0;  // 新增：动态码率调整方法\n\n    virtual void\n    set_dynamic_param(const dynamic_param_t &param) = 0;  // 新增：通用动态参数调整方法\n  };\n\n  // encoders\n  extern encoder_t software;\n\n#if !defined(__APPLE__)\n  extern encoder_t nvenc;  // available for windows and linux\n#endif\n\n#ifdef _WIN32\n  extern encoder_t amdvce;\n  extern encoder_t quicksync;\n#endif\n\n#ifdef __linux__\n  extern encoder_t vaapi;\n#endif\n\n#ifdef __APPLE__\n  extern encoder_t videotoolbox;\n#endif\n\n  struct packet_raw_t {\n    virtual ~packet_raw_t() = default;\n\n    virtual bool\n    is_idr() = 0;\n\n    virtual int64_t\n    frame_index() = 0;\n\n    virtual uint8_t *\n    data() = 0;\n\n    virtual size_t\n    data_size() = 0;\n\n    struct replace_t {\n      std::string_view old;\n      std::string_view _new;\n\n      KITTY_DEFAULT_CONSTR_MOVE(replace_t)\n\n      replace_t(std::string_view old, std::string_view _new) noexcept:\n          old { std::move(old) }, _new { std::move(_new) } {}\n    };\n\n    std::vector<replace_t> *replacements = nullptr;\n    void *channel_data = nullptr;\n    bool after_ref_frame_invalidation = false;\n    std::optional<std::chrono::steady_clock::time_point> frame_timestamp;\n  };\n\n  struct packet_raw_avcodec: packet_raw_t {\n    packet_raw_avcodec() {\n      av_packet = av_packet_alloc();\n    }\n\n    ~packet_raw_avcodec() {\n      av_packet_free(&this->av_packet);\n    }\n\n    bool\n    is_idr() override {\n      return av_packet->flags & AV_PKT_FLAG_KEY;\n    }\n\n    int64_t\n    frame_index() override {\n      return av_packet->pts;\n    }\n\n    uint8_t *\n    data() override {\n      return av_packet->data;\n    }\n\n    size_t\n    data_size() override {\n      return av_packet->size;\n    }\n\n    AVPacket *av_packet;\n  };\n\n  struct packet_raw_generic: packet_raw_t {\n    packet_raw_generic(std::vector<uint8_t> &&frame_data, int64_t frame_index, bool idr):\n        frame_data { std::move(frame_data) }, index { frame_index }, idr { idr } {\n    }\n\n    bool\n    is_idr() override {\n      return idr;\n    }\n\n    int64_t\n    frame_index() override {\n      return index;\n    }\n\n    uint8_t *\n    data() override {\n      return frame_data.data();\n    }\n\n    size_t\n    data_size() override {\n      return frame_data.size();\n    }\n\n    std::vector<uint8_t> frame_data;\n    int64_t index;\n    bool idr;\n  };\n\n  using packet_t = std::unique_ptr<packet_raw_t>;\n\n  struct hdr_info_raw_t {\n    explicit hdr_info_raw_t(bool enabled):\n        enabled { enabled }, metadata {} {};\n    explicit hdr_info_raw_t(bool enabled, const SS_HDR_METADATA &metadata):\n        enabled { enabled }, metadata { metadata } {};\n\n    bool enabled;\n    SS_HDR_METADATA metadata;\n  };\n\n  using hdr_info_t = std::unique_ptr<hdr_info_raw_t>;\n\n  extern int active_hevc_mode;\n  extern int active_av1_mode;\n  extern bool last_encoder_probe_supported_ref_frames_invalidation;\n  extern std::array<bool, 3> last_encoder_probe_supported_yuv444_for_codec;  // 0 - H.264, 1 - HEVC, 2 - AV1\n\n  void\n  capture(\n    safe::mail_t mail,\n    config_t config,\n    void *channel_data,\n    std::optional<safe::mail_raw_t::event_t<dynamic_param_t>> dynamic_param_events = std::nullopt);\n\n  bool\n  validate_encoder(encoder_t &encoder, bool expect_failure);\n\n  /**\n   * @brief Probe encoders and select the preferred encoder.\n   * This is called once at startup and each time a stream is launched to\n   * ensure the best encoder is selected. Encoder availability can change\n   * at runtime due to all sorts of things from driver updates to eGPUs.\n   *\n   * @warning This is only safe to call when there is no client actively streaming.\n   */\n  int\n  probe_encoders();\n}  // namespace video\n"
  },
  {
    "path": "src/video_colorspace.cpp",
    "content": "/**\r\n * @file src/video_colorspace.cpp\r\n * @brief Definitions for colorspace functions.\r\n */\r\n// this include\r\n#include \"video_colorspace.h\"\r\n\r\n// local includes\r\n#include \"logging.h\"\r\n#include \"video.h\"\r\n\r\nextern \"C\" {\r\n#include <libswscale/swscale.h>\r\n}\r\n\r\nnamespace video {\r\n\r\n  bool\r\n  colorspace_is_hdr(const sunshine_colorspace_t &colorspace) {\r\n    return colorspace.colorspace == colorspace_e::bt2020 ||\r\n           colorspace.colorspace == colorspace_e::bt2020hlg;\r\n  }\r\n\r\n  bool\r\n  colorspace_is_hlg(const sunshine_colorspace_t &colorspace) {\r\n    return colorspace.colorspace == colorspace_e::bt2020hlg;\r\n  }\r\n\r\n  bool\r\n  colorspace_is_pq(const sunshine_colorspace_t &colorspace) {\r\n    return colorspace.colorspace == colorspace_e::bt2020;\r\n  }\r\n\r\n  sunshine_colorspace_t\r\n  colorspace_from_client_config(const config_t &config, bool hdr_display) {\r\n    sunshine_colorspace_t colorspace;\r\n\r\n    /* See video::config_t declaration for details */\r\n    /* dynamicRange values:\r\n       0 = SDR 8-bit\r\n       1 = HDR 10-bit with PQ (ST 2084)\r\n       2 = HDR 10-bit with HLG (ARIB STD-B67) */\r\n\r\n    if (config.dynamicRange > 0 && hdr_display) {\r\n      if (config.dynamicRange == 2) {\r\n        // Rec. 2020 with Hybrid Log-Gamma (HLG)\r\n        colorspace.colorspace = colorspace_e::bt2020hlg;\r\n      }\r\n      else {\r\n        // Rec. 2020 with ST 2084 perceptual quantizer (PQ) - default HDR mode\r\n        colorspace.colorspace = colorspace_e::bt2020;\r\n      }\r\n    }\r\n    else {\r\n      switch (config.encoderCscMode >> 1) {\r\n        case 0:\r\n          // Rec. 601\r\n          colorspace.colorspace = colorspace_e::rec601;\r\n          break;\r\n\r\n        case 1:\r\n          // Rec. 709\r\n          colorspace.colorspace = colorspace_e::rec709;\r\n          break;\r\n\r\n        case 2:\r\n          // Rec. 2020\r\n          colorspace.colorspace = colorspace_e::bt2020sdr;\r\n          break;\r\n\r\n        default:\r\n          BOOST_LOG(error) << \"Unknown video colorspace in csc, falling back to Rec. 709\";\r\n          colorspace.colorspace = colorspace_e::rec709;\r\n          break;\r\n      }\r\n    }\r\n\r\n    colorspace.full_range = (config.encoderCscMode & 0x1);\r\n\r\n    switch (config.dynamicRange) {\r\n      case 0:\r\n        colorspace.bit_depth = 8;\r\n        break;\r\n\r\n      case 1:  // HDR PQ\r\n      case 2:  // HDR HLG\r\n        colorspace.bit_depth = 10;\r\n        break;\r\n\r\n      default:\r\n        BOOST_LOG(error) << \"Unknown dynamicRange value, falling back to 10-bit color depth\";\r\n        colorspace.bit_depth = 10;\r\n        break;\r\n    }\r\n\r\n    if (colorspace.colorspace == colorspace_e::bt2020sdr && colorspace.bit_depth != 10) {\r\n      BOOST_LOG(error) << \"BT.2020 SDR colorspace expects 10-bit color depth, falling back to Rec. 709\";\r\n      colorspace.colorspace = colorspace_e::rec709;\r\n    }\r\n\r\n    return colorspace;\r\n  }\r\n\r\n  avcodec_colorspace_t\r\n  avcodec_colorspace_from_sunshine_colorspace(const sunshine_colorspace_t &sunshine_colorspace) {\r\n    avcodec_colorspace_t avcodec_colorspace;\r\n\r\n    switch (sunshine_colorspace.colorspace) {\r\n      case colorspace_e::rec601:\r\n        // Rec. 601\r\n        avcodec_colorspace.primaries = AVCOL_PRI_SMPTE170M;\r\n        avcodec_colorspace.transfer_function = AVCOL_TRC_SMPTE170M;\r\n        avcodec_colorspace.matrix = AVCOL_SPC_SMPTE170M;\r\n        avcodec_colorspace.software_format = SWS_CS_SMPTE170M;\r\n        break;\r\n\r\n      case colorspace_e::rec709:\r\n        // Rec. 709\r\n        avcodec_colorspace.primaries = AVCOL_PRI_BT709;\r\n        avcodec_colorspace.transfer_function = AVCOL_TRC_BT709;\r\n        avcodec_colorspace.matrix = AVCOL_SPC_BT709;\r\n        avcodec_colorspace.software_format = SWS_CS_ITU709;\r\n        break;\r\n\r\n      case colorspace_e::bt2020sdr:\r\n        // Rec. 2020\r\n        avcodec_colorspace.primaries = AVCOL_PRI_BT2020;\r\n        assert(sunshine_colorspace.bit_depth == 10);\r\n        avcodec_colorspace.transfer_function = AVCOL_TRC_BT2020_10;\r\n        avcodec_colorspace.matrix = AVCOL_SPC_BT2020_NCL;\r\n        avcodec_colorspace.software_format = SWS_CS_BT2020;\r\n        break;\r\n\r\n      case colorspace_e::bt2020:\r\n        // Rec. 2020 with ST 2084 perceptual quantizer (PQ)\r\n        avcodec_colorspace.primaries = AVCOL_PRI_BT2020;\r\n        assert(sunshine_colorspace.bit_depth == 10);\r\n        avcodec_colorspace.transfer_function = AVCOL_TRC_SMPTE2084;\r\n        avcodec_colorspace.matrix = AVCOL_SPC_BT2020_NCL;\r\n        avcodec_colorspace.software_format = SWS_CS_BT2020;\r\n        break;\r\n\r\n      case colorspace_e::bt2020hlg:\r\n        // Rec. 2020 with Hybrid Log-Gamma (HLG)\r\n        avcodec_colorspace.primaries = AVCOL_PRI_BT2020;\r\n        assert(sunshine_colorspace.bit_depth == 10);\r\n        avcodec_colorspace.transfer_function = AVCOL_TRC_ARIB_STD_B67;\r\n        avcodec_colorspace.matrix = AVCOL_SPC_BT2020_NCL;\r\n        avcodec_colorspace.software_format = SWS_CS_BT2020;\r\n        break;\r\n    }\r\n\r\n    avcodec_colorspace.range = sunshine_colorspace.full_range ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG;\r\n\r\n    return avcodec_colorspace;\r\n  }\r\n\r\n  const color_t *\r\n  color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace, bool unorm_output) {\r\n    constexpr auto generate_color_vectors = [](const sunshine_colorspace_t &colorspace, bool unorm_output) -> color_t {\r\n      // \"Table 4 – Interpretation of matrix coefficients (MatrixCoefficients) value\" section of ITU-T H.273\r\n      double Kr, Kb;\r\n      switch (colorspace.colorspace) {\r\n        case colorspace_e::rec601:\r\n          Kr = 0.299;\r\n          Kb = 0.114;\r\n          break;\r\n        case colorspace_e::rec709:\r\n        default:\r\n          Kr = 0.2126;\r\n          Kb = 0.0722;\r\n          break;\r\n        case colorspace_e::bt2020:\r\n        case colorspace_e::bt2020sdr:\r\n        case colorspace_e::bt2020hlg:\r\n          Kr = 0.2627;\r\n          Kb = 0.0593;\r\n          break;\r\n      }\r\n      double Kg = 1.0 - Kr - Kb;\r\n\r\n      double y_mult, y_add;\r\n      double uv_mult, uv_add;\r\n\r\n      // \"8.3 Matrix coefficients\" section of ITU-T H.273\r\n      if (colorspace.full_range) {\r\n        y_mult = (1 << colorspace.bit_depth) - 1;\r\n        y_add = 0;\r\n        uv_mult = (1 << colorspace.bit_depth) - 1;\r\n        uv_add = (1 << (colorspace.bit_depth - 1));\r\n      }\r\n      else {\r\n        y_mult = (1 << (colorspace.bit_depth - 8)) * 219;\r\n        y_add = (1 << (colorspace.bit_depth - 8)) * 16;\r\n        uv_mult = (1 << (colorspace.bit_depth - 8)) * 224;\r\n        uv_add = (1 << (colorspace.bit_depth - 8)) * 128;\r\n      }\r\n\r\n      if (unorm_output) {\r\n        const double unorm_range = (1 << colorspace.bit_depth) - 1;\r\n        y_mult /= unorm_range;\r\n        y_add /= unorm_range;\r\n        uv_mult /= unorm_range;\r\n        uv_add /= unorm_range;\r\n      }\r\n      else {\r\n        // For rounding\r\n        y_add += 0.5f;\r\n        uv_add += 0.5f;\r\n      }\r\n\r\n      color_t color_vectors;\r\n\r\n      color_vectors.color_vec_y[0] = Kr * y_mult;\r\n      color_vectors.color_vec_y[1] = Kg * y_mult;\r\n      color_vectors.color_vec_y[2] = Kb * y_mult;\r\n      color_vectors.color_vec_y[3] = y_add;\r\n\r\n      color_vectors.color_vec_u[0] = -0.5 * Kr / (1.0 - Kb) * uv_mult;\r\n      color_vectors.color_vec_u[1] = -0.5 * Kg / (1.0 - Kb) * uv_mult;\r\n      color_vectors.color_vec_u[2] = 0.5 * uv_mult;\r\n      color_vectors.color_vec_u[3] = uv_add;\r\n\r\n      color_vectors.color_vec_v[0] = 0.5 * uv_mult;\r\n      color_vectors.color_vec_v[1] = -0.5 * Kg / (1.0 - Kr) * uv_mult;\r\n      color_vectors.color_vec_v[2] = -0.5 * Kb / (1.0 - Kr) * uv_mult;\r\n      color_vectors.color_vec_v[3] = uv_add;\r\n\r\n      // Unused\r\n      color_vectors.range_y[0] = 1;\r\n      color_vectors.range_y[1] = 0;\r\n      color_vectors.range_uv[0] = 1;\r\n      color_vectors.range_uv[1] = 0;\r\n\r\n      return color_vectors;\r\n    };\r\n\r\n    static constexpr color_t colors[] = {\r\n      generate_color_vectors({ colorspace_e::rec601, false, 8 }, false),\r\n      generate_color_vectors({ colorspace_e::rec601, true, 8 }, false),\r\n      generate_color_vectors({ colorspace_e::rec601, false, 10 }, false),\r\n      generate_color_vectors({ colorspace_e::rec601, true, 10 }, false),\r\n      generate_color_vectors({ colorspace_e::rec709, false, 8 }, false),\r\n      generate_color_vectors({ colorspace_e::rec709, true, 8 }, false),\r\n      generate_color_vectors({ colorspace_e::rec709, false, 10 }, false),\r\n      generate_color_vectors({ colorspace_e::rec709, true, 10 }, false),\r\n      generate_color_vectors({ colorspace_e::bt2020, false, 8 }, false),\r\n      generate_color_vectors({ colorspace_e::bt2020, true, 8 }, false),\r\n      generate_color_vectors({ colorspace_e::bt2020, false, 10 }, false),\r\n      generate_color_vectors({ colorspace_e::bt2020, true, 10 }, false),\r\n\r\n      generate_color_vectors({ colorspace_e::rec601, false, 8 }, true),\r\n      generate_color_vectors({ colorspace_e::rec601, true, 8 }, true),\r\n      generate_color_vectors({ colorspace_e::rec601, false, 10 }, true),\r\n      generate_color_vectors({ colorspace_e::rec601, true, 10 }, true),\r\n      generate_color_vectors({ colorspace_e::rec709, false, 8 }, true),\r\n      generate_color_vectors({ colorspace_e::rec709, true, 8 }, true),\r\n      generate_color_vectors({ colorspace_e::rec709, false, 10 }, true),\r\n      generate_color_vectors({ colorspace_e::rec709, true, 10 }, true),\r\n      generate_color_vectors({ colorspace_e::bt2020, false, 8 }, true),\r\n      generate_color_vectors({ colorspace_e::bt2020, true, 8 }, true),\r\n      generate_color_vectors({ colorspace_e::bt2020, false, 10 }, true),\r\n      generate_color_vectors({ colorspace_e::bt2020, true, 10 }, true),\r\n    };\r\n\r\n    const color_t *result = nullptr;\r\n\r\n    switch (colorspace.colorspace) {\r\n      case colorspace_e::rec601:\r\n        result = &colors[0];\r\n        break;\r\n      case colorspace_e::rec709:\r\n      default:\r\n        result = &colors[4];\r\n        break;\r\n      case colorspace_e::bt2020:\r\n      case colorspace_e::bt2020sdr:\r\n      case colorspace_e::bt2020hlg:\r\n        result = &colors[8];\r\n        break;\r\n    }\r\n\r\n    if (colorspace.bit_depth == 10) {\r\n      result += 2;\r\n    }\r\n    if (colorspace.full_range) {\r\n      result += 1;\r\n    }\r\n    if (unorm_output) {\r\n      result += 12;\r\n    }\r\n\r\n    return result;\r\n  }\r\n}  // namespace video\r\n"
  },
  {
    "path": "src/video_colorspace.h",
    "content": "/**\r\n * @file src/video_colorspace.h\r\n * @brief Declarations for colorspace functions.\r\n */\r\n#pragma once\r\n\r\nextern \"C\" {\r\n#include <libavutil/pixfmt.h>\r\n}\r\n\r\nnamespace video {\r\n\r\n  enum class colorspace_e {\r\n    rec601,  ///< Rec. 601\r\n    rec709,  ///< Rec. 709\r\n    bt2020sdr,  ///< Rec. 2020 SDR\r\n    bt2020,  ///< Rec. 2020 HDR with PQ (ST 2084)\r\n    bt2020hlg,  ///< Rec. 2020 HDR with HLG (ARIB STD-B67)\r\n  };\r\n\r\n  struct sunshine_colorspace_t {\r\n    colorspace_e colorspace;\r\n    bool full_range;\r\n    unsigned bit_depth;\r\n  };\r\n\r\n  bool\r\n  colorspace_is_hdr(const sunshine_colorspace_t &colorspace);\r\n\r\n  bool\r\n  colorspace_is_hlg(const sunshine_colorspace_t &colorspace);\r\n\r\n  bool\r\n  colorspace_is_pq(const sunshine_colorspace_t &colorspace);\r\n\r\n  // Declared in video.h\r\n  struct config_t;\r\n\r\n  sunshine_colorspace_t\r\n  colorspace_from_client_config(const config_t &config, bool hdr_display);\r\n\r\n  struct avcodec_colorspace_t {\r\n    AVColorPrimaries primaries;\r\n    AVColorTransferCharacteristic transfer_function;\r\n    AVColorSpace matrix;\r\n    AVColorRange range;\r\n    int software_format;\r\n  };\r\n\r\n  avcodec_colorspace_t\r\n  avcodec_colorspace_from_sunshine_colorspace(const sunshine_colorspace_t &sunshine_colorspace);\r\n\r\n  struct alignas(16) color_t {\r\n    float color_vec_y[4];\r\n    float color_vec_u[4];\r\n    float color_vec_v[4];\r\n    float range_y[2];\r\n    float range_uv[2];\r\n  };\r\n\r\n  /**\r\n   * @brief Get static RGB->YUV color conversion matrix.\r\n   *        This matrix expects RGB input in UNORM (0.0 to 1.0) range and doesn't perform any\r\n   *        gamut mapping or gamma correction.\r\n   * @param colorspace Targeted YUV colorspace.\r\n   * @param unorm_output Whether the matrix should produce output in UNORM or UINT range.\r\n   * @return `const color_t*` that contains RGB->YUV transformation vectors.\r\n   *         Components `range_y` and `range_uv` are there for backwards compatibility\r\n   *         and can be ignored in the computation.\r\n   */\r\n  const color_t *\r\n  color_vectors_from_colorspace(const sunshine_colorspace_t &colorspace, bool unorm_output);\r\n}  // namespace video\r\n"
  },
  {
    "path": "src/webhook.cpp",
    "content": "/**\n * @file src/webhook.cpp\n * @brief Webhook notification system implementation for Sunshine.\n */\n\n#include <thread>\n#include <chrono>\n#include <iomanip>\n#include <sstream>\n#include <regex>\n#include <mutex>\n#include <algorithm>\n#include <thread>\n#include <atomic>\n#include <map>\n#include <cstdlib>\n#include <Simple-Web-Server/client_http.hpp>\n#include <Simple-Web-Server/client_https.hpp>\n#include <boost/asio/ip/tcp.hpp>\n#include <boost/asio/io_context.hpp>\n\n#include \"config.h\"\n#include \"logging.h\"\n#include \"httpcommon.h\"\n#include \"utility.h\"\n#include \"platform/common.h\"\n#include \"webhook_httpsclient.h\"\n#include \"webhook.h\"\n#include \"webhook_format.h\"\n#include \"network.h\"\n\nusing namespace std::literals;\n\nnamespace webhook {\n  // Rate limiting variables\n  static std::vector<std::chrono::system_clock::time_point> successful_sends;\n  static std::mutex rate_limit_mutex;\n  static const int MAX_SENDS_PER_MINUTE = 10;\n  static const int RATE_LIMIT_WINDOW_MINUTES = 1;\n  static bool rate_limit_notification_sent = false;\n\n  std::string generate_signature(long long timestamp, const std::string& hostname)\n  {\n    // 使用简单的哈希算法生成签名\n    std::string data = hostname + std::to_string(timestamp) + \"Sunshine_Foundation\";\n    std::hash<std::string> hasher;\n    size_t hash_value = hasher(data);\n    return std::to_string(hash_value);\n  }\n\n  SimpleWeb::CaseInsensitiveMultimap generate_webhook_headers()\n  {\n    SimpleWeb::CaseInsensitiveMultimap headers;\n    // 生成时间戳和签名\n    auto now = std::chrono::system_clock::now();\n    auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();\n    std::string hostname = platf::get_host_name();\n    std::string signature = generate_signature(timestamp, hostname);\n    // 校验相关的请求头\n    headers.emplace(\"X-Timestamp\", std::to_string(timestamp));\n    headers.emplace(\"X-Hostname\", hostname);\n    headers.emplace(\"X-Signature\", signature);\n    headers.emplace(\"X-Client-ID\", \"Sunshine_Foundation\");\n    headers.emplace(\"X-Auth-Token\", \"Sunshine_Foundation_\" + std::to_string(timestamp % 10000));\n    headers.emplace(\"X-API-Version\", \"v1.0\");\n    headers.emplace(\"X-Client-Info\", \"Sunshine Foundation\");\n    headers.emplace(\"X-Trace-ID\", \"sf_\" + std::to_string(timestamp) + \"_\" + std::to_string(rand() % 1000));\n    headers.emplace(\"X-Service-Name\", \"Sunshine_Foundation_Service\");\n    headers.emplace(\"X-Component\", \"Sunshine_Foundation_Component\");\n    headers.emplace(\"User-Agent\", \"Sunshine_Foundation/1.0 (System Notification Service)\");\n    headers.emplace(\"Content-Type\", \"application/json\");\n    return headers;\n  }\n\n  /**\n   * @brief 获取本地IP地址\n   * @return 本地IP地址字符串，优先返回IPv4，其次IPv6，都获取不到返回空字符串\n   */\n  std::string get_local_ip() {\n    try {\n      boost::asio::io_context io_context;\n      boost::asio::ip::tcp::resolver resolver(io_context);\n      auto results = resolver.resolve(boost::asio::ip::host_name(), \"\");\n      \n      std::string ipv4_address = \"\";\n      std::string ipv6_address = \"\";\n      \n      for (auto it = results.begin(); it != results.end(); ++it) {\n        boost::asio::ip::tcp::endpoint ep = *it;\n        auto address = ep.address();\n        if (!address.is_loopback()) {\n          auto normalized_address = net::normalize_address(address);\n          auto address_str = normalized_address.to_string();\n          \n          if (normalized_address.is_v4() && ipv4_address.empty()) {\n            ipv4_address = address_str;\n          } else if (normalized_address.is_v6() && ipv6_address.empty()) {\n            ipv6_address = address_str;\n          }\n        }\n      }\n      \n      // 优先返回IPv4，其次IPv6\n      if (!ipv4_address.empty()) {\n        return ipv4_address;\n      } else if (!ipv6_address.empty()) {\n        return ipv6_address;\n      }\n    } catch (const std::exception& e) {\n      BOOST_LOG(debug) << \"Webhook: Failed to get local IP: \" << e.what();\n    }\n    return \"\";\n  }\n\n  // Thread management variables\n  static std::atomic<int> active_thread_count{0};\n  static std::mutex thread_mutex;\n  static const int MAX_CONCURRENT_THREADS = 10;\n\n  /**\n   * @brief 发送webhook请求\n   * @param url Webhook URL\n   * @param json_payload JSON payload to send\n   * @param timeout_duration Request timeout\n   * @return true if successful, false otherwise\n   */\n  bool send_webhook_request(const std::string& url, const std::string& json_payload, std::chrono::milliseconds timeout_duration)\n  {\n    const int max_retries = 2; // Maximum 2 retries, total 3 attempts\n    \n    for (int attempt = 1; attempt <= max_retries + 1; ++attempt) {\n      BOOST_LOG(debug) << \"Webhook attempt \" << attempt << \"/\" << (max_retries + 1) << \": \" << json_payload;\n      \n      if (send_single_webhook_request(url, json_payload, timeout_duration)) {\n        if (attempt > 1) {\n          BOOST_LOG(info) << \"Webhook succeeded on attempt \" << attempt;\n        }\n        return true;\n      }\n      \n      if (attempt <= max_retries) {\n        BOOST_LOG(warning) << \"Webhook attempt \" << attempt << \" failed, retrying...\";\n        std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // Wait 1 second before retry\n      }\n    }\n    \n    BOOST_LOG(error) << \"Webhook failed after \" << (max_retries + 1) << \" attempts\";\n    return false;\n  }\n\n  /**\n   * @brief 发送单个webhook请求\n   * @param url Webhook URL\n   * @param json_payload JSON payload to send\n   * @param timeout_duration Request timeout\n   * @return true if successful, false otherwise\n   */\n  bool send_single_webhook_request(const std::string& url, const std::string& json_payload, std::chrono::milliseconds timeout_duration)\n  {\n    try {\n      // Parse URL to determine protocol and host\n      std::string parsed_url = url;\n      bool is_https = (parsed_url.find(\"https://\") == 0);\n      \n      if (is_https) {\n        parsed_url = parsed_url.substr(8); // Remove \"https://\"\n      } else if (parsed_url.find(\"http://\") == 0) {\n        parsed_url = parsed_url.substr(7); // Remove \"http://\"\n      }\n      \n      // Find path separator\n      size_t path_pos = parsed_url.find('/');\n      std::string host_port = parsed_url.substr(0, path_pos);\n      std::string path = (path_pos != std::string::npos) ? parsed_url.substr(path_pos) : \"/\";\n      \n      if (is_https) {\n        // Use HTTPS client with SSL verification control\n        bool verify_certificate = !config::webhook.skip_ssl_verify;\n        WebhookHttpsClient client(host_port, verify_certificate, host_port);\n        client.config.timeout = timeout_duration.count() / 1000; // Convert to seconds\n        try{\n          auto headers = generate_webhook_headers();\n          auto response = client.request(\"POST\", path, json_payload, headers);\n          return (response->status_code == \"200 OK\");\n        } catch (const std::exception& e) {\n          BOOST_LOG(warning) << \"Webhook HTTPS request error: \" << e.what();\n          return false;\n        }\n      } \n      else {\n        // Use HTTP client\n        SimpleWeb::Client<SimpleWeb::HTTP> client(host_port);\n        client.config.timeout = timeout_duration.count() / 1000; // Convert to seconds\n        try{\n          auto headers = generate_webhook_headers();\n          auto response = client.request(\"POST\", path, json_payload, headers);\n          return (response->status_code == \"200 OK\");\n        } catch (const std::exception& e) {\n          BOOST_LOG(warning) << \"Webhook HTTP request error: \" << e.what();\n          return false;\n        }\n      }\n    } catch (const std::exception& e) {\n      std::string error_msg = e.what();\n      BOOST_LOG(warning) << \"Webhook request error: \" << error_msg;\n      return false;\n    }\n  }\n\n\n  /**\n   * @brief 异步发送webhook事件\n   * @param event Webhook事件数据\n   */\n  void send_event_async(const event_t& event)\n  {\n    // Check if webhook is enabled\n    if (!is_enabled()) {\n      return;\n    }\n\n    // Initialize webhook format if not already done\n    static bool format_initialized = false;\n    if (!format_initialized) {\n      // 默认配置为webhook格式\n      configure_webhook_format(true);\n      format_initialized = true;\n    }\n\n    // Check thread limit and register thread atomically\n    if (!can_create_thread()) {\n      BOOST_LOG(warning) << \"Webhook thread limit reached (\" << MAX_CONCURRENT_THREADS << \"), skipping send\";\n      return;\n    }\n    \n    // Register thread before creating it\n    register_thread();\n\n    // Check rate limiting\n    if (is_rate_limited()) {\n      BOOST_LOG(warning) << \"Webhook rate limited, skipping send\";\n      unregister_thread(); // Unregister since we're not creating the thread\n      send_rate_limit_notification();\n      return;\n    }\n\n    // Run in separate thread to avoid blocking\n    std::thread([event]() {\n      try {\n        // Determine locale\n        bool is_chinese = (config::sunshine.locale == \"zh\" || config::sunshine.locale == \"zh_TW\");\n        \n        // Generate detailed JSON payload based on event type using format config\n        std::string json_payload = g_webhook_format.generate_json_payload(event, is_chinese);\n        \n        BOOST_LOG(debug) << \"Sending webhook: \" << json_payload;\n\n        // Create timeout controller\n        auto timeout_duration = config::webhook.timeout;\n        \n        // Send HTTP POST request using Simple-Web-Server client\n        bool success = send_webhook_request(config::webhook.url, json_payload, timeout_duration);\n        \n        if (success) {\n          BOOST_LOG(info) << \"Webhook sent successfully\";\n          record_successful_send(); // Record successful send for rate limiting\n        } else {\n          BOOST_LOG(warning) << \"Failed to send webhook\";\n        }\n        \n      } catch (const std::exception& e) {\n        BOOST_LOG(error) << \"Webhook error: \" << e.what();\n      } catch (...) {\n        BOOST_LOG(error) << \"Webhook unknown error occurred\";\n      }\n      \n      // Always unregister thread when done\n      unregister_thread();\n    }).detach();\n  }\n\n  /**\n   * @brief 检查webhook是否启用\n   * @return true if enabled, false otherwise\n   */\n  bool is_enabled()\n  {\n    return config::webhook.enabled && !config::webhook.url.empty();\n  }\n\n  /**\n   * @brief 获取告警消息\n   * @param type Webhook事件类型\n   * @param is_chinese 是否使用中文\n   * @return 告警消息\n   */\n  std::string get_alert_message(event_type_t type, bool is_chinese)\n  {\n    switch (type) {\n      case event_type_t::CONFIG_PIN_SUCCESS:\n        return is_chinese ? \"🔗 配置配对成功\" : \"🔗 Config pairing successful\";\n      case event_type_t::CONFIG_PIN_FAILED:\n        return is_chinese ? \"❌ 配置配对失败\" : \"❌ Config pairing failed\";\n      case event_type_t::NV_APP_LAUNCH:\n        return is_chinese ? \"🚀 应用启动\" : \"🚀 application launched\";\n      case event_type_t::NV_APP_RESUME:\n        return is_chinese ? \"▶️ 应用恢复\" : \"▶️ application resumed\";\n      case event_type_t::NV_APP_TERMINATE:\n        return is_chinese ? \"⏹️ 应用终止\" : \"⏹️ application terminated\";\n      case event_type_t::NV_SESSION_START:\n        return is_chinese ? \"📱 会话开始\" : \"📱 session started\";\n      case event_type_t::NV_SESSION_END:\n        return is_chinese ? \"📱 会话结束\" : \"📱 session ended\";\n      default:\n        return is_chinese ? \"🔔 系统通知\" : \"🔔 System notification\";\n    }\n  }\n\n  /**\n   * @brief 清理JSON字符串\n   * @param str 原始字符串\n   * @return 清理后的字符串\n   */\n  std::string sanitize_json_string(const std::string& str)\n  {\n    std::string result = str;\n    \n    // Escape backslashes first\n    result = std::regex_replace(result, std::regex(R\"(\\\\)\"), \"\\\\\\\\\");\n    \n    // Escape double quotes\n    result = std::regex_replace(result, std::regex(R\"(\")\"), \"\\\\\\\"\");\n    \n    // Escape newlines\n    result = std::regex_replace(result, std::regex(R\"(\\n)\"), \"\\\\n\");\n    \n    // Escape carriage returns\n    result = std::regex_replace(result, std::regex(R\"(\\r)\"), \"\\\\r\");\n    \n    // Escape tabs\n    result = std::regex_replace(result, std::regex(R\"(\\t)\"), \"\\\\t\");\n    \n    // Escape other control characters (0x00-0x1F except \\n, \\r, \\t)\n    result = std::regex_replace(result, std::regex(R\"([\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F])\"), \"\");\n    \n    return result;\n  }\n\n  /**\n   * @brief 获取当前时间戳\n   * @return 当前时间戳\n   */\n  std::string get_current_timestamp()\n  {\n    auto now = std::chrono::system_clock::now();\n    auto time_t = std::chrono::system_clock::to_time_t(now);\n    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(\n        now.time_since_epoch()) % 1000;\n    \n    std::ostringstream oss;\n    oss << std::put_time(std::localtime(&time_t), \"%Y-%m-%dT%H:%M:%S\");\n    oss << '.' << std::setfill('0') << std::setw(3) << ms.count();\n    \n    return oss.str();\n  }\n\n  /**\n   * @brief 检查是否达到速率限制\n   * @return true if rate limited, false otherwise\n   */\n  bool is_rate_limited()\n  {\n    std::lock_guard<std::mutex> lock(rate_limit_mutex);\n    \n    auto now = std::chrono::system_clock::now();\n    auto window_start = now - std::chrono::minutes(RATE_LIMIT_WINDOW_MINUTES);\n    \n    // Remove old entries (older than rate limit window)\n    successful_sends.erase(\n      std::remove_if(successful_sends.begin(), successful_sends.end(),\n        [window_start](const std::chrono::system_clock::time_point& time) {\n          return time < window_start;\n        }),\n      successful_sends.end()\n    );\n    \n    return successful_sends.size() >= MAX_SENDS_PER_MINUTE;\n  }\n\n  /**\n   * @brief 记录成功发送\n   */\n  void record_successful_send() {\n    std::lock_guard<std::mutex> lock(rate_limit_mutex);\n    successful_sends.push_back(std::chrono::system_clock::now());\n  }\n\n  /**\n   * @brief 发送速率限制通知\n   */\n  void send_rate_limit_notification()\n  {\n    if (rate_limit_notification_sent) {\n      return; // Only send once per rate limit period\n    }\n    \n    rate_limit_notification_sent = true;\n    \n    // Reset the flag after rate limit window\n    std::thread([]() {\n      std::this_thread::sleep_for(std::chrono::minutes(RATE_LIMIT_WINDOW_MINUTES));\n      rate_limit_notification_sent = false;\n    }).detach();\n    \n    // Send rate limit notification\n    bool is_chinese = (config::sunshine.locale == \"zh\" || config::sunshine.locale == \"zh_TW\");\n    std::string hostname = platf::get_host_name();\n    std::string local_ip = get_local_ip();\n    std::string ip_info = local_ip.empty() ? \"\" : local_ip;\n    std::string message = is_chinese ? \n      \"主机: \" + hostname + \" \" + ip_info + \"\\n ⚠️ Webhook 发送频率过高，已限制发送\\n最近\" + std::to_string(RATE_LIMIT_WINDOW_MINUTES) + \"分钟内发送次数超过\" + std::to_string(MAX_SENDS_PER_MINUTE) + \"次\\n时间: \" + get_current_timestamp() :\n      \"Host: \" + hostname + \" \" + ip_info + \"\\n ⚠️ Webhook sending rate too high, sending limited\\nExceeded \" + std::to_string(MAX_SENDS_PER_MINUTE) + \" sends in the last \" + std::to_string(RATE_LIMIT_WINDOW_MINUTES) + \" minute(s)\\nTime: \" + get_current_timestamp();\n    \n    std::ostringstream json_stream;\n    json_stream << \"{\";\n    json_stream << \"\\\"msgtype\\\":\\\"text\\\",\";\n    json_stream << \"\\\"hostname\\\":\\\"\" << sanitize_json_string(platf::get_host_name()) << \"\\\",\";\n    json_stream << \"\\\"text\\\":{\";\n    json_stream << \"\\\"content\\\":\\\"\" << sanitize_json_string(message) << \"\\\"\";\n    json_stream << \"}\";\n    json_stream << \"}\";\n    \n    std::string json_payload = json_stream.str();\n    \n    // Send the rate limit notification (special case - bypass thread limit)\n    std::thread([json_payload]() {\n      try {\n        send_single_webhook_request(config::webhook.url, json_payload, config::webhook.timeout);\n      } catch (const std::exception& e) {\n        BOOST_LOG(error) << \"Failed to send rate limit notification: \" << e.what();\n      } catch (...) {\n        BOOST_LOG(error) << \"Unknown error in rate limit notification\";\n      }\n    }).detach();\n  }\n\n  /**\n   * @brief 检查是否可以创建线程\n   * @return true if can create, false otherwise\n   */\n  bool can_create_thread()\n  {\n    return active_thread_count.load() < MAX_CONCURRENT_THREADS;\n  }\n\n  /**\n   * @brief 注册线程\n   */\n  void register_thread()\n  {\n    active_thread_count.fetch_add(1);\n    BOOST_LOG(debug) << \"Webhook thread registered, active threads: \" << active_thread_count.load();\n  }\n\n  /**\n   * @brief 注销线程\n   */\n  void unregister_thread()\n  {\n    int count = active_thread_count.fetch_sub(1);\n    BOOST_LOG(debug) << \"Webhook thread unregistered, active threads: \" << (count - 1);\n  }\n\n}  // namespace webhook\n"
  },
  {
    "path": "src/webhook.h",
    "content": "/**\n * @file src/webhook.h\n * @brief Webhook notification system for Sunshine.\n */\n#pragma once\n\n#include <string>\n#include <map>\n#include <chrono>\n\nnamespace webhook {\n\n  /**\n   * @brief Webhook event types for different operations\n   */\n  enum class event_type_t {\n    CONFIG_PIN_SUCCESS,    // 配置配对成功 / Config pairing successful\n    CONFIG_PIN_FAILED,     // 配置配对失败 / Config pairing failed\n    NV_APP_LAUNCH,         // NV应用启动 / NV application launched\n    NV_APP_RESUME,         // NV应用恢复 / NV application resumed\n    NV_APP_TERMINATE,      // NV应用终止 / NV application terminated\n    NV_SESSION_START,      // NV会话开始 / NV session started\n    NV_SESSION_END         // NV会话结束 / NV session ended\n  };\n\n  /**\n   * @brief Webhook event data structure\n   */\n  struct event_t {\n    event_type_t type;\n    std::string alert_type;        // 告警类型 / Alert type\n    std::string timestamp;\n    std::string client_name;\n    std::string client_ip;\n    std::string server_ip;\n    std::string app_name;\n    std::int64_t app_id = 0;\n    std::string session_id;\n    std::map<std::string, std::string> extra_data;\n  };\n\n  /**\n   * @brief Send webhook event asynchronously\n   * @param event The webhook event to send\n   */\n  void send_event_async(const event_t& event);\n\n  /**\n   * @brief Send single webhook HTTP POST request\n   * @param url Webhook URL\n   * @param json_payload JSON payload to send\n   * @param timeout_duration Request timeout\n   * @return true if successful, false otherwise\n   */\n  bool send_single_webhook_request(const std::string& url, const std::string& json_payload, std::chrono::milliseconds timeout_duration);\n\n  /**\n   * @brief Check if webhook is enabled\n   * @return true if webhook is enabled, false otherwise\n   */\n  bool is_enabled();\n\n  /**\n   * @brief Get localized alert message\n   * @param type Event type\n   * @param is_chinese Whether to use Chinese locale\n   * @return Localized alert message\n   */\n  std::string get_alert_message(event_type_t type, bool is_chinese);\n\n  /**\n   * @brief Sanitize string for JSON (escape special characters)\n   * @param str Input string\n   * @return Sanitized string safe for JSON\n   */\n  std::string sanitize_json_string(const std::string& str);\n\n  /**\n   * @brief Get current timestamp in ISO format\n   * @return ISO timestamp string\n   */\n  std::string get_current_timestamp();\n\n  /**\n   * @brief Generate detailed JSON payload for webhook\n   * @param event Webhook event data\n   * @param is_chinese Whether to use Chinese locale\n   * @return JSON string for webhook payload\n   */\n  std::string generate_webhook_json(const event_t& event, bool is_chinese);\n\n  /**\n   * @brief Check if webhook sending is rate limited\n   * @return true if rate limited, false otherwise\n   */\n  bool is_rate_limited();\n\n  /**\n   * @brief Record successful webhook send for rate limiting\n   */\n  void record_successful_send();\n\n  /**\n   * @brief Send rate limit exceeded notification\n   */\n  void send_rate_limit_notification();\n\n  /**\n   * @brief Check if we can create a new async thread\n   * @return true if can create, false if thread limit reached\n   */\n  bool can_create_thread();\n\n  /**\n   * @brief Register a new thread (increment counter)\n   */\n  void register_thread();\n\n  /**\n   * @brief Unregister a thread (decrement counter)\n   */\n  void unregister_thread();\n\n  /**\n   * @brief 获取本地IP地址\n   * @return 本地IP地址字符串，优先返回IPv4，其次IPv6，都获取不到返回空字符串\n   */\n  std::string get_local_ip();\n\n}  // namespace webhook\n"
  },
  {
    "path": "src/webhook_format.cpp",
    "content": "/**\n * @file src/webhook_format.cpp\n * @brief Webhook格式配置和模板实现\n */\n#include \"webhook_format.h\"\n#include \"webhook.h\"\n#include \"config.h\"\n#include \"logging.h\"\n#include \"platform/common.h\"\n#include <sstream>\n#include <regex>\n\nnamespace webhook {\n\n  // 全局webhook格式实例\n  WebhookFormat g_webhook_format;\n\n  WebhookFormat::WebhookFormat(format_type_t format_type)\n    : format_type_(format_type)\n    , use_colors_(true)\n    , simplify_ip_(true)\n    , time_format_(\"%Y-%m-%d %H:%M:%S\")\n  {\n  }\n\n  void WebhookFormat::set_format_type(format_type_t format_type) {\n    format_type_ = format_type;\n  }\n\n  format_type_t WebhookFormat::get_format_type() const {\n    return format_type_;\n  }\n\n  void WebhookFormat::set_custom_template(event_type_t event_type, const std::string& template_str) {\n    custom_templates_[event_type] = template_str;\n  }\n\n  void WebhookFormat::set_use_colors(bool use_colors) {\n    use_colors_ = use_colors;\n  }\n\n  void WebhookFormat::set_simplify_ip(bool simplify_ip) {\n    simplify_ip_ = simplify_ip;\n  }\n\n  void WebhookFormat::set_time_format(const std::string& time_format) {\n    time_format_ = time_format;\n  }\n\n  std::string WebhookFormat::format_ip_address(const std::string& ip) const {\n    if (ip.empty()) return \"\";\n    if (!simplify_ip_) {\n      return ip;\n    }\n    // 处理IPv6地址\n    if (ip.find(':') != std::string::npos) {\n      // 简化IPv6显示\n      if (ip.find(\"fe80::\") == 0) {\n        return \"IPv6 (本地链路)\";\n      } else if (ip.find(\"::1\") != std::string::npos) {\n        return \"IPv6 (回环)\";\n      } else {\n        return \"IPv6\";\n      }\n    }\n    \n    // IPv4地址直接返回\n    return ip;\n  }\n\n  std::string WebhookFormat::format_timestamp(const std::string& timestamp) const\n  {\n    // 将 ISO 8601 格式转换为更友好的格式\n    // 2025-10-07T16:36:33.595 -> 2025-10-07 16:36:33\n    std::string formatted = timestamp;\n    size_t dot_pos = formatted.find('.');\n    if (dot_pos != std::string::npos) {\n      formatted = formatted.substr(0, dot_pos);\n    }\n    size_t t_pos = formatted.find('T');\n    if (t_pos != std::string::npos) {\n      formatted[t_pos] = ' ';\n    }\n    return formatted;\n  }\n\n  std::string WebhookFormat::get_event_color(event_type_t event_type) const\n  {\n    if (!use_colors_) {\n      return \"\";\n    }\n\n    switch (event_type) {\n      case event_type_t::CONFIG_PIN_SUCCESS:\n      case event_type_t::NV_APP_LAUNCH:\n      case event_type_t::NV_APP_RESUME:\n      case event_type_t::NV_SESSION_START:\n        return colors::COLOR_INFO;\n        \n      case event_type_t::CONFIG_PIN_FAILED:\n      case event_type_t::NV_APP_TERMINATE:\n        return colors::COLOR_WARNING;\n        \n      case event_type_t::NV_SESSION_END:\n        return colors::COLOR_COMMENT;\n        \n      default:\n        return colors::COLOR_COMMENT;\n    }\n  }\n\n  std::string WebhookFormat::get_event_title(event_type_t event_type, bool is_chinese) const\n  {\n    switch (event_type) {\n      case event_type_t::CONFIG_PIN_SUCCESS:\n        return is_chinese ? \"配置配对成功\" : \"Config Pairing Successful\";\n      case event_type_t::CONFIG_PIN_FAILED:\n        return is_chinese ? \"配置配对失败\" : \"Config Pairing Failed\";\n      case event_type_t::NV_APP_LAUNCH:\n        return is_chinese ? \"应用启动\" : \"Application Launched\";\n      case event_type_t::NV_APP_RESUME:\n        return is_chinese ? \"应用恢复\" : \"Application Resumed\";\n      case event_type_t::NV_APP_TERMINATE:\n        return is_chinese ? \"应用终止\" : \"Application Terminated\";\n      case event_type_t::NV_SESSION_START:\n        return is_chinese ? \"会话开始\" : \"Session Started\";\n      case event_type_t::NV_SESSION_END:\n        return is_chinese ? \"会话结束\" : \"Session Ended\";\n      default:\n        return is_chinese ? \"系统通知\" : \"System Notification\";\n    }\n  }\n\n  std::string WebhookFormat::generate_markdown_content(const event_t& event, bool is_chinese) const\n  {\n    std::ostringstream content_stream;\n    // 获取主机信息\n    std::string hostname = platf::get_host_name();\n    std::string local_ip = get_local_ip();\n    std::string formatted_ip = format_ip_address(local_ip);\n    content_stream << (is_chinese ? \"**Sunshine系统通知**\" : \"**Sunshine System Notification**\") << \"\\n\\n\";\n    \n    // 根据事件类型设置不同的颜色和内容\n    std::string event_title = get_event_title(event.type, is_chinese);\n    std::string event_color = get_event_color(event.type);\n    \n    if (use_colors_ && !event_color.empty()) {\n      content_stream << \"<font color=\\\"\" << event_color << \"\\\">**\" << event_title << \"**</font>\\n\\n\";\n    } else {\n      content_stream << \"**\" << event_title << \"**\\n\\n\";\n    }\n    // 添加基本信息\n    content_stream << \">主机名:<font color=\\\"comment\\\">\" << hostname << \"</font>\\n\";\n    if (!formatted_ip.empty()) {\n      content_stream << \">IP地址:<font color=\\\"comment\\\">\" << formatted_ip << \"</font>\\n\";\n    }\n    // 添加事件特定信息\n    switch (event.type) {\n      case event_type_t::CONFIG_PIN_SUCCESS:\n      case event_type_t::CONFIG_PIN_FAILED: {\n        if (!event.client_name.empty()) {\n          content_stream << \">客户端名称:<font color=\\\"comment\\\">\" << event.client_name << \"</font>\\n\";\n        }\n        if (!event.client_ip.empty()) {\n          content_stream << \">客户端IP:<font color=\\\"comment\\\">\" << event.client_ip << \"</font>\\n\";\n        }\n        if (!event.server_ip.empty()) {\n          content_stream << \">服务器IP:<font color=\\\"comment\\\">\" << event.server_ip << \"</font>\\n\";\n        }\n        break;\n      }\n      case event_type_t::NV_APP_LAUNCH:\n      case event_type_t::NV_APP_RESUME:\n      case event_type_t::NV_APP_TERMINATE: {\n        if (!event.app_name.empty()) {\n          content_stream << \">应用名称:<font color=\\\"comment\\\">\" << event.app_name << \"</font>\\n\";\n        }\n        if (event.app_id > 0) {\n          content_stream << \">应用ID:<font color=\\\"comment\\\">\" << event.app_id << \"</font>\\n\";\n        }\n        if (!event.client_name.empty()) {\n          content_stream << \">客户端:<font color=\\\"comment\\\">\" << event.client_name << \"</font>\\n\";\n        }\n        if (!event.client_ip.empty()) {\n          content_stream << \">客户端IP:<font color=\\\"comment\\\">\" << event.client_ip << \"</font>\\n\";\n        }\n        if (!event.server_ip.empty()) {\n          content_stream << \">服务器IP:<font color=\\\"comment\\\">\" << event.server_ip << \"</font>\\n\";\n        }\n        // 添加额外信息\n        for (const auto& [key, value] : event.extra_data) {\n          if (key == \"resolution\") {\n            content_stream << \">分辨率:<font color=\\\"comment\\\">\" << value << \"</font>\\n\";\n          } else if (key == \"fps\") {\n            content_stream << \">帧率:<font color=\\\"comment\\\">\" << value << \"</font>\\n\";\n          } else if (key == \"host_audio\") {\n            content_stream << \">音频:<font color=\\\"comment\\\">\" \n                          << (value == \"true\" ? (is_chinese ? \"启用\" : \"Enabled\") : (is_chinese ? \"禁用\" : \"Disabled\")) << \"</font>\\n\";\n          }\n        }\n        break;\n      }\n      case event_type_t::NV_SESSION_START:\n      case event_type_t::NV_SESSION_END: {\n        if (!event.app_name.empty()) {\n          content_stream << \">应用名称:<font color=\\\"comment\\\">\" << event.app_name << \"</font>\\n\";\n        }\n        if (!event.client_name.empty()) {\n          content_stream << \">客户端:<font color=\\\"comment\\\">\" << event.client_name << \"</font>\\n\";\n        }\n        if (!event.session_id.empty()) {\n          content_stream << \">会话ID:<font color=\\\"comment\\\">\" << event.session_id << \"</font>\\n\";\n        }\n        break;\n      }\n      default:\n        break;\n    }\n    content_stream << \">时间:<font color=\\\"comment\\\">\" << format_timestamp(event.timestamp) << \"</font>\";\n    // 添加错误信息\n    auto error_it = event.extra_data.find(\"error\");\n    if (error_it != event.extra_data.end()) {\n      content_stream << \"\\n>错误信息:<font color=\\\"warning\\\">\" << error_it->second << \"</font>\";\n    }\n    return content_stream.str();\n  }\n\n  std::string WebhookFormat::generate_text_content(const event_t& event, bool is_chinese) const {\n    std::ostringstream content_stream;\n    \n    std::string hostname = platf::get_host_name();\n    std::string local_ip = get_local_ip();\n    std::string formatted_ip = format_ip_address(local_ip);\n    // 构建纯文本内容\n    content_stream << (is_chinese ? \"Sunshine系统通知\" : \"Sunshine System Notification\") << \"\\n\";\n    content_stream << \"================================\\n\";\n    content_stream << (is_chinese ? \"事件: \" : \"Event: \") << get_event_title(event.type, is_chinese) << \"\\n\";\n    content_stream << (is_chinese ? \"主机名: \" : \"Hostname: \") << hostname << \"\\n\";\n    \n    if (!formatted_ip.empty()) {\n      content_stream << (is_chinese ? \"IP地址: \" : \"IP Address: \") << formatted_ip << \"\\n\";\n    }\n    // 添加事件特定信息\n    switch (event.type) {\n      case event_type_t::CONFIG_PIN_SUCCESS:\n      case event_type_t::CONFIG_PIN_FAILED: {\n        if (!event.client_name.empty()) {\n          content_stream << (is_chinese ? \"客户端名称: \" : \"Client Name: \") << event.client_name << \"\\n\";\n        }\n        if (!event.client_ip.empty()) {\n          content_stream << (is_chinese ? \"客户端IP: \" : \"Client IP: \") << event.client_ip << \"\\n\";\n        }\n        if (!event.server_ip.empty()) {\n          content_stream << (is_chinese ? \"服务器IP: \" : \"Server IP: \") << event.server_ip << \"\\n\";\n        }\n        break;\n      }\n      case event_type_t::NV_APP_LAUNCH:\n      case event_type_t::NV_APP_RESUME:\n      case event_type_t::NV_APP_TERMINATE: {\n        if (!event.app_name.empty()) {\n          content_stream << (is_chinese ? \"应用名称: \" : \"App Name: \") << event.app_name << \"\\n\";\n        }\n        if (event.app_id > 0) {\n          content_stream << (is_chinese ? \"应用ID: \" : \"App ID: \") << event.app_id << \"\\n\";\n        }\n        if (!event.client_name.empty()) {\n          content_stream << (is_chinese ? \"客户端: \" : \"Client: \") << event.client_name << \"\\n\";\n        }\n        if (!event.client_ip.empty()) {\n          content_stream << (is_chinese ? \"客户端IP: \" : \"Client IP: \") << event.client_ip << \"\\n\";\n        }\n        if (!event.server_ip.empty()) {\n          content_stream << (is_chinese ? \"服务器IP: \" : \"Server IP: \") << event.server_ip << \"\\n\";\n        }\n        break;\n      }\n      case event_type_t::NV_SESSION_START:\n      case event_type_t::NV_SESSION_END: {\n        if (!event.app_name.empty()) {\n          content_stream << (is_chinese ? \"应用名称: \" : \"App Name: \") << event.app_name << \"\\n\";\n        }\n        if (!event.client_name.empty()) {\n          content_stream << (is_chinese ? \"客户端: \" : \"Client: \") << event.client_name << \"\\n\";\n        }\n        if (!event.session_id.empty()) {\n          content_stream << (is_chinese ? \"会话ID: \" : \"Session ID: \") << event.session_id << \"\\n\";\n        }\n        break;\n      }\n      default:\n        break;\n    }\n    content_stream << (is_chinese ? \"时间: \" : \"Time: \") << format_timestamp(event.timestamp) << \"\\n\";\n    // 添加错误信息\n    auto error_it = event.extra_data.find(\"error\");\n    if (error_it != event.extra_data.end()) {\n      content_stream << (is_chinese ? \"错误信息: \" : \"Error: \") << error_it->second << \"\\n\";\n    }\n    return content_stream.str();\n  }\n\n  std::string WebhookFormat::generate_json_content(const event_t& event, bool is_chinese) const\n  {\n    std::ostringstream json_stream;\n    std::string hostname = platf::get_host_name();\n    std::string local_ip = get_local_ip();\n    std::string formatted_ip = format_ip_address(local_ip);\n    json_stream << \"{\";\n    json_stream << \"\\\"system\\\":\\\"Sunshine\\\",\";\n    json_stream << \"\\\"hostname\\\":\\\"\" << hostname << \"\\\",\";\n    if (!formatted_ip.empty()) {\n      json_stream << \"\\\"ip_address\\\":\\\"\" << formatted_ip << \"\\\",\";\n    }\n    json_stream << \"\\\"event_type\\\":\\\"\" << get_event_title(event.type, is_chinese) << \"\\\",\";\n    json_stream << \"\\\"timestamp\\\":\\\"\" << format_timestamp(event.timestamp) << \"\\\"\";\n    \n    // 添加事件特定字段\n    if (!event.client_name.empty()) {\n      json_stream << \",\\\"client_name\\\":\\\"\" << event.client_name << \"\\\"\";\n    }\n    if (!event.client_ip.empty()) {\n      json_stream << \",\\\"client_ip\\\":\\\"\" << event.client_ip << \"\\\"\";\n    }\n    if (!event.server_ip.empty()) {\n      json_stream << \",\\\"server_ip\\\":\\\"\" << event.server_ip << \"\\\"\";\n    }\n    if (!event.app_name.empty()) {\n      json_stream << \",\\\"app_name\\\":\\\"\" << event.app_name << \"\\\"\";\n    }\n    if (event.app_id > 0) {\n      json_stream << \",\\\"app_id\\\":\" << event.app_id;\n    }\n    if (!event.session_id.empty()) {\n      json_stream << \",\\\"session_id\\\":\\\"\" << event.session_id << \"\\\"\";\n    }\n    \n    // 添加额外数据\n    if (!event.extra_data.empty()) {\n      json_stream << \",\\\"extra_data\\\":{\";\n      bool first = true;\n      for (const auto& [key, value] : event.extra_data) {\n        if (!first) json_stream << \",\";\n        json_stream << \"\\\"\" << key << \"\\\":\\\"\" << value << \"\\\"\";\n        first = false;\n      }\n      json_stream << \"}\";\n    }\n    \n    json_stream << \"}\";\n    return json_stream.str();\n  }\n\n  std::string WebhookFormat::generate_custom_content(const event_t& event, bool is_chinese) const\n  {\n    auto it = custom_templates_.find(event.type);\n    if (it != custom_templates_.end()) {\n      return replace_template_variables(it->second, event, is_chinese);\n    }\n    \n    // 如果没有自定义模板，回退到Markdown格式\n    return generate_markdown_content(event, is_chinese);\n  }\n\n  std::string WebhookFormat::replace_template_variables(const std::string& template_str, const event_t& event, bool is_chinese) const\n  {\n    std::string result = template_str;\n    \n    // 替换变量\n    std::string hostname = platf::get_host_name();\n    std::string local_ip = get_local_ip();\n    std::string formatted_ip = format_ip_address(local_ip);\n    \n    // 使用正则表达式替换变量\n    result = std::regex_replace(result, std::regex(\"\\\\{\\\\{hostname\\\\}\\\\}\"), hostname);\n    result = std::regex_replace(result, std::regex(\"\\\\{\\\\{ip_address\\\\}\\\\}\"), formatted_ip);\n    result = std::regex_replace(result, std::regex(\"\\\\{\\\\{event_title\\\\}\\\\}\"), get_event_title(event.type, is_chinese));\n    result = std::regex_replace(result, std::regex(\"\\\\{\\\\{timestamp\\\\}\\\\}\"), format_timestamp(event.timestamp));\n    result = std::regex_replace(result, std::regex(\"\\\\{\\\\{client_name\\\\}\\\\}\"), event.client_name);\n    result = std::regex_replace(result, std::regex(\"\\\\{\\\\{client_ip\\\\}\\\\}\"), event.client_ip);\n    result = std::regex_replace(result, std::regex(\"\\\\{\\\\{server_ip\\\\}\\\\}\"), event.server_ip);\n    result = std::regex_replace(result, std::regex(\"\\\\{\\\\{app_name\\\\}\\\\}\"), event.app_name);\n    result = std::regex_replace(result, std::regex(\"\\\\{\\\\{app_id\\\\}\\\\}\"), std::to_string(event.app_id));\n    result = std::regex_replace(result, std::regex(\"\\\\{\\\\{session_id\\\\}\\\\}\"), event.session_id);\n    \n    return result;\n  }\n\n  std::string WebhookFormat::generate_content(const event_t& event, bool is_chinese) const\n  {\n    switch (format_type_) {\n      case format_type_t::MARKDOWN:\n        return generate_markdown_content(event, is_chinese);\n      case format_type_t::TEXT:\n        return generate_text_content(event, is_chinese);\n      case format_type_t::JSON:\n        return generate_json_content(event, is_chinese);\n      case format_type_t::CUSTOM:\n        return generate_custom_content(event, is_chinese);\n      default:\n        return generate_markdown_content(event, is_chinese);\n    }\n  }\n\n  std::string WebhookFormat::generate_json_payload(const event_t& event, bool is_chinese) const\n  {\n    std::string content = generate_content(event, is_chinese);\n    \n    // 检查内容长度限制（限制4096字节）\n    const size_t MAX_CONTENT_LENGTH = 4096;\n    if (content.length() > MAX_CONTENT_LENGTH) {\n      // 截断内容并添加省略号\n      content = content.substr(0, MAX_CONTENT_LENGTH - 10) + \"...\";\n      BOOST_LOG(warning) << \"Webhook content truncated to \" << MAX_CONTENT_LENGTH << \" bytes\";\n    }\n    \n    switch (format_type_) {\n      case format_type_t::MARKDOWN:\n        return \"{\\\"msgtype\\\":\\\"markdown\\\",\\\"markdown\\\":{\\\"content\\\":\\\"\" + sanitize_json_string(content) + \"\\\"}}\";\n      case format_type_t::TEXT:\n        return \"{\\\"msgtype\\\":\\\"text\\\",\\\"text\\\":{\\\"content\\\":\\\"\" + sanitize_json_string(content) + \"\\\"}}\";\n      case format_type_t::JSON:\n        return content; // JSON格式直接返回内容\n      case format_type_t::CUSTOM:\n        return \"{\\\"msgtype\\\":\\\"markdown\\\",\\\"markdown\\\":{\\\"content\\\":\\\"\" + sanitize_json_string(content) + \"\\\"}}\";\n      default:\n        return \"{\\\"msgtype\\\":\\\"markdown\\\",\\\"markdown\\\":{\\\"content\\\":\\\"\" + sanitize_json_string(content) + \"\\\"}}\";\n    }\n  }\n\n  void init_webhook_format()\n  {\n    // 初始化默认格式配置\n    g_webhook_format.set_format_type(format_type_t::MARKDOWN);\n    g_webhook_format.set_use_colors(true);\n    g_webhook_format.set_simplify_ip(true);\n    g_webhook_format.set_time_format(\"%Y-%m-%d %H:%M:%S\");\n  }\n\n  void load_format_config()\n  {\n    // 从配置文件加载格式设置\n    // 这里可以添加从config::webhook读取格式配置的逻辑\n    init_webhook_format();\n  }\n\n  void configure_webhook_format(bool use_markdown)\n  {\n    if (use_markdown) {\n      g_webhook_format.set_format_type(format_type_t::MARKDOWN);\n    } else {\n      g_webhook_format.set_format_type(format_type_t::TEXT);\n    }\n    \n    // webhook优化设置\n    g_webhook_format.set_use_colors(true);      // 启用颜色支持\n    g_webhook_format.set_simplify_ip(true);     // 简化IP显示\n    g_webhook_format.set_time_format(\"%Y-%m-%d %H:%M:%S\"); // 标准时间格式\n    \n    BOOST_LOG(debug) << \"Webhook configured (Markdown: \" << use_markdown << \")\";\n  }\n\n  bool validate_webhook_content_length(const std::string& content) {\n    const size_t MAX_CONTENT_LENGTH = 4096;\n    return content.length() <= MAX_CONTENT_LENGTH;\n  }\n\n} // namespace webhook\n"
  },
  {
    "path": "src/webhook_format.h",
    "content": "/**\n * @file src/webhook_format.h\n * @brief Webhook格式配置和模板定义\n */\n#pragma once\n\n#include <string>\n#include <map>\n#include <functional>\n#include \"webhook.h\"\n\nnamespace webhook {\n\n  /**\n   * @brief Webhook格式类型\n   */\n  enum class format_type_t {\n    MARKDOWN,     // Markdown格式（支持HTML标签）\n    TEXT,         // 纯文本格式\n    JSON,         // JSON格式\n    CUSTOM        // 自定义格式\n  };\n\n  /**\n   * @brief 颜色类型定义\n   */\n  namespace colors {\n    constexpr const char* COLOR_INFO = \"info\";           // 信息（绿色）\n    constexpr const char* COLOR_WARNING = \"warning\";     // 警告（橙色）\n    constexpr const char* COLOR_ERROR = \"error\";         // 错误（红色）\n    constexpr const char* COLOR_COMMENT = \"comment\";     // 注释（灰色）\n    constexpr const char* COLOR_SUCCESS = \"success\";     // 成功（蓝色）\n  }\n\n  /**\n   * @brief Webhook格式配置类\n   */\n  class WebhookFormat {\n  public:\n    /**\n     * @brief 构造函数\n     * @param format_type 格式类型\n     */\n    explicit WebhookFormat(format_type_t format_type = format_type_t::MARKDOWN);\n\n    /**\n     * @brief 设置格式类型\n     * @param format_type 格式类型\n     */\n    void set_format_type(format_type_t format_type);\n\n    /**\n     * @brief 获取格式类型\n     * @return 格式类型\n     */\n    format_type_t get_format_type() const;\n\n    /**\n     * @brief 设置自定义模板\n     * @param event_type 事件类型\n     * @param template_str 模板字符串\n     */\n    void set_custom_template(event_type_t event_type, const std::string& template_str);\n\n    /**\n     * @brief 设置是否使用颜色\n     * @param use_colors 是否使用颜色\n     */\n    void set_use_colors(bool use_colors);\n\n    /**\n     * @brief 设置是否简化IP显示\n     * @param simplify_ip 是否简化IP显示\n     */\n    void set_simplify_ip(bool simplify_ip);\n\n    /**\n     * @brief 设置时间格式\n     * @param time_format 时间格式字符串\n     */\n    void set_time_format(const std::string& time_format);\n\n    /**\n     * @brief 生成webhook内容\n     * @param event 事件数据\n     * @param is_chinese 是否使用中文\n     * @return 格式化的内容字符串\n     */\n    std::string generate_content(const event_t& event, bool is_chinese) const;\n\n    /**\n     * @brief 生成完整的JSON payload\n     * @param event 事件数据\n     * @param is_chinese 是否使用中文\n     * @return JSON字符串\n     */\n    std::string generate_json_payload(const event_t& event, bool is_chinese) const;\n\n  private:\n    format_type_t format_type_;\n    bool use_colors_;\n    bool simplify_ip_;\n    std::string time_format_;\n    std::map<event_type_t, std::string> custom_templates_;\n\n    /**\n     * @brief 格式化IP地址\n     * @param ip IP地址字符串\n     * @return 格式化后的IP地址\n     */\n    std::string format_ip_address(const std::string& ip) const;\n\n    /**\n     * @brief 格式化时间戳\n     * @param timestamp ISO 8601格式时间戳\n     * @return 格式化后的时间字符串\n     */\n    std::string format_timestamp(const std::string& timestamp) const;\n\n    /**\n     * @brief 获取事件颜色\n     * @param event_type 事件类型\n     * @return 颜色字符串\n     */\n    std::string get_event_color(event_type_t event_type) const;\n\n    /**\n     * @brief 获取事件标题\n     * @param event_type 事件类型\n     * @param is_chinese 是否使用中文\n     * @return 事件标题\n     */\n    std::string get_event_title(event_type_t event_type, bool is_chinese) const;\n\n    /**\n     * @brief 生成Markdown格式内容\n     * @param event 事件数据\n     * @param is_chinese 是否使用中文\n     * @return Markdown内容\n     */\n    std::string generate_markdown_content(const event_t& event, bool is_chinese) const;\n\n    /**\n     * @brief 生成文本格式内容\n     * @param event 事件数据\n     * @param is_chinese 是否使用中文\n     * @return 文本内容\n     */\n    std::string generate_text_content(const event_t& event, bool is_chinese) const;\n\n    /**\n     * @brief 生成JSON格式内容\n     * @param event 事件数据\n     * @param is_chinese 是否使用中文\n     * @return JSON内容\n     */\n    std::string generate_json_content(const event_t& event, bool is_chinese) const;\n\n    /**\n     * @brief 生成自定义格式内容\n     * @param event 事件数据\n     * @param is_chinese 是否使用中文\n     * @return 自定义内容\n     */\n    std::string generate_custom_content(const event_t& event, bool is_chinese) const;\n\n    /**\n     * @brief 替换模板变量\n     * @param template_str 模板字符串\n     * @param event 事件数据\n     * @param is_chinese 是否使用中文\n     * @return 替换后的字符串\n     */\n    std::string replace_template_variables(const std::string& template_str, \n                                          const event_t& event, \n                                          bool is_chinese) const;\n  };\n\n  /**\n   * @brief 全局webhook格式实例\n   */\n  extern WebhookFormat g_webhook_format;\n\n  /**\n   * @brief 初始化webhook格式配置\n   */\n  void init_webhook_format();\n\n  /**\n   * @brief 从配置文件加载格式设置\n   */\n  void load_format_config();\n\n  /**\n   * @brief 配置格式\n   * @param use_markdown 是否使用Markdown格式（默认true）\n   */\n  void configure_webhook_format(bool use_markdown = true);\n\n  /**\n   * @brief 验证内容长度是否符合webhook要求\n   * @param content 内容字符串\n   * @return 是否符合长度要求\n   */\n  bool validate_webhook_content_length(const std::string& content);\n\n} // namespace webhook\n"
  },
  {
    "path": "src/webhook_httpsclient.cpp",
    "content": "/**\n * @file src/webhook_httpsclient.cpp\n * @brief HTTPS client for webhook with certificate verification\n */\n#include \"webhook_httpsclient.h\"\n#include \"logging.h\"\n#include <openssl/ssl.h>\n#include <openssl/x509.h>\n\nextern \"C\" {\n  static int webhook_verify_cb(int ok, X509_STORE_CTX *ctx) {\n    int err_code = X509_STORE_CTX_get_error(ctx);\n\n    switch (err_code) {\n      case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:\n      case X509_V_ERR_CERT_NOT_YET_VALID:\n      case X509_V_ERR_CERT_HAS_EXPIRED:\n        return 1;\n\n      default:\n        return ok;\n    }\n  }\n}\n\nnamespace webhook {\n\n  WebhookHttpsClient::WebhookHttpsClient(const std::string& server_port_path, \n                                        bool verify_certificate, \n                                        const std::string& expected_hostname)\n      : SimpleWeb::Client<SimpleWeb::HTTPS>(server_port_path, false),\n        expected_hostname_(expected_hostname.empty() ? server_port_path : expected_hostname) {\n    \n    if (verify_certificate) {\n      context.set_default_verify_paths();\n      context.set_verify_mode(boost::asio::ssl::verify_peer);\n      context.set_verify_callback([](bool preverified, boost::asio::ssl::verify_context& ctx) {\n        return webhook_verify_cb(preverified ? 1 : 0, ctx.native_handle()) == 1;\n      });\n      BOOST_LOG(debug) << \"WebhookHttpsClient: SSL verification enabled for host: \" << expected_hostname_;\n    } else {\n      context.set_verify_mode(boost::asio::ssl::verify_none);\n      BOOST_LOG(debug) << \"WebhookHttpsClient: SSL verification disabled\";\n    }\n  }\n\n\n}  // namespace webhook"
  },
  {
    "path": "src/webhook_httpsclient.h",
    "content": "/**\n * @file src/webhook_httpsclient.h\n * @brief HTTPS client for webhook with certificate verification\n */\n#pragma once\n\n#include <string>\n#include <Simple-Web-Server/client_https.hpp>\n\nnamespace webhook {\n\n  class WebhookHttpsClient : public SimpleWeb::Client<SimpleWeb::HTTPS> {\n  public:\n    WebhookHttpsClient(const std::string& server_port_path, \n                      bool verify_certificate = true, \n                      const std::string& expected_hostname = \"\");\n\n  private:\n    std::string expected_hostname_;\n  };\n\n}  // namespace webhook\n"
  },
  {
    "path": "src_assets/common/assets/web/apps.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bs-theme=\"auto\">\n  <head>\n    <%- header %>\n  </head>\n\n  <body id=\"app\" v-cloak>\n    <!-- Vue 应用挂载点 -->\n  </body>\n\n  <script type=\"module\">\n    import { createApp } from 'vue'\n    import { initApp } from './init'\n    import Apps from './views/Apps.vue'\n\n    const app = createApp(Apps)\n    initApp(app)\n  </script>\n</html>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/AccordionItem.vue",
    "content": "<template>\n  <div class=\"accordion-item\">\n    <h2 class=\"accordion-header\" :id=\"id + 'Heading'\">\n      <button\n        class=\"accordion-button\"\n        :class=\"{ collapsed: !show }\"\n        type=\"button\"\n        data-bs-toggle=\"collapse\"\n        :data-bs-target=\"'#' + id + 'Collapse'\"\n        :aria-expanded=\"show\"\n        :aria-controls=\"id + 'Collapse'\"\n      >\n        <i :class=\"['fas', icon, 'me-2']\"></i>{{ title }}\n      </button>\n    </h2>\n    <div\n      :id=\"id + 'Collapse'\"\n      class=\"accordion-collapse collapse\"\n      :class=\"{ show }\"\n      :aria-labelledby=\"id + 'Heading'\"\n      :data-bs-parent=\"'#' + parentId\"\n    >\n      <div class=\"accordion-body\">\n        <slot></slot>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'AccordionItem',\n  props: {\n    id: { type: String, required: true },\n    icon: { type: String, required: true },\n    title: { type: String, required: true },\n    parentId: { type: String, required: true },\n    show: { type: Boolean, default: false },\n  },\n}\n</script>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/AppCard.vue",
    "content": "<template>\n  <div class=\"app-card\" :class=\"{ 'app-card-dragging': isDragging }\">\n    <div class=\"app-card-inner\">\n      <!-- 应用图标 -->\n      <div class=\"app-icon-container\">\n        <img \n          v-if=\"app['image-path']\" \n          :src=\"getImageUrl()\" \n          :alt=\"app.name\"\n          class=\"app-icon\"\n          @error=\"handleImageError\"\n        >\n        <div v-else class=\"app-icon-placeholder\">\n          <i class=\"fas fa-desktop\"></i>\n        </div>\n      </div>\n      \n      <!-- 应用信息 -->\n      <div class=\"app-info\" :title=\"app.cmd\" @click=\"copyToClipboard(app.cmd, app.name, $event)\">\n        <h3 class=\"app-name\">{{ app.name }}</h3>\n        <p class=\"app-command\" v-if=\"app.cmd\">\n          <i class=\"fas fa-terminal me-1\"></i>\n          {{ truncateText(app.cmd, 50) }}\n        </p>\n        <div class=\"app-tags\">\n          <span v-if=\"app['exclude-global-prep-cmd'] && app['exclude-global-prep-cmd'] !== 'false'\" class=\"app-tag tag-exclude-global-prep-cmd\">\n            <i class=\"fas fa-ellipsis-h me-1\"></i>全局预处理命令\n          </span>\n          <span v-if=\"app['menu-cmd'] && app['menu-cmd'].length > 0\" class=\"app-tag tag-menu\">\n            <span class=\"badge rounded-pill bg-secondary me-1\">{{ app['menu-cmd'].length }}</span>菜单命令\n          </span>\n          <span v-if=\"app.elevated && app.elevated !== 'false'\" class=\"app-tag tag-elevated\">\n            <i class=\"fas fa-shield-alt me-1\"></i>管理员\n          </span>\n          <span v-if=\"app['auto-detach'] && app['auto-detach'] !== 'false'\" class=\"app-tag tag-detach\">\n            <i class=\"fas fa-unlink me-1\"></i>关闭时不退出串流\n          </span>\n        </div>\n      </div>\n      \n      <!-- 操作按钮 -->\n      <div class=\"app-actions\">\n        <button \n          class=\"btn btn-edit\" \n          @click=\"$emit('edit')\"\n          :title=\"$t('apps.edit')\"\n        >\n          <i class=\"fas fa-edit\"></i>\n        </button>\n        <button \n          class=\"btn btn-delete\" \n          @click=\"$emit('delete')\"\n          :title=\"$t('apps.delete')\"\n        >\n          <i class=\"fas fa-trash\"></i>\n        </button>\n      </div>\n      \n      <!-- 拖拽手柄 -->\n      <div v-if=\"draggable\" class=\"drag-handle\">\n        <i class=\"fas fa-grip-vertical\"></i>\n      </div>\n      \n      <!-- 搜索状态指示 -->\n      <div v-if=\"isSearchResult\" class=\"search-indicator\">\n        <i class=\"fas fa-search\"></i>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { getImagePreviewUrl } from '../utils/imageUtils.js';\n\nexport default {\n  name: 'AppCard',\n  props: {\n    app: {\n      type: Object,\n      required: true\n    },\n    draggable: {\n      type: Boolean,\n      default: true\n    },\n    isSearchResult: {\n      type: Boolean,\n      default: false\n    },\n    isDragging: {\n      type: Boolean,\n      default: false\n    }\n  },\n  emits: ['edit', 'delete', 'copy-success', 'copy-error'],\n  methods: {\n    /**\n     * 处理图像错误\n     */\n    handleImageError(event) {\n      const element = event.target;\n      element.style.display = 'none';\n      if (element.nextElementSibling) {\n        element.nextElementSibling.style.display = 'flex';\n      }\n    },\n    \n    /**\n     * 获取图片URL\n     */\n    getImageUrl() {\n      return getImagePreviewUrl(this.app['image-path']);\n    },\n    \n    /**\n     * 截断文本\n     */\n    truncateText(text, length) {\n      if (!text) return '';\n      if (text.length <= length) return text;\n      return text.substring(0, length) + '...';\n    },\n    \n    /**\n     * 复制到剪贴板\n     */\n    async copyToClipboard(text, appName, event) {\n      if (!text) {\n        this.$emit('copy-error', '没有可复制的命令');\n        return;\n      }\n      \n      const targetElement = event.currentTarget;\n      \n      try {\n        // 使用现代的 Clipboard API\n        if (navigator.clipboard && window.isSecureContext) {\n          await navigator.clipboard.writeText(text);\n          this.showCopySuccess(targetElement, appName);\n        } else {\n          // 回退方案：使用传统的 execCommand\n          const textArea = document.createElement('textarea');\n          textArea.value = text;\n          textArea.style.position = 'fixed';\n          textArea.style.left = '-999999px';\n          textArea.style.top = '-999999px';\n          document.body.appendChild(textArea);\n          textArea.focus();\n          textArea.select();\n          \n          try {\n            document.execCommand('copy');\n            this.showCopySuccess(targetElement, appName);\n          } catch (err) {\n            console.error('复制失败:', err);\n            this.$emit('copy-error', '复制失败，请手动复制');\n          } finally {\n            document.body.removeChild(textArea);\n          }\n        }\n      } catch (err) {\n        console.error('复制到剪贴板失败:', err);\n        this.$emit('copy-error', '复制失败，请检查浏览器权限');\n      }\n    },\n    \n    /**\n     * 显示复制成功动画和消息\n     */\n    showCopySuccess(element, appName) {\n      // 添加动画类\n      element.classList.add('copy-success');\n      \n      // 发出成功事件\n      this.$emit('copy-success', `📋 已复制 \"${appName}\" 的命令`);\n      \n      // 400ms后移除动画类\n      setTimeout(() => {\n        element.classList.remove('copy-success');\n      }, 400);\n    },\n  }\n}\n</script> "
  },
  {
    "path": "src_assets/common/assets/web/components/AppEditor.vue",
    "content": "<template>\n  <div class=\"modal fade\" id=\"editAppModal\" tabindex=\"-1\" aria-labelledby=\"editAppModalLabel\" ref=\"modalElement\">\n    <div class=\"modal-dialog modal-xl\">\n      <div class=\"modal-content\">\n        <div class=\"modal-header\">\n          <h5 class=\"modal-title\" id=\"editAppModalLabel\">\n            <i class=\"fas fa-edit me-2\"></i>\n            {{ isNewApp ? t('apps.add_new') : t('apps.edit') }}\n          </h5>\n          <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n        </div>\n        <div class=\"modal-body\">\n          <input type=\"file\" ref=\"fileInput\" style=\"display: none\" />\n          <input type=\"file\" ref=\"dirInput\" style=\"display: none\" webkitdirectory />\n\n          <form v-if=\"formData\" @submit.prevent=\"saveApp\">\n            <div class=\"accordion\" id=\"appFormAccordion\">\n              <AccordionItem\n                id=\"basicInfo\"\n                icon=\"fa-info-circle\"\n                :title=\"t('apps.basic_info')\"\n                parent-id=\"appFormAccordion\"\n                :show=\"true\"\n              >\n                <FormField\n                  id=\"appName\"\n                  :label=\"t('apps.app_name')\"\n                  :hint=\"t('apps.app_name_desc')\"\n                  :validation=\"validation.name\"\n                  :value=\"formData.name\"\n                  required\n                >\n                  <input\n                    type=\"text\"\n                    class=\"form-control form-control-enhanced\"\n                    id=\"appName\"\n                    v-model=\"formData.name\"\n                    :class=\"getFieldClass('name')\"\n                    @blur=\"validateField('name')\"\n                    required\n                  />\n                </FormField>\n\n                <FormField\n                  id=\"appOutput\"\n                  :label=\"t('apps.output_name')\"\n                  :hint=\"t('apps.output_desc')\"\n                  :validation=\"validation.output\"\n                >\n                  <input\n                    type=\"text\"\n                    class=\"form-control form-control-enhanced monospace\"\n                    id=\"appOutput\"\n                    v-model=\"formData.output\"\n                    :class=\"getFieldClass('output')\"\n                    @blur=\"validateField('output')\"\n                  />\n                </FormField>\n\n                <FormField\n                  id=\"appCmd\"\n                  :label=\"t('apps.cmd')\"\n                  :validation=\"validation.cmd\"\n                  :value=\"formData.cmd\"\n                >\n                  <template #default>\n                    <div class=\"input-group\">\n                      <input\n                        type=\"text\"\n                        class=\"form-control form-control-enhanced monospace\"\n                        id=\"appCmd\"\n                        v-model=\"formData.cmd\"\n                        :class=\"getFieldClass('cmd')\"\n                        @blur=\"validateField('cmd')\"\n                        @input=\"handleCmdInput\"\n                        :placeholder=\"getPlaceholderText('cmd')\"\n                      />\n                      <button\n                        class=\"btn btn-outline-secondary\"\n                        type=\"button\"\n                        @click=\"selectFile('cmd')\"\n                        :title=\"getButtonTitle('file')\"\n                      >\n                        <i class=\"fas fa-folder-open\"></i>\n                      </button>\n                    </div>\n                  </template>\n                  <template #hint>\n                    {{ t('apps.cmd_desc') }}<br />\n                    <strong>{{ t('_common.note') }}</strong> {{ t('apps.cmd_note') }}<br />\n                    <div class=\"cmd-examples\">\n                      <div class=\"cmd-examples-header\"><i class=\"fas fa-lightbulb me-1\"></i>{{ t('apps.cmd_examples_title') }}</div>\n                      <div class=\"cmd-examples-tags\">\n                        <span class=\"cmd-tag\">\n                          <code>cmd /c \"start xbox:\"</code>\n                          <span class=\"cmd-tag-desc\">Xbox Game</span>\n                        </span>\n                        <span class=\"cmd-tag\">\n                          <code>steam://open/bigpicture</code>\n                          <span class=\"cmd-tag-desc\">Steam Big Picture</span>\n                        </span>\n                        <span class=\"cmd-tag\">\n                          <code>cmd /c \"start ms-gamebar:\"</code>\n                          <span class=\"cmd-tag-desc\">Xbox Game Bar</span>\n                        </span>\n                        <span class=\"cmd-tag\">\n                          <code>cmd /c \"start playnite://playnite/showMainWindow\"</code>\n                          <span class=\"cmd-tag-desc\">Playnite</span>\n                        </span>\n                        <span class=\"cmd-tag\">\n                          <code>\"C:\\Program Files\\...\\game.exe\"</code>\n                          <span class=\"cmd-tag-desc\">Start program directly</span>\n                        </span>\n                      </div>\n                    </div>\n                  </template>\n                </FormField>\n\n                <FormField\n                  id=\"appWorkingDir\"\n                  :label=\"t('apps.working_dir')\"\n                  :hint=\"t('apps.working_dir_desc')\"\n                  :validation=\"validation['working-dir']\"\n                >\n                  <div class=\"input-group\">\n                    <input\n                      type=\"text\"\n                      class=\"form-control form-control-enhanced monospace\"\n                      id=\"appWorkingDir\"\n                      v-model=\"formData['working-dir']\"\n                      :class=\"getFieldClass('working-dir')\"\n                      @blur=\"validateField('working-dir')\"\n                      :placeholder=\"getPlaceholderText('working-dir')\"\n                    />\n                    <button\n                      class=\"btn btn-outline-secondary\"\n                      type=\"button\"\n                      @click=\"selectDirectory('working-dir')\"\n                      :title=\"getButtonTitle('directory')\"\n                    >\n                      <i class=\"fas fa-folder-open\"></i>\n                    </button>\n                  </div>\n                </FormField>\n              </AccordionItem>\n\n              <AccordionItem id=\"commands\" icon=\"fa-terminal\" :title=\"t('apps.command_settings')\" parent-id=\"appFormAccordion\">\n                <div class=\"form-group-enhanced\">\n                  <div class=\"form-check form-switch\">\n                    <input\n                      type=\"checkbox\"\n                      class=\"form-check-input\"\n                      id=\"excludeGlobalPrepSwitch\"\n                      v-model=\"formData['exclude-global-prep-cmd']\"\n                      :true-value=\"'true'\"\n                      :false-value=\"'false'\"\n                    />\n                    <label class=\"form-check-label\" for=\"excludeGlobalPrepSwitch\">\n                      {{ t('apps.global_prep_name') }}\n                    </label>\n                  </div>\n                  <div class=\"field-hint\">{{ t('apps.global_prep_desc') }}</div>\n                </div>\n\n                <div class=\"form-group-enhanced\">\n                  <label class=\"form-label-enhanced\">{{ t('apps.cmd_prep_name') }}</label>\n                  <div class=\"field-hint mb-3\">{{ t('apps.cmd_prep_desc') }}</div>\n                  <CommandTable\n                    :commands=\"formData['prep-cmd']\"\n                    :platform=\"platform\"\n                    type=\"prep\"\n                    @add-command=\"addPrepCommand\"\n                    @remove-command=\"removePrepCommand\"\n                    @order-changed=\"handlePrepCommandOrderChanged\"\n                  />\n                </div>\n\n                <div class=\"form-group-enhanced\">\n                  <label class=\"form-label-enhanced\">{{ t('apps.menu_cmd_name') }}</label>\n                  <div class=\"field-hint mb-3\">{{ t('apps.menu_cmd_desc') }}</div>\n                  <CommandTable\n                    :commands=\"formData['menu-cmd']\"\n                    :platform=\"platform\"\n                    type=\"menu\"\n                    @add-command=\"addMenuCommand\"\n                    @remove-command=\"removeMenuCommand\"\n                    @test-command=\"testMenuCommand\"\n                    @order-changed=\"handleMenuCommandOrderChanged\"\n                  />\n                </div>\n\n                <div class=\"form-group-enhanced\">\n                  <label class=\"form-label-enhanced\">{{ t('apps.detached_cmds') }}</label>\n                  <div class=\"field-hint mb-3\">\n                    {{ t('apps.detached_cmds_desc') }}<br>\n                    <strong>{{ t('_common.note') }}</strong> {{ t('apps.detached_cmds_note') }}\n                  </div>\n                  <CommandTable\n                    :commands=\"formData.detached\"\n                    :platform=\"platform\"\n                    type=\"detached\"\n                    @add-command=\"addDetachedCommand\"\n                    @remove-command=\"removeDetachedCommand\"\n                    @order-changed=\"handleDetachedCommandOrderChanged\"\n                  />\n                </div>\n              </AccordionItem>\n\n              <AccordionItem id=\"advanced\" icon=\"fa-cogs\" :title=\"t('apps.advanced_options')\" parent-id=\"appFormAccordion\">\n                <CheckboxField\n                  v-if=\"isWindows\"\n                  id=\"appElevation\"\n                  v-model=\"formData.elevated\"\n                  :label=\"t('_common.run_as')\"\n                  :hint=\"t('apps.run_as_desc')\"\n                />\n\n                <FormField\n                  v-if=\"isWindows\"\n                  id=\"mouseMode\"\n                  :label=\"t('apps.mouse_mode')\"\n                  :hint=\"t('apps.mouse_mode_desc')\"\n                >\n                  <select\n                    id=\"mouseMode\"\n                    class=\"form-select form-control-enhanced\"\n                    v-model=\"formData['mouse-mode']\"\n                  >\n                    <option :value=\"0\">{{ t('apps.mouse_mode_auto') }}</option>\n                    <option :value=\"1\">{{ t('apps.mouse_mode_vmouse') }}</option>\n                    <option :value=\"2\">{{ t('apps.mouse_mode_sendinput') }}</option>\n                  </select>\n                </FormField>\n\n                <CheckboxField\n                  id=\"autoDetach\"\n                  v-model=\"formData['auto-detach']\"\n                  :label=\"t('apps.auto_detach')\"\n                  :hint=\"t('apps.auto_detach_desc')\"\n                />\n\n                <CheckboxField\n                  id=\"waitAll\"\n                  v-model=\"formData['wait-all']\"\n                  :label=\"t('apps.wait_all')\"\n                  :hint=\"t('apps.wait_all_desc')\"\n                />\n\n                <FormField\n                  id=\"exitTimeout\"\n                  :label=\"t('apps.exit_timeout')\"\n                  :hint=\"t('apps.exit_timeout_desc')\"\n                  :validation=\"validation['exit-timeout']\"\n                >\n                  <input\n                    type=\"number\"\n                    class=\"form-control form-control-enhanced\"\n                    id=\"exitTimeout\"\n                    v-model=\"formData['exit-timeout']\"\n                    min=\"0\"\n                    :class=\"getFieldClass('exit-timeout')\"\n                    @blur=\"validateField('exit-timeout')\"\n                  />\n                </FormField>\n              </AccordionItem>\n\n              <AccordionItem id=\"image\" icon=\"fa-image\" :title=\"t('apps.image_settings')\" parent-id=\"appFormAccordion\">\n                <ImageSelector\n                  :image-path=\"formData['image-path']\"\n                  :app-name=\"formData.name\"\n                  @update-image=\"updateImage\"\n                  @image-error=\"handleImageError\"\n                />\n              </AccordionItem>\n            </div>\n          </form>\n        </div>\n        <div class=\"modal-footer modal-footer-enhanced\">\n          <div class=\"save-status\">\n            <span v-if=\"isFormValid\" class=\"text-success\"> <i class=\"fas fa-check-circle me-1\"></i>{{ t('apps.form_valid') }} </span>\n            <span v-else class=\"text-warning\"> <i class=\"fas fa-exclamation-triangle me-1\"></i>{{ t('apps.form_invalid') }} </span>\n            <div v-if=\"imageError\" class=\"text-danger mt-1\">\n              <i class=\"fas fa-exclamation-circle me-1\"></i>{{ imageError }}\n            </div>\n          </div>\n          <div>\n            <button type=\"button\" class=\"btn btn-secondary me-2\" @click=\"closeModal\">\n              <i class=\"fas fa-times me-1\"></i>{{ t('_common.cancel') }}\n            </button>\n            <button type=\"button\" class=\"btn btn-primary\" @click=\"saveApp\" :disabled=\"disabled || !isFormValid\">\n              <i class=\"fas fa-save me-1\"></i>{{ t('_common.save') }}\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { validateField as validateFieldHelper, validateAppForm } from '../utils/validation.js'\nimport { nanoid } from 'nanoid'\nimport CommandTable from './CommandTable.vue'\nimport ImageSelector from './ImageSelector.vue'\nimport AccordionItem from './AccordionItem.vue'\nimport FormField from './FormField.vue'\nimport CheckboxField from './CheckboxField.vue'\nimport { createFileSelector } from '../utils/fileSelection.js'\n\nconst DEFAULT_FORM_DATA = Object.freeze({\n  name: '',\n  output: '',\n  cmd: '',\n  index: -1,\n  'exclude-global-prep-cmd': false,\n  elevated: false,\n  'auto-detach': true,\n  'wait-all': true,\n  'exit-timeout': 5,\n  'mouse-mode': 0,\n  'prep-cmd': [],\n  'menu-cmd': [],\n  detached: [],\n  'image-path': '',\n  'working-dir': '',\n})\n\nconst FIELD_VALIDATION_MAP = Object.freeze({\n  name: 'appName',\n  cmd: 'command',\n  output: 'outputName',\n  'working-dir': 'workingDir',\n  'exit-timeout': 'timeout',\n  'image-path': 'imagePath',\n})\n\nconst props = defineProps({\n  app: { type: Object, default: null },\n  platform: { type: String, default: 'linux' },\n  disabled: { type: Boolean, default: false },\n})\n\nconst emit = defineEmits(['close', 'save-app'])\n\nconst { t } = useI18n()\n\nconst modalElement = ref(null)\nconst fileInput = ref(null)\nconst dirInput = ref(null)\nconst formData = ref(null)\nconst validation = ref({})\nconst imageError = ref('')\nconst modalInstance = ref(null)\nconst fileSelector = ref(null)\n\nconst isWindows = computed(() => props.platform === 'windows')\nconst isNewApp = computed(() => !props.app || props.app.index === -1)\nconst isFormValid = computed(() => {\n  // name 字段是必填的，必须验证通过\n  const nameValid = validation.value.name?.isValid === true\n  \n  // cmd 字段不是必填的，如果已验证则使用验证结果，如果未验证或为空则认为有效\n  const cmdValid = validation.value.cmd?.isValid !== false  // undefined 或 true 都认为有效\n  \n  return nameValid && cmdValid\n})\n\nconst showMessage = (message, type = 'info') => {\n  if (window.showToast) {\n    window.showToast(message, type)\n  } else if (type === 'error') {\n    alert(message)\n  } else {\n    console.info(message)\n  }\n}\n\nconst initializeModal = () => {\n  if (modalInstance.value || !modalElement.value) return\n\n  const Modal = window.bootstrap?.Modal\n  if (!Modal) {\n    console.warn('Bootstrap Modal not available')\n    return\n  }\n\n  try {\n    modalInstance.value = new Modal(modalElement.value, {\n      backdrop: 'static',\n      keyboard: false,\n    })\n  } catch (error) {\n    console.warn('Modal initialization failed:', error)\n  }\n}\n\nconst initializeFileSelector = () => {\n  const notify = (type) => (message) => showMessage(message, type)\n  fileSelector.value = createFileSelector({\n    platform: props.platform,\n    onSuccess: notify('info'),\n    onError: notify('error'),\n    onInfo: notify('info'),\n  })\n}\n\nconst ensureDefaultValues = () => {\n  const arrayDefaults = ['prep-cmd', 'menu-cmd', 'detached']\n  arrayDefaults.forEach((key) => {\n    if (!formData.value[key]) formData.value[key] = []\n  })\n\n  if (!formData.value['exclude-global-prep-cmd']) {\n    formData.value['exclude-global-prep-cmd'] = false\n  }\n  if (!formData.value['working-dir']) {\n    formData.value['working-dir'] = ''\n  }\n\n  if (isWindows.value && formData.value.elevated === undefined) {\n    formData.value.elevated = false\n  }\n  if (formData.value['auto-detach'] === undefined) {\n    formData.value['auto-detach'] = true\n  }\n  if (formData.value['wait-all'] === undefined) {\n    formData.value['wait-all'] = true\n  }\n  if (formData.value['exit-timeout'] === undefined) {\n    formData.value['exit-timeout'] = 5\n  }\n  if (isWindows.value && formData.value['mouse-mode'] === undefined) {\n    formData.value['mouse-mode'] = 0\n  }\n}\n\nconst initializeForm = (app) => {\n  formData.value = { ...DEFAULT_FORM_DATA, ...JSON.parse(JSON.stringify(app)) }\n  ensureDefaultValues()\n  validation.value = {}\n  imageError.value = ''\n  // 立即验证所有字段，确保表单状态正确\n  nextTick(() => {\n    // 验证必填字段 name（总是验证）\n    validateField('name')\n    // 验证 cmd 字段（如果有值则验证，没有值则标记为有效）\n    if (formData.value.cmd && formData.value.cmd.trim()) {\n      validateField('cmd')\n    } else {\n      // cmd 字段不是必填的，如果为空则标记为有效\n      validation.value.cmd = { isValid: true, message: '' }\n    }\n  })\n}\n\nconst showModal = () => {\n  if (!modalInstance.value) initializeModal()\n  modalInstance.value?.show()\n}\n\nconst resetFileSelection = () => {\n  fileSelector.value?.resetState()\n  fileSelector.value?.cleanupFileInputs(fileInput.value, dirInput.value)\n}\n\nconst closeModal = () => {\n  modalInstance.value?.hide()\n  setTimeout(() => {\n    resetFileSelection()\n    emit('close')\n  }, 300)\n}\n\nconst cleanup = () => {\n  modalInstance.value?.dispose()\n  resetFileSelection()\n}\n\nconst validateField = (fieldName) => {\n  const validationKey = FIELD_VALIDATION_MAP[fieldName] || fieldName\n  const result = validateFieldHelper(validationKey, formData.value[fieldName])\n  validation.value[fieldName] = result\n  return result\n}\n\n// 处理 cmd 字段输入，如果清空则立即更新验证状态\nconst handleCmdInput = () => {\n  // 如果 cmd 字段被清空，立即标记为有效（因为不是必填字段）\n  if (!formData.value.cmd || !formData.value.cmd.trim()) {\n    validation.value.cmd = { isValid: true, message: '' }\n  }\n}\n\nconst getFieldClass = (fieldName) => {\n  const v = validation.value[fieldName]\n  if (!v) return ''\n  return {\n    'is-invalid': !v.isValid,\n    'is-valid': v.isValid && formData.value[fieldName],\n  }\n}\n\nconst createCommand = (type) => {\n  const baseCmd = type === 'prep' ? { do: '', undo: '' } : { id: nanoid(10), name: '', cmd: '' }\n  if (isWindows.value) baseCmd.elevated = false\n  return baseCmd\n}\n\nconst addPrepCommand = () => {\n  formData.value['prep-cmd'].push(createCommand('prep'))\n}\n\nconst removePrepCommand = (index) => {\n  formData.value['prep-cmd'].splice(index, 1)\n}\n\nconst addMenuCommand = () => {\n  formData.value['menu-cmd'].push(createCommand('menu'))\n}\n\nconst removeMenuCommand = (index) => {\n  formData.value['menu-cmd'].splice(index, 1)\n}\n\nconst handlePrepCommandOrderChanged = (newOrder) => {\n  formData.value['prep-cmd'] = newOrder\n}\n\nconst handleMenuCommandOrderChanged = (newOrder) => {\n  formData.value['menu-cmd'] = newOrder\n}\n\nconst testMenuCommand = async (index) => {\n  const menuCmd = formData.value['menu-cmd'][index]\n  if (!menuCmd.cmd) {\n    showMessage(t('apps.test_menu_cmd_empty'), 'error')\n    return\n  }\n\n  try {\n    showMessage(t('apps.test_menu_cmd_executing'))\n    const response = await fetch('/api/apps/test-menu-cmd', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        cmd: menuCmd.cmd,\n        working_dir: formData.value['working-dir'] || '',\n        elevated: menuCmd.elevated === 'true' || menuCmd.elevated === true,\n      }),\n    })\n\n    const result = await response.json()\n    const isSuccess = result.status\n    showMessage(\n      isSuccess\n        ? t('apps.test_menu_cmd_success')\n        : `${t('apps.test_menu_cmd_failed')}: ${result.error || 'Unknown error'}`,\n      isSuccess ? 'info' : 'error'\n    )\n  } catch (error) {\n    showMessage(`${t('apps.test_menu_cmd_failed')}: ${error.message}`, 'error')\n  }\n}\n\nconst addDetachedCommand = () => {\n  formData.value.detached.push('')\n}\n\nconst removeDetachedCommand = (index) => {\n  formData.value.detached.splice(index, 1)\n}\n\nconst handleDetachedCommandOrderChanged = (newOrder) => {\n  formData.value.detached = newOrder\n}\n\nconst updateImage = (imagePath) => {\n  formData.value['image-path'] = imagePath\n  imageError.value = ''\n}\n\nconst handleImageError = (error) => {\n  imageError.value = error\n}\n\nconst onFilePathSelected = (fieldName, filePath) => {\n  formData.value[fieldName] = filePath\n  validateField(fieldName)\n}\n\nconst selectFile = (fieldName) => {\n  if (!fileSelector.value) {\n    showMessage(t('apps.file_selector_not_initialized'), 'error')\n    return\n  }\n  fileSelector.value.selectFile(fieldName, fileInput.value, onFilePathSelected)\n}\n\nconst selectDirectory = (fieldName) => {\n  if (!fileSelector.value) {\n    showMessage(t('apps.file_selector_not_initialized'), 'error')\n    return\n  }\n  fileSelector.value.selectDirectory(fieldName, dirInput.value, onFilePathSelected)\n}\n\nconst getPlaceholderText = (fieldName) => fileSelector.value?.getPlaceholderText(fieldName) || ''\n\nconst getButtonTitle = (type) => fileSelector.value?.getButtonTitle(type) || t('apps.select')\n\nconst saveApp = async () => {\n  const formValidation = validateAppForm(formData.value)\n  if (!formValidation.isValid) {\n    if (formValidation.errors.length) alert(formValidation.errors[0])\n    return\n  }\n\n  const editedApp = { ...formData.value }\n  if (editedApp['image-path']) {\n    editedApp['image-path'] = editedApp['image-path'].toString().replace(/\"/g, '')\n  }\n\n  emit('save-app', editedApp)\n}\n\nwatch(\n  () => props.app,\n  (newApp) => {\n    if (newApp) {\n      initializeForm(newApp)\n      nextTick(showModal)\n    }\n  },\n  { immediate: true }\n)\n\nonMounted(() => {\n  nextTick(() => {\n    initializeModal()\n    initializeFileSelector()\n  })\n})\n\nonBeforeUnmount(cleanup)\n</script>\n\n<style lang=\"less\" scoped>\n.modal-body {\n  max-height: calc(100vh - 200px);\n  overflow-y: auto;\n\n  /* 滚动条美化 */\n  &::-webkit-scrollbar {\n    width: 6px;\n  }\n\n  &::-webkit-scrollbar-track {\n    background: transparent;\n    border-radius: 3px;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background: rgba(99, 102, 241, 0.3);\n    border-radius: 3px;\n    transition: background 0.2s ease;\n\n    &:hover {\n      background: rgba(99, 102, 241, 0.5);\n    }\n  }\n}\n\n.modal-footer-enhanced {\n  border-top: 1px solid rgba(99, 102, 241, 0.2);\n  padding: 1rem 1.5rem;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  \n  [data-bs-theme='light'] & {\n    background: #e0e7ff;\n  }\n}\n\n.save-status {\n  font-size: 0.875rem;\n  color: #64748b;\n}\n\n.is-invalid {\n  border-color: #ef4444;\n}\n\n.is-valid {\n  border-color: #22c55e;\n}\n\n.monospace {\n  font-family: monospace;\n}\n\n.cmd-examples {\n  margin-top: 0.5rem;\n  padding: 0.75rem;\n  border-radius: 10px;\n  \n  [data-bs-theme='light'] & {\n    background: #e8efff;\n    border: 1px solid rgba(99, 102, 241, 0.2);\n  }\n  \n  [data-bs-theme='dark'] & {\n    background: rgba(0, 0, 0, 0.15);\n  }\n\n  &-header {\n    font-size: 0.75rem;\n    font-weight: 600;\n    margin-bottom: 0.5rem;\n    \n    [data-bs-theme='light'] & {\n      color: #6366f1;\n    }\n  }\n\n  &-tags {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 0.5rem;\n    line-height: 1.5;\n  }\n}\n\n.cmd-tag {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.375rem;\n  padding: 0.375rem 0.625rem;\n  border-radius: 8px;\n  font-size: 0.75rem;\n  transition: all 0.2s ease;\n  \n  [data-bs-theme='light'] & {\n    background: #f5f8ff;\n    border: 1px solid rgba(99, 102, 241, 0.2);\n    box-shadow: 0 1px 3px rgba(99, 102, 241, 0.05);\n  }\n\n  &:hover {\n    transform: translateY(-2px);\n    \n    [data-bs-theme='light'] & {\n      background: #eef2ff;\n      border-color: rgba(99, 102, 241, 0.3);\n      box-shadow: 0 4px 12px rgba(99, 102, 241, 0.12);\n    }\n  }\n\n  code {\n    font-family: monospace;\n    font-size: 0.7rem;\n    padding: 0.125rem 0.5rem;\n    border-radius: 5px;\n    border: none;\n    \n    [data-bs-theme='light'] & {\n      background: #dce4ff;\n      color: #6366f1;\n    }\n  }\n\n  &-desc {\n    font-size: 0.7rem;\n    white-space: nowrap;\n    \n    [data-bs-theme='light'] & {\n      color: #64748b;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/AppListItem.vue",
    "content": "<template>\n  <div class=\"app-list-item\" :class=\"{ 'app-list-item-dragging': isDragging }\">\n    <div class=\"app-list-item-inner\">\n      <!-- 拖拽手柄 -->\n      <div v-if=\"draggable\" class=\"drag-handle-list\">\n        <i class=\"fas fa-grip-vertical\"></i>\n      </div>\n      \n      <!-- 应用图标 -->\n      <div class=\"app-icon-container-list\">\n        <img \n          v-if=\"app['image-path']\" \n          :src=\"getImageUrl()\" \n          :alt=\"app.name\"\n          class=\"app-icon-list\"\n          @error=\"handleImageError\"\n        >\n        <div v-else class=\"app-icon-placeholder-list\">\n          <i class=\"fas fa-desktop\"></i>\n        </div>\n      </div>\n      \n      <!-- 应用信息 -->\n      <div class=\"app-info-list\">\n        <div class=\"app-name-row\">\n          <h3 class=\"app-name-list\">{{ app.name }}</h3>\n          <div class=\"app-tags-list\">\n            <span v-if=\"app['exclude-global-prep-cmd'] && app['exclude-global-prep-cmd'] !== 'false'\" class=\"app-tag-list tag-exclude-global-prep-cmd\">\n              <i class=\"fas fa-ellipsis-h me-1\"></i>全局预处理\n            </span>\n            <span v-if=\"app['menu-cmd'] && app['menu-cmd'].length > 0\" class=\"app-tag-list tag-menu\">\n              <span class=\"badge rounded-pill bg-secondary me-1\">{{ app['menu-cmd'].length }}</span>菜单\n            </span>\n            <span v-if=\"app.elevated && app.elevated !== 'false'\" class=\"app-tag-list tag-elevated\">\n              <i class=\"fas fa-shield-alt me-1\"></i>管理员\n            </span>\n            <span v-if=\"app['auto-detach'] && app['auto-detach'] !== 'false'\" class=\"app-tag-list tag-detach\">\n              <i class=\"fas fa-unlink me-1\"></i>自动分离\n            </span>\n          </div>\n        </div>\n        <p class=\"app-command-list\" v-if=\"app.cmd\" :title=\"app.cmd\" @click=\"copyToClipboard(app.cmd, app.name, $event)\">\n          <i class=\"fas fa-terminal me-2\"></i>\n          <span>{{ app.cmd }}</span>\n        </p>\n        <p v-if=\"app['working-dir']\" class=\"app-working-dir-list\">\n          <i class=\"fas fa-folder me-2\"></i>\n          <span>{{ app['working-dir'] }}</span>\n        </p>\n      </div>\n      \n      <!-- 操作按钮 -->\n      <div class=\"app-actions-list\">\n        <button \n          class=\"btn btn-edit-list\" \n          @click=\"$emit('edit')\"\n          :title=\"$t('apps.edit')\"\n        >\n          <i class=\"fas fa-edit\"></i>\n        </button>\n        <button \n          class=\"btn btn-delete-list\" \n          @click=\"$emit('delete')\"\n          :title=\"$t('apps.delete')\"\n        >\n          <i class=\"fas fa-trash\"></i>\n        </button>\n      </div>\n      \n      <!-- 搜索状态指示 -->\n      <div v-if=\"isSearchResult\" class=\"search-indicator-list\">\n        <i class=\"fas fa-search\"></i>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { getImagePreviewUrl } from '../utils/imageUtils.js';\n\nexport default {\n  name: 'AppListItem',\n  props: {\n    app: {\n      type: Object,\n      required: true\n    },\n    draggable: {\n      type: Boolean,\n      default: true\n    },\n    isSearchResult: {\n      type: Boolean,\n      default: false\n    },\n    isDragging: {\n      type: Boolean,\n      default: false\n    }\n  },\n  emits: ['edit', 'delete', 'copy-success', 'copy-error'],\n  methods: {\n    /**\n     * 处理图像错误\n     */\n    handleImageError(event) {\n      const element = event.target;\n      element.style.display = 'none';\n      if (element.nextElementSibling) {\n        element.nextElementSibling.style.display = 'flex';\n      }\n    },\n    \n    /**\n     * 获取图片URL\n     */\n    getImageUrl() {\n      return getImagePreviewUrl(this.app['image-path']);\n    },\n    \n    /**\n     * 复制到剪贴板\n     */\n    async copyToClipboard(text, appName, event) {\n      if (!text) {\n        this.$emit('copy-error', '没有可复制的命令');\n        return;\n      }\n      \n      const targetElement = event.currentTarget;\n      \n      try {\n        // 使用现代的 Clipboard API\n        if (navigator.clipboard && window.isSecureContext) {\n          await navigator.clipboard.writeText(text);\n          this.showCopySuccess(targetElement, appName);\n        } else {\n          // 回退方案：使用传统的 execCommand\n          const textArea = document.createElement('textarea');\n          textArea.value = text;\n          textArea.style.position = 'fixed';\n          textArea.style.left = '-999999px';\n          textArea.style.top = '-999999px';\n          document.body.appendChild(textArea);\n          textArea.focus();\n          textArea.select();\n          \n          try {\n            document.execCommand('copy');\n            this.showCopySuccess(targetElement, appName);\n          } catch (err) {\n            console.error('复制失败:', err);\n            this.$emit('copy-error', '复制失败，请手动复制');\n          } finally {\n            document.body.removeChild(textArea);\n          }\n        }\n      } catch (err) {\n        console.error('复制到剪贴板失败:', err);\n        this.$emit('copy-error', '复制失败，请检查浏览器权限');\n      }\n    },\n    \n    /**\n     * 显示复制成功动画和消息\n     */\n    showCopySuccess(element, appName) {\n      // 添加动画类\n      element.classList.add('copy-success');\n      \n      // 发出成功事件\n      this.$emit('copy-success', `📋 已复制 \"${appName}\" 的命令`);\n      \n      // 400ms后移除动画类\n      setTimeout(() => {\n        element.classList.remove('copy-success');\n      }, 400);\n    },\n  }\n}\n</script>\n\n\n"
  },
  {
    "path": "src_assets/common/assets/web/components/Checkbox.vue",
    "content": "<template>\n  <div :class=\"containerClass\">\n    <div class=\"form-check\">\n      <input\n        class=\"form-check-input\"\n        type=\"checkbox\"\n        :id=\"id\"\n        v-model=\"model\"\n        :true-value=\"trueValue\"\n        :false-value=\"falseValue\"\n      />\n      <label class=\"form-check-label\" :for=\"id\">\n        {{ t(`${localePrefix}.${id}`) }}\n      </label>\n    </div>\n    <div class=\"form-text\" v-if=\"hasDescription\">\n      {{ t(descKey) }}\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nconst props = defineProps({\n  id: {\n    type: String,\n    required: true\n  },\n  localePrefix: {\n    type: String,\n    default: 'config'\n  },\n  modelValue: {\n    type: [String, Boolean],\n    default: undefined\n  },\n  default: {\n    type: [String, Boolean],\n    default: undefined\n  },\n  trueValue: {\n    type: [String, Boolean],\n    default: true\n  },\n  falseValue: {\n    type: [String, Boolean],\n    default: false\n  },\n  containerClass: {\n    type: String,\n    default: ''\n  }\n})\n\nconst emit = defineEmits(['update:modelValue'])\n\nconst { t, te } = useI18n()\n\nconst model = computed({\n  get: () => props.modelValue ?? props.default ?? props.falseValue,\n  set: (val) => emit('update:modelValue', val)\n})\n\nconst descKey = computed(() => `${props.localePrefix}.${props.id}_desc`)\nconst hasDescription = computed(() => te(descKey.value))\n</script>\n\n<style scoped>\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/CheckboxField.vue",
    "content": "<template>\n  <div class=\"form-group-enhanced\">\n    <div class=\"form-check\">\n      <input type=\"checkbox\" class=\"form-check-input\" :id=\"id\" v-model=\"checked\" />\n      <label :for=\"id\" class=\"form-check-label\">{{ label }}</label>\n    </div>\n    <div v-if=\"hint\" class=\"field-hint\">{{ hint }}</div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'CheckboxField',\n  props: {\n    id: { type: String, required: true },\n    modelValue: { type: [Boolean, String], default: false },\n    label: { type: String, required: true },\n    hint: { type: String, default: '' },\n  },\n  emits: ['update:modelValue'],\n  computed: {\n    checked: {\n      get() {\n        return this.modelValue === true || this.modelValue === 'true'\n      },\n      set(val) {\n        this.$emit('update:modelValue', val ? 'true' : 'false')\n      },\n    },\n  },\n}\n</script>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/CommandTable.vue",
    "content": "<template>\n  <div class=\"form-group-enhanced\">\n    <div v-if=\"localCommands.length > 0\" class=\"command-table\">\n      <table class=\"table table-sm\">\n        <thead>\n          <tr>\n            <th class=\"drag-column\"></th>\n            <template v-if=\"isPrepType\">\n              <th><i class=\"fas fa-play\"></i> {{ $t('_common.do_cmd') }}</th>\n              <th><i class=\"fas fa-undo\"></i> {{ $t('_common.undo_cmd') }}</th>\n            </template>\n            <template v-else-if=\"isMenuType\">\n              <th><i class=\"fas fa-tag\"></i> {{ $t('apps.menu_cmd_display_name') }}</th>\n              <th><i class=\"fas fa-terminal\"></i> {{ $t('apps.menu_cmd_command') }}</th>\n            </template>\n            <template v-else-if=\"isDetachedType\">\n              <th><i class=\"fas fa-terminal\"></i> {{ $t('apps.menu_cmd_command') }}</th>\n            </template>\n            <th v-if=\"showElevatedColumn\" class=\"elevated-column\"><i class=\"fas fa-shield-alt\"></i> {{ $t('_common.run_as') }}</th>\n            <th class=\"actions-column\">{{ $t('apps.menu_cmd_actions') }}</th>\n          </tr>\n        </thead>\n        <draggable\n          v-model=\"localCommands\"\n          tag=\"tbody\"\n          :item-key=\"getItemKey\"\n          :animation=\"300\"\n          :delay=\"0\"\n          :disabled=\"localCommands.length <= 1\"\n          filter=\"input, button, .form-check-input\"\n          :prevent-on-filter=\"false\"\n          ghost-class=\"command-row-ghost\"\n          chosen-class=\"command-row-chosen\"\n          drag-class=\"command-row-drag\"\n          @end=\"onDragEnd\"\n        >\n          <template #item=\"{ element: command, index }\">\n            <tr>\n              <td class=\"drag-handle-cell\">\n                <div\n                  class=\"drag-handle\"\n                  :class=\"{ 'drag-disabled': localCommands.length <= 1 }\"\n                  :title=\"$t('apps.menu_cmd_drag_sort')\"\n                >\n                  <i class=\"fas fa-grip-vertical\"></i>\n                </div>\n              </td>\n\n              <template v-if=\"isPrepType\">\n                <td>\n                  <input\n                    type=\"text\"\n                    class=\"form-control form-control-sm monospace\"\n                    :value=\"command.do\"\n                    :placeholder=\"$t('apps.menu_cmd_placeholder_execute')\"\n                    @input=\"updateCommandField(index, 'do', $event.target.value)\"\n                  />\n                </td>\n                <td>\n                  <input\n                    type=\"text\"\n                    class=\"form-control form-control-sm monospace\"\n                    :value=\"command.undo\"\n                    :placeholder=\"$t('apps.menu_cmd_placeholder_undo')\"\n                    @input=\"updateCommandField(index, 'undo', $event.target.value)\"\n                  />\n                </td>\n              </template>\n\n              <template v-else-if=\"isMenuType\">\n                <td>\n                  <input\n                    type=\"text\"\n                    class=\"form-control form-control-sm\"\n                    :value=\"command.name\"\n                    :placeholder=\"$t('apps.menu_cmd_placeholder_display_name')\"\n                    @input=\"updateCommandField(index, 'name', $event.target.value)\"\n                  />\n                </td>\n                <td>\n                  <input\n                    type=\"text\"\n                    class=\"form-control form-control-sm monospace\"\n                    :value=\"command.cmd\"\n                    :placeholder=\"$t('apps.menu_cmd_placeholder_command')\"\n                    @input=\"updateCommandField(index, 'cmd', $event.target.value)\"\n                  />\n                </td>\n              </template>\n\n              <template v-else-if=\"isDetachedType\">\n                <td>\n                  <input\n                    type=\"text\"\n                    class=\"form-control form-control-sm monospace\"\n                    :value=\"command.cmd\"\n                    :placeholder=\"$t('apps.menu_cmd_placeholder_command')\"\n                    @input=\"updateCommandField(index, 'cmd', $event.target.value)\"\n                  />\n                </td>\n              </template>\n\n              <td v-if=\"showElevatedColumn\" class=\"elevated-column\">\n                <div class=\"form-check\">\n                  <input\n                    :id=\"`${type}-cmd-admin-${index}`\"\n                    type=\"checkbox\"\n                    class=\"form-check-input\"\n                    :checked=\"isElevated(command)\"\n                    @change=\"updateCommandField(index, 'elevated', $event.target.checked ? 'true' : 'false')\"\n                  />\n                  <label :for=\"`${type}-cmd-admin-${index}`\" class=\"form-check-label\">\n                    {{ $t('_common.elevated') }}\n                  </label>\n                </div>\n              </td>\n\n              <td>\n                <div class=\"action-buttons-group\">\n                  <button\n                    v-if=\"isMenuType\"\n                    type=\"button\"\n                    class=\"btn btn-success btn-sm me-1\"\n                    :title=\"$t('apps.test_menu_cmd')\"\n                    :disabled=\"!command.cmd\"\n                    @click=\"testCommand(index)\"\n                  >\n                    <i class=\"fas fa-play\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-sm\" :title=\"removeButtonTitle\" @click=\"removeCommand(index)\">\n                    <i class=\"fas fa-trash\"></i>\n                  </button>\n                </div>\n              </td>\n            </tr>\n          </template>\n        </draggable>\n      </table>\n    </div>\n\n    <button type=\"button\" class=\"btn btn-outline-success add-command-btn\" @click=\"addCommand\">\n      <i class=\"fas fa-plus me-1\"></i>{{ addButtonText }}\n    </button>\n  </div>\n</template>\n\n<script setup>\nimport { ref, computed, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport draggable from 'vuedraggable-es'\n\nconst { t } = useI18n()\n\nconst props = defineProps({\n  commands: {\n    type: Array,\n    required: true,\n  },\n  platform: {\n    type: String,\n    default: 'linux',\n  },\n  type: {\n    type: String,\n    required: true,\n    validator: (value) => ['prep', 'menu', 'detached'].includes(value),\n  },\n})\n\nconst emit = defineEmits(['add-command', 'remove-command', 'test-command', 'order-changed'])\n\nconst localCommands = ref([])\n\nconst isWindows = computed(() => props.platform === 'windows')\nconst isPrepType = computed(() => props.type === 'prep')\nconst isMenuType = computed(() => props.type === 'menu')\nconst isDetachedType = computed(() => props.type === 'detached')\nconst showElevatedColumn = computed(() => isWindows.value && !isDetachedType.value)\n\nconst addButtonText = computed(() => {\n  const textMap = {\n    prep: 'apps.add_cmds',\n    detached: 'apps.detached_cmds_add',\n    menu: 'apps.menu_cmd_add',\n  }\n  return t(textMap[props.type] || textMap.menu)\n})\n\nconst removeButtonTitle = computed(() => {\n  const titleMap = {\n    prep: 'apps.menu_cmd_remove_prep',\n    detached: 'apps.detached_cmds_remove',\n    menu: 'apps.menu_cmd_remove_menu',\n  }\n  return t(titleMap[props.type] || titleMap.menu)\n})\n\nconst normalizeCommand = (cmd) => {\n  if (typeof cmd === 'string') return { cmd }\n  if (cmd && typeof cmd === 'object' && 'cmd' in cmd) return { cmd: cmd.cmd || '' }\n  return { cmd: '' }\n}\n\nwatch(\n  () => props.commands,\n  (newVal) => {\n    const commands = newVal || []\n    localCommands.value = isDetachedType.value ? commands.map(normalizeCommand) : JSON.parse(JSON.stringify(commands))\n  },\n  { immediate: true, deep: true }\n)\n\nconst getItemKey = (_, index) => `${props.type}-${index}`\n\nconst isElevated = (command) => command.elevated === 'true' || command.elevated === true\n\nconst emitOrderChanged = () => {\n  const data = isDetachedType.value ? localCommands.value.map((cmd) => cmd.cmd || '') : localCommands.value\n  emit('order-changed', data)\n}\n\nconst updateCommandField = (index, field, value) => {\n  if (isDetachedType.value) {\n    localCommands.value[index].cmd = value\n  } else {\n    localCommands.value[index][field] = value\n  }\n  emitOrderChanged()\n}\n\nconst addCommand = () => emit('add-command')\nconst removeCommand = (index) => emit('remove-command', index)\nconst testCommand = (index) => emit('test-command', index)\nconst onDragEnd = () => emitOrderChanged()\n</script>\n\n<style scoped lang=\"less\">\n.command-table {\n  margin-bottom: var(--spacing-md);\n}\n\n.monospace {\n  font-family: 'Courier New', monospace;\n}\n\n.drag-column {\n  width: 40px;\n}\n\n.actions-column {\n  width: 100px;\n}\n\n.elevated-column {\n  text-align: center;\n}\n\n.table {\n  color: var(--bs-secondary-color);\n  border-color: var(--modal-border-color, rgba(255, 255, 255, 0.15));\n  margin-bottom: 0;\n\n  th {\n    border-top: none;\n    border-bottom: 1px solid var(--glass-border, rgba(255, 255, 255, 0.2));\n    font-weight: 600;\n    font-size: 0.875rem;\n    padding: 1rem 0.75rem;\n    background: var(--glass-medium, rgba(255, 255, 255, 0.2));\n  }\n\n  thead th {\n    &:first-child {\n      border-radius: 12px 0 0 0;\n    }\n\n    &:last-child {\n      border-radius: 0 12px 0 0;\n      text-align: center;\n    }\n  }\n\n  tr:last-child td {\n    border-bottom: none;\n  }\n\n  td {\n    vertical-align: middle;\n    border-color: var(--modal-border-color, rgba(255, 255, 255, 0.1));\n    padding: 0.75rem;\n    background: var(--glass-light, rgba(255, 255, 255, 0.1));\n    transition: background 0.3s ease;\n  }\n\n  tbody tr {\n    &:hover td {\n      background: var(--glass-medium, rgba(255, 255, 255, 0.2));\n    }\n\n    &:last-child td {\n      &:first-child {\n        border-radius: 0 0 0 12px;\n      }\n\n      &:last-child {\n        border-radius: 0 0 12px 0;\n      }\n    }\n\n    &:hover .drag-handle:not(.drag-disabled) {\n      opacity: 1;\n    }\n  }\n}\n\n.form-control {\n  max-width: 480px;\n}\n\n.form-control-sm {\n  font-size: 0.875rem;\n  background: var(--glass-light, rgba(255, 255, 255, 0.1));\n  border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.2));\n  border-radius: 8px;\n  backdrop-filter: blur(5px);\n  transition: all 0.3s ease;\n\n  &:focus {\n    background: var(--glass-medium, rgba(255, 255, 255, 0.15));\n    border-color: var(--btn-outline-primary-border, rgba(255, 255, 255, 0.4));\n    box-shadow: 0 0 0 0.2rem var(--btn-outline-primary-hover, rgba(255, 255, 255, 0.25));\n  }\n\n  &::placeholder {\n    color: var(--modal-text-muted, rgba(255, 255, 255, 0.6));\n  }\n}\n\n.btn-sm {\n  padding: 0.25rem 0.5rem;\n  font-size: 0.75rem;\n  border-radius: 8px;\n  transition: all 0.3s ease;\n}\n\n.form-check {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.form-check-label {\n  font-size: 0.875rem;\n  font-weight: 500;\n  margin-left: 0.5rem;\n}\n\n.action-buttons-group {\n  display: flex;\n  gap: 0.25rem;\n  align-items: center;\n  justify-content: center;\n}\n\n.drag-handle-cell {\n  width: 40px;\n  padding: 0.5rem !important;\n  text-align: center;\n  cursor: move;\n  user-select: none;\n}\n\n.drag-handle {\n  color: var(--modal-text-muted, rgba(255, 255, 255, 0.5));\n  font-size: 1.2rem;\n  display: inline-block;\n  padding: 0.5rem;\n  opacity: 0;\n  cursor: move;\n  transition: opacity 0.3s ease, color 0.3s ease;\n\n  &:hover {\n    color: var(--modal-text-secondary, rgba(255, 255, 255, 0.9));\n  }\n\n  &.drag-disabled {\n    cursor: not-allowed;\n    opacity: 0.3 !important;\n    pointer-events: none;\n  }\n}\n\n.command-row-ghost {\n  opacity: 0;\n  pointer-events: none;\n}\n\n.command-row-chosen {\n  background: var(--glass-medium, rgba(255, 255, 255, 0.15));\n  z-index: 1000;\n  position: relative;\n}\n\n.command-row-drag {\n  opacity: 0.95;\n  transform: rotate(2deg);\n  box-shadow: 0 10px 30px var(--modal-shadow, rgba(0, 0, 0, 0.3));\n  z-index: 1001;\n  position: relative;\n}\n\n@media (max-width: 768px) {\n  .command-table {\n    padding: 1rem;\n    margin-top: 0.5rem;\n  }\n\n  .table {\n    th,\n    td {\n      padding: 0.5rem;\n      font-size: 0.8rem;\n    }\n  }\n\n  .form-control-sm {\n    font-size: 0.8rem;\n    padding: 0.25rem 0.5rem;\n  }\n\n  .btn-sm {\n    padding: 0.2rem 0.4rem;\n    font-size: 0.7rem;\n  }\n\n  .add-command-btn {\n    padding: 0.4rem 0.8rem;\n    font-size: 0.8rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/CoverFinder.vue",
    "content": "<template>\n  <Transition name=\"finder-fade\">\n    <div v-if=\"visible\" class=\"cover-finder-overlay\" @click.self=\"closeFinder\">\n      <div class=\"cover-finder-panel\" @click.stop>\n        <!-- 头部 -->\n        <div class=\"cover-finder__header\">\n          <div class=\"cover-finder__title\">\n            <i class=\"fas fa-image me-2\"></i>\n            <span>{{ $t('apps.find_cover') }}</span>\n          </div>\n          <button type=\"button\" class=\"cover-finder__close\" @click=\"closeFinder\">\n            <i class=\"fas fa-times\"></i>\n          </button>\n        </div>\n\n        <!-- 搜索框 -->\n        <div class=\"cover-finder__search\">\n          <div class=\"cover-finder__search-wrapper\">\n            <i class=\"fas fa-search cover-finder__search-icon\"></i>\n            <input\n              ref=\"searchInput\"\n              v-model=\"localSearchTerm\"\n              type=\"text\"\n              class=\"cover-finder__search-input\"\n              placeholder=\"输入游戏名称搜索...\"\n              @keydown.enter=\"searchCovers\"\n            />\n            <button v-if=\"localSearchTerm\" class=\"cover-finder__search-clear\" @click=\"clearSearch\" type=\"button\">\n              <i class=\"fas fa-times\"></i>\n            </button>\n            <button class=\"cover-finder__search-btn\" @click=\"searchCovers\" :disabled=\"!localSearchTerm || loading\" type=\"button\">\n              <i class=\"fas fa-arrow-right\"></i>\n            </button>\n          </div>\n        </div>\n\n        <!-- 数据源筛选 -->\n        <div class=\"cover-finder__tabs\">\n          <button\n            v-for=\"tab in tabs\"\n            :key=\"tab.key\"\n            class=\"cover-finder__tab\"\n            :class=\"{ 'cover-finder__tab--active': coverFilter === tab.key }\"\n            @click.stop.prevent=\"coverFilter = tab.key\"\n          >\n            <i :class=\"tab.icon\"></i>\n            <span>{{ tab.label }}</span>\n            <span class=\"cover-finder__tab-badge\" v-if=\"getTabCount(tab.key) > 0\">\n              {{ getTabCount(tab.key) }}\n            </span>\n          </button>\n        </div>\n\n        <!-- 内容区域 -->\n        <div class=\"cover-finder__content\">\n          <!-- 加载状态 -->\n          <div v-if=\"loading\" class=\"cover-finder__loading\">\n            <div class=\"cover-finder__loading-spinner\"></div>\n            <p class=\"cover-finder__loading-text\">正在搜索封面...</p>\n          </div>\n\n          <!-- 封面网格 -->\n          <div v-else-if=\"filteredCovers.length > 0\" class=\"cover-finder__grid\">\n            <div\n              v-for=\"(cover, index) in filteredCovers\"\n              :key=\"cover.key || `cover-${index}`\"\n              class=\"cover-finder__card\"\n              @click=\"selectCover(cover)\"\n            >\n              <div class=\"cover-finder__card-image\">\n                <img :src=\"cover.url\" :alt=\"cover.name\" loading=\"lazy\" @error=\"handleImageError\" />\n                <div class=\"cover-finder__card-overlay\">\n                  <i class=\"fas fa-check-circle\"></i>\n                </div>\n                <div class=\"cover-finder__card-badge\" :class=\"`cover-finder__card-badge--${cover.source}`\">\n                  <i :class=\"cover.source === 'steam' ? 'fab fa-steam' : 'fas fa-gamepad'\"></i>\n                </div>\n              </div>\n              <div class=\"cover-finder__card-info\">\n                <p class=\"cover-finder__card-name\" :title=\"cover.name\">{{ cover.name }}</p>\n              </div>\n            </div>\n          </div>\n\n          <!-- 无结果 -->\n          <div v-else class=\"cover-finder__empty\">\n            <div class=\"cover-finder__empty-icon\">\n              <i class=\"fas fa-search\"></i>\n            </div>\n            <h4>未找到相关封面</h4>\n            <p>尝试使用不同的关键词搜索</p>\n          </div>\n        </div>\n\n        <!-- 底部提示 -->\n        <div class=\"cover-finder__footer\">\n          <span class=\"cover-finder__footer-hint\">\n            <i class=\"fas fa-info-circle me-1\"></i>\n            点击封面即可应用\n          </span>\n        </div>\n      </div>\n    </div>\n  </Transition>\n</template>\n\n<script>\nimport { searchAllCovers } from '../utils/coverSearch.js'\n\nconst PLACEHOLDER_IMAGE =\n  'data:image/svg+xml,' +\n  encodeURIComponent(`\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"300\" viewBox=\"0 0 200 300\">\n    <rect fill=\"#1a1a2e\" width=\"200\" height=\"300\"/>\n    <text x=\"100\" y=\"150\" text-anchor=\"middle\" fill=\"#4a4a6a\" font-size=\"14\">无法加载</text>\n  </svg>\n`)\n\nexport default {\n  name: 'CoverFinder',\n  props: {\n    visible: {\n      type: Boolean,\n      default: false,\n    },\n    searchTerm: {\n      type: String,\n      default: '',\n    },\n  },\n  emits: ['close', 'cover-selected', 'loading', 'error'],\n  data() {\n    return {\n      coverFilter: 'all',\n      loading: false,\n      igdbCovers: [],\n      steamCovers: [],\n      localSearchTerm: '',\n      searchAbortController: null,\n      tabs: [\n        { key: 'all', icon: 'fas fa-globe', label: '全部' },\n        { key: 'igdb', icon: 'fas fa-gamepad', label: 'IGDB' },\n        { key: 'steam', icon: 'fab fa-steam', label: 'Steam' },\n      ],\n    }\n  },\n  computed: {\n    allCovers() {\n      const result = []\n      const maxLen = Math.max(this.igdbCovers.length, this.steamCovers.length)\n      for (let i = 0; i < maxLen; i++) {\n        if (i < this.igdbCovers.length) result.push(this.igdbCovers[i])\n        if (i < this.steamCovers.length) result.push(this.steamCovers[i])\n      }\n      return result\n    },\n    filteredCovers() {\n      const filterMap = {\n        igdb: this.igdbCovers,\n        steam: this.steamCovers,\n        all: this.allCovers,\n      }\n      return filterMap[this.coverFilter] || this.allCovers\n    },\n  },\n  watch: {\n    visible(newVal) {\n      if (newVal) {\n        this.onOpen()\n      } else {\n        this.abortPendingSearch()\n      }\n      document.body.style.overflow = newVal ? 'hidden' : ''\n    },\n  },\n  beforeUnmount() {\n    document.body.style.overflow = ''\n    this.abortPendingSearch()\n  },\n  methods: {\n    getTabCount(key) {\n      const countMap = {\n        all: this.allCovers.length,\n        igdb: this.igdbCovers.length,\n        steam: this.steamCovers.length,\n      }\n      return countMap[key] || 0\n    },\n\n    onOpen() {\n      this.localSearchTerm = this.searchTerm\n      this.$nextTick(() => {\n        this.$refs.searchInput?.focus()\n        this.$refs.searchInput?.select()\n      })\n      if (this.localSearchTerm) {\n        this.searchCovers()\n      }\n    },\n\n    abortPendingSearch() {\n      if (this.searchAbortController) {\n        this.searchAbortController.abort()\n        this.searchAbortController = null\n      }\n    },\n\n    clearSearch() {\n      this.localSearchTerm = ''\n      this.igdbCovers = []\n      this.steamCovers = []\n      this.abortPendingSearch()\n      this.$refs.searchInput?.focus()\n    },\n\n    async searchCovers() {\n      if (!this.localSearchTerm) {\n        this.igdbCovers = []\n        this.steamCovers = []\n        return\n      }\n\n      this.abortPendingSearch()\n      this.searchAbortController = new AbortController()\n\n      this.loading = true\n      this.igdbCovers = []\n      this.steamCovers = []\n\n      try {\n        const results = await searchAllCovers(this.localSearchTerm, this.searchAbortController.signal)\n        this.igdbCovers = results.igdb\n        this.steamCovers = results.steam\n      } catch (error) {\n        if (error.name === 'AbortError') return\n        console.error('搜索封面失败:', error)\n        this.$emit('error', '搜索封面失败，请稍后重试')\n      } finally {\n        this.loading = false\n      }\n    },\n\n    handleImageError(event) {\n      event.target.src = PLACEHOLDER_IMAGE\n    },\n\n    async selectCover(cover) {\n      this.$emit('loading', true)\n\n      try {\n        if (cover.source === 'steam') {\n          this.$emit('cover-selected', { path: cover.saveUrl, source: 'steam' })\n        } else {\n          const response = await fetch('/api/covers/upload', {\n            method: 'POST',\n            headers: { 'Content-Type': 'application/json' },\n            body: JSON.stringify({ key: cover.key, url: cover.saveUrl }),\n          })\n\n          if (!response.ok) throw new Error('Failed to download cover')\n\n          const { path } = await response.json()\n          this.$emit('cover-selected', { path, source: 'igdb' })\n        }\n        this.closeFinder()\n      } catch (error) {\n        console.error('使用封面失败:', error)\n        this.$emit('error', '使用封面失败，请稍后重试')\n      } finally {\n        this.$emit('loading', false)\n      }\n    },\n\n    closeFinder() {\n      this.$emit('close')\n    },\n  },\n}\n</script>\n\n<style scoped lang=\"less\">\n@purple-dark: #7c3aed;\n@purple-dark-deep: #5b21b6;\n@purple-light: #6366f1;\n@purple-light-deep: #4f46e5;\n\n.cover-finder-overlay {\n  position: fixed;\n  inset: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  z-index: 1050;\n  padding: 2rem;\n}\n\n.cover-finder-panel {\n  background: linear-gradient(145deg, #1e1e2e, #16161e);\n  border-radius: 16px;\n  width: 100%;\n  max-width: 900px;\n  max-height: 85vh;\n  display: flex;\n  flex-direction: column;\n  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);\n  overflow: hidden;\n\n  [data-bs-theme='light'] & {\n    background: linear-gradient(145deg, #f8faff, #f0f4ff);\n    box-shadow: 0 25px 50px -12px rgba(99, 102, 241, 0.15);\n  }\n}\n\n.cover-finder__header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 1.25rem 1.5rem;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.06);\n\n  [data-bs-theme='light'] & {\n    border-bottom-color: rgba(99, 102, 241, 0.1);\n  }\n}\n\n.cover-finder__title {\n  display: flex;\n  align-items: center;\n  font-size: 1.1rem;\n  font-weight: 600;\n  color: #e0e0e0;\n\n  i {\n    color: @purple-dark;\n  }\n\n  [data-bs-theme='light'] & {\n    color: #1e293b;\n\n    i {\n      color: @purple-light;\n    }\n  }\n}\n\n.cover-finder__close {\n  width: 36px;\n  height: 36px;\n  border-radius: 10px;\n  border: none;\n  background: rgba(255, 255, 255, 0.05);\n  color: #888;\n  cursor: pointer;\n  transition: all 0.2s;\n\n  &:hover {\n    background: rgba(239, 68, 68, 0.2);\n    color: #ef4444;\n  }\n\n  [data-bs-theme='light'] & {\n    background: rgba(99, 102, 241, 0.08);\n    color: #64748b;\n\n    &:hover {\n      background: rgba(239, 68, 68, 0.15);\n    }\n  }\n}\n\n.cover-finder__search {\n  padding: 1rem 1.5rem;\n\n  &-wrapper {\n    display: flex;\n    align-items: center;\n    background: rgba(0, 0, 0, 0.3);\n    border: 1px solid rgba(255, 255, 255, 0.08);\n    border-radius: 12px;\n    padding: 0 0.5rem;\n\n    &:focus-within {\n      border-color: rgba(124, 58, 237, 0.5);\n    }\n\n    [data-bs-theme='light'] & {\n      background: #fff;\n      border-color: rgba(99, 102, 241, 0.2);\n\n      &:focus-within {\n        border-color: rgba(99, 102, 241, 0.5);\n        box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);\n      }\n    }\n  }\n\n  &-icon {\n    color: #666;\n    padding: 0 0.75rem;\n\n    [data-bs-theme='light'] & {\n      color: #94a3b8;\n    }\n  }\n\n  &-input {\n    flex: 1;\n    background: transparent;\n    border: none;\n    outline: none;\n    color: #e0e0e0;\n    padding: 0.85rem 0.25rem;\n\n    &::placeholder {\n      color: #555;\n    }\n\n    [data-bs-theme='light'] & {\n      color: #1e293b;\n\n      &::placeholder {\n        color: #94a3b8;\n      }\n    }\n  }\n\n  &-clear,\n  &-btn {\n    width: 36px;\n    height: 36px;\n    border-radius: 8px;\n    border: none;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    transition: all 0.2s;\n  }\n\n  &-clear {\n    background: transparent;\n    color: #666;\n\n    &:hover {\n      color: #aaa;\n    }\n\n    [data-bs-theme='light'] & {\n      color: #94a3b8;\n\n      &:hover {\n        color: #64748b;\n      }\n    }\n  }\n\n  &-btn {\n    background: linear-gradient(135deg, @purple-dark, @purple-dark-deep);\n    color: white;\n\n    &:disabled {\n      opacity: 0.5;\n      cursor: not-allowed;\n    }\n\n    [data-bs-theme='light'] & {\n      background: linear-gradient(135deg, @purple-light, @purple-light-deep);\n    }\n  }\n}\n\n.cover-finder__tabs {\n  display: flex;\n  gap: 0.5rem;\n  padding: 0 1.5rem 1rem;\n}\n\n.cover-finder__tab {\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n  padding: 0.6rem 1.2rem;\n  border-radius: 10px;\n  border: none;\n  background: rgba(255, 255, 255, 0.03);\n  color: #888;\n  cursor: pointer;\n  transition: all 0.2s;\n\n  &:hover {\n    background: rgba(255, 255, 255, 0.06);\n  }\n\n  &--active {\n    background: linear-gradient(135deg, @purple-dark, @purple-dark-deep);\n    color: white;\n  }\n\n  [data-bs-theme='light'] & {\n    background: rgba(99, 102, 241, 0.06);\n    color: #64748b;\n\n    &:hover {\n      background: rgba(99, 102, 241, 0.1);\n    }\n\n    &--active {\n      background: linear-gradient(135deg, @purple-light, @purple-light-deep);\n      color: white;\n    }\n  }\n\n  &-badge {\n    background: rgba(255, 255, 255, 0.2);\n    padding: 0.1rem 0.5rem;\n    border-radius: 10px;\n    font-size: 0.75rem;\n  }\n}\n\n.cover-finder__content {\n  flex: 1;\n  overflow-y: auto;\n  padding: 1.5rem;\n  min-height: 300px;\n\n  &::-webkit-scrollbar {\n    width: 8px;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background: rgba(255, 255, 255, 0.1);\n    border-radius: 4px;\n  }\n\n  [data-bs-theme='light'] & {\n    &::-webkit-scrollbar-thumb {\n      background: rgba(99, 102, 241, 0.2);\n    }\n  }\n}\n\n.cover-finder__loading {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  padding: 4rem 2rem;\n\n  &-spinner {\n    width: 40px;\n    height: 40px;\n    border: 3px solid rgba(124, 58, 237, 0.2);\n    border-top-color: @purple-dark;\n    border-radius: 50%;\n    animation: cover-finder-spin 1s linear infinite;\n\n    [data-bs-theme='light'] & {\n      border-color: rgba(99, 102, 241, 0.2);\n      border-top-color: @purple-light;\n    }\n  }\n\n  &-text {\n    margin-top: 1rem;\n    color: #888;\n\n    [data-bs-theme='light'] & {\n      color: #64748b;\n    }\n  }\n}\n\n@keyframes cover-finder-spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.cover-finder__grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));\n  gap: 1.25rem;\n}\n\n.cover-finder__card {\n  cursor: pointer;\n  transition: transform 0.2s;\n\n  &:hover {\n    transform: translateY(-4px);\n\n    .cover-finder__card-overlay {\n      opacity: 1;\n    }\n  }\n\n  &-image {\n    position: relative;\n    aspect-ratio: 2/3;\n    border-radius: 10px;\n    overflow: hidden;\n    background: #0d0d14;\n\n    img {\n      width: 100%;\n      height: 100%;\n      object-fit: cover;\n    }\n\n    [data-bs-theme='light'] & {\n      background: #e8efff;\n    }\n  }\n\n  &-overlay {\n    position: absolute;\n    inset: 0;\n    background: rgba(124, 58, 237, 0.85);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    opacity: 0;\n    transition: opacity 0.2s;\n    color: white;\n    font-size: 2rem;\n\n    [data-bs-theme='light'] & {\n      background: rgba(99, 102, 241, 0.85);\n    }\n  }\n\n  &-badge {\n    position: absolute;\n    top: 8px;\n    right: 8px;\n    width: 24px;\n    height: 24px;\n    border-radius: 6px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 0.7rem;\n    color: white;\n\n    &--steam {\n      background: #1b2838;\n    }\n\n    &--igdb {\n      background: #9147ff;\n    }\n  }\n\n  &-info {\n    padding: 0.5rem 0;\n  }\n\n  &-name {\n    font-size: 0.8rem;\n    color: #d0d0d0;\n    margin: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n\n    [data-bs-theme='light'] & {\n      color: #475569;\n    }\n  }\n}\n\n.cover-finder__empty {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  padding: 3rem 2rem;\n  text-align: center;\n\n  h4 {\n    color: #aaa;\n    margin: 0 0 0.5rem;\n  }\n\n  p {\n    color: #666;\n    margin: 0;\n  }\n\n  [data-bs-theme='light'] & {\n    h4 {\n      color: #475569;\n    }\n\n    p {\n      color: #94a3b8;\n    }\n  }\n\n  &-icon {\n    width: 80px;\n    height: 80px;\n    border-radius: 50%;\n    background: rgba(255, 255, 255, 0.03);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    margin-bottom: 1.5rem;\n\n    i {\n      font-size: 2rem;\n      color: #4a4a6a;\n    }\n\n    [data-bs-theme='light'] & {\n      background: rgba(99, 102, 241, 0.08);\n\n      i {\n        color: #94a3b8;\n      }\n    }\n  }\n}\n\n.cover-finder__footer {\n  padding: 0.75rem 1.5rem;\n  border-top: 1px solid rgba(255, 255, 255, 0.06);\n\n  [data-bs-theme='light'] & {\n    border-top-color: rgba(99, 102, 241, 0.1);\n  }\n\n  &-hint {\n    font-size: 0.8rem;\n    color: #666;\n\n    i {\n      color: @purple-dark;\n    }\n\n    [data-bs-theme='light'] & {\n      color: #64748b;\n\n      i {\n        color: @purple-light;\n      }\n    }\n  }\n}\n\n.finder-fade-enter-active,\n.finder-fade-leave-active {\n  transition: opacity 0.3s;\n}\n\n.finder-fade-enter-from,\n.finder-fade-leave-to {\n  opacity: 0;\n}\n\n@media (max-width: 768px) {\n  .cover-finder-overlay {\n    padding: 1rem;\n  }\n\n  .cover-finder__grid {\n    grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));\n    gap: 1rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/FormField.vue",
    "content": "<template>\n  <div class=\"form-group-enhanced\">\n    <label :for=\"id\" class=\"form-label-enhanced\" :class=\"{ 'required-field': required }\">{{ label }}</label>\n    <slot></slot>\n    <div v-if=\"validation && !validation.isValid\" class=\"invalid-feedback\">{{ validation.message }}</div>\n    <div v-if=\"validation && validation.isValid && value\" class=\"valid-feedback\">有效</div>\n    <div v-if=\"$slots.hint\" class=\"field-hint\"><slot name=\"hint\"></slot></div>\n    <div v-else-if=\"hint\" class=\"field-hint\">{{ hint }}</div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'FormField',\n  props: {\n    id: { type: String, required: true },\n    label: { type: String, required: true },\n    hint: { type: String, default: '' },\n    validation: { type: Object, default: null },\n    value: { type: [String, Number], default: '' },\n    required: { type: Boolean, default: false },\n  },\n}\n</script>\n\n<style scoped>\n.required-field::after {\n  content: ' *';\n  color: #dc3545;\n}\n\n.invalid-feedback,\n.valid-feedback {\n  display: block;\n  font-size: 0.875rem;\n  margin-top: 0.25rem;\n}\n\n.invalid-feedback {\n  color: #dc3545;\n}\n\n.valid-feedback {\n  color: #198754;\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/ImageSelector.vue",
    "content": "<template>\n  <div class=\"form-group-enhanced\">\n    <label for=\"appImagePath\" class=\"form-label-enhanced\">{{ $t('apps.image') }}</label>\n\n    <!-- 使用桌面图片选项 -->\n    <div class=\"form-check mb-3\">\n      <input\n        type=\"checkbox\"\n        class=\"form-check-input\"\n        id=\"useDesktopImage\"\n        :checked=\"isDesktopImage\"\n        @change=\"handleDesktopImageChange\"\n      />\n      <label for=\"useDesktopImage\" class=\"form-check-label\">{{ $t('apps.use_desktop_image') }}</label>\n    </div>\n\n    <!-- 图片路径输入 -->\n    <div v-if=\"!isDesktopImage\" class=\"input-group\">\n      <input\n        type=\"file\"\n        class=\"form-control\"\n        @change=\"handleFileSelect\"\n        accept=\"image/png,image/jpg,image/jpeg,image/gif,image/bmp,image/webp\"\n        style=\"width: 90px; flex: none\"\n      />\n      <input\n        type=\"text\"\n        class=\"form-control form-control-enhanced monospace\"\n        id=\"appImagePath\"\n        :value=\"imagePath\"\n        @input=\"updateImagePath\"\n        @dragenter=\"handleDragEnter\"\n        @dragleave=\"handleDragLeave\"\n        @dragover.prevent\n        @drop.prevent.stop=\"handleDrop\"\n        placeholder=\"选择图片文件或拖拽到此处\"\n      />\n      <button\n        class=\"btn btn-outline-secondary\"\n        type=\"button\"\n        @click=\"openCoverFinder\"\n        :disabled=\"!appName\"\n      >\n        <i class=\"fas fa-search me-1\"></i>{{ $t('apps.find_cover') }}\n      </button>\n    </div>\n\n    <!-- 图片预览 -->\n    <div v-if=\"!isDesktopImage && imagePath\" class=\"image-preview-container mt-3\">\n      <div class=\"image-preview\">\n        <img :src=\"previewUrl\" alt=\"图片预览\" @error=\"handleImageError\" />\n      </div>\n      <div class=\"image-preview-circle\">\n        <img :src=\"previewUrl\" alt=\"图片预览\" @error=\"handleImageError\" />\n      </div>\n    </div>\n\n    <div class=\"field-hint\">{{ $t('apps.image_desc') }}</div>\n\n    <!-- 封面查找器 -->\n    <CoverFinder\n      :visible=\"showCoverFinder\"\n      :search-term=\"appName\"\n      @close=\"closeCoverFinder\"\n      @cover-selected=\"handleCoverSelected\"\n      @loading=\"handleCoverLoading\"\n      @error=\"handleCoverError\"\n    />\n  </div>\n</template>\n\n<script>\nimport CoverFinder from './CoverFinder.vue'\nimport { validateFile } from '../utils/validation.js'\nimport { getImagePreviewUrl } from '../utils/imageUtils.js'\n\nexport default {\n  name: 'ImageSelector',\n  components: {\n    CoverFinder,\n  },\n  props: {\n    imagePath: {\n      type: String,\n      default: '',\n    },\n    appName: {\n      type: String,\n      default: '',\n    },\n  },\n  data() {\n    return {\n      showCoverFinder: false,\n      coverLoading: false,\n      dragCounter: 0,\n    }\n  },\n  computed: {\n    isDesktopImage() {\n      return this.imagePath === 'desktop'\n    },\n    previewUrl() {\n      return getImagePreviewUrl(this.imagePath)\n    },\n  },\n  methods: {\n    /**\n     * 处理桌面图片选择变化\n     */\n    handleDesktopImageChange(event) {\n      this.$emit('update-image', event.target.checked ? 'desktop' : '')\n    },\n\n    /**\n     * 更新图片路径\n     */\n    updateImagePath(event) {\n      this.$emit('update-image', event.target.value)\n    },\n\n    /**\n     * 处理文件选择\n     */\n    async handleFileSelect(event) {\n      const file = event.target.files[0]\n      if (!file) return\n\n      await this.processFile(file)\n    },\n\n    /**\n     * 处理拖拽进入\n     */\n    handleDragEnter(event) {\n      event.preventDefault()\n      this.dragCounter++\n      this.$emit('image-error', '杂鱼~快放进来呀~')\n    },\n\n    /**\n     * 处理拖拽离开\n     */\n    handleDragLeave(event) {\n      event.preventDefault()\n      this.dragCounter--\n      if (this.dragCounter === 0) {\n        this.$emit('image-error', '')\n      }\n    },\n\n    /**\n     * 处理拖拽放置\n     */\n    async handleDrop(event) {\n      event.preventDefault()\n      this.dragCounter = 0\n\n      const file = event.dataTransfer.files[0]\n      if (!file) {\n        this.$emit('image-error', '其他地方不可以！')\n        return\n      }\n\n      await this.processFile(file)\n    },\n\n    /**\n     * 处理文件上传\n     */\n    async processFile(file) {\n      const validation = validateFile(file)\n      if (!validation.isValid) {\n        this.$emit('image-error', validation.message)\n        return\n      }\n\n      try {\n        this.$emit('image-error', '正在上传图片...')\n        const path = await this.uploadImageToSunshine(file)\n        this.$emit('update-image', path)\n        this.$emit('image-error', '')\n      } catch (error) {\n        console.error('上传图片失败:', error)\n        this.$emit('image-error', `上传图片失败: ${error.message}`)\n      }\n    },\n\n    /**\n     * 上传图片到 Sunshine API\n     */\n    async uploadImageToSunshine(file) {\n      const base64Data = await this.readFileAsBase64(file)\n      const key = this.generateImageKey()\n\n      const response = await fetch('/api/covers/upload', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ key, data: base64Data }),\n      })\n\n      if (!response.ok) {\n        throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n      }\n\n      const result = await response.json()\n      console.log('✅ Sunshine API 上传成功，文件路径:', result.path)\n\n      return `${key}.png`\n    },\n\n    /**\n     * 读取文件为 Base64\n     */\n    readFileAsBase64(file) {\n      return new Promise((resolve, reject) => {\n        const reader = new FileReader()\n        reader.onload = () => resolve(reader.result.split(',')[1])\n        reader.onerror = reject\n        reader.readAsDataURL(file)\n      })\n    },\n\n    /**\n     * 生成图片 key\n     */\n    generateImageKey() {\n      const timestamp = Date.now()\n      const appName = this.appName || 'custom'\n      return `app_${appName}_${timestamp}`.replace(/[^a-zA-Z0-9_-]/g, '_')\n    },\n\n    /**\n     * 获取图片预览URL\n     */\n    getImagePreviewUrl() {\n      return getImagePreviewUrl(this.imagePath)\n    },\n\n    /**\n     * 处理图片加载错误\n     */\n    handleImageError() {\n      this.$emit('image-error', '图片加载失败，请检查文件路径')\n    },\n\n    /**\n     * 打开封面查找器\n     */\n    openCoverFinder() {\n      if (!this.appName) {\n        this.$emit('image-error', '请先输入应用名称')\n        return\n      }\n      this.showCoverFinder = true\n    },\n\n    /**\n     * 关闭封面查找器\n     */\n    closeCoverFinder() {\n      this.showCoverFinder = false\n    },\n\n    /**\n     * 处理封面选择\n     */\n    handleCoverSelected(coverData) {\n      this.$emit('update-image', coverData.path)\n      this.showCoverFinder = false\n    },\n\n    /**\n     * 处理封面加载状态\n     */\n    handleCoverLoading(loading) {\n      this.coverLoading = loading\n    },\n\n    /**\n     * 处理封面错误\n     */\n    handleCoverError(error) {\n      this.$emit('image-error', error)\n    },\n  },\n}\n</script>\n\n<style scoped>\n.monospace {\n  font-family: monospace;\n}\n\n.image-preview-container {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.image-preview {\n  max-width: 300px;\n  max-height: 200px;\n  border-radius: 0.375rem;\n  padding: 1rem;\n  text-align: center;\n}\n\n.image-preview img {\n  max-width: 100%;\n  max-height: 150px;\n  object-fit: contain;\n  border-radius: 0.25rem;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.image-preview-circle {\n  width: 150px;\n  height: 150px;\n  border-radius: 50%;\n  padding: 1px;\n  text-align: center;\n  overflow: hidden;\n  position: relative;\n  background-color: #f8f9fa;\n  border: 1px solid #dee2e6;\n}\n\n.image-preview-circle img {\n  width: 98%;\n  height: 98%;\n  object-fit: cover;\n  border-radius: 50%;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n\n.image-preview-circle::after {\n  content: '';\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  width: 15%;\n  height: 15%;\n  background-color: #f8f9fa;\n  transform: translate(-50%, -50%);\n  border-radius: 50%;\n}\n\n.input-group .form-control[type='file'] {\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n}\n\n.input-group .form-control:not([type='file']) {\n  border-left: none;\n  border-right: none;\n  border-radius: 0;\n}\n\n.input-group .btn {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n\n.btn:disabled {\n  opacity: 0.6;\n  cursor: not-allowed;\n}\n\n/* 拖拽状态样式 */\n.form-control-enhanced[data-dragging='true'] {\n  border-color: #0d6efd;\n  background-color: #e7f1ff;\n}\n\n/* 响应式设计 */\n@media (max-width: 768px) {\n  .input-group {\n    flex-direction: column;\n  }\n\n  .input-group .form-control,\n  .input-group .btn {\n    border-radius: 0.375rem !important;\n    margin-bottom: 0.5rem;\n  }\n\n  .input-group .form-control:not(:last-child) {\n    margin-bottom: 0.5rem;\n  }\n\n  .image-preview {\n    max-width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/LogDiagnosisModal.vue",
    "content": "<template>\n  <Transition name=\"fade\">\n    <div v-if=\"show\" class=\"diagnosis-overlay\" @click.self=\"$emit('close')\">\n      <div class=\"diagnosis-modal\">\n        <div class=\"diagnosis-header\">\n          <h5>\n            <i class=\"fas fa-robot me-2\"></i>{{ $t('troubleshooting.ai_diagnosis_title') }}\n          </h5>\n          <button class=\"btn-close\" :aria-label=\"$t('close')\" @click=\"$emit('close')\"></button>\n        </div>\n\n        <div class=\"diagnosis-body\">\n          <!-- Config Section (collapsible) -->\n          <div class=\"config-section mb-3\">\n            <button\n              class=\"btn btn-sm btn-outline-secondary w-100 text-start\"\n              @click=\"showConfig = !showConfig\"\n            >\n              <i class=\"fas fa-cog me-1\"></i>\n              {{ $t('troubleshooting.ai_config') }}\n              <i class=\"fas ms-1\" :class=\"showConfig ? 'fa-chevron-up' : 'fa-chevron-down'\"></i>\n            </button>\n\n            <div v-if=\"showConfig\" class=\"config-form mt-2\">\n              <div class=\"row g-2\">\n                <div class=\"col-md-4\">\n                  <label class=\"form-label form-label-sm\">{{ $t('troubleshooting.ai_provider') }}</label>\n                  <select class=\"form-select form-select-sm\" v-model=\"config.provider\" @change=\"onProviderChange(config.provider)\">\n                    <option v-for=\"p in providers\" :key=\"p.value\" :value=\"p.value\">{{ p.label }}</option>\n                  </select>\n                </div>\n                <div class=\"col-md-4\">\n                  <label class=\"form-label form-label-sm\">API Key</label>\n                  <input type=\"password\" class=\"form-control form-control-sm\" v-model=\"config.apiKey\" placeholder=\"sk-...\" />\n                </div>\n                <div class=\"col-md-4\">\n                  <label class=\"form-label form-label-sm\">{{ $t('troubleshooting.ai_model') }}</label>\n                  <input type=\"text\" class=\"form-control form-control-sm\" v-model=\"config.model\" :placeholder=\"getAvailableModels()[0] || 'model'\" list=\"ai-models\" />\n                  <datalist id=\"ai-models\">\n                    <option v-for=\"m in getAvailableModels()\" :key=\"m\" :value=\"m\" />\n                  </datalist>\n                </div>\n              </div>\n              <div class=\"row g-2 mt-1\" v-if=\"config.provider === 'custom'\">\n                <div class=\"col-12\">\n                  <label class=\"form-label form-label-sm\">API Base</label>\n                  <input type=\"text\" class=\"form-control form-control-sm\" v-model=\"config.apiBase\" placeholder=\"https://api.example.com/v1\" />\n                </div>\n              </div>\n              <div class=\"text-muted small mt-1\">\n                <i class=\"fas fa-lock me-1\"></i>{{ $t('troubleshooting.ai_key_local') }}\n              </div>\n            </div>\n          </div>\n\n          <!-- Diagnose Button -->\n          <div class=\"text-center mb-3\" v-if=\"!result && !error\">\n            <button class=\"btn btn-primary\" :disabled=\"isLoading\" @click=\"$emit('diagnose')\">\n              <span v-if=\"isLoading\">\n                <span class=\"spinner-border spinner-border-sm me-1\"></span>\n                {{ $t('troubleshooting.ai_analyzing') }}\n              </span>\n              <span v-else>\n                <i class=\"fas fa-search-plus me-1\"></i>\n                {{ $t('troubleshooting.ai_start_diagnosis') }}\n              </span>\n            </button>\n          </div>\n\n          <!-- Loading -->\n          <div v-if=\"isLoading && !result\" class=\"text-center text-muted py-4\">\n            <div class=\"spinner-border text-primary mb-2\"></div>\n            <p>{{ $t('troubleshooting.ai_analyzing_logs') }}</p>\n          </div>\n\n          <!-- Error -->\n          <div v-if=\"error\" class=\"alert alert-danger d-flex align-items-start\">\n            <i class=\"fas fa-exclamation-circle me-2 mt-1\"></i>\n            <div>\n              <strong>{{ $t('troubleshooting.ai_error') }}</strong>\n              <p class=\"mb-1\">{{ error }}</p>\n              <button class=\"btn btn-sm btn-outline-danger\" @click=\"$emit('diagnose')\">\n                <i class=\"fas fa-redo me-1\"></i>{{ $t('troubleshooting.ai_retry') }}\n              </button>\n            </div>\n          </div>\n\n          <!-- Result -->\n          <div v-if=\"result\" class=\"diagnosis-result\">\n            <div class=\"result-header mb-2\">\n              <i class=\"fas fa-clipboard-check text-success me-1\"></i>\n              <strong>{{ $t('troubleshooting.ai_result') }}</strong>\n              <button class=\"btn btn-sm btn-outline-secondary ms-auto\" @click=\"copyResult\">\n                <i class=\"fas fa-copy me-1\"></i>{{ $t('troubleshooting.ai_copy_result') }}\n              </button>\n            </div>\n            <div class=\"result-content\" v-html=\"renderMarkdown(result)\"></div>\n            <div class=\"text-center mt-3\">\n              <button class=\"btn btn-sm btn-outline-primary\" @click=\"$emit('diagnose')\">\n                <i class=\"fas fa-redo me-1\"></i>{{ $t('troubleshooting.ai_reanalyze') }}\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </Transition>\n</template>\n\n<script setup>\nimport { ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nconst { t } = useI18n()\n\ndefineProps({\n  show: Boolean,\n  config: Object,\n  providers: Array,\n  isLoading: Boolean,\n  result: String,\n  error: String,\n  onProviderChange: Function,\n  getAvailableModels: Function,\n})\n\ndefineEmits(['close', 'diagnose'])\n\nconst showConfig = ref(false)\n\nfunction copyResult() {\n  const el = document.querySelector('.result-content')\n  if (el) {\n    navigator.clipboard.writeText(el.innerText)\n  }\n}\n\nfunction renderMarkdown(text) {\n  if (!text) return ''\n  return text\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/```([\\s\\S]*?)```/g, '<pre class=\"code-block\">$1</pre>')\n    .replace(/`([^`]+)`/g, '<code>$1</code>')\n    .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')\n    .replace(/^\\s*[-*]\\s+(.+)$/gm, '<li>$1</li>')\n    .replace(/(<li>.*<\\/li>)/s, '<ul>$1</ul>')\n    .replace(/^### (.+)$/gm, '<h6 class=\"mt-3 mb-1\">$1</h6>')\n    .replace(/^## (.+)$/gm, '<h5 class=\"mt-3 mb-1\">$1</h5>')\n    .replace(/^# (.+)$/gm, '<h4 class=\"mt-3 mb-1\">$1</h4>')\n    .replace(/\\n/g, '<br>')\n}\n</script>\n\n<style scoped>\n.diagnosis-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background: rgba(0, 0, 0, 0.5);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  z-index: 1050;\n  backdrop-filter: blur(4px);\n}\n\n.diagnosis-modal {\n  background: var(--bs-body-bg, #fff);\n  border-radius: 12px;\n  width: 90%;\n  max-width: 700px;\n  max-height: 80vh;\n  display: flex;\n  flex-direction: column;\n  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n}\n\n.diagnosis-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 1rem 1.5rem;\n  border-bottom: 1px solid var(--bs-border-color, #dee2e6);\n}\n\n.diagnosis-header h5 {\n  margin: 0;\n  font-weight: 600;\n}\n\n.diagnosis-body {\n  padding: 1.5rem;\n  overflow-y: auto;\n  flex: 1;\n}\n\n.config-form {\n  background: var(--bs-tertiary-bg, #f8f9fa);\n  border-radius: 8px;\n  padding: 1rem;\n}\n\n.result-header {\n  display: flex;\n  align-items: center;\n}\n\n.result-content {\n  background: var(--bs-tertiary-bg, #f8f9fa);\n  border-radius: 8px;\n  padding: 1rem 1.25rem;\n  font-size: 0.9rem;\n  line-height: 1.6;\n  max-height: 400px;\n  overflow-y: auto;\n}\n\n.result-content :deep(code) {\n  background: rgba(0, 0, 0, 0.08);\n  padding: 0.15em 0.4em;\n  border-radius: 3px;\n  font-size: 0.85em;\n}\n\n.result-content :deep(.code-block) {\n  background: #1e1e1e;\n  color: #d4d4d4;\n  padding: 0.75rem 1rem;\n  border-radius: 6px;\n  font-size: 0.8rem;\n  overflow-x: auto;\n}\n\n.result-content :deep(ul) {\n  padding-left: 1.5rem;\n  margin: 0.5rem 0;\n}\n\n.result-content :deep(li) {\n  margin-bottom: 0.25rem;\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.2s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/LogsSection.vue",
    "content": "<template>\n  <div class=\"card shadow-sm mb-4\">\n    <div class=\"card-header bg-dark bg-opacity-10 border-bottom-0\">\n      <div class=\"d-flex justify-content-between align-items-center flex-wrap gap-2\">\n        <h5 class=\"card-title mb-0\">\n          <i class=\"fas fa-file-alt text-dark me-2\"></i>\n          {{ $t('troubleshooting.logs') }}\n        </h5>\n        <div class=\"d-flex align-items-center gap-2\">\n          <div class=\"input-group\" style=\"width: 480px\">\n            <span class=\"input-group-text\">\n              <i class=\"fas fa-search text-muted\"></i>\n            </span>\n            <input\n              type=\"text\"\n              class=\"form-control\"\n              v-model=\"logFilterModel\"\n              :placeholder=\"$t('troubleshooting.logs_find')\"\n            />\n            <input type=\"checkbox\" class=\"btn-check\" id=\"ignoreCase\" v-model=\"ignoreCaseModel\" />\n            <label\n              class=\"btn btn-outline-secondary match-mode-btn\"\n              for=\"ignoreCase\"\n              :title=\"$t('troubleshooting.ignore_case')\"\n            >\n              <i class=\"fas fa-font\"></i>\n            </label>\n            <template v-for=\"mode in matchModes\" :key=\"mode.value\">\n              <input\n                type=\"radio\"\n                class=\"btn-check\"\n                name=\"matchMode\"\n                :id=\"`matchMode${mode.value}`\"\n                :value=\"mode.value\"\n                v-model=\"matchModeModel\"\n              />\n              <label\n                class=\"btn btn-outline-secondary match-mode-btn\"\n                :for=\"`matchMode${mode.value}`\"\n                :title=\"$t(mode.labelKey)\"\n              >\n                <i :class=\"mode.icon\"></i>\n              </label>\n            </template>\n          </div>\n          <button class=\"btn btn-outline-success\" @click=\"downloadLogs\">\n            <i class=\"fas fa-download me-1\"></i>\n            {{ $t('troubleshooting.download_logs') }}\n          </button>\n          <button class=\"btn btn-outline-info\" @click=\"$emit('openDiagnosis')\">\n            <i class=\"fas fa-robot me-1\"></i>\n            {{ $t('troubleshooting.ai_diagnosis') }}\n          </button>\n          <button class=\"btn btn-outline-primary\" @click=\"copyConfig\">\n            <i class=\"fas fa-copy me-1\"></i>\n            {{ $t('troubleshooting.copy_config') }}\n          </button>\n        </div>\n      </div>\n    </div>\n    <div class=\"card-body\">\n      <p class=\"text-muted mb-3\">{{ $t('troubleshooting.logs_desc') }}</p>\n      <div class=\"logs-container\">\n        <button class=\"copy-btn\" @click=\"copyLogs\" :title=\"$t('troubleshooting.copy_logs')\">\n          <i class=\"fas fa-copy\"></i>\n        </button>\n        <pre class=\"logs-content\">{{ actualLogs }}</pre>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { computed } from 'vue'\n\nconst props = defineProps({\n  logFilter: {\n    type: String,\n    default: null,\n  },\n  actualLogs: {\n    type: String,\n    required: true,\n  },\n  copyLogs: {\n    type: Function,\n    required: true,\n  },\n  copyConfig: {\n    type: Function,\n    required: true,\n  },\n  matchMode: {\n    type: String,\n    default: 'contains',\n  },\n  ignoreCase: {\n    type: Boolean,\n    default: true,\n  },\n})\n\nconst emit = defineEmits(['update:logFilter', 'update:matchMode', 'update:ignoreCase', 'openDiagnosis'])\n\nconst matchModes = [\n  { value: 'contains', labelKey: 'troubleshooting.match_contains', icon: 'fas fa-filter' },\n  { value: 'regex', labelKey: 'troubleshooting.match_regex', icon: 'fas fa-code' },\n  { value: 'exact', labelKey: 'troubleshooting.match_exact', icon: 'fas fa-equals' },\n]\n\nconst logFilterModel = computed({\n  get: () => props.logFilter ?? '',\n  set: (value) => emit('update:logFilter', value || null),\n})\n\nconst matchModeModel = computed({\n  get: () => props.matchMode,\n  set: (value) => emit('update:matchMode', value),\n})\n\nconst ignoreCaseModel = computed({\n  get: () => props.ignoreCase,\n  set: (value) => emit('update:ignoreCase', value),\n})\n\nconst downloadLogs = async () => {\n  const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')\n  const filename = `sunshine-logs-${timestamp}.txt`\n\n  // Tauri WebView2: fetch into memory then use Rust save_text_file command (dialog + fs write)\n  if (window.__TAURI_INTERNALS__) {\n    try {\n      const response = await fetch('/api/logs')\n      if (!response.ok) throw new Error(`HTTP ${response.status}`)\n      const content = await response.text()\n      await window.__TAURI_INTERNALS__.invoke('save_text_file', {\n        content,\n        defaultName: filename,\n        filterName: 'Text Files',\n        extensions: ['txt'],\n      })\n      return\n    } catch (e) {\n      if (e === 'cancelled') return\n      console.warn('Tauri save_text_file failed, falling back:', e)\n    }\n  }\n\n  // Standard browser: let browser download directly via <a> link\n  // Server returns Content-Disposition: attachment, so the browser handles\n  // the download natively without buffering the entire file in JS memory.\n  try {\n    const link = Object.assign(document.createElement('a'), {\n      href: '/api/logs',\n      download: filename,\n    })\n    document.body.appendChild(link)\n    link.click()\n    document.body.removeChild(link)\n  } catch (e) {\n    console.error('Failed to download logs:', e)\n  }\n}\n</script>\n\n<style scoped>\n.input-group .btn {\n  border-radius: 0;\n  padding: 0.375rem 0.5rem;\n  font-size: 0.875rem;\n}\n\n.input-group .btn:last-of-type {\n  border-radius: 0 0.375rem 0.375rem 0;\n}\n\n.input-group .btn:hover {\n  transform: none;\n}\n\n.match-mode-btn {\n  min-width: 36px;\n  padding: 0.375rem 0.75rem !important;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  transition: all 0.2s ease;\n}\n\n.match-mode-btn i {\n  font-size: 0.875rem;\n  line-height: 1;\n}\n\n.match-mode-btn:hover {\n  background-color: rgba(108, 117, 125, 0.1);\n  border-color: #6c757d;\n}\n\n.btn-check:checked + .match-mode-btn {\n  background-color: #6c757d;\n  border-color: #6c757d;\n  color: #fff;\n}\n\n.btn-check:checked + .match-mode-btn:hover {\n  background-color: #5a6268;\n  border-color: #545b62;\n}\n\n.logs-container {\n  position: relative;\n  background: #1e1e1e;\n  border-radius: 10px;\n  overflow: hidden;\n}\n\n.logs-content {\n  margin: 0;\n  padding: 1.25rem;\n  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;\n  font-size: 0.85rem;\n  line-height: 1.5;\n  color: #d4d4d4;\n  overflow: auto;\n  max-height: 450px;\n  min-height: 300px;\n  white-space: pre-wrap;\n  word-break: break-all;\n  scrollbar-width: thin;\n  scrollbar-color: #666 #1e1e1e;\n}\n\n.logs-content::-webkit-scrollbar {\n  width: 8px;\n  height: 8px;\n}\n\n.logs-content::-webkit-scrollbar-track {\n  background: #1e1e1e;\n  border-radius: 4px;\n}\n\n.logs-content::-webkit-scrollbar-thumb {\n  background: #666;\n  border-radius: 4px;\n}\n\n.logs-content::-webkit-scrollbar-thumb:hover {\n  background: #888;\n}\n\n.copy-btn {\n  position: absolute;\n  top: 12px;\n  right: 12px;\n  padding: 8px 12px;\n  cursor: pointer;\n  color: #ffffff;\n  background: rgba(255, 255, 255, 0.1);\n  border: none;\n  border-radius: 6px;\n  transition: all 0.2s ease;\n  z-index: 10;\n}\n\n.copy-btn:hover {\n  background: rgba(255, 255, 255, 0.2);\n  transform: scale(1.05);\n}\n\n.copy-btn:active {\n  transform: scale(0.95);\n}\n\n.input-group-text {\n  border-right: none;\n  background-color: #fff;\n  \n  [data-bs-theme='dark'] & {\n    background-color: #212529;\n    color: #fff;\n  }\n}\n\n.input-group .form-control {\n  border-left: none;\n  border-right: none;\n}\n\n.input-group .form-control:focus {\n  border-color: #ced4da;\n  box-shadow: none;\n}\n\n.input-group:focus-within {\n  box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n  border-radius: 0.375rem;\n}\n\n.input-group:focus-within .input-group-text,\n.input-group:focus-within .form-control {\n  border-color: #86b7fe;\n}\n\n@media (max-width: 991.98px) {\n  .card-header .d-flex {\n    flex-direction: column;\n    align-items: flex-start !important;\n  }\n\n  .card-header .input-group {\n    width: 100% !important;\n    margin-top: 0.5rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/ScanResultModal.vue",
    "content": "<template>\n  <Transition name=\"fade\">\n    <div v-if=\"show\" class=\"scan-result-overlay\" @click.self=\"$emit('close')\">\n      <div class=\"scan-result-modal\">\n        <!-- 标题栏 -->\n        <div class=\"scan-result-header\">\n          <h5>\n            <i class=\"fas fa-search me-2\"></i>{{ t('apps.scan_result_title') }}\n            <span class=\"badge bg-primary ms-2\">{{ apps.length }}</span>\n            <span v-if=\"stats.games > 0\" class=\"badge bg-warning text-dark ms-2\">\n              <i class=\"fas fa-gamepad me-1\"></i>{{ stats.games }}\n            </span>\n            <span v-if=\"hasActiveFilter\" class=\"badge bg-info ms-2\"> {{ t('apps.scan_result_matched', { count: filteredApps.length }) }} </span>\n          </h5>\n          <button class=\"btn-close\" :aria-label=\"t('close')\" @click=\"$emit('close')\"></button>\n        </div>\n\n        <!-- 搜索框和过滤器 -->\n        <div v-if=\"apps.length > 0\" class=\"scan-result-search\">\n          <div class=\"search-box\">\n            <i class=\"fas fa-search search-icon\"></i>\n            <input\n              type=\"text\"\n              class=\"form-control search-input\"\n              :placeholder=\"t('apps.scan_result_search_placeholder')\"\n              v-model=\"searchQuery\"\n            />\n            <button v-if=\"searchQuery\" class=\"btn-clear-search\" @click=\"searchQuery = ''\" type=\"button\">\n              <i class=\"fas fa-times\"></i>\n            </button>\n          </div>\n\n          <!-- 过滤器按钮组 -->\n          <div class=\"scan-result-filters mt-2\">\n            <div class=\"d-flex flex-wrap gap-2 align-items-center\">\n              <!-- 应用类型过滤 -->\n              <div class=\"btn-group btn-group-sm flex-wrap\" role=\"group\">\n                <button\n                  class=\"btn\"\n                  :class=\"selectedType === 'all' ? 'btn-primary' : 'btn-outline-primary'\"\n                  @click=\"selectedType = 'all'\"\n                  type=\"button\"\n                >\n                  {{ t('apps.scan_result_filter_all') }}\n                  <span class=\"badge bg-dark ms-1\">{{ stats.all }}</span>\n                </button>\n                <button\n                  v-if=\"stats.shortcut > 0\"\n                  class=\"btn\"\n                  :class=\"selectedType === 'shortcut' ? 'btn-info' : 'btn-outline-info'\"\n                  @click=\"selectedType = 'shortcut'\"\n                  type=\"button\"\n                  :title=\"t('apps.scan_result_filter_shortcut_title')\"\n                >\n                  <i class=\"fas fa-link me-1\"></i>{{ t('apps.scan_result_filter_shortcut') }}\n                  <span class=\"badge bg-dark ms-1\">{{ stats.shortcut }}</span>\n                </button>\n                <button\n                  v-if=\"stats.executable > 0\"\n                  class=\"btn\"\n                  :class=\"selectedType === 'executable' ? 'btn-primary' : 'btn-outline-primary'\"\n                  @click=\"selectedType = 'executable'\"\n                  type=\"button\"\n                  :title=\"t('apps.scan_result_filter_executable_title')\"\n                >\n                  <i class=\"fas fa-file-code me-1\"></i>{{ t('apps.scan_result_filter_executable') }}\n                  <span class=\"badge bg-dark ms-1\">{{ stats.executable }}</span>\n                </button>\n                <button\n                  v-if=\"stats.batch > 0 || stats.command > 0\"\n                  class=\"btn\"\n                  :class=\"\n                    selectedType === 'batch' || selectedType === 'command' ? 'btn-secondary' : 'btn-outline-secondary'\n                  \"\n                  @click=\"selectedType = stats.batch > 0 ? 'batch' : 'command'\"\n                  type=\"button\"\n                  :title=\"t('apps.scan_result_filter_script_title')\"\n                >\n                  <i class=\"fas fa-terminal me-1\"></i>{{ t('apps.scan_result_filter_script') }}\n                  <span class=\"badge bg-dark ms-1\">{{ stats.batch + stats.command }}</span>\n                </button>\n                <button\n                  v-if=\"stats.url > 0\"\n                  class=\"btn\"\n                  :class=\"selectedType === 'url' ? 'btn-success' : 'btn-outline-success'\"\n                  @click=\"selectedType = 'url'\"\n                  type=\"button\"\n                  :title=\"t('apps.scan_result_filter_url_title')\"\n                >\n                  <i class=\"fas fa-globe me-1\"></i>{{ t('apps.scan_result_filter_url') }}\n                  <span class=\"badge bg-dark ms-1\">{{ stats.url }}</span>\n                </button>\n                <button\n                  v-if=\"stats.steam > 0\"\n                  class=\"btn\"\n                  :class=\"selectedType === 'steam' ? 'btn-primary' : 'btn-outline-primary'\"\n                  @click=\"selectedType = 'steam'\"\n                  type=\"button\"\n                  :title=\"t('apps.scan_result_filter_steam_title')\"\n                >\n                  <i class=\"fab fa-steam me-1\"></i>Steam\n                  <span class=\"badge bg-dark ms-1\">{{ stats.steam }}</span>\n                </button>\n                <button\n                  v-if=\"stats.epic > 0\"\n                  class=\"btn\"\n                  :class=\"selectedType === 'epic' ? 'btn-dark' : 'btn-outline-dark'\"\n                  @click=\"selectedType = 'epic'\"\n                  type=\"button\"\n                  :title=\"t('apps.scan_result_filter_epic_title')\"\n                >\n                  <i class=\"fas fa-store me-1\"></i>Epic\n                  <span class=\"badge bg-dark ms-1\">{{ stats.epic }}</span>\n                </button>\n                <button\n                  v-if=\"stats.gog > 0\"\n                  class=\"btn\"\n                  :class=\"selectedType === 'gog' ? 'btn-secondary' : 'btn-outline-secondary'\"\n                  @click=\"selectedType = 'gog'\"\n                  type=\"button\"\n                  :title=\"t('apps.scan_result_filter_gog_title')\"\n                >\n                  <i class=\"fas fa-compact-disc me-1\"></i>GOG\n                  <span class=\"badge bg-dark ms-1\">{{ stats.gog }}</span>\n                </button>\n              </div>\n\n              <!-- 游戏过滤 -->\n              <button\n                v-if=\"stats.games > 0\"\n                class=\"btn btn-sm\"\n                :class=\"gamesOnly ? 'btn-warning' : 'btn-outline-warning'\"\n                @click=\"gamesOnly = !gamesOnly\"\n                type=\"button\"\n              >\n                <i class=\"fas fa-gamepad me-1\"></i>\n                {{ gamesOnly ? t('apps.scan_result_show_all') : t('apps.scan_result_games_only') }}\n                <span class=\"badge bg-dark ms-1\">{{ stats.games }}</span>\n              </button>\n            </div>\n          </div>\n        </div>\n\n        <!-- 应用列表 -->\n        <div class=\"scan-result-body\">\n          <div v-if=\"apps.length === 0\" class=\"text-center text-muted py-4\">\n            <i class=\"fas fa-folder-open fa-3x mb-3\"></i>\n            <p>{{ t('apps.scan_result_no_apps') }}</p>\n          </div>\n          <div v-else-if=\"filteredApps.length === 0\" class=\"text-center text-muted py-4\">\n            <i class=\"fas fa-search fa-3x mb-3\"></i>\n            <p>{{ t('apps.scan_result_no_matches') }}</p>\n            <p class=\"small\">{{ t('apps.scan_result_try_different_keywords') }}</p>\n          </div>\n          <div v-else class=\"scan-result-list\">\n            <div v-for=\"app in filteredApps\" :key=\"app.source_path\" class=\"scan-result-item\">\n              <!-- 应用图标 -->\n              <div class=\"scan-app-icon\">\n                <img\n                  v-if=\"app['image-path']\"\n                  :src=\"app['image-path']\"\n                  :alt=\"app.name\"\n                  @error=\"$event.target.style.display = 'none'\"\n                />\n                <svg v-else width=\"80\" height=\"80\" viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n                  <rect width=\"100\" height=\"100\" fill=\"#667eea\" />\n                  <text\n                    x=\"50\"\n                    y=\"50\"\n                    font-size=\"40\"\n                    font-weight=\"bold\"\n                    fill=\"#fff\"\n                    text-anchor=\"middle\"\n                    dominant-baseline=\"central\"\n                  >\n                    {{ app.name.charAt(0).toUpperCase() }}\n                  </text>\n                </svg>\n              </div>\n\n              <!-- 应用信息 -->\n              <div class=\"scan-app-info\">\n                <div class=\"scan-app-name\">\n                  <i v-if=\"app['is-game']\" class=\"fas fa-gamepad me-1 text-warning\" :title=\"t('apps.scan_result_game')\"></i>\n                  {{ app.name }}\n                  <span v-if=\"app['app-type']\" class=\"badge ms-2\" :class=\"getAppTypeBadgeClass(app['app-type'])\">\n                    {{ getAppTypeLabel(app['app-type']) }}\n                  </span>\n                  <span v-if=\"app['is-game']\" class=\"badge bg-warning text-dark ms-2\">\n                    <i class=\"fas fa-gamepad me-1\"></i>{{ t('apps.scan_result_game') }}\n                  </span>\n                </div>\n                <div class=\"scan-app-cmd small\">{{ app.cmd }}</div>\n                <div class=\"scan-app-path small\"><i class=\"fas fa-folder-open me-1\"></i>{{ app.source_path }}</div>\n              </div>\n\n              <!-- 操作按钮 -->\n              <div class=\"scan-app-actions\">\n                <button class=\"btn btn-sm btn-outline-primary\" @click=\"$emit('edit', app)\" :title=\"t('apps.scan_result_edit_title')\">\n                  <i class=\"fas fa-edit\"></i>\n                </button>\n                <button\n                  class=\"btn btn-sm btn-outline-success\"\n                  @click=\"$emit('quick-add', app, apps.indexOf(app))\"\n                  :title=\"t('apps.scan_result_quick_add_title')\"\n                >\n                  <i class=\"fas fa-plus\"></i>\n                </button>\n                <button\n                  class=\"btn btn-sm btn-outline-danger\"\n                  @click=\"$emit('remove', apps.indexOf(app))\"\n                  :title=\"t('apps.scan_result_remove_title')\"\n                >\n                  <i class=\"fas fa-times\"></i>\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- 底部操作栏 -->\n        <div v-if=\"apps.length > 0\" class=\"scan-result-footer\">\n          <button class=\"btn btn-secondary\" @click=\"$emit('close')\"><i class=\"fas fa-times me-1\"></i>{{ t('_common.cancel') }}</button>\n          <button class=\"btn btn-primary\" @click=\"$emit('add-all')\" :disabled=\"saving\">\n            <i class=\"fas\" :class=\"saving ? 'fa-spinner fa-spin' : 'fa-check-double'\"></i>\n            <span class=\"ms-1\">{{ t('apps.scan_result_add_all') }}</span>\n          </button>\n        </div>\n      </div>\n    </div>\n  </Transition>\n</template>\n\n<script setup>\nimport { ref, computed, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nconst { t } = useI18n()\n\nconst props = defineProps({\n  show: {\n    type: Boolean,\n    default: false,\n  },\n  apps: {\n    type: Array,\n    default: () => [],\n  },\n  saving: {\n    type: Boolean,\n    default: false,\n  },\n})\n\ndefineEmits(['close', 'edit', 'quick-add', 'remove', 'add-all'])\n\n// 本地状态\nconst searchQuery = ref('')\nconst selectedType = ref('all')\nconst gamesOnly = ref(false)\n\n// 重置过滤器\nwatch(\n  () => props.show,\n  (newVal) => {\n    if (!newVal) {\n      searchQuery.value = ''\n      selectedType.value = 'all'\n      gamesOnly.value = false\n    }\n  }\n)\n\n// 当 apps 引用被整体替换时（新扫描结果），重置所有筛选状态\nwatch(\n  () => props.apps,\n  (newApps) => {\n    const hasGames = newApps.length > 0 && newApps.some((app) => app['is-game'] === true)\n    gamesOnly.value = hasGames\n    selectedType.value = 'all'\n    searchQuery.value = ''\n  }\n)\n\n// 当数组内部变化时（quick-add/remove via splice），仅做防御性校正\nwatch(\n  () => [props.apps.length, ...props.apps.map((a) => a['app-type'])],\n  () => {\n    // 如果当前选中的 type 已经没有对应项了，回退到 'all'\n    if (selectedType.value !== 'all') {\n      const hasType = props.apps.some((app) => app['app-type'] === selectedType.value)\n      if (!hasType) {\n        selectedType.value = 'all'\n      }\n    }\n    // 如果游戏过滤开启但已无游戏项，关闭过滤\n    if (gamesOnly.value && !props.apps.some((app) => app['is-game'] === true)) {\n      gamesOnly.value = false\n    }\n  }\n)\n\n// 统计信息\nconst stats = computed(() => ({\n  all: props.apps.length,\n  games: props.apps.filter((app) => app['is-game'] === true).length,\n  executable: props.apps.filter((app) => app['app-type'] === 'executable').length,\n  shortcut: props.apps.filter((app) => app['app-type'] === 'shortcut').length,\n  batch: props.apps.filter((app) => app['app-type'] === 'batch').length,\n  command: props.apps.filter((app) => app['app-type'] === 'command').length,\n  url: props.apps.filter((app) => app['app-type'] === 'url').length,\n  steam: props.apps.filter((app) => app['app-type'] === 'steam').length,\n  epic: props.apps.filter((app) => app['app-type'] === 'epic').length,\n  gog: props.apps.filter((app) => app['app-type'] === 'gog').length,\n}))\n\n// 是否有激活的过滤器\nconst hasActiveFilter = computed(() => searchQuery.value || gamesOnly.value || selectedType.value !== 'all')\n\n// 过滤后的应用列表\nconst filteredApps = computed(() => {\n  let filtered = props.apps\n\n  // 按应用类型过滤\n  if (selectedType.value !== 'all') {\n    filtered = filtered.filter((app) => app['app-type'] === selectedType.value)\n  }\n\n  // 按游戏过滤\n  if (gamesOnly.value) {\n    filtered = filtered.filter((app) => app['is-game'] === true)\n  }\n\n  // 按搜索关键词过滤\n  if (searchQuery.value) {\n    const query = searchQuery.value.toLowerCase()\n    filtered = filtered.filter((app) => {\n      const name = (app.name || '').toLowerCase()\n      const cmd = (app.cmd || '').toLowerCase()\n      const sourcePath = (app.source_path || '').toLowerCase()\n      return name.includes(query) || cmd.includes(query) || sourcePath.includes(query)\n    })\n  }\n\n  return filtered\n})\n\n// 应用类型标签\nconst getAppTypeLabel = (appType) => {\n  const typeMap = {\n    executable: t('apps.scan_result_type_executable'),\n    shortcut: t('apps.scan_result_type_shortcut'),\n    batch: t('apps.scan_result_type_batch'),\n    command: t('apps.scan_result_type_command'),\n    url: t('apps.scan_result_type_url'),\n    steam: 'Steam',\n    epic: 'Epic Games',\n    gog: 'GOG',\n  }\n  return typeMap[appType] || appType\n}\n\n// 应用类型徽章样式\nconst getAppTypeBadgeClass = (appType) => {\n  const classMap = {\n    executable: 'bg-primary',\n    shortcut: 'bg-info',\n    batch: 'bg-warning text-dark',\n    command: 'bg-warning text-dark',\n    url: 'bg-success',\n    steam: 'bg-primary',\n    epic: 'bg-dark',\n    gog: 'bg-secondary',\n  }\n  return classMap[appType] || 'bg-secondary'\n}\n</script>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/SetupWizard.vue",
    "content": "<template>\n  <div class=\"setup-container\">\n    <div class=\"setup-card\">\n      <div class=\"setup-header\">\n        <img src=\"/images/logo-sunshine-256.png\" height=\"60\" alt=\"Sunshine\">\n        <h1>{{ $t('setup.welcome') }}</h1>\n        <p>{{ $t('setup.description') }}</p>\n      </div>\n\n      <div class=\"setup-content\">\n        <!-- 步骤指示器 -->\n        <div class=\"step-indicator\">\n          <div class=\"step\" :class=\"{ active: currentStep === 1, completed: currentStep > 1 }\">\n            <div class=\"step-number\">1</div>\n            <span>{{ $t('setup.step0_title') }}</span>\n          </div>\n          <div class=\"step-connector\"></div>\n          <div class=\"step\" :class=\"{ active: currentStep === 2, completed: currentStep > 2 }\">\n            <div class=\"step-number\">2</div>\n            <span>{{ $t('setup.step2_title') }}</span>\n          </div>\n          <div class=\"step-connector\"></div>\n          <div class=\"step\" :class=\"{ active: currentStep === 3, completed: currentStep > 3 }\">\n            <div class=\"step-number\">3</div>\n            <span>{{ $t('setup.step1_title') }}</span>\n          </div>\n          <div class=\"step-connector\"></div>\n          <div class=\"step\" :class=\"{ active: currentStep === 4, completed: currentStep > 4 }\">\n            <div class=\"step-number\">4</div>\n            <span>{{ $t('setup.step3_title') }}</span>\n          </div>\n          <div class=\"step-connector\"></div>\n          <div class=\"step\" :class=\"{ active: currentStep === 5 }\">\n            <div class=\"step-number\">5</div>\n            <span>{{ $t('setup.step4_title') }}</span>\n          </div>\n        </div>\n\n        <!-- 步骤内容 -->\n        <div class=\"step-content\">\n          <!-- 步骤 1: 选择语言 -->\n          <div v-if=\"currentStep === 1\">\n            <h3 class=\"mb-4\">{{ $t('setup.step0_description') }}</h3>\n            \n            <div class=\"option-card\" \n                 :class=\"{ selected: selectedLocale === 'zh' }\"\n                 @click=\"selectedLocale = 'zh'\">\n              <div class=\"option-icon\">\n                <i class=\"fas fa-language\"></i>\n              </div>\n              <h4>简体中文</h4>\n              <p>使用简体中文界面</p>\n            </div>\n\n            <div class=\"option-card\" \n                 :class=\"{ selected: selectedLocale === 'en' }\"\n                 @click=\"selectedLocale = 'en'\">\n              <div class=\"option-icon\">\n                <i class=\"fas fa-language\"></i>\n              </div>\n              <h4>English</h4>\n              <p>Use English interface</p>\n            </div>\n          </div>\n\n          <!-- 步骤 2: 选择显卡 -->\n          <div v-else-if=\"currentStep === 2\">\n            <h3 class=\"mb-4\">{{ $t('setup.step2_description') }}</h3>\n            \n            <div class=\"mb-3\">\n              <label for=\"adapterSelect\" class=\"form-label adapter-label\">{{ $t('setup.select_adapter') }}</label>\n              <select id=\"adapterSelect\" \n                      class=\"form-select form-select-large\" \n                      v-model=\"selectedAdapter\">\n                <option value=\"\">{{ $t('setup.choose_adapter') }}</option>\n                <option v-for=\"adapter in uniqueAdapters\" :key=\"adapter.name\" :value=\"adapter.name\">\n                  {{ adapter.name }}\n                </option>\n              </select>\n            </div>\n\n              <div v-if=\"selectedAdapter\" class=\"adapter-info\">\n                <h5>\n                  <i class=\"fas fa-info-circle\"></i>\n                  {{ $t('setup.adapter_info') }}\n                </h5>\n                <p><strong>{{ $t('setup.selected_adapter') }}:</strong> {{ selectedAdapter }}</p>\n              </div>\n\n              <!-- GPU选择提示框 -->\n              <div class=\"form-text mt-3 adapter-hint-box\" v-html=\"$t('config.adapter_name_desc_windows')\"></div>\n          </div>\n\n          <!-- 步骤 3: 选择串流显示器 -->\n          <div v-else-if=\"currentStep === 3\">\n            <h3 class=\"mb-4\">{{ $t('setup.step1_description') }}</h3>\n            <p class=\"vdd-intro-text mb-4\">{{ $t('setup.step1_vdd_intro') }}</p>\n            \n            <!-- 基地显示器标题 -->\n            <h5 class=\"my-3 physical-display-title\">\n              <i class=\"fas fa-tv\"></i>\n              {{ $t('setup.base_display_title') }}\n            </h5>\n            <!-- 虚拟显示器选项 -->\n            <div class=\"option-card\" \n                 :class=\"{ selected: selectedDisplay === 'ZakoHDR' }\"\n                 @click=\"selectedDisplay = 'ZakoHDR'\">\n              <div class=\"d-flex align-items-center\">\n                <div class=\"option-icon-small\">\n                  <i class=\"fas fa-tv\"></i>\n                </div>\n                <div class=\"flex-grow-1\">\n                  <h4>{{ $t('setup.virtual_display') }}</h4>\n                  <p>{{ $t('setup.virtual_display_desc') }}</p>\n                </div>\n              </div>\n            </div>\n\n            <!-- 物理显示器列表 -->\n            <div v-if=\"displayDevices && displayDevices.length > 0\">\n              <h5 class=\"my-3 physical-display-title\">\n                <i class=\"fas fa-desktop\"></i>\n                {{ $t('setup.physical_display') }}\n              </h5>\n              <div class=\"option-card\" \n                   v-for=\"device in displayDevices\" \n                   :key=\"device.device_id\"\n                   :class=\"{ selected: selectedDisplay === device.device_id }\"\n                   @click=\"selectedDisplay = device.device_id\">\n                <div class=\"d-flex align-items-center\">\n                  <div class=\"option-icon-small\">\n                    <i class=\"fas fa-desktop\"></i>\n                  </div>\n                  <div class=\"flex-grow-1\">\n                    <h4>{{ getDisplayName(device) }}</h4>\n                    <p>{{ getDisplayInfo(device) }}</p>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <!-- 步骤 4: 选择显示器组合策略 -->\n          <div v-else-if=\"currentStep === 4\">\n            <h3 class=\"mb-4\">{{ $t('setup.step3_description') }}</h3>\n            \n            <!-- 显示器组合策略（VDD/物理模式统一） -->\n              <div class=\"option-card-compact\"\n                   :class=\"{ selected: displayDevicePrep === 'ensure_only_display' }\"\n                   @click=\"displayDevicePrep = 'ensure_only_display'\">\n                <div class=\"option-icon-compact\"><i class=\"fas fa-desktop\"></i></div>\n                <div class=\"option-text\">\n                  <h4>{{ $t('setup.step3_ensure_only_display') }}</h4>\n                  <p>{{ $t('setup.step3_ensure_only_display_desc') }}</p>\n                </div>\n              </div>\n\n              <div class=\"option-card-compact\" \n                   :class=\"{ selected: displayDevicePrep === 'ensure_primary' }\"\n                   @click=\"displayDevicePrep = 'ensure_primary'\">\n                <div class=\"option-icon-compact\"><i class=\"fas fa-star\"></i></div>\n                <div class=\"option-text\">\n                  <h4>{{ $t('setup.step3_ensure_primary') }}</h4>\n                  <p>{{ $t('setup.step3_ensure_primary_desc') }}</p>\n                </div>\n              </div>\n\n              <div class=\"option-card-compact\" \n                   :class=\"{ selected: displayDevicePrep === 'ensure_active' }\"\n                   @click=\"displayDevicePrep = 'ensure_active'\">\n                <div class=\"option-icon-compact\"><i class=\"fas fa-check-circle\"></i></div>\n                <div class=\"option-text\">\n                  <h4>{{ $t('setup.step3_ensure_active') }}</h4>\n                  <p>{{ $t('setup.step3_ensure_active_desc') }}</p>\n                </div>\n              </div>\n\n              <div class=\"option-card-compact\" \n                   :class=\"{ selected: displayDevicePrep === 'ensure_secondary' }\"\n                   @click=\"displayDevicePrep = 'ensure_secondary'\">\n                <div class=\"option-icon-compact\"><i class=\"fas fa-columns\"></i></div>\n                <div class=\"option-text\">\n                  <h4>{{ $t('setup.step3_ensure_secondary') }}</h4>\n                  <p>{{ $t('setup.step3_ensure_secondary_desc') }}</p>\n                </div>\n              </div>\n\n              <div class=\"option-card-compact\" \n                   :class=\"{ selected: displayDevicePrep === 'no_operation' }\"\n                   @click=\"displayDevicePrep = 'no_operation'\">\n                <div class=\"option-icon-compact\"><i class=\"fas fa-hand-paper\"></i></div>\n                <div class=\"option-text\">\n                  <h4>{{ $t('setup.step3_no_operation') }}</h4>\n                  <p>{{ $t('setup.step3_no_operation_desc') }}</p>\n                </div>\n              </div>\n          </div>\n\n          <!-- 步骤 5: 完成 -->\n          <div v-else-if=\"currentStep === 5\">\n            <div>\n              <div class=\"text-center mb-3\">\n                <h3 class=\"mb-1\">\n                  <i class=\"fas fa-check-circle setup-complete-icon\"></i>\n                  {{ $t('setup.setup_complete') }}\n                </h3>\n                <p class=\"mb-0\">{{ $t('setup.setup_complete_desc') }}</p>\n              </div>\n              \n              <div class=\"alert alert-info text-center\" v-if=\"saveSuccess\">\n                <i class=\"fas fa-info-circle\"></i>\n                {{ $t('setup.config_saved') }}\n              </div>\n              \n              <div class=\"alert alert-danger\" v-if=\"saveError\">\n                <i class=\"fas fa-exclamation-triangle\"></i>\n                {{ saveError }}\n              </div>\n\n              <!-- 客户端下载 -->\n              <div class=\"client-download-section mt-3\">\n                <h5 class=\"mb-3\">\n                  <i class=\"fas fa-download\"></i>\n                  {{ $t('setup.download_clients') }}\n                </h5>\n                <div class=\"client-download-layout\">\n                  <!-- 左侧：应用下载链接 -->\n                  <div class=\"client-links\">\n                    <a class=\"resource-link resource-link-android\"\n                       href=\"https://github.com/qiin2333/moonlight-vplus\"\n                       target=\"_blank\">\n                      <div class=\"resource-icon\"><i class=\"fab fa-android\"></i></div>\n                      <div class=\"resource-content\">\n                        <span class=\"resource-title\">安卓 Moonlight V+</span>\n                        <span class=\"resource-desc\">Android / Android TV</span>\n                      </div>\n                      <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n                    </a>\n                    <a class=\"resource-link resource-link-harmony\"\n                       href=\"javascript:void(0)\"\n                       @click.prevent=\"openHarmonyModal\">\n                      <div class=\"resource-icon\"><i class=\"fas fa-mobile-alt\"></i></div>\n                      <div class=\"resource-content\">\n                        <span class=\"resource-title\">鸿蒙 Moonlight V+</span>\n                        <span class=\"resource-desc\">HarmonyOS NEXT</span>\n                      </div>\n                      <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n                    </a>\n                    <a class=\"resource-link resource-link-apple\"\n                       href=\"https://apps.apple.com/cn/app/voidlink/id6747717070\"\n                       target=\"_blank\">\n                      <div class=\"resource-icon\"><i class=\"fab fa-apple\"></i></div>\n                      <div class=\"resource-content\">\n                        <span class=\"resource-title\">虚空终端 (VoidLink)</span>\n                        <span class=\"resource-desc\">iOS / iPadOS</span>\n                      </div>\n                      <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n                    </a>\n                    <a class=\"resource-link resource-link-desktop\"\n                       href=\"https://github.com/qiin2333/moonlight-qt\"\n                       target=\"_blank\">\n                      <div class=\"resource-icon\"><i class=\"fas fa-desktop\"></i></div>\n                      <div class=\"resource-content\">\n                        <span class=\"resource-title\">Moonlight PC</span>\n                        <span class=\"resource-desc\">Windows / macOS / Linux</span>\n                      </div>\n                      <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n                    </a>\n                  </div>\n                  <!-- 右侧：二维码 -->\n                  <div class=\"client-qrcodes\">\n                    <div class=\"qr-code-item\">\n                      <div class=\"qr-code-box\">\n                        <img :src=\"androidQrCode\" alt=\"Android QR Code\" class=\"qr-code-image\">\n                      </div>\n                      <div class=\"qr-code-label\">\n                        <i class=\"fab fa-android\"></i>\n                        {{ $t('setup.android_client') }}\n                      </div>\n                    </div>\n                    <div class=\"qr-code-item\">\n                      <div class=\"qr-code-box\">\n                        <img :src=\"iosQrCode\" alt=\"iOS QR Code\" class=\"qr-code-image\">\n                      </div>\n                      <div class=\"qr-code-label\">\n                        <i class=\"fab fa-apple\"></i>\n                        {{ $t('setup.ios_client') }}\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n      </div>\n\n      <!-- 操作按钮（固定底栏） -->\n      <div class=\"action-buttons\">\n          <button class=\"btn btn-setup btn-setup-secondary\" \n                  @click=\"previousStep\" \n                  v-if=\"currentStep > 1 && currentStep < 5\"\n                  :disabled=\"saving\">\n            <i class=\"fas fa-arrow-left\"></i>\n            {{ $t('setup.previous') }}\n          </button>\n          <button class=\"btn btn-setup btn-setup-skip\" \n                  @click=\"skipWizard\" \n                  v-if=\"currentStep < 5\"\n                  :disabled=\"saving\"\n                  type=\"button\">\n            <i class=\"fas fa-forward\"></i>\n            {{ $t('setup.skip') }}\n          </button>\n          <div v-else></div>\n\n          <button class=\"btn btn-setup btn-setup-primary\" \n                  @click=\"nextStep\" \n                  v-if=\"currentStep < 5\"\n                  :disabled=\"!canProceed || saving\">\n            {{ currentStep === 4 ? $t('setup.finish') : $t('setup.next') }}\n            <i class=\"fas fa-arrow-right\"></i>\n          </button>\n\n          <button class=\"btn btn-setup btn-setup-primary\" \n                  @click=\"goToApps\" \n                  v-if=\"currentStep === 5\">\n            {{ $t('setup.go_to_apps') }}\n            <i class=\"fas fa-arrow-right\"></i>\n          </button>\n      </div>\n    </div>\n    <!-- Skip Wizard Modal -->\n    <Transition name=\"fade\">\n      <div v-if=\"showSkipModal\" class=\"skip-wizard-overlay\" @click.self=\"closeSkipModal\">\n        <div class=\"skip-wizard-modal\">\n          <div class=\"skip-wizard-header\">\n            <h5>{{ $t('setup.skip_confirm_title')}}</h5>\n            <button class=\"btn-close\" @click=\"closeSkipModal\"></button>\n          </div>\n          <div class=\"skip-wizard-body\">\n            <p>{{ $t('setup.skip_confirm') }}</p>\n          </div>\n          <div class=\"skip-wizard-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" @click=\"closeSkipModal\">{{ $t('_common.cancel') }}</button>\n            <button type=\"button\" class=\"btn btn-warning\" @click=\"confirmSkipWizard\">{{ $t('setup.skip') }}</button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n\n    <!-- Harmony Link Modal -->\n    <Teleport to=\"body\">\n    <Transition name=\"fade\">\n      <div v-if=\"showHarmonyModal\" class=\"skip-wizard-overlay\" @click.self=\"closeHarmonyModal\">\n        <div class=\"skip-wizard-modal\">\n          <div class=\"skip-wizard-header\">\n            <h5>鸿蒙Moonlight V+</h5>\n            <button class=\"btn-close\" @click=\"closeHarmonyModal\"></button>\n          </div>\n          <div class=\"skip-wizard-body\">\n            <p>{{ $t('setup.harmony_modal_link_notice') }}</p>\n            <p>{{ $t('setup.harmony_modal_desc') }}</p>\n          </div>\n          <div class=\"skip-wizard-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" @click=\"closeHarmonyModal\">{{ $t('_common.cancel') }}</button>\n            <button type=\"button\" class=\"btn btn-primary\" @click=\"confirmHarmonyLink\">\n              <i class=\"fas fa-external-link-alt me-1\"></i>\n              {{ $t('setup.harmony_goto_repo') }}\n            </button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n    </Teleport>\n\n    <!-- Restart Countdown Modal -->\n    <Teleport to=\"body\">\n    <Transition name=\"fade\">\n      <div v-if=\"showRestartModal\" class=\"skip-wizard-overlay\">\n        <div class=\"skip-wizard-modal\">\n          <div class=\"skip-wizard-header\">\n            <h5><i class=\"fas fa-sync-alt me-2\"></i>{{ $t('setup.restart_title') }}</h5>\n          </div>\n          <div class=\"skip-wizard-body text-center\">\n            <p>{{ $t('setup.restart_desc') }}</p>\n            <div class=\"restart-countdown my-3\">\n              <span class=\"display-4 fw-bold text-primary\">{{ restartCountdown }}</span>\n              <p class=\"text-muted mt-1\">{{ $t('setup.restart_countdown_unit') }}</p>\n            </div>\n            <div class=\"progress\" style=\"height: 6px;\">\n              <div class=\"progress-bar bg-primary\" :style=\"{ width: (restartCountdown / 8 * 100) + '%' }\" role=\"progressbar\"></div>\n            </div>\n          </div>\n          <div class=\"skip-wizard-footer\">\n            <button type=\"button\" class=\"btn btn-primary\" @click=\"skipRestartCountdown\">\n              <i class=\"fas fa-arrow-right me-1\"></i>\n              {{ $t('setup.restart_go_now') }}\n            </button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n    </Teleport>\n  </div>\n</template>\n\n<script>\nimport { trackEvents } from '../config/firebase.js'\nimport { openExternalUrl } from '../utils/helpers.js'\n\nexport default {\n  name: 'SetupWizard',\n  props: {\n    adapters: {\n      type: Array,\n      default: () => []\n    },\n    displayDevices: {\n      type: Array,\n      default: () => []\n    },\n    hasLocale: {\n      type: Boolean,\n      default: false\n    }\n  },\n  data() {\n    return {\n      currentStep: 1,\n      selectedLocale: 'zh', // 默认中文\n      selectedDisplay: 'ZakoHDR', // 默认选择基地显示器\n      selectedAdapter: '',\n      displayDevicePrep: 'ensure_only_display', // 默认选择：确保唯一显示器（VDD 和普通模式通用）\n      saveSuccess: false,\n      saveError: null,\n      saving: false,\n      showSkipModal: false, // 跳过向导确认弹窗\n      showHarmonyModal: false, // 鸿蒙链接提醒弹窗\n      showRestartModal: false, // 重启倒计时弹窗\n      restartCountdown: 8, // 倒计时秒数\n      restartTimer: null, // 倒计时定时器\n      // 客户端下载链接\n      androidQrCode: 'https://assets.alkaidlab.com/androidQrCode.png',\n      iosQrCode: 'https://assets.alkaidlab.com/iosQrCode.png',\n    }\n  },\n  setup() {\n    return {}\n  },\n  mounted() {\n    // 记录进入设置向导\n    trackEvents.pageView('setup_wizard')\n    trackEvents.userAction('setup_wizard_started', {\n      has_locale: this.hasLocale,\n      adapter_count: this.adapters.length\n    })\n    \n    // 如果已经有语言配置，跳过第一步\n    if (this.hasLocale) {\n      this.currentStep = 2\n      trackEvents.userAction('setup_wizard_skip_language', { \n        reason: 'already_configured' \n      })\n    }\n    \n    // 如果只有一个显卡，自动选择\n    if (this.uniqueAdapters.length === 1) {\n      this.selectedAdapter = this.uniqueAdapters[0].name\n    }\n  },\n  beforeUnmount() {\n    if (this.restartTimer) {\n      clearInterval(this.restartTimer)\n      this.restartTimer = null\n    }\n  },\n  computed: {\n    canProceed() {\n      if (this.currentStep === 1) {\n        return this.selectedLocale !== null\n      } else if (this.currentStep === 2) {\n        return this.selectedAdapter !== null\n      } else if (this.currentStep === 3) {\n        return this.selectedDisplay !== null\n      } else if (this.currentStep === 4) {\n        return this.displayDevicePrep !== null\n      }\n      return false\n    },\n    isVirtualDisplay() {\n      return this.selectedDisplay === 'ZakoHDR'\n    },\n    // 按 name 去重，同一名称只保留一项（保持首次出现顺序）\n    uniqueAdapters() {\n      const list = this.adapters ?? []\n      const seen = new Set()\n      return list.filter((a) => {\n        const name = a?.name ?? ''\n        if (seen.has(name)) return false\n        seen.add(name)\n        return true\n      })\n    }\n  },\n  methods: {\n    previousStep() {\n      if (this.currentStep > 1) {\n        this.currentStep--\n      }\n    },\n    async nextStep() {\n      if (this.currentStep === 1 && this.canProceed) {\n        // 保存语言设置并刷新\n        await this.saveLanguage()\n      } else if (this.currentStep === 2 && this.canProceed) {\n        this.currentStep++\n      } else if (this.currentStep === 3 && this.canProceed) {\n        this.currentStep++\n      } else if (this.currentStep === 4 && this.canProceed) {\n        await this.saveConfiguration()\n      }\n    },\n    async saveLanguage() {\n      try {\n        await fetch('/api/config', {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify({\n            locale: this.selectedLocale\n          }),\n        })\n        // 重新加载页面以应用新语言\n        window.location.reload()\n      } catch (error) {\n        console.error('Failed to save language:', error)\n      }\n    },\n    async saveConfiguration() {\n      this.saving = true\n      this.saveError = null\n\n      try {\n        // 先获取当前完整配置，保留所有已有设置\n        const currentConfig = await fetch('/api/config').then(r => r.json())\n        \n        // 从完整配置中复制所有字段，避免覆盖其他配置\n        const config = { ...currentConfig }\n\n        // 标记新手引导已完成\n        config.setup_wizard_completed = true\n        \n        // 确保 locale 被保存（如果用户在步骤1选择了语言，或者已有配置中有 locale）\n        if (this.selectedLocale) {\n          config.locale = this.selectedLocale\n        } else if (currentConfig.locale) {\n          config.locale = currentConfig.locale\n        }\n        \n        // 设置 adapter_name\n        config.adapter_name = this.selectedAdapter || ''\n\n        // 设置选择的显示器\n        config.output_name = this.selectedDisplay\n\n        // 统一保存 display_device_prep（VDD 和物理模式通用）\n        config.display_device_prep = this.displayDevicePrep\n\n        console.log('保存配置:', config)\n\n        const response = await fetch('/api/config', {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify(config),\n        })\n\n        if (response.ok) {\n          this.saveSuccess = true\n          this.currentStep = 5\n          \n          // 记录设置完成\n          trackEvents.userAction('setup_wizard_completed', {\n            selected_display: this.selectedDisplay,\n            adapter: this.selectedAdapter,\n            display_device_prep: this.displayDevicePrep,\n            is_virtual_display: this.isVirtualDisplay\n          })\n          \n          this.$emit('setup-complete', config)\n        } else {\n          const errorText = await response.text()\n          this.saveError = `${this.$t('setup.save_error')}: ${errorText}`\n          \n          // 记录保存失败\n          trackEvents.errorOccurred('setup_config_save_failed', errorText)\n        }\n      } catch (error) {\n        console.error('Failed to save configuration:', error)\n        this.saveError = `${this.$t('setup.save_error')}: ${error.message}`\n      } finally {\n        this.saving = false\n      }\n    },\n    skipWizard(event) {\n      if (event) {\n        event.preventDefault()\n        event.stopPropagation()\n      }\n      \n      if (this.saving) return\n      \n      this.openSkipModal()\n    },\n    openSkipModal() {\n      this.showSkipModal = true\n    },\n    closeSkipModal() {\n      this.showSkipModal = false\n    },\n    openHarmonyModal() {\n      this.showHarmonyModal = true\n    },\n    closeHarmonyModal() {\n      this.showHarmonyModal = false\n    },\n    async confirmHarmonyLink() {\n      this.closeHarmonyModal()\n      try {\n        await openExternalUrl('https://github.com/AlkaidLab/moonlight-harmony')\n      } catch (error) {\n        console.error('Failed to open URL:', error)\n      }\n    },\n    async confirmSkipWizard() {\n      // 关闭模态框\n      this.closeSkipModal()\n      \n      if (this.saving) return\n\n      this.saving = true\n      this.saveError = null\n\n      try {\n        // 先获取当前完整配置，保留所有已有设置\n        const currentConfig = await fetch('/api/config').then(r => r.json())\n        \n        // 从完整配置中复制所有字段，避免覆盖其他配置\n        const config = { ...currentConfig }\n        // 标记新手引导已完成\n        config.setup_wizard_completed = true\n        console.log('跳过新手引导，保存配置:', config)\n        const response = await fetch('/api/config', {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n          },\n          body: JSON.stringify(config),\n        })\n\n        if (response.ok) {\n          // 记录跳过事件\n          trackEvents.userAction('setup_wizard_skipped', {\n            from_step: this.currentStep\n          })\n          \n          // 触发完成事件，让父组件知道设置向导已完成\n          this.$emit('setup-complete', config)\n          \n          // 重新加载页面以隐藏设置向导\n          window.location.reload()\n        } else {\n          const errorText = await response.text()\n          this.saveError = `${this.$t('setup.skip_error')}: ${errorText}`\n          \n          // 记录跳过失败\n          trackEvents.errorOccurred('setup_wizard_skip_failed', errorText)\n        }\n      } catch (error) {\n        console.error('Failed to skip wizard:', error)\n        this.saveError = `${this.$t('setup.skip_error')}: ${error.message}`\n      } finally {\n        this.saving = false\n      }\n    },\n    goToApps() {\n      // 记录跳转到应用配置页面\n      trackEvents.userAction('setup_go_to_apps', {\n        from_step: this.currentStep\n      })\n      // 触发重启并显示倒计时\n      this.triggerRestartAndRedirect()\n    },\n    async triggerRestartAndRedirect() {\n      // 调用重启 API\n      try {\n        await fetch('/api/restart', { method: 'POST' })\n      } catch {\n        // 重启请求可能会断开连接，忽略错误\n      }\n      // 显示倒计时弹窗\n      this.showRestartModal = true\n      this.restartCountdown = 8\n      this.restartTimer = setInterval(() => {\n        this.restartCountdown--\n        if (this.restartCountdown <= 0) {\n          this.finishRedirect()\n        }\n      }, 1000)\n    },\n    skipRestartCountdown() {\n      this.finishRedirect()\n    },\n    finishRedirect() {\n      if (this.restartTimer) {\n        clearInterval(this.restartTimer)\n        this.restartTimer = null\n      }\n      this.showRestartModal = false\n      window.location.href = '/'\n    },\n    getDisplayName(device) {\n      // 解析 device.data，提取友好名称\n      // 数据格式：\n      // DISPLAY NAME: \\\\.\\\\DISPLAY1\n      // FRIENDLY NAME: F32D80U\n      // DEVICE STATE: PRIMARY\n      // HDR STATE: ENABLED\n      try {\n        const data = device.data || ''\n        const name = data\n          .replace(\n            /.*?(DISPLAY\\d+)?\\nFRIENDLY NAME: (.*[^\\n])*?\\n.*\\n.*/g,\n            \"$2 ($1)\"\n          )\n          .replace(\"()\", \"\")\n        \n        return name || device.device_id || this.$t('setup.unknown_display')\n      } catch (e) {\n        return device.device_id || this.$t('setup.unknown_display')\n      }\n    },\n    getDisplayInfo(device) {\n      // 解析 device.data，提取详细信息\n      try {\n        const data = device.data || ''\n        \n        // 提取 DEVICE STATE\n        const stateMatch = data.match(/DEVICE STATE: (\\w+)/)\n        const state = stateMatch ? stateMatch[1].toLowerCase() : 'unknown'\n        \n        // 提取 HDR STATE\n        const hdrMatch = data.match(/HDR STATE: (\\w+)/)\n        const hdr = hdrMatch ? hdrMatch[1] : ''\n        \n        const stateKey = {\n          'primary': 'setup.state_primary',\n          'active': 'setup.state_active',\n          'inactive': 'setup.state_inactive'\n        }[state] || 'setup.state_unknown'\n        const stateText = this.$t(stateKey)\n        \n        let info = `${this.$t('setup.device_state')}: ${stateText}`\n        if (hdr) {\n          info += ` | HDR: ${hdr}`\n        }\n        \n        return info\n      } catch (e) {\n        return device.device_id\n      }\n    }\n  }\n}\n</script>\n\n<style scoped>\n.setup-container {\n  position: fixed;\n  inset: 0;\n  padding: 1em;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  z-index: 1000;\n}\n\n.setup-card {\n  background: var(--bs-body-bg);\n  border-radius: 16px;\n  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  max-width: 900px;\n  flex: 1;\n  min-height: 0;\n}\n\n.setup-header {\n  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n  color: white;\n  padding: 1.2em;\n  text-align: center;\n  flex-shrink: 0;\n}\n\n.setup-header h1 {\n  margin: 0.3em 0 0 0;\n  font-size: 1.5em;\n  font-weight: 600;\n}\n\n.setup-header p {\n  margin: 0.3em 0 0 0;\n  opacity: 0.9;\n  font-size: 0.95em;\n}\n\n.setup-content {\n  padding: 1.2em 1.5em;\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n  overflow: hidden;\n  min-height: 0;\n}\n\n.step-indicator {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  margin-bottom: 1.2em;\n  gap: 0.5em;\n  flex-shrink: 0;\n}\n\n.step {\n  display: flex;\n  align-items: center;\n  gap: 0.3em;\n  font-size: 0.85em;\n}\n\n.step-number {\n  width: 28px;\n  height: 28px;\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-weight: 600;\n  font-size: 0.9em;\n  background: var(--bs-secondary-bg);\n  color: var(--bs-secondary-color);\n  transition: all 0.3s ease;\n  flex-shrink: 0;\n}\n\n.step.active .step-number {\n  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n  color: white;\n  transform: scale(1.05);\n}\n\n.step.completed .step-number {\n  background: #28a745;\n  color: white;\n}\n\n.step-connector {\n  width: 30px;\n  height: 2px;\n  background: var(--bs-secondary-bg);\n  flex-shrink: 0;\n}\n\n.step-content {\n  flex: 1;\n  overflow-y: auto;\n  overflow-x: hidden;\n  padding-right: 0.5em;\n}\n\n.step-content h3 {\n  font-size: 1.1em;\n  margin-bottom: 0.8em;\n}\n\n.option-card {\n  border: 2px solid var(--bs-border-color);\n  border-radius: 10px;\n  padding: 0.8em 1em;\n  margin-bottom: 0.6em;\n  cursor: pointer;\n  transition: all 0.3s ease;\n  background: var(--bs-body-bg);\n}\n\n.option-card:hover {\n  border-color: #667eea;\n  transform: translateY(-1px);\n  box-shadow: 0 3px 10px rgba(102, 126, 234, 0.2);\n}\n\n.option-card.selected {\n  border-color: #667eea;\n  background: rgba(102, 126, 234, 0.1);\n}\n\n.option-card .option-icon {\n  font-size: 1.8em;\n  margin-bottom: 0.3em;\n  color: #667eea;\n}\n\n.option-card h4 {\n  margin: 0.3em 0;\n  font-weight: 600;\n  font-size: 1em;\n}\n\n.option-card p {\n  margin: 0;\n  color: var(--bs-body-color);\n  opacity: 0.85;\n  font-size: 0.85em;\n  line-height: 1.3;\n}\n\n/* 紧凑型选项卡片（水平布局） */\n.option-card-compact {\n  display: flex;\n  align-items: center;\n  border: 2px solid var(--bs-border-color);\n  border-radius: 8px;\n  padding: 0.5em 0.8em;\n  margin-bottom: 0.4em;\n  cursor: pointer;\n  transition: all 0.3s ease;\n  background: var(--bs-body-bg);\n  gap: 0.7em;\n}\n\n.option-card-compact:hover {\n  border-color: #667eea;\n  transform: translateY(-1px);\n  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);\n}\n\n.option-card-compact.selected {\n  border-color: #667eea;\n  background: rgba(102, 126, 234, 0.1);\n}\n\n.option-icon-compact {\n  font-size: 1.2em;\n  color: #667eea;\n  flex-shrink: 0;\n  width: 2em;\n  text-align: center;\n}\n\n.option-card-compact .option-text h4 {\n  margin: 0;\n  font-weight: 600;\n  font-size: 0.9em;\n}\n\n.option-card-compact .option-text p {\n  margin: 0;\n  color: var(--bs-body-color);\n  opacity: 0.75;\n  font-size: 0.85em;\n  line-height: 1.3;\n}\n\n.form-select-large {\n  padding: 0.7em;\n  font-size: 1em;\n  border-radius: 8px;\n  border: 2px solid var(--bs-border-color);\n  transition: all 0.3s ease;\n}\n\n/* 显卡适配器标签 */\n.adapter-label {\n  font-size: 1.05em;\n  font-weight: 600;\n}\n\n/* 物理显示器标题 */\n.physical-display-title {\n  font-size: 0.95em;\n}\n\n.form-select-large:focus {\n  border-color: #667eea;\n  box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);\n}\n\n.action-buttons {\n  display: flex;\n  justify-content: space-between;\n  gap: 0.8em;\n  flex-shrink: 0;\n  padding: 1em 1.5em;\n  border-top: 1px solid var(--bs-border-color);\n  background: var(--bs-body-bg);\n}\n\n.btn-setup {\n  padding: 0.6em 1.5em;\n  font-size: 1em;\n  border-radius: 8px;\n  font-weight: 500;\n  transition: all 0.3s ease;\n}\n\n.btn-setup-primary {\n  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n  border: none;\n  color: white;\n}\n\n.btn-setup-primary:hover:not(:disabled) {\n  transform: translateY(-2px);\n  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);\n  color: white;\n}\n\n.btn-setup-secondary {\n  background: var(--bs-secondary-bg);\n  border: none;\n  color: var(--bs-body-color);\n}\n\n.btn-setup-secondary:hover:not(:disabled) {\n  background: var(--bs-tertiary-bg);\n  transform: translateY(-1px);\n}\n\n.btn-setup-skip {\n  background: rgba(255, 255, 255, 0.95);\n  border: 2px solid rgba(102, 126, 234, 0.7);\n  color: rgba(70, 90, 200, 1);\n  font-weight: 500;\n}\n\n.btn-setup-skip:hover:not(:disabled) {\n  background: rgba(255, 255, 255, 1);\n  border-color: rgba(102, 126, 234, 0.9);\n  color: rgba(50, 70, 180, 1);\n  transform: translateY(-1px);\n  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);\n}\n\n.adapter-info {\n  background: var(--bs-secondary-bg);\n  padding: 0.8em;\n  border-radius: 8px;\n  margin-top: 0.8em;\n  font-size: 0.9em;\n}\n\n.adapter-info h5 {\n  font-size: 1em;\n  margin-bottom: 0.5em;\n}\n\n.adapter-info p {\n  margin-bottom: 0.3em;\n  font-size: 0.95em;\n}\n\n/* GPU选择提示框样式 */\n.adapter-hint-box {\n  background: rgba(102, 126, 234, 0.08);\n  padding: 0.8em 1em;\n  border-radius: 8px;\n  border-left: 3px solid #667eea;\n  font-size: 0.9em;\n  line-height: 1.5;\n  color: var(--bs-body-color);\n  font-weight: 500;\n}\n\n/* VDD 介绍文字样式 */\n.vdd-intro-text {\n  color: var(--bs-body-color);\n  opacity: 0.75;\n  font-size: 0.95em;\n}\n\n.adapter-vdd-hint {\n  margin: 0.5em 0 0 0;\n  padding: 0.5em 0.8em;\n  background: rgba(40, 167, 69, 0.1);\n  border-radius: 4px;\n  font-size: 0.95em;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n}\n\n/* 滚动条样式 */\n.step-content::-webkit-scrollbar {\n  width: 6px;\n}\n\n.step-content::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n.step-content::-webkit-scrollbar-thumb {\n  background: rgba(102, 126, 234, 0.3);\n  border-radius: 3px;\n}\n\n.step-content::-webkit-scrollbar-thumb:hover {\n  background: rgba(102, 126, 234, 0.5);\n}\n\n/* 完成页面标题 */\n.setup-complete-icon {\n  font-size: 1.2em;\n  color: #28a745;\n  margin-right: 0.3em;\n  vertical-align: middle;\n}\n\n/* 客户端下载样式 */\n.client-download-section {\n  background: var(--bs-secondary-bg);\n  padding: 1em;\n  border-radius: 10px;\n}\n\n.client-download-section h5 {\n  font-size: 1em;\n  margin-bottom: 0.8em;\n  color: var(--bs-body-color);\n}\n\n.client-download-layout {\n  display: flex;\n  gap: 1.5em;\n  align-items: flex-start;\n}\n\n.client-links {\n  flex: 0 0 auto;\n  display: flex;\n  flex-direction: column;\n  gap: 0.5em;\n  min-width: 240px;\n}\n\n.client-qrcodes {\n  display: flex;\n  flex-direction: row;\n  gap: 1em;\n  align-items: flex-start;\n  flex: 1;\n  min-width: 0;\n}\n\n/* Resource link styles (from ResourceCard) */\n.resource-link {\n  display: flex;\n  align-items: center;\n  padding: 0.6em 0.8em;\n  border-radius: 8px;\n  text-decoration: none;\n  background: linear-gradient(135deg, rgba(var(--link-color), 0.15) 0%, rgba(var(--link-color), 0.08) 100%);\n  border: 1px solid transparent;\n  transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;\n}\n\n.resource-link:hover {\n  transform: translateY(-1px);\n  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);\n  text-decoration: none;\n  border-color: rgba(var(--link-color), 0.4);\n}\n\n.resource-icon {\n  width: 36px;\n  height: 36px;\n  border-radius: 8px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 1.1rem;\n  flex-shrink: 0;\n  margin-right: 0.8em;\n  color: white;\n  background: var(--icon-gradient);\n}\n\n.resource-content {\n  flex: 1;\n  min-width: 0;\n}\n\n.resource-title {\n  display: block;\n  font-weight: 600;\n  font-size: 0.9rem;\n  color: var(--bs-body-color);\n  margin-bottom: 1px;\n}\n\n.resource-desc {\n  display: block;\n  font-size: 0.75rem;\n  color: var(--bs-secondary-color);\n}\n\n.resource-arrow {\n  font-size: 0.8rem;\n  color: var(--bs-secondary-color);\n  margin-left: 0.5rem;\n  transition: transform 0.2s ease;\n}\n\n.resource-link:hover .resource-arrow {\n  transform: translateX(3px);\n}\n\n.resource-link-android {\n  --link-color: 61, 220, 132;\n  --icon-gradient: linear-gradient(135deg, #3ddc84 0%, #00c853 100%);\n}\n\n.resource-link-apple {\n  --link-color: 128, 128, 128;\n  --icon-gradient: linear-gradient(135deg, #555 0%, #777 100%);\n}\n\n.resource-link-desktop {\n  --link-color: 108, 117, 125;\n  --icon-gradient: linear-gradient(135deg, #6c757d 0%, #495057 100%);\n}\n\n.resource-link-harmony {\n  --link-color: 206, 48, 48;\n  --icon-gradient: linear-gradient(135deg, #ce3030 0%, #e74c3c 100%);\n}\n\n.qr-code-item {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 0.3em;\n  flex: 1;\n}\n\n.qr-code-box {\n  background: white;\n  padding: 0.4em;\n  border-radius: 8px;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n  width: 100%;\n}\n\n.qr-code-image {\n  width: 100%;\n  aspect-ratio: 1;\n  display: block;\n}\n\n.qr-code-label {\n  font-size: 0.8em;\n  font-weight: 500;\n  color: var(--bs-body-color);\n}\n\n.qr-code-label i {\n  margin-right: 0.3em;\n}\n\n/* 小图标样式 */\n.option-icon-small {\n  font-size: 1.5em;\n  color: #667eea;\n  margin-right: 0.8em;\n  flex-shrink: 0;\n}\n\n.d-flex {\n  display: flex;\n}\n\n.align-items-center {\n  align-items: center;\n}\n\n.flex-grow-1 {\n  flex-grow: 1;\n}\n\n.my-3 {\n  margin-top: 1rem;\n  margin-bottom: 1rem;\n}\n\n/* Skip Wizard Modal - 使用 ScanResultModal 样式 */\n.skip-wizard-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 100vw;\n  height: 100vh;\n  margin: 0;\n  background: var(--overlay-bg, rgba(0, 0, 0, 0.7));\n  backdrop-filter: blur(8px);\n  z-index: 9999;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: var(--spacing-lg, 20px);\n  overflow: hidden;\n  \n  [data-bs-theme='light'] & {\n    background: rgba(0, 0, 0, 0.5);\n  }\n}\n\n.skip-wizard-modal {\n  background: var(--modal-bg, rgba(30, 30, 50, 0.95));\n  border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.2));\n  border-radius: var(--border-radius-xl, 12px);\n  width: 100%;\n  max-width: 500px;\n  max-height: 80vh;\n  display: flex;\n  flex-direction: column;\n  backdrop-filter: blur(20px);\n  box-shadow: var(--shadow-xl, 0 25px 50px rgba(0, 0, 0, 0.5));\n  animation: modalSlideUp 0.3s ease;\n  \n  [data-bs-theme='light'] & {\n    background: rgba(255, 255, 255, 0.95);\n    border: 1px solid rgba(0, 0, 0, 0.15);\n    box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);\n  }\n}\n\n@keyframes modalSlideUp {\n  from {\n    transform: translateY(20px);\n    opacity: 0;\n  }\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n.skip-wizard-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-bottom: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  h5 {\n    margin: 0;\n    color: var(--text-primary, #fff);\n    font-size: var(--font-size-lg, 1.1rem);\n    font-weight: 600;\n    display: flex;\n    align-items: center;\n    gap: var(--spacing-sm, 8px);\n  }\n  \n  [data-bs-theme='light'] & {\n    border-bottom: 1px solid rgba(0, 0, 0, 0.1);\n    \n    h5 {\n      color: #000000;\n    }\n  }\n}\n\n.skip-wizard-body {\n  padding: var(--spacing-lg, 24px);\n  font-size: var(--font-size-md, 0.95rem);\n  line-height: 1.5;\n  overflow-y: auto;\n  flex: 1;\n  color: var(--text-primary, #fff);\n  \n  [data-bs-theme='light'] & {\n    color: #000000;\n  }\n}\n\n.skip-wizard-footer {\n  display: flex;\n  justify-content: flex-end;\n  gap: 10px;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-top: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n  \n  [data-bs-theme='light'] & {\n    border-top: 1px solid rgba(0, 0, 0, 0.1);\n  }\n}\n\n.skip-wizard-footer button {\n  padding: 8px 16px;\n  font-size: 0.9rem;\n}\n\n/* Vue 过渡动画 */\n.fade-enter-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n\n"
  },
  {
    "path": "src_assets/common/assets/web/components/TroubleshootingCard.vue",
    "content": "<template>\n  <div class=\"card shadow-sm\">\n    <div :class=\"['card-header', 'border-bottom-0', headerClass]\">\n      <h5 class=\"card-title mb-0\">\n        <i :class=\"iconClass\"></i>\n        {{ title }}\n      </h5>\n    </div>\n    <div class=\"card-body\">\n      <p class=\"text-muted mb-3\" :class=\"{ 'pre-line': preLine }\">\n        {{ description }}\n      </p>\n      <slot name=\"alerts\" />\n      <slot />\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { computed } from 'vue'\n\nconst props = defineProps({\n  icon: {\n    type: String,\n    required: true,\n  },\n  color: {\n    type: String,\n    required: true,\n    validator: (value) => ['warning', 'info', 'danger', 'secondary', 'primary', 'success'].includes(value),\n  },\n  title: {\n    type: String,\n    required: true,\n  },\n  description: {\n    type: String,\n    required: true,\n  },\n  preLine: {\n    type: Boolean,\n    default: false,\n  },\n})\n\nconst headerClass = computed(() => `bg-${props.color} bg-opacity-10`)\nconst iconClass = computed(() => `fas ${props.icon} text-${props.color} me-2`)\n</script>\n\n<style scoped>\n.alert {\n  border-radius: 8px;\n  font-size: 0.9rem;\n  padding: 0.75rem 1rem;\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/common/ErrorLogs.vue",
    "content": "<template>\n  <div v-if=\"fatalLogs.length > 0\" class=\"alert alert-danger\">\n    <div style=\"line-height: 32px\">\n      <i class=\"fas fa-circle-exclamation\" style=\"font-size: 32px; margin-right: 0.25em\"></i>\n      <p v-html=\"$t('index.startup_errors')\"></p>\n      <br />\n    </div>\n    <ul>\n      <li v-for=\"log in fatalLogs\" :key=\"log.timestamp\">{{ log.value }}</li>\n    </ul>\n    <a class=\"btn btn-danger\" href=\"/troubleshooting/#logs\">\n      {{ $t('index.view_logs') || 'View Logs' }}\n    </a>\n  </div>\n</template>\n\n<script setup>\ndefineProps({\n  fatalLogs: {\n    type: Array,\n    required: true,\n    default: () => []\n  }\n})\n</script>\n\n<style scoped>\n.alert-danger {\n  margin-bottom: 1.5rem;\n}\n</style>\n\n"
  },
  {
    "path": "src_assets/common/assets/web/components/common/Icon.vue",
    "content": "<template>\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    :width=\"size\"\n    :height=\"size\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    :stroke-width=\"strokeWidth\"\n    stroke-linecap=\"round\"\n    stroke-linejoin=\"round\"\n    :class=\"iconClass\"\n  >\n    <component :is=\"iconPaths[name]\" />\n  </svg>\n</template>\n\n<script setup>\nimport { computed, h } from 'vue'\n\nconst props = defineProps({\n  name: {\n    type: String,\n    required: true,\n    validator: (value) => ['lock', 'clock', 'star'].includes(value),\n  },\n  size: {\n    type: [String, Number],\n    default: 24,\n  },\n  strokeWidth: {\n    type: [String, Number],\n    default: 2,\n  },\n  iconClass: {\n    type: String,\n    default: '',\n  },\n})\n\nconst iconPaths = {\n  lock: {\n    render: () => [\n      h('rect', { x: 3, y: 11, width: 18, height: 11, rx: 2, ry: 2 }),\n      h('path', { d: 'M7 11V7a5 5 0 0 1 10 0v4' }),\n    ],\n  },\n  clock: {\n    render: () => [\n      h('circle', { cx: 12, cy: 12, r: 10 }),\n      h('polyline', { points: '12 6 12 12 16 14' }),\n    ],\n  },\n  star: {\n    render: () => [\n      h('path', {\n        d: 'M12 3l1.912 5.813a2 2 0 0 0 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 0-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 0-1.275-1.275L3 12l5.813-1.912a2 2 0 0 0 1.275-1.275L12 3z',\n      }),\n    ],\n  },\n}\n</script>\n\n<style scoped>\nsvg {\n  display: inline-block;\n  vertical-align: middle;\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/common/Locale.vue",
    "content": "<template>\n</template>\n\n<script>\nimport { createI18n } from \"vue-i18n\";\n\n// Import translation files\nimport de from '../../public/assets/locale/de.json'\nimport en from '../../public/assets/locale/en.json'\nimport en_GB from '../../public/assets/locale/en_GB.json'\nimport en_US from '../../public/assets/locale/en_US.json'\nimport es from '../../public/assets/locale/es.json'\nimport fr from '../../public/assets/locale/fr.json'\nimport it from '../../public/assets/locale/it.json'\nimport ru from '../../public/assets/locale/ru.json'\nimport sv from '../../public/assets/locale/sv.json'\nimport zh from '../../public/assets/locale/zh.json'\n\n// Create the i18n instance\nconst i18n = createI18n({\n    locale: \"en\",  // initial locale, todo: how to get this from config?\n    fallbackLocale: \"en\",  // fallback locale\n    messages: {\n        de: de,\n        en: en,\n        \"en-GB\": en_GB,\n        \"en-US\": en_US,\n        es: es,\n        fr: fr,\n        it: it,\n        ru: ru,\n        sv: sv,\n        zh: zh\n    },\n});\n\nexport {\n    i18n\n};\n\nexport default {\n    created() {\n        this.fetchLocale();\n    },\n    methods: {\n        fetchLocale() {\n            fetch(\"/api/configLocale$\")\n                .then((r) => r.json())\n                .then((r) => {\n                    this.response = r;\n                    i18n.locale = this.response.locale;\n                });\n        },\n    },\n};\n</script>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/common/ResourceCard.vue",
    "content": "<template>\n  <div class=\"resource-section\">\n    <!-- Resources Card -->\n    <div class=\"card shadow-sm\">\n      <div class=\"card-header bg-primary bg-opacity-10 border-bottom-0\">\n        <h5 class=\"card-title mb-0\">\n          <i class=\"fas fa-book-open text-primary me-2\"></i>\n          {{ $t('resource_card.resources') }}\n        </h5>\n      </div>\n      <div class=\"card-body\">\n\n        <!-- 基地官网 -->\n        <div class=\"resource-group mb-4\">\n          <h6 class=\"resource-group-title\">\n            <i class=\"fas fa-globe text-primary me-2\"></i>\n            {{ $t('resource_card.official_website') }}\n          </h6>\n          <div class=\"row g-3\">\n            <div class=\"col-md-6\">\n              <a class=\"resource-link resource-link-primary\" :href=\"officialWebsiteUrl\" target=\"_blank\">\n                <div class=\"resource-icon\">\n                  <i class=\"fas fa-globe\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">{{ $t('resource_card.official_website_title') }}</span>\n                </div>\n                <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n              </a>\n            </div>\n            <div class=\"col-md-6\">\n              <a\n                class=\"resource-link resource-link-github\"\n                href=\"https://github.com/AlkaidLab/foundation-sunshine\"\n                target=\"_blank\"\n              >\n                <div class=\"resource-icon\">\n                  <i class=\"fab fa-github\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">Sunshine Foundation</span>\n                  <span class=\"resource-desc\">{{ $t('resource_card.open_source_desc') }}</span>\n                </div>\n                <i class=\"fas fa-star resource-arrow text-warning\"></i>\n              </a>\n            </div>\n          </div>\n        </div>\n\n        <!-- 快速入门 -->\n        <div class=\"resource-group mb-4\">\n          <h6 class=\"resource-group-title\">\n            <i class=\"fas fa-rocket text-success me-2\"></i>\n            {{ $t('resource_card.quick_start') }}\n          </h6>\n          <div class=\"row g-3\">\n            <div class=\"col-md-6\">\n              <a\n                class=\"resource-link resource-link-primary\"\n                href=\"https://docs.qq.com/aio/DSGdQc3htbFJjSFdO\"\n                target=\"_blank\"\n              >\n                <div class=\"resource-icon\">\n                  <i class=\"fas fa-file-alt\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">{{ $t('resource_card.tutorial') }}</span>\n                  <span class=\"resource-desc\">{{ $t('resource_card.tutorial_desc') }}</span>\n                </div>\n                <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n              </a>\n            </div>\n            <div class=\"col-md-6\">\n              <a class=\"resource-link resource-link-info\" href=\"https://qm.qq.com/q/3tWBFVNZ\" target=\"_blank\">\n                <div class=\"resource-icon\">\n                  <i class=\"fab fa-qq\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">{{ $t('resource_card.join_group') }}</span>\n                  <span class=\"resource-desc\">{{ $t('resource_card.join_group_desc') }}</span>\n                </div>\n                <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n              </a>\n            </div>\n          </div>\n        </div>\n\n        <!-- 客户端下载 -->\n        <div class=\"resource-group mb-4\">\n          <h6 class=\"resource-group-title\">\n            <i class=\"fas fa-download text-primary me-2\"></i>\n            {{ $t('resource_card.client_downloads') }}\n          </h6>\n          <div class=\"row g-3\">\n            <div class=\"col-md-6 col-lg-4\">\n              <a\n                class=\"resource-link resource-link-android\"\n                href=\"https://github.com/qiin2333/moonlight-vplus\"\n                target=\"_blank\"\n              >\n                <div class=\"resource-icon\">\n                  <i class=\"fab fa-android\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">安卓 Moonlight V+</span>\n                  <span class=\"resource-desc\">Android / Android TV</span>\n                </div>\n                <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n              </a>\n            </div>\n            <div class=\"col-md-6 col-lg-4\">\n              <a\n                class=\"resource-link resource-link-harmony\"\n                href=\"javascript:void(0)\"\n                @click.prevent=\"openHarmonyModal\"\n              >\n                <div class=\"resource-icon\">\n                  <i class=\"fas fa-mobile-alt\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">{{ $t('resource_card.harmony_client') }}</span>\n                  <span class=\"resource-desc\">HarmonyOS NEXT</span>\n                </div>\n                <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n              </a>\n            </div>\n            <div class=\"col-md-6 col-lg-4\">\n              <a\n                class=\"resource-link resource-link-desktop\"\n                href=\"https://github.com/qiin2333/moonlight-qt\"\n                target=\"_blank\"\n              >\n                <div class=\"resource-icon\">\n                  <i class=\"fas fa-desktop\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">Moonlight PC</span>\n                  <span class=\"resource-desc\">Windows / macOS / Linux</span>\n                </div>\n                <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n              </a>\n            </div>\n            <div class=\"col-md-6 col-lg-4\">\n              <a\n                class=\"resource-link resource-link-apple\"\n                href=\"https://apps.apple.com/cn/app/voidlink/id6747717070\"\n                target=\"_blank\"\n              >\n                <div class=\"resource-icon\">\n                  <i class=\"fab fa-apple\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">{{ $t('resource_card.voidlink_title') }}</span>\n                  <span class=\"resource-desc\">iOS / iPadOS</span>\n                </div>\n                <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n              </a>\n            </div>\n          </div>\n        </div>\n\n        <!-- 友情链接 -->\n        <div class=\"resource-group\">\n          <h6 class=\"resource-group-title\">\n            <i class=\"fas fa-code-branch text-dark me-2\"></i>\n            {{ $t('resource_card.third_party_moonlight') }}\n          </h6>\n          <div class=\"row g-3\">\n            <div class=\"col-12\">\n              <a\n                class=\"resource-link resource-link-android\"\n                href=\"https://github.com/WACrown/moonlight-android\"\n                target=\"_blank\"\n              >\n                <div class=\"resource-icon\">\n                  <i class=\"fas fa-crown\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">{{ $t('resource_card.crown_edition') }}</span>\n                  <span class=\"resource-desc\">{{ $t('resource_card.crown_edition_desc') }}</span>\n                </div>\n                <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n              </a>\n            </div>\n            <div class=\"col-12\">\n              <a\n                class=\"resource-link resource-link-harmony\"\n                href=\"https://gitee.com/smdsbz/moonlight-ohos\"\n                target=\"_blank\"\n              >\n                <div class=\"resource-icon\">\n                  <i class=\"fas fa-mobile-alt\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">{{ $t('resource_card.moonlight_ohos') }}</span>\n                  <span class=\"resource-desc\">{{ $t('resource_card.moonlight_ohos_desc') }}</span>\n                </div>\n                <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n              </a>\n            </div>\n            <div class=\"col-12\">\n              <a\n                class=\"resource-link resource-link-apple\"\n                href=\"https://github.com/skyhua0224/moonlight-macos-enhanced\"\n                target=\"_blank\"\n              >\n                <div class=\"resource-icon\">\n                  <i class=\"fab fa-apple\"></i>\n                </div>\n                <div class=\"resource-content\">\n                  <span class=\"resource-title\">{{ $t('resource_card.moonlight_macos_enhanced') }}</span>\n                  <span class=\"resource-desc\">{{ $t('resource_card.moonlight_macos_enhanced_desc') }}</span>\n                </div>\n                <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n              </a>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Legal Card -->\n    <div class=\"card shadow-sm mt-4\">\n      <div class=\"card-header bg-danger bg-opacity-10 border-bottom-0\">\n        <h5 class=\"card-title mb-0\">\n          <i class=\"fas fa-gavel text-danger me-2\"></i>\n          {{ $t('resource_card.legal') }}\n        </h5>\n      </div>\n      <div class=\"card-body\">\n        <p class=\"mb-4\">{{ $t('resource_card.legal_desc') }}</p>\n\n        <!-- GPL v3.0 Badge -->\n        <div class=\"gpl-badge mb-4\">\n          <div class=\"d-flex align-items-center justify-content-center\">\n            <span class=\"badge-gpl\">\n              <i class=\"fas fa-balance-scale me-2\"></i>\n              GNU General Public License v3.0\n            </span>\n          </div>\n          <p class=\"text-center small mt-2 mb-0\">\n            {{ $t('resource_card.gpl_license_text_1') }}\n            <br />\n            {{ $t('resource_card.gpl_license_text_2') }}\n          </p>\n        </div>\n\n        <div class=\"row g-3\">\n          <div class=\"col-md-6\">\n            <a\n              class=\"resource-link resource-link-danger\"\n              href=\"https://github.com/qiin2333/Sunshine/blob/master/LICENSE\"\n              target=\"_blank\"\n            >\n              <div class=\"resource-icon\">\n                <i class=\"fas fa-file-alt\"></i>\n              </div>\n              <div class=\"resource-content\">\n                <span class=\"resource-title\">{{ $t('resource_card.license') }}</span>\n                <span class=\"resource-desc\">{{ $t('resource_card.view_license') }}</span>\n              </div>\n              <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n            </a>\n          </div>\n          <div class=\"col-md-6\">\n            <a\n              class=\"resource-link resource-link-danger\"\n              href=\"https://github.com/qiin2333/Sunshine/blob/master/NOTICE\"\n              target=\"_blank\"\n            >\n              <div class=\"resource-icon\">\n                <i class=\"fas fa-exclamation-triangle\"></i>\n              </div>\n              <div class=\"resource-content\">\n                <span class=\"resource-title\">{{ $t('resource_card.third_party_notice') }}</span>\n                <span class=\"resource-desc\">{{ $t('resource_card.third_party_desc') }}</span>\n              </div>\n              <i class=\"fas fa-external-link-alt resource-arrow\"></i>\n            </a>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Harmony Link Modal -->\n    <Transition name=\"fade\">\n      <div v-if=\"showHarmonyModal\" class=\"harmony-modal-overlay\" @click.self=\"closeHarmonyModal\">\n        <div class=\"harmony-modal\">\n          <div class=\"harmony-modal-header\">\n            <h5>{{ $t('resource_card.harmony_client') }}</h5>\n            <button class=\"btn-close\" @click=\"closeHarmonyModal\"></button>\n          </div>\n          <div class=\"harmony-modal-body\">\n            <p>{{ $t('setup.harmony_modal_link_notice') }}</p>\n            <p>{{ $t('setup.harmony_modal_desc') }}</p>\n          </div>\n          <div class=\"harmony-modal-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" @click=\"closeHarmonyModal\">{{ $t('_common.cancel') }}</button>\n            <button type=\"button\" class=\"btn btn-primary\" @click=\"confirmHarmonyLink\">\n              <i class=\"fas fa-external-link-alt me-1\"></i>\n              {{ $t('setup.harmony_goto_repo') }}\n            </button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'ResourceCard',\n  data() {\n    return {\n      showHarmonyModal: false\n    }\n  },\n  computed: {\n    officialWebsiteUrl() {\n      return 'https://www.alkaidlab.com/'\n    }\n  },\n  methods: {\n    openHarmonyModal() {\n      this.showHarmonyModal = true\n    },\n    closeHarmonyModal() {\n      this.showHarmonyModal = false\n    },\n    confirmHarmonyLink() {\n      window.open('https://github.com/AlkaidLab/moonlight-harmony', '_blank')\n      this.closeHarmonyModal()\n    }\n  }\n}\n</script>\n\n<style scoped>\n.resource-group {\n  padding-bottom: 1rem;\n  border-bottom: 1px solid rgba(128, 128, 128, 0.15);\n}\n\n.resource-group:last-child {\n  padding-bottom: 0;\n  border-bottom: none;\n}\n\n.resource-group-title {\n  font-size: 0.875rem;\n  font-weight: 600;\n  color: var(--bs-secondary-color, #6c757d);\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  margin-bottom: 1rem;\n}\n\n/* Resource Link Base */\n.resource-link {\n  display: flex;\n  align-items: center;\n  padding: 1rem;\n  border-radius: 10px;\n  text-decoration: none;\n  background: var(--bs-tertiary-bg, #f8f9fa);\n  border: 1px solid transparent;\n  transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;\n}\n\n.resource-link:hover {\n  transform: translateY(-2px);\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n  text-decoration: none;\n}\n\n.resource-icon {\n  width: 44px;\n  height: 44px;\n  border-radius: 10px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 1.25rem;\n  flex-shrink: 0;\n  margin-right: 1rem;\n  color: white;\n}\n\n.resource-content {\n  flex: 1;\n  min-width: 0;\n}\n\n.resource-title {\n  display: block;\n  font-weight: 600;\n  font-size: 0.95rem;\n  color: var(--bs-body-color, #2c3e50);\n  margin-bottom: 2px;\n}\n\n.resource-desc {\n  display: block;\n  font-size: 0.8rem;\n  color: var(--bs-body-color, #495057);\n}\n\n.resource-arrow {\n  font-size: 0.875rem;\n  color: var(--bs-secondary-color, #adb5bd);\n  margin-left: 0.5rem;\n  transition: transform 0.2s ease;\n}\n\n.resource-link:hover .resource-arrow {\n  transform: translateX(3px);\n}\n\n/* Color Variants - Using CSS Custom Properties */\n.resource-link-primary {\n  --link-color: 40, 167, 69;\n  --icon-gradient: linear-gradient(135deg, #28a745 0%, #20c997 100%);\n}\n\n.resource-link-info {\n  --link-color: 0, 123, 255;\n  --icon-gradient: linear-gradient(135deg, #007bff 0%, #17a2b8 100%);\n}\n\n.resource-link-android {\n  --link-color: 61, 220, 132;\n  --icon-gradient: linear-gradient(135deg, #3ddc84 0%, #00c853 100%);\n}\n\n.resource-link-apple {\n  --link-color: 128, 128, 128;\n  --icon-gradient: linear-gradient(135deg, #555 0%, #777 100%);\n}\n\n.resource-link-desktop,\n.resource-link-github {\n  --link-color: 108, 117, 125;\n}\n\n.resource-link-desktop {\n  --icon-gradient: linear-gradient(135deg, #6c757d 0%, #495057 100%);\n}\n\n.resource-link-harmony {\n  --link-color: 206, 48, 48;\n  --icon-gradient: linear-gradient(135deg, #ce3030 0%, #e74c3c 100%);\n}\n\n.resource-link-github {\n  --icon-gradient: linear-gradient(135deg, #6c757d 0%, #868e96 100%);\n}\n\n.resource-link-danger {\n  --link-color: 220, 53, 69;\n  --icon-gradient: linear-gradient(135deg, #dc3545 0%, #e94560 100%);\n}\n\n/* Apply variant styles */\n.resource-link-primary,\n.resource-link-info,\n.resource-link-android,\n.resource-link-apple,\n.resource-link-desktop,\n.resource-link-github,\n.resource-link-harmony,\n.resource-link-danger {\n  background: linear-gradient(135deg, rgba(var(--link-color), 0.15) 0%, rgba(var(--link-color), 0.08) 100%);\n}\n\n.resource-link-primary .resource-icon,\n.resource-link-info .resource-icon,\n.resource-link-android .resource-icon,\n.resource-link-apple .resource-icon,\n.resource-link-desktop .resource-icon,\n.resource-link-github .resource-icon,\n.resource-link-harmony .resource-icon,\n.resource-link-danger .resource-icon {\n  background: var(--icon-gradient);\n}\n\n.resource-link-primary:hover,\n.resource-link-info:hover,\n.resource-link-android:hover,\n.resource-link-apple:hover,\n.resource-link-desktop:hover,\n.resource-link-github:hover,\n.resource-link-harmony:hover,\n.resource-link-danger:hover {\n  border-color: rgba(var(--link-color), 0.4);\n}\n\n/* GPL Badge */\n.gpl-badge {\n  border-radius: 12px;\n  padding: 1.25rem;\n}\n\n.badge-gpl {\n  display: inline-flex;\n  align-items: center;\n  background: linear-gradient(135deg, #e94560 0%, #ff6b6b 100%);\n  color: white;\n  font-size: 1.1rem;\n  font-weight: 700;\n  padding: 0.75rem 1.5rem;\n  border-radius: 50px;\n  box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4);\n  letter-spacing: 0.5px;\n}\n\n.gpl-badge .text-muted {\n  color: var(--bs-secondary-color, rgba(255, 255, 255, 0.7)) !important;\n}\n\n/* GPL Violation Warning */\n.violation-warning {\n  background: linear-gradient(135deg, rgba(220, 53, 69, 0.1) 0%, rgba(220, 53, 69, 0.05) 100%);\n  border: 1px solid rgba(220, 53, 69, 0.3);\n  border-radius: 12px;\n  padding: 1.25rem;\n}\n\n.violation-header {\n  display: flex;\n  align-items: center;\n  margin-bottom: 0.75rem;\n}\n\n.violation-title {\n  font-size: 1rem;\n  font-weight: 700;\n  color: #dc3545;\n}\n\n.violation-desc {\n  font-size: 0.875rem;\n  color: var(--bs-secondary-color, #6c757d);\n  margin-bottom: 1rem;\n}\n\n.violation-list {\n  display: flex;\n  flex-direction: column;\n  gap: 0.75rem;\n  margin-bottom: 1rem;\n}\n\n.violation-item {\n  display: flex;\n  align-items: flex-start;\n  padding: 0.875rem;\n  background: rgba(220, 53, 69, 0.08);\n  border-radius: 8px;\n  border-left: 3px solid #dc3545;\n}\n\n.violation-icon {\n  width: 32px;\n  height: 32px;\n  border-radius: 50%;\n  background: linear-gradient(135deg, #dc3545 0%, #e94560 100%);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: white;\n  font-size: 0.875rem;\n  flex-shrink: 0;\n  margin-right: 0.875rem;\n}\n\n.violation-content {\n  flex: 1;\n  min-width: 0;\n}\n\n.violation-name {\n  display: block;\n  font-weight: 600;\n  font-size: 0.9rem;\n  color: #dc3545;\n  margin-bottom: 2px;\n}\n\n.violation-reason {\n  display: block;\n  font-size: 0.8rem;\n  color: var(--bs-secondary-color, #6c757d);\n}\n\n.violation-notice {\n  font-size: 0.8rem;\n  color: var(--bs-secondary-color, #6c757d);\n  margin-bottom: 0;\n  padding: 0.75rem;\n  background: rgba(0, 0, 0, 0.03);\n  border-radius: 8px;\n}\n\n/* Dark Mode */\n[data-bs-theme='dark'] .resource-link-primary,\n[data-bs-theme='dark'] .resource-link-info,\n[data-bs-theme='dark'] .resource-link-android,\n[data-bs-theme='dark'] .resource-link-harmony,\n[data-bs-theme='dark'] .resource-link-danger {\n  background: linear-gradient(135deg, rgba(var(--link-color), 0.25) 0%, rgba(var(--link-color), 0.12) 100%);\n}\n\n[data-bs-theme='dark'] .resource-link-apple {\n  --link-color: 170, 170, 170;\n  background: linear-gradient(135deg, rgba(var(--link-color), 0.2) 0%, rgba(var(--link-color), 0.1) 100%);\n}\n\n[data-bs-theme='dark'] .resource-link-apple .resource-icon {\n  background: linear-gradient(135deg, #aaa 0%, #ccc 100%);\n  color: #222;\n}\n\n[data-bs-theme='dark'] .resource-link-desktop,\n[data-bs-theme='dark'] .resource-link-github {\n  --link-color: 150, 160, 170;\n  background: linear-gradient(135deg, rgba(var(--link-color), 0.2) 0%, rgba(var(--link-color), 0.1) 100%);\n}\n\n[data-bs-theme='dark'] .resource-link-desktop .resource-icon {\n  background: linear-gradient(135deg, #8c959d 0%, #6c757d 100%);\n}\n\n[data-bs-theme='dark'] .resource-link-github .resource-icon {\n  background: linear-gradient(135deg, #8c959d 0%, #adb5bd 100%);\n}\n\n[data-bs-theme='dark'] .resource-link:hover {\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n}\n\n[data-bs-theme='dark'] .resource-group {\n  border-bottom-color: rgba(255, 255, 255, 0.1);\n}\n\n/* Harmony Modal */\n.harmony-modal-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 100vw;\n  height: 100vh;\n  margin: 0;\n  background: rgba(0, 0, 0, 0.7);\n  backdrop-filter: blur(8px);\n  z-index: 9999;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 20px;\n  overflow: hidden;\n}\n\n[data-bs-theme='light'] .harmony-modal-overlay {\n  background: rgba(0, 0, 0, 0.5);\n}\n\n.harmony-modal {\n  background: rgba(30, 30, 50, 0.95);\n  border: 1px solid rgba(255, 255, 255, 0.2);\n  border-radius: 12px;\n  width: 100%;\n  max-width: 500px;\n  display: flex;\n  flex-direction: column;\n  backdrop-filter: blur(20px);\n  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);\n  animation: harmonyModalSlideUp 0.3s ease;\n}\n\n[data-bs-theme='light'] .harmony-modal {\n  background: rgba(255, 255, 255, 0.95);\n  border: 1px solid rgba(0, 0, 0, 0.15);\n  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);\n}\n\n@keyframes harmonyModalSlideUp {\n  from { transform: translateY(20px); opacity: 0; }\n  to { transform: translateY(0); opacity: 1; }\n}\n\n.harmony-modal-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 20px 24px;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.harmony-modal-header h5 {\n  margin: 0;\n  color: #fff;\n  font-size: 1.1rem;\n  font-weight: 600;\n}\n\n[data-bs-theme='light'] .harmony-modal-header {\n  border-bottom: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n[data-bs-theme='light'] .harmony-modal-header h5 {\n  color: #000;\n}\n\n.harmony-modal-body {\n  padding: 24px;\n  font-size: 0.95rem;\n  line-height: 1.5;\n  color: #fff;\n}\n\n[data-bs-theme='light'] .harmony-modal-body {\n  color: #000;\n}\n\n.harmony-modal-footer {\n  display: flex;\n  justify-content: flex-end;\n  gap: 10px;\n  padding: 16px 24px;\n  border-top: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n[data-bs-theme='light'] .harmony-modal-footer {\n  border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/common/ThemeToggle.vue",
    "content": "<script setup>\nimport { onMounted } from 'vue'\nimport { loadAutoTheme, setStoredTheme, setTheme, showActiveTheme, getPreferredTheme } from '../../utils/theme.js'\n\n// 处理主题切换\nconst handleThemeChange = (theme) => {\n  setStoredTheme(theme)\n  setTheme(theme)\n  showActiveTheme(theme, true)\n}\n\nonMounted(() => {\n  loadAutoTheme()\n  showActiveTheme(getPreferredTheme(), false)\n})\n</script>\n\n<template>\n  <div class=\"dropdown bd-mode-toggle\">\n    <a\n      class=\"nav-link dropdown-toggle align-items-center\"\n      id=\"bd-theme\"\n      type=\"button\"\n      aria-expanded=\"false\"\n      data-bs-toggle=\"dropdown\"\n      :aria-label=\"`${$t('navbar.toggle_theme')} (${$t('navbar.theme_auto')})`\"\n    >\n      <span class=\"bi my-1 theme-icon-active\">\n        <i class=\"fa-solid fa-circle-half-stroke\"></i>\n      </span>\n      <span id=\"bd-theme-text\">{{ $t('navbar.toggle_theme') }}</span>\n    </a>\n    <ul class=\"dropdown-menu dropdown-menu-end\" aria-labelledby=\"bd-theme-text\">\n      <li>\n        <button\n          type=\"button\"\n          class=\"dropdown-item d-flex align-items-center\"\n          data-bs-theme-value=\"light\"\n          aria-pressed=\"false\"\n          @click=\"handleThemeChange('light')\"\n        >\n          <i class=\"bi me-2 theme-icon fas fa-fw fa-solid fa-sun\"></i>\n          {{ $t('navbar.theme_light') }}\n        </button>\n      </li>\n      <li>\n        <button\n          type=\"button\"\n          class=\"dropdown-item d-flex align-items-center\"\n          data-bs-theme-value=\"dark\"\n          aria-pressed=\"false\"\n          @click=\"handleThemeChange('dark')\"\n        >\n          <i class=\"bi me-2 theme-icon fas fa-fw fa-solid fa-moon\"></i>\n          {{ $t('navbar.theme_dark') }}\n        </button>\n      </li>\n      <li>\n        <button\n          type=\"button\"\n          class=\"dropdown-item d-flex align-items-center active\"\n          data-bs-theme-value=\"auto\"\n          aria-pressed=\"true\"\n          @click=\"handleThemeChange('auto')\"\n        >\n          <i class=\"bi me-2 theme-icon fas fa-fw fa-solid fa-circle-half-stroke\"></i>\n          {{ $t('navbar.theme_auto') }}\n        </button>\n      </li>\n    </ul>\n  </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/common/VersionCard.vue",
    "content": "<template>\n  <div class=\"card shadow-sm mb-4\" v-if=\"version\">\n    <div class=\"card-header bg-info bg-opacity-10 border-bottom-0\">\n      <h5 class=\"card-title mb-0\">\n        <i class=\"fas fa-code-branch text-info me-2\"></i>\n        Version {{ version.version }}\n      </h5>\n    </div>\n    <div class=\"card-body\">\n      <!-- 加载状态 -->\n      <div v-if=\"loading\" class=\"version-loading\">\n        <i class=\"fas fa-spinner fa-spin me-2\"></i>\n        {{ $t('index.loading_latest') }}\n      </div>\n\n      <!-- 开发版本标识 -->\n      <div class=\"version-alert version-alert-success\" v-if=\"buildVersionIsDirty\">\n        <i class=\"fas fa-code me-2\"></i>\n        {{ $t('index.version_dirty') }} 🌇\n      </div>\n\n      <!-- 已安装版本不是稳定版 -->\n      <div class=\"version-alert version-alert-info\" v-if=\"installedVersionNotStable\">\n        <i class=\"fas fa-info-circle me-2\"></i>\n        {{ $t('index.installed_version_not_stable') }}\n      </div>\n\n      <!-- 已是最新版本 -->\n      <div\n        v-else-if=\"(!preReleaseBuildAvailable || !notifyPreReleases) && !stableBuildAvailable && !buildVersionIsDirty\"\n        class=\"version-alert version-alert-success\"\n      >\n        <i class=\"fas fa-check-circle me-2\"></i>\n        {{ $t('index.version_latest') }}\n      </div>\n\n      <!-- 预发布版本可用 -->\n      <div v-if=\"notifyPreReleases && preReleaseBuildAvailable\" class=\"version-update\">\n        <div class=\"version-update-header\">\n          <div class=\"version-update-title\">\n            <i class=\"fas fa-rocket text-warning me-2\"></i>\n            <span>有新的 <b>基地版</b> sunshine可以更新!</span>\n          </div>\n          <button type=\"button\" class=\"btn btn-success btn-download\" @click=\"handleDownloadClick(preReleaseVersion.release.html_url)\">\n            <i class=\"fas fa-download me-2\"></i>\n            {{ $t('index.download') }}\n          </button>\n        </div>\n        <h3 class=\"version-release-name\">{{ preReleaseVersion.release.name }}</h3>\n        <div class=\"markdown-content\" v-html=\"parsedPreReleaseBody\"></div>\n      </div>\n\n      <!-- 稳定版本可用 -->\n      <div v-if=\"stableBuildAvailable\" class=\"version-update\">\n        <div class=\"version-update-header\">\n          <div class=\"version-update-title\">\n            <i class=\"fas fa-star text-warning me-2\"></i>\n            <span>{{ $t('index.new_stable') }}</span>\n          </div>\n          <button type=\"button\" class=\"btn btn-success btn-download\" @click=\"handleDownloadClick(githubVersion.release.html_url)\">\n            <i class=\"fas fa-download me-2\"></i>\n            {{ $t('index.download') }}\n          </button>\n        </div>\n        <h3 class=\"version-release-name\">{{ githubVersion.release.name }}</h3>\n        <div class=\"markdown-content\" v-html=\"parsedStableBody\"></div>\n      </div>\n    </div>\n\n    <!-- 下载确认弹窗（与配置页虚拟麦克风下载相同方式，确认后打开下载页） -->\n    <Transition name=\"fade\">\n      <div v-if=\"showDownloadConfirm\" class=\"download-confirm-overlay\" @click.self=\"cancelDownload\">\n        <div class=\"download-confirm-modal\">\n          <div class=\"download-confirm-header\">\n            <h5>\n              <i class=\"fas fa-external-link-alt me-2\"></i>{{ $t('_common.download') }}\n            </h5>\n            <button class=\"btn-close\" @click=\"cancelDownload\"></button>\n          </div>\n          <div class=\"download-confirm-body\">\n            <p>{{ $t('index.update_download_confirm') }}</p>\n          </div>\n          <div class=\"download-confirm-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" @click=\"cancelDownload\">{{ $t('_common.cancel') }}</button>\n            <button type=\"button\" class=\"btn btn-primary\" @click=\"confirmDownload\">\n              <i class=\"fas fa-download me-1\"></i>{{ $t('_common.download') }}\n            </button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n  </div>\n</template>\n\n<script setup>\nimport { ref } from 'vue'\nimport { openExternalUrl } from '../../utils/helpers.js'\n\ndefineProps({\n  version: Object,\n  githubVersion: Object,\n  preReleaseVersion: Object,\n  notifyPreReleases: Boolean,\n  loading: Boolean,\n  installedVersionNotStable: Boolean,\n  stableBuildAvailable: Boolean,\n  preReleaseBuildAvailable: Boolean,\n  buildVersionIsDirty: Boolean,\n  parsedStableBody: String,\n  parsedPreReleaseBody: String,\n})\n\nconst showDownloadConfirm = ref(false)\nconst pendingDownloadUrl = ref('')\n\nconst handleDownloadClick = (url) => {\n  pendingDownloadUrl.value = url\n  showDownloadConfirm.value = true\n}\n\nconst confirmDownload = async () => {\n  const url = pendingDownloadUrl.value\n  showDownloadConfirm.value = false\n  pendingDownloadUrl.value = ''\n  if (!url) return\n  try {\n    await openExternalUrl(url)\n  } catch (error) {\n    console.error('Failed to open download URL:', error)\n  }\n}\n\nconst cancelDownload = () => {\n  showDownloadConfirm.value = false\n  pendingDownloadUrl.value = ''\n}\n</script>\n\n<style scoped>\n/* Loading State */\n.version-loading {\n  display: flex;\n  align-items: center;\n  padding: 1rem;\n  color: var(--bs-secondary-color, #6c757d);\n  font-size: 0.95rem;\n}\n\n/* Version Alerts */\n.version-alert {\n  border-radius: 8px;\n  font-size: 0.9rem;\n  padding: 0.75rem 1rem;\n  margin-bottom: 1rem;\n  display: flex;\n  align-items: center;\n  border: none;\n}\n\n.version-alert-success {\n  background: rgba(40, 167, 69, 0.1);\n  color: #155724;\n  border-left: 4px solid #28a745;\n}\n\n.version-alert-info {\n  background: rgba(0, 123, 255, 0.1);\n  color: #004085;\n  border-left: 4px solid #007bff;\n}\n\n[data-bs-theme='dark'] .version-alert-success {\n  background: rgba(40, 167, 69, 0.2);\n  color: #6cff8f;\n}\n\n[data-bs-theme='dark'] .version-alert-info {\n  background: rgba(0, 123, 255, 0.2);\n  color: #6cb2ff;\n}\n\n/* Version Update Section */\n.version-update {\n  background: linear-gradient(135deg, rgba(255, 193, 7, 0.1) 0%, rgba(255, 193, 7, 0.05) 100%);\n  border: 1px solid rgba(255, 193, 7, 0.3);\n  border-radius: 10px;\n  padding: 1.25rem;\n  margin-top: 1rem;\n}\n\n.version-update-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  flex-wrap: wrap;\n  gap: 1rem;\n  margin-bottom: 1rem;\n}\n\n.version-update-title {\n  display: flex;\n  align-items: center;\n  font-size: 1rem;\n  font-weight: 600;\n  color: var(--bs-body-color, #2c3e50);\n  flex: 1;\n  min-width: 200px;\n}\n\n.btn-download {\n  border-radius: 8px;\n  padding: 0.5rem 1.25rem;\n  font-weight: 600;\n  transition: transform 0.2s ease, box-shadow 0.2s ease;\n  white-space: nowrap;\n}\n\n.btn-download:hover {\n  transform: translateY(-2px);\n  box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);\n}\n\n.version-release-name {\n  font-size: 1.3rem;\n  font-weight: 600;\n  margin: 1rem 0 0.75rem 0;\n  color: var(--bs-body-color, #2c3e50);\n}\n\n/* Markdown Content */\n.markdown-content {\n  background: rgba(0, 0, 0, 0.03);\n  border-radius: 8px;\n  padding: 1.25rem;\n  margin-top: 1rem;\n  line-height: 1.6;\n  border: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n[data-bs-theme='dark'] .markdown-content {\n  background: rgba(255, 255, 255, 0.05);\n  border-color: rgba(255, 255, 255, 0.1);\n}\n\n.markdown-content h1,\n.markdown-content h2,\n.markdown-content h3,\n.markdown-content h4,\n.markdown-content h5,\n.markdown-content h6 {\n  margin-top: 1.25rem;\n  margin-bottom: 0.75rem;\n  font-weight: 600;\n  line-height: 1.25;\n  color: var(--bs-body-color, #2c3e50);\n}\n\n.markdown-content h1:first-child,\n.markdown-content h2:first-child,\n.markdown-content h3:first-child {\n  margin-top: 0;\n}\n\n.markdown-content h1 {\n  font-size: 1.5em;\n}\n\n.markdown-content h2 {\n  font-size: 1.3em;\n}\n\n.markdown-content h3 {\n  font-size: 1.1em;\n}\n\n.markdown-content p {\n  margin-bottom: 0.75rem;\n  white-space: pre-line;\n  color: var(--bs-body-color, #495057);\n}\n\n.markdown-content ul,\n.markdown-content ol {\n  margin-bottom: 0.75rem;\n  padding-left: 1.5rem;\n}\n\n.markdown-content li {\n  margin-bottom: 0.5rem;\n  color: var(--bs-body-color, #495057);\n}\n\n.markdown-content code {\n  background: rgba(0, 0, 0, 0.08);\n  padding: 0.2em 0.4em;\n  border-radius: 4px;\n  font-family: 'Courier New', 'Consolas', 'Monaco', monospace;\n  font-size: 0.9em;\n  color: #e83e8c;\n}\n\n[data-bs-theme='dark'] .markdown-content code {\n  background: rgba(255, 255, 255, 0.15);\n  color: #ff6b9d;\n}\n\n.markdown-content pre {\n  background: rgba(0, 0, 0, 0.08);\n  padding: 1rem;\n  border-radius: 8px;\n  overflow-x: auto;\n  margin: 1rem 0;\n  border: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n[data-bs-theme='dark'] .markdown-content pre {\n  background: rgba(255, 255, 255, 0.1);\n  border-color: rgba(255, 255, 255, 0.15);\n}\n\n.markdown-content pre code {\n  background: none;\n  padding: 0;\n  color: inherit;\n}\n\n.markdown-content blockquote {\n  border-left: 4px solid #007bff;\n  margin: 1rem 0;\n  padding-left: 1rem;\n  color: var(--bs-secondary-color, #6c757d);\n  font-style: italic;\n}\n\n.markdown-content a {\n  color: #007bff;\n  text-decoration: none;\n  font-weight: 500;\n  transition: color 0.2s ease;\n}\n\n.markdown-content a:hover {\n  color: #0056b3;\n  text-decoration: underline;\n}\n\n.markdown-content table {\n  border-collapse: collapse;\n  width: 100%;\n  margin: 1rem 0;\n  border-radius: 8px;\n  overflow: hidden;\n}\n\n.markdown-content th,\n.markdown-content td {\n  border: 1px solid rgba(0, 0, 0, 0.1);\n  padding: 0.75rem 1rem;\n  text-align: left;\n}\n\n[data-bs-theme='dark'] .markdown-content th,\n[data-bs-theme='dark'] .markdown-content td {\n  border-color: rgba(255, 255, 255, 0.15);\n}\n\n.markdown-content th {\n  background: rgba(0, 0, 0, 0.05);\n  font-weight: 600;\n}\n\n[data-bs-theme='dark'] .markdown-content th {\n  background: rgba(255, 255, 255, 0.1);\n}\n\n/* Dark Mode Adjustments */\n[data-bs-theme='dark'] .version-update {\n  background: linear-gradient(135deg, rgba(255, 193, 7, 0.15) 0%, rgba(255, 193, 7, 0.08) 100%);\n  border-color: rgba(255, 193, 7, 0.3);\n}\n\n[data-bs-theme='dark'] .version-update-title {\n  color: #e0e0e0;\n}\n\n[data-bs-theme='dark'] .version-release-name {\n  color: #e0e0e0;\n}\n\n/* Download Confirm Modal（与 AudioVideo 一致） */\n.download-confirm-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 100vw;\n  height: 100vh;\n  margin: 0;\n  background: var(--overlay-bg, rgba(0, 0, 0, 0.7));\n  backdrop-filter: blur(8px);\n  z-index: 9999;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: var(--spacing-lg, 20px);\n  overflow: hidden;\n}\n\n[data-bs-theme='light'] .download-confirm-overlay {\n  background: rgba(0, 0, 0, 0.5);\n}\n\n.download-confirm-modal {\n  background: var(--modal-bg, rgba(30, 30, 50, 0.95));\n  border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.2));\n  border-radius: var(--border-radius-xl, 12px);\n  width: 100%;\n  max-width: 500px;\n  display: flex;\n  flex-direction: column;\n  backdrop-filter: blur(20px);\n  box-shadow: var(--shadow-xl, 0 25px 50px rgba(0, 0, 0, 0.5));\n  animation: modalSlideUp 0.3s ease;\n}\n\n[data-bs-theme='light'] .download-confirm-modal {\n  background: rgba(255, 255, 255, 0.95);\n  border: 1px solid rgba(0, 0, 0, 0.15);\n  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);\n}\n\n.download-confirm-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 1.25rem 1.5rem;\n  border-bottom: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n}\n\n[data-bs-theme='light'] .download-confirm-header {\n  border-bottom-color: rgba(0, 0, 0, 0.1);\n}\n\n.download-confirm-header h5 {\n  margin: 0;\n  font-size: 1.125rem;\n  font-weight: 600;\n  color: var(--bs-body-color);\n  display: flex;\n  align-items: center;\n}\n\n.download-confirm-header h5 i {\n  color: var(--bs-primary);\n}\n\n.download-confirm-header .btn-close {\n  background: none;\n  border: none;\n  font-size: 1.5rem;\n  color: var(--bs-secondary-color);\n  cursor: pointer;\n  padding: 0;\n  width: 1.5rem;\n  height: 1.5rem;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  opacity: 0.6;\n  transition: opacity 0.2s;\n}\n\n.download-confirm-header .btn-close:hover {\n  opacity: 1;\n}\n\n.download-confirm-header .btn-close::before {\n  content: '×';\n}\n\n.download-confirm-body {\n  padding: 1.5rem;\n  color: var(--bs-body-color);\n}\n\n.download-confirm-body p {\n  margin: 0;\n  line-height: 1.6;\n}\n\n.download-confirm-footer {\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n  gap: 0.75rem;\n  padding: 1.25rem 1.5rem;\n  border-top: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n}\n\n[data-bs-theme='light'] .download-confirm-footer {\n  border-top-color: rgba(0, 0, 0, 0.1);\n}\n\n@keyframes modalSlideUp {\n  from {\n    transform: translateY(20px);\n    opacity: 0;\n  }\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/layout/Navbar.vue",
    "content": "<template>\n  <nav class=\"navbar navbar-light navbar-expand-lg navbar-background header\">\n    <div class=\"container-fluid\">\n      <a class=\"navbar-brand brand-enhanced\" href=\"/\" title=\"Sunshine\">\n        <img src=\"/images/logo-sunshine-256.png\" height=\"50\" alt=\"Sunshine-Foundation\" class=\"brand-logo\" />\n      </a>\n      <button\n        class=\"navbar-toggler\"\n        type=\"button\"\n        data-bs-toggle=\"collapse\"\n        data-bs-target=\"#navbarSupportedContent\"\n        aria-controls=\"navbarSupportedContent\"\n        aria-expanded=\"false\"\n        aria-label=\"Toggle navigation\"\n      >\n        <span class=\"navbar-toggler-icon\"></span>\n      </button>\n      <div class=\"collapse navbar-collapse\" id=\"navbarSupportedContent\">\n        <ul class=\"navbar-nav me-auto mb-2 mb-lg-0\">\n          <li v-for=\"item in navItems\" :key=\"item.path\" class=\"nav-item\">\n            <a class=\"nav-link\" :class=\"{ active: isActive(item.path) }\" :href=\"item.path\">\n              <i :class=\"['fas', 'fa-fw', item.icon]\"></i> {{ $t(item.label) }}\n            </a>\n          </li>\n          <li class=\"nav-item\">\n            <ThemeToggle />\n          </li>\n        </ul>\n      </div>\n    </div>\n  </nav>\n</template>\n\n<script setup>\nimport { onMounted, onUnmounted, ref } from 'vue'\nimport ThemeToggle from '../common/ThemeToggle.vue'\nimport { useBackground } from '../../composables/useBackground.js'\n\n// 导航项配置\nconst navItems = Object.freeze([\n  { path: '/', icon: 'fa-home', label: 'navbar.home' },\n  { path: '/pin', icon: 'fa-unlock', label: 'navbar.pin' },\n  { path: '/apps', icon: 'fa-stream', label: 'navbar.applications' },\n  { path: '/config', icon: 'fa-cog', label: 'navbar.configuration' },\n  { path: '/password', icon: 'fa-user-shield', label: 'navbar.password' },\n  { path: '/troubleshooting', icon: 'fa-info', label: 'navbar.troubleshoot' },\n])\n\n// 使用背景管理 composable\nconst { loadBackground, addDragListeners } = useBackground()\n\n// 当前路径（响应式）\nconst currentPath = ref(window.location.pathname)\n\n// 检查路径是否激活\nconst isActive = (path) => {\n  const current = currentPath.value\n  if (path === '/') {\n    return current === '/' || current === '/index.html'\n  }\n  const normalizedPath = path.replace(/\\.html$/, '')\n  return current === normalizedPath || current.startsWith(normalizedPath)\n}\n\n// 更新当前路径\nconst updateCurrentPath = () => {\n  currentPath.value = window.location.pathname\n}\n\n// 清理函数引用\nlet removeDragListeners = null\n\n// 链接点击处理函数\nconst handleLinkClick = (e) => {\n  if (e.target.closest('a.nav-link')?.href) {\n    setTimeout(updateCurrentPath, 0)\n  }\n}\n\n// 错误处理函数\nconst handleBackgroundError = (error) => {\n  console.error('Background error:', error)\n}\n\nonMounted(async () => {\n  await loadBackground()\n  updateCurrentPath()\n  removeDragListeners = addDragListeners(handleBackgroundError)\n  window.addEventListener('popstate', updateCurrentPath)\n  document.addEventListener('click', handleLinkClick)\n})\n\nonUnmounted(() => {\n  window.removeEventListener('popstate', updateCurrentPath)\n  document.removeEventListener('click', handleLinkClick)\n  removeDragListeners?.()\n})\n</script>\n\n<style scoped>\n.navbar-background {\n  background-color: #f9d86bee;\n  /* box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2), 0 2px 8px rgba(0, 0, 0, 0.15); */\n}\n\n.brand-enhanced {\n  transition: transform 0.3s ease;\n}\n\n.brand-enhanced:hover {\n  transform: scale(1.05) rotate(-5deg);\n}\n</style>\n\n<style>\n.header .nav-link {\n  color: rgba(0, 0, 0, 0.65) !important;\n  transition: color 0.2s ease, font-weight 0.2s ease;\n}\n\n.header .nav-link:hover,\n.header .nav-link.active {\n  color: rgb(0, 0, 0) !important;\n}\n\n.header .navbar-toggler {\n  color: rgba(var(--bs-dark-rgb), 0.65) !important;\n  border: var(--bs-border-width) solid rgba(var(--bs-dark-rgb), 0.15) !important;\n}\n\n.header .navbar-toggler-icon {\n  --bs-navbar-toggler-icon-bg: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\") !important;\n}\n\n.form-control::placeholder {\n  opacity: 0.5;\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/components/layout/PlatformLayout.vue",
    "content": "<script setup>\nconst props = defineProps({\n  platform: {\n    type: String,\n    required: true\n  }\n})\n</script>\n\n<template>\n  <template v-if=\"$slots.windows && platform === 'windows'\">\n    <slot name=\"windows\"></slot>\n  </template>\n\n  <template v-if=\"$slots.linux && platform === 'linux'\">\n    <slot name=\"linux\"></slot>\n  </template>\n\n  <template v-if=\"$slots.macos && platform === 'macos'\">\n    <slot name=\"macos\"></slot>\n  </template>\n</template>\n\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useAiDiagnosis.js",
    "content": "import { ref, reactive } from 'vue'\n\nconst STORAGE_KEY = 'sunshine-ai-diagnosis-config'\n\nconst PROVIDERS = [\n  { label: 'OpenAI', value: 'openai', base: 'https://api.openai.com/v1', models: ['gpt-4o-mini', 'gpt-4o'] },\n  { label: 'DeepSeek', value: 'deepseek', base: 'https://api.deepseek.com/v1', models: ['deepseek-chat'] },\n  { label: '通义千问', value: 'qwen', base: 'https://dashscope.aliyuncs.com/compatible-mode/v1', models: ['qwen-plus', 'qwen-turbo'] },\n  { label: '智谱 (GLM)', value: 'glm', base: 'https://open.bigmodel.cn/api/paas/v4', models: ['glm-4-flash', 'glm-4'] },\n  { label: 'OpenRouter', value: 'openrouter', base: 'https://openrouter.ai/api/v1', models: ['deepseek/deepseek-chat-v3-0324', 'google/gemini-2.0-flash-001'] },\n  { label: 'Ollama (本地)', value: 'ollama', base: 'http://localhost:11434/v1', models: ['llama3', 'qwen2'] },\n  { label: '自定义', value: 'custom', base: '', models: [] },\n]\n\nconst SYSTEM_PROMPT = `你是 Sunshine 串流软件的日志诊断助手。用户会提供 Sunshine 的运行日志，请分析日志内容并给出诊断结果。\n\n请关注以下内容：\n- **Fatal/Error 级别日志**：通常是问题的直接原因\n- **Warning 日志**：可能暗示潜在问题\n- **编码器相关**：NVENC/AMF/软件编码的错误或回退\n- **网络/连接**：Moonlight 客户端连接失败、超时、配对问题\n- **音视频管道**：音频设备问题、视频捕获失败\n- **配置加载**：配置项无效或冲突\n\n诊断格式要求：\n1. **问题摘要**：一句话概括发现的问题\n2. **详细分析**：解释问题原因（引用具体日志行）\n3. **解决建议**：给出具体可操作的建议\n4. 如果日志中没有明显错误，告知用户当前运行正常\n\n请用中文回复，语言简洁清晰。`\n\nfunction loadConfig() {\n  try {\n    const saved = localStorage.getItem(STORAGE_KEY)\n    if (saved) {\n      const parsed = JSON.parse(saved)\n      return { provider: 'openai', apiKey: '', apiBase: 'https://api.openai.com/v1', model: 'gpt-4o-mini', ...parsed }\n    }\n  } catch { /* ignore */ }\n  return { provider: 'openai', apiKey: '', apiBase: 'https://api.openai.com/v1', model: 'gpt-4o-mini' }\n}\n\nexport function useAiDiagnosis() {\n  const config = reactive(loadConfig())\n  const isLoading = ref(false)\n  const result = ref('')\n  const error = ref('')\n\n  function saveConfig() {\n    localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...config }))\n  }\n\n  function onProviderChange(value) {\n    const p = PROVIDERS.find((x) => x.value === value)\n    if (p) {\n      config.apiBase = p.base\n      if (p.models.length > 0) config.model = p.models[0]\n    }\n  }\n\n  function getAvailableModels() {\n    const p = PROVIDERS.find((x) => x.value === config.provider)\n    return p?.models || []\n  }\n\n  async function diagnose(logs) {\n    if (!logs) {\n      error.value = '没有可用的日志内容'\n      return\n    }\n    if (!config.apiKey && config.provider !== 'ollama') {\n      error.value = '请先配置 API Key'\n      return\n    }\n    if (config.provider === 'custom') {\n      try {\n        const url = new URL(config.apiBase)\n        if (!['http:', 'https:'].includes(url.protocol)) {\n          throw new Error('invalid protocol')\n        }\n      } catch {\n        error.value = '自定义提供商需要完整的 API 地址（以 http:// 或 https:// 开头）'\n        return\n      }\n    }\n\n    saveConfig()\n    isLoading.value = true\n    result.value = ''\n    error.value = ''\n\n    // Truncate logs to last 200 lines to fit token limits\n    const lines = logs.split('\\n')\n    const truncated = lines.slice(-200).join('\\n')\n\n    try {\n      const base = config.apiBase.replace(/\\/+$/, '')\n      const headers = { 'Content-Type': 'application/json' }\n      if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`\n\n      const resp = await fetch(`${base}/chat/completions`, {\n        method: 'POST',\n        headers,\n        body: JSON.stringify({\n          model: config.model,\n          messages: [\n            { role: 'system', content: SYSTEM_PROMPT },\n            { role: 'user', content: `请分析以下 Sunshine 日志：\\n\\n\\`\\`\\`\\n${truncated}\\n\\`\\`\\`` },\n          ],\n          temperature: 0.3,\n          max_tokens: 2048,\n        }),\n      })\n\n      if (!resp.ok) {\n        const text = await resp.text()\n        throw new Error(`API 请求失败 (${resp.status}): ${text.substring(0, 200)}`)\n      }\n\n      const data = await resp.json()\n      result.value = data.choices?.[0]?.message?.content || '无法获取分析结果'\n    } catch (e) {\n      error.value = e.message\n    } finally {\n      isLoading.value = false\n    }\n  }\n\n  return {\n    config,\n    providers: PROVIDERS,\n    isLoading,\n    result,\n    error,\n    onProviderChange,\n    getAvailableModels,\n    diagnose,\n    saveConfig,\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useApps.js",
    "content": "import { ref, computed } from 'vue'\nimport { AppService } from '../services/appService.js'\nimport { APP_CONSTANTS, ENV_VARS_CONFIG } from '../utils/constants.js'\nimport { debounce, deepClone } from '../utils/helpers.js'\nimport { trackEvents } from '../config/firebase.js'\nimport { searchCoverImage, batchSearchCoverImages } from '../utils/coverSearch.js'\n\nconst MESSAGE_DURATION = 3000\n\n/**\n * 应用管理组合式函数\n */\nexport function useApps() {\n  // 状态\n  const apps = ref([])\n  const originalApps = ref([])\n  const filteredApps = ref([])\n  const searchQuery = ref('')\n  const editingApp = ref(null)\n  const platform = ref('')\n  const isSaving = ref(false)\n  const isDragging = ref(false)\n  const viewMode = ref('grid')\n  const message = ref('')\n  const messageType = ref('success')\n  const envVars = ref({})\n  const debouncedSearch = ref(null)\n  const isScanning = ref(false)\n  const scannedApps = ref([])\n  const showScanResult = ref(false)\n  const scannedAppsSearchQuery = ref('')\n  const showGamesOnly = ref(false)\n  const selectedAppType = ref('all') // 'all', 'executable', 'shortcut', 'batch', 'command', 'url'\n  const deleteConfirmIndex = ref(null)\n\n  // 计算属性\n  const messageClass = computed(() => ({\n    [`alert-${messageType.value}`]: true,\n  }))\n\n  // 消息图标映射\n  const MESSAGE_ICONS = {\n    success: 'fa-check-circle',\n    error: 'fa-exclamation-circle',\n    warning: 'fa-exclamation-triangle',\n    info: 'fa-info-circle',\n  }\n\n  const showMessage = (msg, type = APP_CONSTANTS.MESSAGE_TYPES.SUCCESS) => {\n    message.value = msg\n    messageType.value = type\n    setTimeout(() => {\n      message.value = ''\n    }, MESSAGE_DURATION)\n  }\n\n  const getMessageIcon = () => MESSAGE_ICONS[messageType.value] || MESSAGE_ICONS.success\n\n  const createDefaultApp = (overrides = {}) => ({\n    ...APP_CONSTANTS.DEFAULT_APP,\n    index: -1,\n    ...overrides,\n  })\n\n  // 初始化\n  const init = (t) => {\n    envVars.value = Object.fromEntries(\n      Object.entries(ENV_VARS_CONFIG).map(([key, translationKey]) => [key, t(translationKey)])\n    )\n    debouncedSearch.value = debounce(performSearch, APP_CONSTANTS.SEARCH_DEBOUNCE_TIME)\n  }\n\n  // 数据加载\n  const loadApps = async () => {\n    try {\n      apps.value = await AppService.getApps()\n      originalApps.value = deepClone(apps.value)\n      filteredApps.value = [...apps.value]\n    } catch (error) {\n      console.error('加载应用失败:', error)\n      showMessage('加载应用失败', APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n    }\n  }\n\n  const loadPlatform = async () => {\n    try {\n      platform.value = await AppService.getPlatform()\n    } catch (error) {\n      console.error('加载平台信息失败:', error)\n      platform.value = APP_CONSTANTS.PLATFORMS.WINDOWS\n    }\n  }\n\n  // 搜索\n  const performSearch = () => {\n    filteredApps.value = AppService.searchApps(apps.value, searchQuery.value)\n  }\n\n  const clearSearch = () => {\n    searchQuery.value = ''\n    performSearch()\n  }\n\n  // 应用操作\n  const getOriginalIndex = (app) => apps.value.indexOf(app)\n\n  const newApp = () => {\n    trackEvents.userAction('new_app_clicked')\n    editingApp.value = createDefaultApp()\n  }\n\n  const editApp = (index) => {\n    editingApp.value = { ...deepClone(apps.value[index]), index }\n  }\n\n  const closeAppEditor = () => {\n    editingApp.value = null\n  }\n\n  const handleSaveApp = async (appData) => {\n    try {\n      isSaving.value = true\n      await AppService.saveApps(apps.value, appData)\n      await loadApps()\n      editingApp.value = null\n      showMessage('应用保存成功', APP_CONSTANTS.MESSAGE_TYPES.SUCCESS)\n    } catch (error) {\n      console.error('保存应用失败:', error)\n      showMessage('保存应用失败', APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n    } finally {\n      isSaving.value = false\n    }\n  }\n\n  const showDeleteForm = (index) => {\n    deleteConfirmIndex.value = index\n  }\n\n  const cancelDeleteApp = () => {\n    deleteConfirmIndex.value = null\n  }\n\n  const confirmDeleteApp = async () => {\n    const index = deleteConfirmIndex.value\n    if (index === null) return\n    deleteConfirmIndex.value = null\n    await deleteApp(index)\n  }\n\n  const deleteApp = async (index) => {\n    const appName = apps.value[index]?.name || 'unknown'\n    try {\n      apps.value.splice(index, 1)\n      await AppService.saveApps(apps.value, null)\n      await loadApps()\n      showMessage('应用删除成功', APP_CONSTANTS.MESSAGE_TYPES.SUCCESS)\n      trackEvents.appDeleted(appName)\n    } catch (error) {\n      console.error('删除应用失败:', error)\n      showMessage('删除应用失败', APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n    }\n  }\n\n  // 检测是否有未保存的更改\n  const hasUnsavedChanges = () => {\n    if (apps.value.length !== originalApps.value.length) {\n      return true\n    }\n  \n    // 深度比较应用列表\n    const appsStr = JSON.stringify(apps.value.map(app => ({ ...app, index: undefined })))\n    const originalStr = JSON.stringify(originalApps.value.map(app => ({ ...app, index: undefined })))\n    \n    return appsStr !== originalStr\n  }\n\n  const save = async () => {\n    // 如果没有更改，直接返回\n    if (!hasUnsavedChanges()) {\n      showMessage('没有需要保存的更改', APP_CONSTANTS.MESSAGE_TYPES.INFO)\n      return\n    }\n\n    try {\n      isSaving.value = true\n      await AppService.saveApps(apps.value, null)\n      // 保存成功后更新原始列表\n      originalApps.value = deepClone(apps.value)\n      showMessage('应用列表保存成功', APP_CONSTANTS.MESSAGE_TYPES.SUCCESS)\n      trackEvents.userAction('apps_saved', { count: apps.value.length })\n    } catch (error) {\n      console.error('保存应用列表失败:', error)\n      showMessage('保存应用列表失败', APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n    } finally {\n      isSaving.value = false\n    }\n  }\n\n  // 拖拽排序\n  const onDragStart = () => {\n    isDragging.value = true\n  }\n\n  const onDragEnd = async () => {\n    isDragging.value = false\n    await save()\n  }\n\n  // 封面搜索相关（使用共享的 coverSearch 模块）\n\n  // Tauri 环境检测\n  const isTauriEnv = () => !!window.__TAURI__?.core?.invoke\n\n  // 扫描目录功能\n  const scanDirectory = async (extractIcons = true) => {\n    const tauri = window.__TAURI__\n    if (!tauri?.core?.invoke) {\n      showMessage('扫描功能仅在 Tauri 环境下可用', APP_CONSTANTS.MESSAGE_TYPES.WARNING)\n      return\n    }\n\n    if (!tauri?.dialog?.open) {\n      showMessage('无法打开文件对话框', APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n      return\n    }\n\n    try {\n      const selectedDir = await tauri.dialog.open({\n        directory: true,\n        multiple: false,\n        title: '选择要扫描的目录',\n      })\n\n      if (!selectedDir) return\n\n      isScanning.value = true\n      showMessage('正在扫描目录...', APP_CONSTANTS.MESSAGE_TYPES.INFO)\n\n      const foundApps = await tauri.core.invoke('scan_directory_for_apps', {\n        directory: selectedDir,\n        extractIcons,\n      })\n\n      if (foundApps.length === 0) {\n        scannedApps.value = []\n        showScanResult.value = true\n        showMessage('未找到可添加的应用程序', APP_CONSTANTS.MESSAGE_TYPES.INFO)\n      } else {\n        // 先显示扫描结果（无封面）\n        scannedApps.value = foundApps\n        showScanResult.value = true\n        showMessage(`找到 ${foundApps.length} 个应用程序，正在搜索封面...`, APP_CONSTANTS.MESSAGE_TYPES.INFO)\n\n        // 异步更新封面图片\n        asyncUpdateCovers(foundApps)\n      }\n\n      trackEvents.userAction('directory_scanned', { count: foundApps.length, extractIcons })\n    } catch (error) {\n      console.error('扫描目录失败:', error)\n      showMessage(`扫描失败: ${error}`, APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n    } finally {\n      isScanning.value = false\n    }\n  }\n\n  // 扫描游戏平台库（Steam/Epic/GOG）\n  const scanGameLibraries = async () => {\n    const tauri = window.__TAURI__\n    if (!tauri?.core?.invoke) {\n      showMessage('扫描功能仅在 Tauri 环境下可用', APP_CONSTANTS.MESSAGE_TYPES.WARNING)\n      return\n    }\n\n    try {\n      isScanning.value = true\n      showMessage('正在扫描游戏平台库...', APP_CONSTANTS.MESSAGE_TYPES.INFO)\n\n      const result = await tauri.core.invoke('scan_game_libraries')\n\n      // 将 PlatformGame 转换为 scannedApps 格式\n      const steamGames = result.steam || []\n      const epicGames = result.epic || []\n      const gogGames = result.gog || []\n      const allGames = [...steamGames, ...epicGames, ...gogGames]\n\n      if (allGames.length === 0) {\n        scannedApps.value = []\n        showScanResult.value = true\n        showMessage('未检测到已安装的游戏', APP_CONSTANTS.MESSAGE_TYPES.INFO)\n      } else {\n        const mapped = allGames.map((game) => ({\n          name: game.name,\n          cmd: game.cmd,\n          'working-dir': game['working-dir'] || game.working_dir || '',\n          'image-path': game['cover-url'] || game.cover_url || '',\n          source_path: game.install_dir,\n          'app-type': game.platform,\n          'is-game': true,\n        }))\n\n        scannedApps.value = mapped\n        showScanResult.value = true\n\n        const parts = []\n        if (steamGames.length) parts.push(`Steam ${steamGames.length}`)\n        if (epicGames.length) parts.push(`Epic ${epicGames.length}`)\n        if (gogGames.length) parts.push(`GOG ${gogGames.length}`)\n        showMessage(\n          `找到 ${result.total ?? allGames.length} 个游戏 (${parts.join(', ')})，耗时 ${result.scan_time_ms ?? 0}ms`,\n          APP_CONSTANTS.MESSAGE_TYPES.SUCCESS\n        )\n      }\n\n      trackEvents.userAction('game_libraries_scanned', {\n        steam: steamGames.length,\n        epic: epicGames.length,\n        gog: gogGames.length,\n        total: result.total ?? allGames.length,\n      })\n    } catch (error) {\n      console.error('扫描游戏库失败:', error)\n      showMessage(`扫描游戏库失败: ${error}`, APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n    } finally {\n      isScanning.value = false\n    }\n  }\n\n  // 异步更新封面图片\n  const asyncUpdateCovers = async (appList) => {\n    let coversFound = 0\n    const total = appList.length\n\n    // 并行搜索所有封面，但逐个更新UI\n    const promises = appList.map(async (app, index) => {\n      try {\n        const imagePath = await searchCoverImage(encodeURIComponent(app.name))\n        if (imagePath && scannedApps.value[index]) {\n          // 更新对应位置的应用封面\n          scannedApps.value[index] = { ...scannedApps.value[index], 'image-path': imagePath }\n          coversFound++\n        }\n      } catch (error) {\n        console.warn(`搜索封面失败: ${app.name}`, error)\n      }\n    })\n\n    await Promise.allSettled(promises)\n\n    // 搜索完成后显示结果\n    showMessage(\n      `已匹配 ${coversFound}/${total} 个封面`,\n      coversFound > 0 ? APP_CONSTANTS.MESSAGE_TYPES.SUCCESS : APP_CONSTANTS.MESSAGE_TYPES.INFO\n    )\n  }\n\n  // 扫描应用字段处理\n  const getScannedAppField = (app, field) => app[field] || app[field.replace(/-/g, '_')] || ''\n\n  const getScannedAppImage = (app) => getScannedAppField(app, 'image-path')\n\n  const createAppFromScanned = (scannedApp) => ({\n    ...APP_CONSTANTS.DEFAULT_APP,\n    name: scannedApp.name,\n    cmd: scannedApp.cmd,\n    'working-dir': getScannedAppField(scannedApp, 'working-dir'),\n    'image-path': getScannedAppField(scannedApp, 'image-path'),\n  })\n\n  const removeFromScannedList = (sourcePath) => {\n    const index = scannedApps.value.findIndex((a) => a.source_path === sourcePath)\n    if (index !== -1) {\n      scannedApps.value.splice(index, 1)\n      if (scannedApps.value.length === 0) {\n        showScanResult.value = false\n      }\n    }\n  }\n\n  const addScannedApp = (scannedApp) => {\n    editingApp.value = createDefaultApp({\n      name: scannedApp.name,\n      cmd: scannedApp.cmd,\n      'working-dir': getScannedAppField(scannedApp, 'working-dir'),\n      'image-path': getScannedAppField(scannedApp, 'image-path'),\n    })\n\n    removeFromScannedList(scannedApp.source_path)\n    showMessage(`正在编辑应用: ${scannedApp.name}`, APP_CONSTANTS.MESSAGE_TYPES.INFO)\n    trackEvents.userAction('scanned_app_edit', { name: scannedApp.name })\n  }\n\n  const quickAddScannedApp = async (scannedApp, index) => {\n    try {\n      apps.value.push(createAppFromScanned(scannedApp))\n      await AppService.saveApps(apps.value, null)\n      await loadApps()\n\n      scannedApps.value.splice(index, 1)\n      if (scannedApps.value.length === 0) {\n        showScanResult.value = false\n      }\n\n      showMessage(`已添加应用: ${scannedApp.name}`, APP_CONSTANTS.MESSAGE_TYPES.SUCCESS)\n      trackEvents.userAction('scanned_app_quick_added', { name: scannedApp.name })\n    } catch (error) {\n      console.error('快速添加应用失败:', error)\n      showMessage('添加失败', APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n    }\n  }\n\n  const addAllScannedApps = async () => {\n    if (scannedApps.value.length === 0) return\n\n    try {\n      isSaving.value = true\n      const appsToAdd = scannedApps.value.map(createAppFromScanned)\n\n      apps.value.push(...appsToAdd)\n      await AppService.saveApps(apps.value, null)\n      await loadApps()\n\n      showMessage(`已添加 ${appsToAdd.length} 个应用`, APP_CONSTANTS.MESSAGE_TYPES.SUCCESS)\n      trackEvents.userAction('scanned_apps_batch_added', { count: appsToAdd.length })\n\n      scannedApps.value = []\n      showScanResult.value = false\n    } catch (error) {\n      console.error('批量添加应用失败:', error)\n      showMessage('批量添加失败', APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n    } finally {\n      isSaving.value = false\n    }\n  }\n\n  const closeScanResult = () => {\n    showScanResult.value = false\n    scannedApps.value = []\n    scannedAppsSearchQuery.value = ''\n    showGamesOnly.value = false\n    selectedAppType.value = 'all'\n  }\n\n  // 获取各分类的统计信息\n  const scanResultStats = computed(() => ({\n    all: scannedApps.value.length,\n    games: scannedApps.value.filter((app) => app['is-game'] === true).length,\n    executable: scannedApps.value.filter((app) => app['app-type'] === 'executable').length,\n    shortcut: scannedApps.value.filter((app) => app['app-type'] === 'shortcut').length,\n    batch: scannedApps.value.filter((app) => app['app-type'] === 'batch').length,\n    command: scannedApps.value.filter((app) => app['app-type'] === 'command').length,\n    url: scannedApps.value.filter((app) => app['app-type'] === 'url').length,\n    steam: scannedApps.value.filter((app) => app['app-type'] === 'steam').length,\n    epic: scannedApps.value.filter((app) => app['app-type'] === 'epic').length,\n    gog: scannedApps.value.filter((app) => app['app-type'] === 'gog').length,\n  }))\n\n  // 过滤扫描结果\n  const filteredScannedApps = computed(() => {\n    let filtered = scannedApps.value\n    \n    // 先按应用类型过滤\n    if (selectedAppType.value !== 'all') {\n      filtered = filtered.filter((app) => app['app-type'] === selectedAppType.value)\n    }\n    \n    // 再按游戏过滤\n    if (showGamesOnly.value) {\n      filtered = filtered.filter((app) => app['is-game'] === true)\n    }\n    \n    // 最后按搜索关键词过滤\n    if (scannedAppsSearchQuery.value) {\n      const query = scannedAppsSearchQuery.value.toLowerCase()\n      filtered = filtered.filter((app) => {\n        const name = (app.name || '').toLowerCase()\n        const cmd = (app.cmd || '').toLowerCase()\n        const sourcePath = (app.source_path || '').toLowerCase()\n        return name.includes(query) || cmd.includes(query) || sourcePath.includes(query)\n      })\n    }\n    \n    return filtered\n  })\n\n  const removeScannedApp = (index) => {\n    scannedApps.value.splice(index, 1)\n    if (scannedApps.value.length === 0) {\n      showScanResult.value = false\n    }\n  }\n\n  const searchCoverForScannedApp = async (index) => {\n    const app = scannedApps.value[index]\n    if (!app) return\n\n    try {\n      showMessage(`正在搜索封面: ${app.name}`, APP_CONSTANTS.MESSAGE_TYPES.INFO)\n      const imagePath = await searchCoverImage(app.name)\n\n      if (imagePath) {\n        scannedApps.value[index] = { ...app, 'image-path': imagePath }\n        showMessage(`已找到封面: ${app.name}`, APP_CONSTANTS.MESSAGE_TYPES.SUCCESS)\n      } else {\n        showMessage(`未找到封面: ${app.name}`, APP_CONSTANTS.MESSAGE_TYPES.WARNING)\n      }\n    } catch (error) {\n      console.error('搜索封面失败:', error)\n      showMessage('搜索封面失败', APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n    }\n  }\n\n  const handleCopySuccess = () => showMessage('复制成功', APP_CONSTANTS.MESSAGE_TYPES.SUCCESS)\n  const handleCopyError = () => showMessage('复制失败', APP_CONSTANTS.MESSAGE_TYPES.ERROR)\n\n  return {\n    // 状态\n    apps,\n    filteredApps,\n    searchQuery,\n    editingApp,\n    platform,\n    isSaving,\n    isDragging,\n    viewMode,\n    message,\n    messageType,\n    envVars,\n    debouncedSearch,\n    isScanning,\n    scannedApps,\n    showScanResult,\n    scannedAppsSearchQuery,\n    showGamesOnly,\n    selectedAppType,\n    // 计算属性\n    messageClass,\n    filteredScannedApps,\n    scanResultStats,\n    // 方法\n    init,\n    loadApps,\n    loadPlatform,\n    performSearch,\n    clearSearch,\n    getOriginalIndex,\n    newApp,\n    editApp,\n    closeAppEditor,\n    handleSaveApp,\n    showDeleteForm,\n    deleteApp,\n    cancelDeleteApp,\n    confirmDeleteApp,\n    deleteConfirmIndex,\n    save,\n    hasUnsavedChanges,\n    onDragStart,\n    onDragEnd,\n    scanDirectory,\n    scanGameLibraries,\n    addScannedApp,\n    quickAddScannedApp,\n    addAllScannedApps,\n    closeScanResult,\n    removeScannedApp,\n    getScannedAppImage,\n    searchCoverForScannedApp,\n    isTauriEnv,\n    showMessage,\n    getMessageIcon,\n    handleCopySuccess,\n    handleCopyError,\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useBackground.js",
    "content": "import ColorThief from 'colorthief'\n\nconst DEFAULT_BACKGROUND = 'https://assets.alkaidlab.com/sunshine-bg0.webp'\nconst STORAGE_KEY = 'customBackground'\n\nconst COLOR_CONFIG = {\n  textLightnessRange: { min: 15, max: 95 },\n  brightnessThreshold: 50,\n  paletteSize: 5,\n}\n\nconst DEFAULT_COLOR_INFO = {\n  dominantColor: { r: 128, g: 128, b: 128 },\n  hsl: { h: 0, s: 0, l: 50 },\n}\n\nconst clamp = (value, min, max) => Math.max(min, Math.min(max, value))\n\nconst isLocalImage = (imageUrl) => imageUrl.startsWith('data:') || imageUrl.startsWith('blob:')\n\nconst loadImage = (imageUrl) =>\n  new Promise((resolve, reject) => {\n    const img = new Image()\n    img.onload = () => resolve(img)\n    img.onerror = () => reject(new Error('图片加载失败'))\n    img.src = imageUrl\n  })\n\nconst rgbToHsl = (r, g, b) => {\n  r /= 255\n  g /= 255\n  b /= 255\n\n  const max = Math.max(r, g, b)\n  const min = Math.min(r, g, b)\n  const l = (max + min) / 2\n  const d = max - min\n\n  if (d === 0) return { h: 0, s: 0, l: Math.round(l * 100) }\n\n  const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)\n  let h = 0\n\n  if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6\n  else if (max === g) h = ((b - r) / d + 2) / 6\n  else h = ((r - g) / d + 4) / 6\n\n  return {\n    h: Math.round(h * 360),\n    s: Math.round(s * 100),\n    l: Math.round(l * 100),\n  }\n}\n\nconst hslToRgb = (h, s, l) => {\n  h /= 360\n  s /= 100\n  l /= 100\n\n  if (s === 0) {\n    const val = Math.round(l * 255)\n    return { r: val, g: val, b: val }\n  }\n\n  const hue2rgb = (p, q, t) => {\n    if (t < 0) t += 1\n    if (t > 1) t -= 1\n    if (t < 1 / 6) return p + (q - p) * 6 * t\n    if (t < 1 / 2) return q\n    if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6\n    return p\n  }\n\n  const q = l < 0.5 ? l * (1 + s) : l + s - l * s\n  const p = 2 * l - q\n\n  return {\n    r: Math.round(hue2rgb(p, q, h + 1 / 3) * 255),\n    g: Math.round(hue2rgb(p, q, h) * 255),\n    b: Math.round(hue2rgb(p, q, h - 1 / 3) * 255),\n  }\n}\n\nconst rgbToHex = (r, g, b) => {\n  const toHex = (x) => Math.round(x).toString(16).padStart(2, '0')\n  return `#${toHex(r)}${toHex(g)}${toHex(b)}`\n}\n\nconst analyzeImageColors = (img) => {\n  if (!img.complete) return { ...DEFAULT_COLOR_INFO }\n\n  try {\n    const colorThief = new ColorThief()\n    const dominantColorArray = colorThief.getColor(img)\n\n    if (dominantColorArray?.length !== 3) return { ...DEFAULT_COLOR_INFO }\n\n    let selectedColor = dominantColorArray\n\n    try {\n      const palette = colorThief.getPalette(img, COLOR_CONFIG.paletteSize)\n      if (palette?.length > 0) {\n        let maxSaturation = 0\n        for (const color of palette) {\n          if (color?.length === 3) {\n            const hsl = rgbToHsl(color[0], color[1], color[2])\n            if (hsl.s > maxSaturation) {\n              maxSaturation = hsl.s\n              selectedColor = color\n            }\n          }\n        }\n      }\n    } catch {\n      // 使用主要颜色\n    }\n\n    const [r, g, b] = selectedColor\n    return {\n      dominantColor: { r, g, b },\n      hsl: rgbToHsl(r, g, b),\n    }\n  } catch {\n    return { ...DEFAULT_COLOR_INFO }\n  }\n}\n\nconst detectImageColorInfo = async (imageUrl) => {\n  try {\n    const img = await loadImage(imageUrl)\n    return analyzeImageColors(img)\n  } catch {\n    return { ...DEFAULT_COLOR_INFO }\n  }\n}\n\nconst calculateTextColors = (colorInfo) => {\n  const { hsl } = colorInfo\n  const { textLightnessRange, brightnessThreshold } = COLOR_CONFIG\n  const isLightBg = hsl.l > brightnessThreshold\n\n  const textH = hsl.h\n  let textS = hsl.s\n  let textL\n\n  if (isLightBg) {\n    textL = Math.max(textLightnessRange.min, hsl.l - 60)\n    if (hsl.s < 30) textS = Math.min(20, hsl.s)\n  } else {\n    textL = Math.min(textLightnessRange.max, hsl.l + 70)\n    textS = Math.min(40, hsl.s * 0.6)\n  }\n\n  const createColor = (s, l) => {\n    const rgb = hslToRgb(textH, s, l)\n    return rgbToHex(rgb.r, rgb.g, rgb.b)\n  }\n\n  return {\n    primary: createColor(textS, textL),\n    secondary: createColor(textS * 0.7, clamp(isLightBg ? textL - 15 : textL + 10, 10, 90)),\n    muted: createColor(textS * 0.4, clamp(isLightBg ? textL - 25 : textL + 20, 5, 85)),\n    title: createColor(textS * 0.3, isLightBg ? textLightnessRange.min : textLightnessRange.max),\n    bgClass: isLightBg ? 'bg-light' : 'bg-dark',\n  }\n}\n\nconst setTextColorTheme = (colorInfo) => {\n  const root = document.documentElement\n  const textColors = calculateTextColors(colorInfo)\n\n  root.style.setProperty('--text-primary-color', textColors.primary)\n  root.style.setProperty('--text-secondary-color', textColors.secondary)\n  root.style.setProperty('--text-muted-color', textColors.muted)\n  root.style.setProperty('--text-title-color', textColors.title)\n\n  document.body.classList.remove('bg-light', 'bg-dark')\n  document.body.classList.add(textColors.bgClass)\n\n  root.classList.add('text-color-transitioning')\n  setTimeout(() => root.classList.remove('text-color-transitioning'), 500)\n}\n\n/**\n * 背景图片管理组合式函数\n */\nexport function useBackground(options = {}) {\n  const {\n    defaultBackground = DEFAULT_BACKGROUND,\n    storageKey = STORAGE_KEY,\n    maxWidth = 1920,\n    maxHeight = 1080,\n    maxSizeMB = 2,\n  } = options\n\n  const getCurrentBackground = () => localStorage.getItem(storageKey) ?? defaultBackground\n\n  const setBackground = async (imageUrl) => {\n    document.body.style.background = `url(${imageUrl}) center/cover fixed no-repeat`\n    if (isLocalImage(imageUrl)) {\n      try {\n        const colorInfo = await detectImageColorInfo(imageUrl)\n        setTextColorTheme(colorInfo)\n      } catch {\n        // 静默失败\n      }\n    }\n  }\n\n  const recheckBackgroundBrightness = async () => {\n    const currentBg = getCurrentBackground()\n    if (!isLocalImage(currentBg)) return\n    try {\n      const colorInfo = await detectImageColorInfo(currentBg)\n      setTextColorTheme(colorInfo)\n    } catch {\n      // 静默失败\n    }\n  }\n\n  const loadBackground = () => setBackground(getCurrentBackground())\n\n  const saveBackground = async (imageData) => {\n    try {\n      localStorage.setItem(storageKey, imageData)\n    } catch (error) {\n      if (error.name === 'QuotaExceededError') {\n        localStorage.removeItem(storageKey)\n        try {\n          localStorage.setItem(storageKey, imageData)\n        } catch {\n          throw new Error('图片太大，无法存储。请选择更小的图片或降低图片质量。')\n        }\n      } else {\n        throw error\n      }\n    }\n    await setBackground(imageData)\n  }\n\n  const calculateResizedDimensions = (width, height) => {\n    if (width <= maxWidth && height <= maxHeight) return { width, height }\n    const ratio = Math.min(maxWidth / width, maxHeight / height)\n    return { width: width * ratio, height: height * ratio }\n  }\n\n  const compressWithQuality = (img, width, height, quality) => {\n    const canvas = document.createElement('canvas')\n    canvas.width = width\n    canvas.height = height\n    canvas.getContext('2d').drawImage(img, 0, 0, width, height)\n\n    const dataUrl = canvas.toDataURL('image/jpeg', quality)\n    const sizeInMB = (dataUrl.length * 3) / 4 / 1024 / 1024\n\n    if (sizeInMB <= maxSizeMB) return dataUrl\n    if (quality > 0.3) return compressWithQuality(img, width, height, quality - 0.1)\n    return null\n  }\n\n  const compressImage = (file, initialQuality = 0.8) =>\n    new Promise((resolve, reject) => {\n      const reader = new FileReader()\n      reader.onload = (event) => {\n        const img = new Image()\n        img.onload = () => {\n          const { width, height } = calculateResizedDimensions(img.width, img.height)\n          const result = compressWithQuality(img, width, height, initialQuality)\n          result ? resolve(result) : reject(new Error('图片太大，无法存储。请选择更小的图片。'))\n        }\n        img.onerror = () => reject(new Error('图片加载失败'))\n        img.src = event.target.result\n      }\n      reader.onerror = () => reject(new Error('文件读取失败'))\n      reader.readAsDataURL(file)\n    })\n\n  const handleDragOver = (e) => {\n    e.preventDefault()\n    if (e.dataTransfer?.types?.includes('Files')) {\n      document.body.classList.add('dragover')\n    }\n  }\n\n  const handleDragLeave = () => document.body.classList.remove('dragover')\n\n  const handleDrop = async (e, onError) => {\n    e.preventDefault()\n    document.body.classList.remove('dragover')\n\n    const file = e.dataTransfer?.files?.[0]\n    if (!file?.type.startsWith('image/')) return\n\n    try {\n      await saveBackground(await compressImage(file))\n    } catch (error) {\n      onError?.(error) ?? alert(error.message || '处理图片时发生错误')\n    }\n  }\n\n  const addDragListeners = (onError) => {\n    const handlers = {\n      dragover: handleDragOver,\n      dragleave: handleDragLeave,\n      drop: (e) => handleDrop(e, onError),\n    }\n\n    Object.entries(handlers).forEach(([event, handler]) => document.addEventListener(event, handler))\n    return () => Object.entries(handlers).forEach(([event, handler]) => document.removeEventListener(event, handler))\n  }\n\n  const clearBackground = () => {\n    localStorage.removeItem(storageKey)\n    return setBackground(defaultBackground)\n  }\n\n  // 监听主题切换\n  if (typeof document !== 'undefined') {\n    const handleThemeChange = () => setTimeout(recheckBackgroundBrightness, 100)\n    const observerConfig = { attributes: true, attributeFilter: ['data-bs-theme'] }\n    const observer = new MutationObserver(handleThemeChange)\n    observer.observe(document.documentElement, observerConfig)\n    observer.observe(document.body, observerConfig)\n  }\n\n  return {\n    setBackground,\n    loadBackground,\n    saveBackground,\n    compressImage,\n    handleDragOver,\n    handleDragLeave,\n    handleDrop,\n    addDragListeners,\n    clearBackground,\n    getCurrentBackground,\n    recheckBackgroundBrightness,\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useConfig.js",
    "content": "import { ref } from 'vue'\nimport { trackEvents } from '../config/firebase.js'\n\n// 平台相关的标签页排除规则\nconst PLATFORM_EXCLUSIONS = {\n  windows: ['vt', 'vaapi'],\n  linux: ['amd', 'qsv', 'vt'],\n  macos: ['amd', 'nv', 'qsv', 'vaapi'],\n}\n\n// 不参与默认值比较的键\nconst EXCLUDED_DEFAULT_KEYS = new Set(['resolutions', 'fps', 'adapter_name'])\n\n// 默认标签页配置\nconst DEFAULT_TABS = [\n  {\n    id: 'general',\n    name: 'General',\n    options: {\n      locale: 'en',\n      sunshine_name: '',\n      min_log_level: 2,\n      global_prep_cmd: '[]',\n      notify_pre_releases: 'disabled',\n    },\n  },\n  {\n    id: 'input',\n    name: 'Input',\n    options: {\n      controller: 'enabled',\n      gamepad: 'auto',\n      ds4_back_as_touchpad_click: 'enabled',\n      motion_as_ds4: 'enabled',\n      touchpad_as_ds4: 'enabled',\n      back_button_timeout: -1,\n      keyboard: 'enabled',\n      key_repeat_delay: 500,\n      key_repeat_frequency: 24.9,\n      always_send_scancodes: 'enabled',\n      key_rightalt_to_key_win: 'disabled',\n      mouse: 'enabled',\n      high_resolution_scrolling: 'enabled',\n      native_pen_touch: 'enabled',\n      keybindings: '[0x10,0xA0,0x11,0xA2,0x12,0xA4]',\n    },\n  },\n  {\n    id: 'av',\n    name: 'Audio/Video',\n    options: {\n      audio_sink: '',\n      virtual_sink: '',\n      install_steam_audio_drivers: 'enabled',\n      adapter_name: '',\n      output_name: '',\n      capture_target: 'display',\n      window_title: '',\n      display_device_prep: 'no_operation',\n      vdd_reuse: 'disabled',\n      resolution_change: 'automatic',\n      manual_resolution: '',\n      refresh_rate_change: 'automatic',\n      manual_refresh_rate: '',\n      hdr_prep: 'automatic',\n      display_mode_remapping: '[]',\n      resolutions: '[1280x720,1920x1080,2560x1080,2560x1440,2560x1600,3440x1440,3840x2160]',\n      fps: '[60,90,120,144]',\n      max_bitrate: 0,\n      variable_refresh_rate: 'disabled',\n      minimum_fps_target: 0,\n    },\n  },\n  {\n    id: 'network',\n    name: 'Network',\n    options: {\n      upnp: 'disabled',\n      address_family: 'ipv4',\n      port: 47989,\n      origin_web_ui_allowed: 'lan',\n      external_ip: '',\n      lan_encryption_mode: 0,\n      wan_encryption_mode: 1,\n      close_verify_safe: 'disabled',\n      mdns_broadcast: 'enabled',\n      ping_timeout: 10000,\n      webhook_url: '',\n      webhook_enabled: 'disabled',\n      webhook_skip_ssl_verify: 'disabled',\n      webhook_timeout: 1000,\n    },\n  },\n  {\n    id: 'files',\n    name: 'Config Files',\n    options: {\n      file_apps: '',\n      credentials_file: '',\n      log_path: '',\n      pkey: '',\n      cert: '',\n      file_state: '',\n    },\n  },\n  {\n    id: 'advanced',\n    name: 'Advanced',\n    options: {\n      fec_percentage: 20,\n      qp: 28,\n      min_threads: 2,\n      hevc_mode: 0,\n      av1_mode: 0,\n      capture: '',\n      encoder: '',\n    },\n  },\n  {\n    id: 'encoders',\n    name: 'Encoders',\n    type: 'group',\n    children: [\n      {\n        id: 'nv',\n        name: 'NVIDIA NVENC Encoder',\n        options: {\n          nvenc_preset: 1,\n          nvenc_twopass: 'quarter_res',\n          nvenc_spatial_aq: 'disabled',\n          nvenc_temporal_aq: 'disabled',\n          nvenc_vbv_increase: 0,\n          nvenc_lookahead_depth: 0,\n          nvenc_lookahead_level: 'disabled',\n          nvenc_temporal_filter: 'disabled',\n          nvenc_rate_control: 'cbr',\n          nvenc_target_quality: 0,\n          nvenc_realtime_hags: 'enabled',\n          nvenc_split_encode: 'driver_decides',\n          nvenc_latency_over_power: 'enabled',\n          nvenc_opengl_vulkan_on_dxgi: 'enabled',\n          nvenc_h264_cavlc: 'disabled',\n        },\n      },\n      {\n        id: 'qsv',\n        name: 'Intel QuickSync Encoder',\n        options: {\n          qsv_preset: 'medium',\n          qsv_coder: 'auto',\n          qsv_slow_hevc: 'disabled',\n        },\n      },\n      {\n        id: 'amd',\n        name: 'AMD AMF Encoder',\n        options: {\n          amd_usage: 'ultralowlatency',\n          amd_rc: 'vbr_latency',\n          amd_enforce_hrd: 'disabled',\n          amd_quality: 'balanced',\n          amd_preanalysis: 'disabled',\n          amd_vbaq: 'enabled',\n          amd_coder: 'auto',\n        },\n      },\n      {\n        id: 'vt',\n        name: 'VideoToolbox Encoder',\n        options: {\n          vt_coder: 'auto',\n          vt_software: 'auto',\n          vt_realtime: 'enabled',\n        },\n      },\n      {\n        id: 'sw',\n        name: 'Software Encoder',\n        options: {\n          sw_preset: 'superfast',\n          sw_tune: 'zerolatency',\n        },\n      },\n    ],\n  },\n]\n\n/**\n * 深拷贝对象\n */\nconst deepClone = (obj) => JSON.parse(JSON.stringify(obj))\n\n/**\n * 安全解析 JSON\n */\nconst safeParseJSON = (str, fallback = []) => {\n  try {\n    return JSON.parse(str || JSON.stringify(fallback))\n  } catch {\n    return fallback\n  }\n}\n\n/**\n * 判断是否应该删除默认值\n */\nconst shouldDeleteDefault = (configData, tab, optionKey) => {\n  if (EXCLUDED_DEFAULT_KEYS.has(optionKey)) return false\n\n  const currentValue = configData[optionKey]\n  const defaultValue = tab.options[optionKey]\n\n  try {\n    return JSON.stringify(JSON.parse(currentValue)) === JSON.stringify(JSON.parse(defaultValue))\n  } catch {\n    return String(currentValue) === String(defaultValue)\n  }\n}\n\n/**\n * 遍历所有标签页选项\n */\nconst forEachTabOption = (tabs, callback) => {\n  for (const tab of tabs) {\n    if (tab.type === 'group' && tab.children) {\n      for (const childTab of tab.children) {\n        callback(childTab)\n      }\n    } else if (tab.options) {\n      callback(tab)\n    }\n  }\n}\n\n/**\n * 序列化分辨率数组\n */\nconst serializeResolutions = (resolutions) =>\n  JSON.stringify(resolutions).replace(/\",\"/g, ',').replace(/^\\[\"/, '[').replace(/\"\\]$/, ']')\n\n/**\n * 序列化 FPS 数组\n */\nconst serializeFps = (fps) => JSON.stringify(fps).replace(/\"/g, '')\n\n/**\n * 解析分辨率字符串\n */\nconst parseResolutions = (resStr) => {\n  try {\n    return JSON.parse((resStr || '').replace(/(\\d+)x(\\d+)/g, '\"$1x$2\"'))\n  } catch {\n    return []\n  }\n}\n\n/**\n * 过滤有效的 FPS 值\n */\nconst filterValidFps = (fps) => fps.filter((item) => +item >= 30 && +item <= 500)\n\n/**\n * 配置管理组合式函数\n */\nexport function useConfig() {\n  const platform = ref('')\n  const saved = ref(false)\n  const restarted = ref(false)\n  const config = ref(null)\n  const fps = ref([])\n  const resolutions = ref([])\n  const currentTab = ref('general')\n  const global_prep_cmd = ref([])\n  const display_mode_remapping = ref([])\n  const tabs = ref([])\n\n  // 原始配置快照\n  const snapshots = ref({\n    config: null,\n    fps: null,\n    resolutions: null,\n    global_prep_cmd: null,\n    display_mode_remapping: null,\n  })\n\n  /**\n   * 保存当前状态快照\n   */\n  const saveSnapshots = () => {\n    snapshots.value = {\n      config: deepClone(config.value),\n      fps: deepClone(fps.value),\n      resolutions: deepClone(resolutions.value),\n      global_prep_cmd: deepClone(global_prep_cmd.value),\n      display_mode_remapping: deepClone(display_mode_remapping.value),\n    }\n  }\n\n  /**\n   * 初始化标签页配置\n   */\n  const initTabs = () => {\n    tabs.value = deepClone(DEFAULT_TABS)\n  }\n\n  /**\n   * 根据平台过滤标签页\n   */\n  const filterTabsByPlatform = (platformName) => {\n    const exclusions = PLATFORM_EXCLUSIONS[platformName] || []\n\n    tabs.value = tabs.value\n      .map((tab) => {\n        if (tab.type === 'group' && tab.children) {\n          const filteredChildren = tab.children.filter((child) => !exclusions.includes(child.id))\n          return filteredChildren.length > 0 ? { ...tab, children: filteredChildren } : null\n        }\n        return exclusions.includes(tab.id) ? null : tab\n      })\n      .filter(Boolean)\n  }\n\n  /**\n   * 填充配置默认值\n   */\n  const fillDefaultValues = () => {\n    forEachTabOption(tabs.value, (tab) => {\n      for (const [key, defaultVal] of Object.entries(tab.options)) {\n        if (config.value[key] === undefined) {\n          config.value[key] = defaultVal\n        }\n      }\n    })\n  }\n\n  /**\n   * 解析特殊字段\n   */\n  const parseSpecialFields = () => {\n    fps.value = safeParseJSON(config.value.fps)\n    resolutions.value = parseResolutions(config.value.resolutions)\n    global_prep_cmd.value = safeParseJSON(config.value.global_prep_cmd)\n    display_mode_remapping.value = safeParseJSON(config.value.display_mode_remapping)\n\n    config.value.global_prep_cmd = config.value.global_prep_cmd || []\n    config.value.display_mode_remapping = config.value.display_mode_remapping || []\n  }\n\n  /**\n   * 加载配置\n   */\n  const loadConfig = async () => {\n    try {\n      const response = await fetch('/api/config')\n      const data = await response.json()\n\n      platform.value = data.platform || ''\n      filterTabsByPlatform(platform.value)\n\n      const { platform: _, status, version, ...configData } = data\n      config.value = configData\n\n      fillDefaultValues()\n      parseSpecialFields()\n      saveSnapshots()\n    } catch (error) {\n      console.error('Failed to load config:', error)\n    }\n  }\n\n  /**\n   * 序列化配置\n   */\n  const serialize = () => {\n    config.value.resolutions = serializeResolutions(resolutions.value)\n    fps.value = filterValidFps(fps.value)\n    config.value.fps = serializeFps(fps.value)\n    config.value.global_prep_cmd = JSON.stringify(global_prep_cmd.value)\n    config.value.display_mode_remapping = JSON.stringify(display_mode_remapping.value)\n  }\n\n  /**\n   * 移除默认值\n   */\n  const removeDefaultValues = (configData) => {\n    forEachTabOption(tabs.value, (tab) => {\n      for (const optionKey of Object.keys(tab.options)) {\n        if (shouldDeleteDefault(configData, tab, optionKey)) {\n          delete configData[optionKey]\n        }\n      }\n    })\n  }\n\n  /**\n   * 保存配置\n   */\n  const save = async () => {\n    saved.value = false\n    restarted.value = false\n    serialize()\n\n    const configData = deepClone(config.value)\n    removeDefaultValues(configData)\n\n    try {\n      const response = await fetch('/api/config', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(configData),\n      })\n\n      saved.value = response.ok\n\n      if (saved.value) {\n        trackEvents.configChanged(currentTab.value, 'save')\n        saveSnapshots()\n      }\n\n      return saved.value\n    } catch (error) {\n      console.error('Save failed:', error)\n      trackEvents.errorOccurred('config_save', error.message)\n      return false\n    }\n  }\n\n  /**\n   * 应用配置（保存并重启）\n   */\n  const apply = async () => {\n    saved.value = false\n    restarted.value = false\n\n    const result = await save()\n    if (!result) return\n\n    restarted.value = true\n    setTimeout(() => {\n      saved.value = false\n      restarted.value = false\n    }, 5000)\n\n    try {\n      await fetch('/api/restart', { method: 'POST' })\n      trackEvents.userAction('config_applied')\n    } catch (error) {\n      console.error('Failed to restart:', error)\n    }\n  }\n\n  /**\n   * 在标签页中查找目标\n   */\n  const findTabByHash = (hash) => {\n    for (const tab of tabs.value) {\n      if (tab.id === hash || (tab.options && Object.keys(tab.options).includes(hash))) {\n        return tab\n      }\n\n      if (tab.type === 'group' && tab.children) {\n        const childTab = tab.children.find(\n          (child) => child.id === hash || Object.keys(child.options).includes(hash)\n        )\n        if (childTab) return childTab\n      }\n    }\n    return null\n  }\n\n  /**\n   * 处理哈希导航\n   */\n  const handleHash = () => {\n    const hash = window.location.hash.slice(1)\n    if (!hash) return\n\n    const targetTab = findTabByHash(hash)\n    if (targetTab) {\n      currentTab.value = targetTab.id\n      setTimeout(() => {\n        document.getElementById(hash)?.scrollIntoView({ behavior: 'smooth' })\n      }, 100)\n    }\n  }\n\n  /**\n   * 比较两个值是否相等\n   */\n  const isEqual = (a, b) => {\n    if (a === b) return true\n    if (a === undefined || b === undefined) return false\n\n    try {\n      return JSON.stringify(JSON.parse(a)) === JSON.stringify(JSON.parse(b))\n    } catch {\n      return String(a) === String(b)\n    }\n  }\n\n  /**\n   * 比较两个配置对象\n   */\n  const configsAreEqual = (current, original) => {\n    const allKeys = new Set([...Object.keys(current), ...Object.keys(original)])\n\n    for (const key of allKeys) {\n      if (!isEqual(current[key], original[key])) {\n        return false\n      }\n    }\n    return true\n  }\n\n  /**\n   * 检测是否有未保存的更改\n   */\n  const hasUnsavedChanges = () => {\n    if (!config.value || !snapshots.value.config) {\n      return false\n    }\n\n    // 序列化当前配置用于比较\n    const tempConfig = deepClone(config.value)\n    tempConfig.resolutions = serializeResolutions(resolutions.value)\n    tempConfig.fps = serializeFps(filterValidFps(fps.value))\n    tempConfig.global_prep_cmd = JSON.stringify(global_prep_cmd.value)\n    tempConfig.display_mode_remapping = JSON.stringify(display_mode_remapping.value)\n\n    return !configsAreEqual(tempConfig, snapshots.value.config)\n  }\n\n  return {\n    platform,\n    saved,\n    restarted,\n    config,\n    fps,\n    resolutions,\n    currentTab,\n    global_prep_cmd,\n    display_mode_remapping,\n    tabs,\n    initTabs,\n    loadConfig,\n    save,\n    apply,\n    handleHash,\n    hasUnsavedChanges,\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useLogout.js",
    "content": "/**\n * Logout composable\n */\nexport function useLogout() {\n  /**\n   * @param { { onLocalhost?: () => void } } [opts]\n   */\n  const logout = (opts = {}) => {\n    const xhr = new XMLHttpRequest()\n    xhr.open('GET', '/api/logout?t=' + Date.now(), true)\n    xhr.setRequestHeader('Authorization', 'Basic ' + btoa('logout:logout'))\n\n    // 既然浏览器可能会卡在 401 重试里不回调 JS，\n    // 那我们就不等了。设定 200ms 后，无论如何必须跳回首页。\n    // 这能强制打断浏览器的 XHR 死循环。\n    const watchdog = setTimeout(() => {\n      // 如果到了这里，说明 200 没触发，或者浏览器卡在 401 了\n      // 强行中止请求，跳转首页\n      try { xhr.abort() } catch(e) {}\n      window.location.href = '/'\n    }, 200)\n\n    xhr.onreadystatechange = () => {\n      // 只有一种情况我们取消跳转：后端明确返回了 200 (Localhost)\n      if (xhr.readyState === 4 && xhr.status === 200) {\n        clearTimeout(watchdog)\n        opts.onLocalhost?.()   // 执行 Localhost 逻辑\n      }\n    }\n    xhr.send()\n  }\n  return { logout }\n}"
  },
  {
    "path": "src_assets/common/assets/web/composables/useLogs.js",
    "content": "import { ref, computed } from 'vue'\n\nconst LOG_REGEX = /(\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}]):\\s/g\n\n/**\n * 日志管理组合式函数\n */\nexport function useLogs() {\n  const logs = ref(null)\n\n  const fancyLogs = computed(() => {\n    if (!logs.value) return []\n    const parts = logs.value.split(LOG_REGEX).slice(1)\n    const result = []\n    for (let i = 0; i < parts.length; i += 2) {\n      const content = parts[i + 1] || ''\n      result.push({\n        timestamp: parts[i],\n        level: content.split(':')[0] || 'Unknown',\n        value: content,\n      })\n    }\n    return result\n  })\n\n  const fatalLogs = computed(() => fancyLogs.value.filter((log) => log.level === 'Fatal'))\n\n  const fetchLogs = async () => {\n    try {\n      // Use X-Log-Offset: 0 to get cached tail (not full file download)\n      const response = await fetch('/api/logs', { headers: { 'X-Log-Offset': '0' } })\n      if (response.ok) {\n        logs.value = await response.text()\n        return true\n      }\n      console.error('Failed to fetch logs: HTTP', response.status)\n      return false\n    } catch (e) {\n      console.error('Failed to fetch logs:', e)\n      return false\n    }\n  }\n\n  return {\n    logs,\n    fancyLogs,\n    fatalLogs,\n    fetchLogs,\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/usePin.js",
    "content": "import { ref, reactive } from 'vue'\n\nconst STATUS_RESET_DELAY = 5000\n\n/**\n * PIN 配对组合式函数\n */\nexport function usePin() {\n  const pairingDeviceName = ref('')\n  const unpairAllPressed = ref(false)\n  const unpairAllStatus = ref(null)\n  const showApplyMessage = ref(false)\n  const config = ref(null)\n  const clients = ref([])\n  const hdrProfileList = ref([])\n  const hasIccFileList = ref(false)\n  const loading = ref(false)\n  const saving = ref(false)\n  const deleting = ref(new Set())\n  const editingStates = reactive({})\n  const originalValues = reactive({})\n\n  const initClientEditingState = (client) => {\n    if (!editingStates[client.uuid]) {\n      editingStates[client.uuid] = false\n      originalValues[client.uuid] = { ...client }\n    }\n  }\n\n  const clearEditingState = (uuid) => {\n    delete editingStates[uuid]\n    delete originalValues[uuid]\n  }\n\n  const clearAllEditingStates = () => {\n    Object.keys(editingStates).forEach(clearEditingState)\n  }\n\n  const parseClients = () => {\n    try {\n      return JSON.parse(config.value?.clients || '[]')\n    } catch {\n      return []\n    }\n  }\n\n  const serialize = (listArray = []) => {\n    const nl = '\\n'\n    return '[' + nl + '    ' + listArray.map((item) => JSON.stringify(item)).join(',' + nl + '    ') + nl + ']'\n  }\n\n  const refreshClients = async () => {\n    loading.value = true\n    try {\n      const response = await fetch('/api/clients/list')\n      const data = await response.json()\n\n      if (data.status === 'true' && data.named_certs?.length) {\n        clients.value = data.named_certs\n      }\n\n      const tmpClients = parseClients()\n      clients.value = clients.value.map((client) => {\n        const merged = { ...client, ...tmpClients.find(({ uuid }) => uuid === client.uuid) }\n        // 如果客户端没有deviceSize，设置默认值为medium\n        if (!merged.deviceSize) {\n          merged.deviceSize = 'medium'\n        }\n        initClientEditingState(merged)\n        return merged\n      })\n    } catch (error) {\n      console.error('Failed to refresh clients:', error)\n    } finally {\n      loading.value = false\n    }\n  }\n\n  const unpairAll = async () => {\n    unpairAllPressed.value = true\n    try {\n      const response = await fetch('/api/clients/unpair-all', { method: 'POST' })\n      const data = await response.json()\n      showApplyMessage.value = true\n      unpairAllStatus.value = data.status.toString() === 'true'\n\n      if (unpairAllStatus.value) {\n        clearAllEditingStates()\n        await refreshClients()\n      }\n\n      setTimeout(() => {\n        unpairAllStatus.value = null\n      }, STATUS_RESET_DELAY)\n    } catch (error) {\n      console.error('Failed to unpair all:', error)\n      unpairAllStatus.value = false\n    } finally {\n      unpairAllPressed.value = false\n    }\n  }\n\n  const unpairSingle = async (uuid) => {\n    deleting.value.add(uuid)\n    try {\n      const response = await fetch('/api/clients/unpair', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ uuid }),\n      })\n      const data = await response.json()\n      const status = data.status?.toString().toLowerCase()\n      if (status === '1' || status === 'true') {\n        showApplyMessage.value = true\n        clearEditingState(uuid)\n        await refreshClients()\n        return true\n      }\n      return false\n    } catch (error) {\n      console.error('Failed to unpair client:', error)\n      return false\n    } finally {\n      deleting.value.delete(uuid)\n    }\n  }\n\n  const startEdit = (uuid) => {\n    const client = clients.value.find((c) => c.uuid === uuid)\n    if (client && !originalValues[uuid]) {\n      originalValues[uuid] = { ...client }\n    }\n    editingStates[uuid] = true\n  }\n\n  const cancelEdit = (uuid) => {\n    const client = clients.value.find((c) => c.uuid === uuid)\n    if (client && originalValues[uuid]) {\n      Object.assign(client, originalValues[uuid])\n    }\n    editingStates[uuid] = false\n  }\n\n  const saveClient = async (uuid) => {\n    if (!config.value) return false\n\n    saving.value = true\n    try {\n      const tmpClients = parseClients()\n      const client = clients.value.find((c) => c.uuid === uuid)\n      if (!client) return false\n\n      const index = tmpClients.findIndex((c) => c.uuid === uuid)\n      if (index >= 0) {\n        tmpClients[index] = { ...client }\n      } else {\n        tmpClients.push({ ...client })\n      }\n\n      config.value.clients = serialize(tmpClients)\n      const response = await fetch('/api/clients/list', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(config.value),\n      })\n\n      if (response.status === 200) {\n        editingStates[uuid] = false\n        originalValues[uuid] = { ...client }\n        return true\n      }\n      return false\n    } catch (error) {\n      console.error('Failed to save client:', error)\n      return false\n    } finally {\n      saving.value = false\n    }\n  }\n\n  const save = async () => {\n    if (!config.value) return false\n\n    saving.value = true\n    try {\n      config.value.clients = serialize(clients.value)\n      const response = await fetch('/api/clients/list', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(config.value),\n      })\n\n      if (response.status === 200) {\n        clients.value.forEach((client) => {\n          editingStates[client.uuid] = false\n          originalValues[client.uuid] = { ...client }\n        })\n        return true\n      }\n      return false\n    } catch (error) {\n      console.error('Failed to save clients:', error)\n      return false\n    } finally {\n      saving.value = false\n    }\n  }\n\n  const hasUnsavedChanges = (uuid) => {\n    const client = clients.value.find((c) => c.uuid === uuid)\n    const original = originalValues[uuid]\n    return (\n      client && original && (client.hdrProfile !== original.hdrProfile || client.deviceSize !== original.deviceSize)\n    )\n  }\n\n  const initPinForm = (onSuccess) => {\n    const form = document.querySelector('#form')\n    if (!form) return\n\n    form.addEventListener('submit', async (e) => {\n      e.preventDefault()\n      const pinInput = document.querySelector('#pin-input')\n      const nameInput = document.querySelector('#name-input')\n      const statusDiv = document.querySelector('#status')\n\n      if (!pinInput || !nameInput || !statusDiv) return\n\n      statusDiv.innerHTML = ''\n\n      try {\n        const response = await fetch('/api/pin', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ pin: pinInput.value, name: nameInput.value }),\n        })\n        const data = await response.json()\n\n        if (data.status.toString().toLowerCase() === 'true') {\n          statusDiv.innerHTML =\n            '<div class=\"alert alert-success\" role=\"alert\">Success! Please check Moonlight to continue</div>'\n          pinInput.value = ''\n          nameInput.value = ''\n          onSuccess?.()\n        } else {\n          statusDiv.innerHTML =\n            '<div class=\"alert alert-danger\" role=\"alert\">Pairing Failed: Check if the PIN is typed correctly</div>'\n        }\n      } catch (error) {\n        console.error('Pairing failed:', error)\n        statusDiv.innerHTML = '<div class=\"alert alert-danger\" role=\"alert\">Pairing Failed: Network error</div>'\n      }\n    })\n  }\n\n  const clickedApplyBanner = async () => {\n    showApplyMessage.value = false\n    try {\n      await fetch('/api/restart', { method: 'POST' })\n    } catch (error) {\n      console.error('Failed to restart:', error)\n    }\n  }\n\n  const loadConfig = async () => {\n    try {\n      const response = await fetch('/api/config')\n      const data = await response.json()\n      config.value = data\n      pairingDeviceName.value = data.pair_name ?? ''\n    } catch (error) {\n      console.error('Failed to load config:', error)\n    }\n  }\n\n  return {\n    pairingDeviceName,\n    unpairAllPressed,\n    unpairAllStatus,\n    showApplyMessage,\n    config,\n    clients,\n    hdrProfileList,\n    hasIccFileList,\n    loading,\n    saving,\n    deleting,\n    editingStates,\n    originalValues,\n    refreshClients,\n    unpairAll,\n    unpairSingle,\n    save,\n    saveClient,\n    startEdit,\n    cancelEdit,\n    hasUnsavedChanges,\n    serialize,\n    initPinForm,\n    clickedApplyBanner,\n    loadConfig,\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useQrPair.js",
    "content": "import { ref, computed, onUnmounted } from 'vue'\nimport QRCode from 'qrcode'\n\nexport function useQrPair() {\n  const qrDataUrl = ref('')\n  const qrPin = ref('')\n  const qrUrl = ref('')\n  const qrExpiresAt = ref(0)\n  const qrRemaining = ref(0)\n  const qrLoading = ref(false)\n  const qrError = ref('')\n  const qrPaired = ref(false)\n\n  let countdownTimer = null\n  let pollTick = 0\n\n  const qrActive = computed(() => qrRemaining.value > 0 && qrDataUrl.value !== '')\n\n  const resetQrDisplay = () => {\n    qrDataUrl.value = ''\n    qrPin.value = ''\n    qrUrl.value = ''\n  }\n\n  const stopCountdown = () => {\n    if (countdownTimer) {\n      clearInterval(countdownTimer)\n      countdownTimer = null\n    }\n  }\n\n  const startCountdown = () => {\n    stopCountdown()\n    pollTick = 0\n    countdownTimer = setInterval(async () => {\n      const now = Date.now()\n      const remaining = Math.max(0, Math.floor((qrExpiresAt.value - now) / 1000))\n      qrRemaining.value = remaining\n      if (remaining <= 0) {\n        stopCountdown()\n        resetQrDisplay()\n        return\n      }\n\n      // Poll status every 2 ticks\n      if (++pollTick % 2 === 0) {\n        try {\n          const res = await fetch('/api/qr-pair')\n          const data = await res.json()\n          if (data.status === 'paired') {\n            stopCountdown()\n            resetQrDisplay()\n            qrPaired.value = true\n          }\n        } catch (e) { /* ignore */ }\n      }\n    }, 1000)\n  }\n\n  const generateQrCode = async () => {\n    qrLoading.value = true\n    qrError.value = ''\n    qrPaired.value = false\n    try {\n      const response = await fetch('/api/qr-pair', { method: 'POST' })\n      const data = await response.json()\n\n      if (data.status?.toString() !== 'true') {\n        qrError.value = data.error || 'Failed to generate QR code'\n        return\n      }\n\n      qrPin.value = data.pin\n      qrUrl.value = data.url\n      qrExpiresAt.value = Date.now() + data.expires_in * 1000\n      qrRemaining.value = data.expires_in\n\n      qrDataUrl.value = await QRCode.toDataURL(data.url, {\n        width: 280,\n        margin: 2,\n        color: { dark: '#000000', light: '#ffffff' },\n      })\n\n      startCountdown()\n    } catch (error) {\n      console.error('Failed to generate QR pair info:', error)\n      qrError.value = 'Network error'\n    } finally {\n      qrLoading.value = false\n    }\n  }\n\n  const cancelQrCode = async () => {\n    stopCountdown()\n    resetQrDisplay()\n    qrRemaining.value = 0\n    try {\n      await fetch('/api/qr-pair/cancel', { method: 'POST' })\n    } catch (e) {\n      // Best-effort cancel, ignore network errors\n    }\n  }\n\n  onUnmounted(() => {\n    stopCountdown()\n  })\n\n  return {\n    qrDataUrl,\n    qrPin,\n    qrUrl,\n    qrRemaining,\n    qrLoading,\n    qrError,\n    qrPaired,\n    qrActive,\n    generateQrCode,\n    cancelQrCode,\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useSetupWizard.js",
    "content": "import { ref } from 'vue'\n\n/**\n * 设置向导组合式函数\n */\nexport function useSetupWizard() {\n  const showSetupWizard = ref(false)\n  const adapters = ref([])\n  const displayDevices = ref([])\n  const hasLocale = ref(false)\n\n  // 检查是否需要显示设置向导\n  const checkSetupWizard = (config) => {\n    const isFirstTime = config.setup_wizard_completed == true || \n                       config.setup_wizard_completed == 'true'\n    \n    if (!isFirstTime) {\n      showSetupWizard.value = true\n      adapters.value = config.adapters || []\n      displayDevices.value = config.display_devices || []\n      hasLocale.value = !!(config.locale && config.locale !== '')\n      return true\n    }\n    return false\n  }\n\n  // 设置完成回调\n  const onSetupComplete = (config) => {\n    console.log('设置完成:', config)\n    // 用户点击\"配置应用程序\"按钮后会自动跳转到 /apps\n  }\n\n  return {\n    showSetupWizard,\n    adapters,\n    displayDevices,\n    hasLocale,\n    checkSetupWizard,\n    onSetupComplete,\n  }\n}\n\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useTheme.js",
    "content": "import { onMounted } from 'vue'\nimport { loadAutoTheme, showActiveTheme, getPreferredTheme } from '../utils/theme.js'\n\nexport function useTheme() {\n  onMounted(() => {\n    loadAutoTheme()\n    showActiveTheme(getPreferredTheme(), false)\n  })\n\n  return {\n    // 可以在这里暴露更多主题相关的功能\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useTroubleshooting.js",
    "content": "import { ref, computed, onUnmounted } from 'vue'\n\nconst LOG_REFRESH_INTERVAL = 5000\nconst STATUS_RESET_DELAY = 5000\nconst MAX_LOG_DISPLAY_SIZE = 4 * 1024 * 1024 // 4 MB cap for in-memory log string\n\n/**\n * Creates a delayed status reset helper\n */\nconst createStatusResetter = (statusRef) => {\n  return setTimeout(() => {\n    statusRef.value = null\n  }, STATUS_RESET_DELAY)\n}\n\n/**\n * Wraps an async action with pressed state management\n */\nconst withPressedState = async (pressedRef, action, autoReset = false) => {\n  pressedRef.value = true\n  try {\n    return await action()\n  } finally {\n    if (autoReset) {\n      setTimeout(() => {\n        pressedRef.value = false\n      }, STATUS_RESET_DELAY)\n    } else {\n      pressedRef.value = false\n    }\n  }\n}\n\n/**\n * Troubleshooting composable\n */\nexport function useTroubleshooting() {\n  const platform = ref('')\n  const closeAppPressed = ref(false)\n  const closeAppStatus = ref(null)\n  const restartPressed = ref(false)\n  const boomPressed = ref(false)\n  const resetDisplayDevicePressed = ref(false)\n  const resetDisplayDeviceStatus = ref(null)\n  const logs = ref('Loading...')\n  const logOffset = ref(0)\n  const logFilter = ref(null)\n  const matchMode = ref('contains')\n  const ignoreCase = ref(true)\n  const logInterval = ref(null)\n\n  const actualLogs = computed(() => {\n    if (!logFilter.value) return logs.value\n\n    const filter = ignoreCase.value ? logFilter.value.toLowerCase() : logFilter.value\n    const lines = logs.value.split('\\n')\n\n    const filterFn = (() => {\n      switch (matchMode.value) {\n        case 'exact':\n          return (line) => {\n            const searchLine = ignoreCase.value ? line.toLowerCase() : line\n            return searchLine === filter\n          }\n        case 'regex':\n          try {\n            const regex = new RegExp(logFilter.value, ignoreCase.value ? 'i' : '')\n            return (line) => regex.test(line)\n          } catch {\n            return () => false\n          }\n        default:\n          return (line) => {\n            const searchLine = ignoreCase.value ? line.toLowerCase() : line\n            return searchLine.includes(filter)\n          }\n      }\n    })()\n\n    return lines.filter(filterFn).join('\\n')\n  })\n\n  const fetchJson = async (url, options = {}) => {\n    const response = await fetch(url, options)\n    if (!response.ok) {\n      throw new Error(`HTTP ${response.status}`)\n    }\n    return response.json()\n  }\n\n  const refreshLogs = async () => {\n    try {\n      const offset = Number(logOffset.value)\n      // Always send X-Log-Offset to use cached tail mode (without it, server returns full file for download)\n      const headers = { 'X-Log-Offset': String(Number.isNaN(offset) ? 0 : offset) }\n      const response = await fetch('/api/logs', { headers })\n\n      if (response.status === 304) {\n        const sizeHeader = response.headers.get('X-Log-Size')\n        const size = Number.parseInt(sizeHeader || '0', 10)\n        logOffset.value = Number.isNaN(size) || size < 0 ? 0 : size\n        return\n      }\n\n      if (!response.ok) {\n        console.error('Failed to refresh logs: HTTP', response.status)\n        return\n      }\n\n      const rawSize = Number.parseInt(response.headers.get('X-Log-Size') || '0', 10)\n      const newSize = Number.isNaN(rawSize) || rawSize < 0 ? 0 : rawSize\n      const logRange = (response.headers.get('X-Log-Range') || '').trim().toLowerCase()\n      const text = await response.text()\n\n      if (logRange === 'incremental' && text.length > 0) {\n        logs.value += text\n      } else {\n        logs.value = text\n      }\n      // Cap in-memory log string to prevent unbounded frontend memory growth\n      if (logs.value.length > MAX_LOG_DISPLAY_SIZE) {\n        logs.value = logs.value.slice(-MAX_LOG_DISPLAY_SIZE)\n      }\n\n      logOffset.value = newSize\n    } catch (e) {\n      console.error('Failed to refresh logs:', e)\n    }\n  }\n\n  const closeApp = () =>\n    withPressedState(closeAppPressed, async () => {\n      try {\n        const data = await fetchJson('/api/apps/close', { method: 'POST' })\n        closeAppStatus.value = data.status.toString() === 'true'\n        createStatusResetter(closeAppStatus)\n      } catch {\n        closeAppStatus.value = false\n      }\n    })\n\n  const restart = () =>\n    withPressedState(\n      restartPressed,\n      async () => {\n        try {\n          await fetch('/api/restart', { method: 'POST' })\n        } catch {}\n      },\n      true\n    )\n\n  const boom = async () => {\n    boomPressed.value = true\n    try {\n      await fetch('/api/boom')\n    } catch {}\n  }\n\n  const resetDisplayDevicePersistence = () =>\n    withPressedState(resetDisplayDevicePressed, async () => {\n      try {\n        const data = await fetchJson('/api/reset-display-device-persistence', { method: 'POST' })\n        resetDisplayDeviceStatus.value = data.status.toString() === 'true'\n        createStatusResetter(resetDisplayDeviceStatus)\n      } catch {\n        resetDisplayDeviceStatus.value = false\n      }\n    })\n\n  const copyLogs = async () => {\n    try {\n      await navigator.clipboard.writeText(actualLogs.value)\n    } catch {}\n  }\n\n  const copyConfig = async (t) => {\n    try {\n      const data = await fetchJson('/api/config')\n      await navigator.clipboard.writeText(JSON.stringify(data, null, 2))\n      alert(t('troubleshooting.copy_config_success'))\n    } catch {\n      alert(t('troubleshooting.copy_config_error'))\n    }\n  }\n\n  const reopenSetupWizard = async (t) => {\n    try {\n      const config = await fetchJson('/api/config')\n      config.setup_wizard_completed = false\n\n      const saveResponse = await fetch('/api/config', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(config),\n      })\n\n      if (saveResponse.ok) {\n        window.location.href = '/'\n      } else {\n        alert(t('troubleshooting.reopen_setup_wizard_error'))\n      }\n    } catch {\n      alert(t('troubleshooting.reopen_setup_wizard_error'))\n    }\n  }\n\n  const loadPlatform = async () => {\n    try {\n      const data = await fetchJson('/api/config')\n      platform.value = data.platform || ''\n    } catch {}\n  }\n\n  const startLogRefresh = () => {\n    logInterval.value = setInterval(refreshLogs, LOG_REFRESH_INTERVAL)\n  }\n\n  const stopLogRefresh = () => {\n    if (logInterval.value) {\n      clearInterval(logInterval.value)\n      logInterval.value = null\n    }\n  }\n\n  // Pause polling when page is invisible, resume when visible\n  const handleVisibilityChange = () => {\n    if (document.hidden) {\n      stopLogRefresh()\n    } else {\n      refreshLogs()\n      startLogRefresh()\n    }\n  }\n\n  document.addEventListener('visibilitychange', handleVisibilityChange)\n\n  onUnmounted(() => {\n    stopLogRefresh()\n    document.removeEventListener('visibilitychange', handleVisibilityChange)\n  })\n\n  return {\n    platform,\n    closeAppPressed,\n    closeAppStatus,\n    restartPressed,\n    boomPressed,\n    resetDisplayDevicePressed,\n    resetDisplayDeviceStatus,\n    logs,\n    logFilter,\n    matchMode,\n    ignoreCase,\n    actualLogs,\n    refreshLogs,\n    closeApp,\n    restart,\n    boom,\n    resetDisplayDevicePersistence,\n    copyLogs,\n    copyConfig,\n    reopenSetupWizard,\n    loadPlatform,\n    startLogRefresh,\n    stopLogRefresh,\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useVersion.js",
    "content": "import { ref, computed } from 'vue'\nimport { marked } from 'marked'\nimport SunshineVersion from '../sunshine_version.js'\nimport { trackEvents } from '../config/firebase.js'\n\nconst GITHUB_API_BASE = 'https://api.github.com/repos/qiin2333/Sunshine/releases'\n\n/**\n * 解析 Markdown 内容\n */\nconst parseMarkdown = (text) => {\n  if (!text) return ''\n  const normalized = text.replace(/\\r\\n?/g, '\\n')\n  return marked(normalized, { breaks: true, gfm: true })\n}\n\n/**\n * 安全获取 GitHub 数据\n */\nconst fetchGitHub = async (url) => {\n  try {\n    const response = await fetch(url)\n    if (!response.ok) return null\n    return await response.json()\n  } catch (e) {\n    console.error(`Failed to fetch ${url}:`, e)\n    return null\n  }\n}\n\n/**\n * 将配置值转换为布尔值\n */\nconst toBoolean = (value) => {\n  if (typeof value === 'string') {\n    return value.toLowerCase() === 'true'\n  }\n  return Boolean(value)\n}\n\n/**\n * 版本管理组合式函数\n */\nexport function useVersion() {\n  const version = ref(null)\n  const githubVersion = ref(null)\n  const preReleaseVersion = ref(null)\n  const notifyPreReleases = ref(false)\n  const loading = ref(true)\n\n  // 计算属性\n  const installedVersionNotStable = computed(() => \n    githubVersion.value?.isLessThan?.(version.value) ?? false\n  )\n\n  const stableBuildAvailable = computed(() => \n    githubVersion.value?.isGreater?.(version.value) ?? false\n  )\n\n  const preReleaseBuildAvailable = computed(() => \n    preReleaseVersion.value?.isGreater?.(version.value) ?? false\n  )\n\n  const buildVersionIsDirty = computed(() => {\n    const v = version.value?.version\n    if (!v) return false\n    const parts = v.split('.')\n    return parts.length === 5 && v.includes('dirty')\n  })\n\n  const parsedStableBody = computed(() => \n    parseMarkdown(githubVersion.value?.release?.body)\n  )\n  \n  const parsedPreReleaseBody = computed(() => \n    parseMarkdown(preReleaseVersion.value?.release?.body)\n  )\n\n  /**\n   * 获取版本信息\n   */\n  const fetchVersions = async (config) => {\n    loading.value = true\n    \n    try {\n      notifyPreReleases.value = toBoolean(config.notify_pre_releases)\n      version.value = new SunshineVersion(null, config.version)\n      \n      // 并行获取 GitHub 版本信息\n      const [latestData, releases] = await Promise.all([\n        fetchGitHub(`${GITHUB_API_BASE}/latest`),\n        fetchGitHub(GITHUB_API_BASE)\n      ])\n\n      if (latestData) {\n        githubVersion.value = new SunshineVersion(latestData, null)\n      }\n\n      if (Array.isArray(releases)) {\n        const preRelease = releases.find((r) => r.prerelease)\n        if (preRelease) {\n          preReleaseVersion.value = new SunshineVersion(preRelease, null)\n        }\n      }\n\n      // 记录版本检查事件\n      if (githubVersion.value && version.value) {\n        trackEvents.versionChecked(version.value.version, githubVersion.value.version)\n      }\n    } catch (e) {\n      console.error('Version check failed:', e)\n      trackEvents.errorOccurred('version_check', e.message)\n    } finally {\n      loading.value = false\n    }\n  }\n\n  return {\n    version,\n    githubVersion,\n    preReleaseVersion,\n    notifyPreReleases,\n    loading,\n    installedVersionNotStable,\n    stableBuildAvailable,\n    preReleaseBuildAvailable,\n    buildVersionIsDirty,\n    parsedStableBody,\n    parsedPreReleaseBody,\n    fetchVersions,\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/composables/useWelcome.js",
    "content": "import { ref, reactive, computed } from 'vue'\n\n/**\n * 欢迎页面组合式函数\n */\nexport function useWelcome() {\n  const error = ref(null)\n  const success = ref(false)\n  const loading = ref(false)\n\n  const passwordData = reactive({\n    newUsername: 'sunshine',\n    newPassword: '',\n    confirmNewPassword: '',\n  })\n\n  const passwordsMatch = computed(\n    () =>\n      !passwordData.newPassword ||\n      !passwordData.confirmNewPassword ||\n      passwordData.newPassword === passwordData.confirmNewPassword\n  )\n\n  const isFormValid = computed(\n    () =>\n      passwordData.newUsername && passwordData.newPassword && passwordData.confirmNewPassword && passwordsMatch.value\n  )\n\n  const save = async () => {\n    error.value = null\n\n    if (!passwordsMatch.value) {\n      error.value = 'welcome.password_mismatch'\n      return\n    }\n\n    loading.value = true\n\n    try {\n      const response = await fetch('/api/password', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(passwordData),\n      })\n\n      const result = await response.json()\n\n      if (response.ok && result.status?.toString() === 'true') {\n        success.value = true\n        setTimeout(() => {\n          window.location.href = '/'\n        }, 2000)\n      } else {\n        // 如果服务器返回了错误消息，使用它；否则使用翻译键\n        error.value = result.error || 'welcome.server_error'\n      }\n    } catch (err) {\n      console.error('Failed to save password:', err)\n      error.value = 'welcome.network_error'\n    } finally {\n      loading.value = false\n    }\n  }\n\n  return {\n    error,\n    success,\n    loading,\n    passwordData,\n    passwordsMatch,\n    isFormValid,\n    save,\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/config/firebase.js",
    "content": "// Firebase配置和初始化\nimport { initializeApp } from 'firebase/app'\nimport { getAnalytics, logEvent } from 'firebase/analytics'\n\nconst firebaseConfig = {\n  apiKey: 'AIzaSyD7VDKZyA1PG6mCO2QdPAUXIziVfTahV0g',\n  authDomain: 'sunshine-foundation-3c551.firebaseapp.com',\n  projectId: 'sunshine-foundation-3c551',\n  storageBucket: 'sunshine-foundation-3c551.firebasestorage.app',\n  messagingSenderId: '72108120145',\n  appId: '1:72108120145:web:d8f4933fd63766290a4070',\n  measurementId: 'G-F20721ZDL1',\n}\n\nlet analytics = null\n\nexport function initFirebase() {\n  try {\n    const app = initializeApp(firebaseConfig)\n    analytics = getAnalytics(app)\n    return { app, analytics }\n  } catch (error) {\n    console.error('Firebase 初始化失败:', error)\n    return null\n  }\n}\n\nexport function trackEvent(eventName, params = {}) {\n  if (!analytics) return\n\n  // 处理参数：数组转字符串，截断过长值\n  const sanitized = Object.fromEntries(\n    Object.entries(params).map(([k, v]) => {\n      let val = Array.isArray(v) ? v.join(', ') : typeof v === 'object' && v ? JSON.stringify(v) : v\n      if (typeof val === 'string' && val.length > 100) val = val.substring(0, 97) + '...'\n      return [k, val]\n    })\n  )\n\n  try {\n    logEvent(analytics, eventName, sanitized)\n  } catch (error) {\n    console.error('记录事件失败:', error)\n  }\n}\n\nexport const trackEvents = {\n  pageView: (pageName) => trackEvent('page_view', { page_name: pageName }),\n  userAction: (action, details = {}) => trackEvent('user_action', { action, ...details }),\n  appAdded: (appName) => trackEvent('app_added', { app_name: appName }),\n  appDeleted: (appName) => trackEvent('app_deleted', { app_name: appName }),\n  configChanged: (section, setting) => trackEvent('config_changed', { section, setting }),\n  errorOccurred: (type, message) => trackEvent('error_occurred', { error_type: type, error_message: message }),\n  versionChecked: (current, latest) => trackEvent('version_checked', { current_version: current, latest_version: latest }),\n  devicePaired: (type) => trackEvent('device_paired', { device_type: type }),\n  gpuReported: (info) => trackEvent('gpu_reported', info),\n}\n\nexport { analytics }\n"
  },
  {
    "path": "src_assets/common/assets/web/config/i18n.js",
    "content": "import {createI18n} from \"vue-i18n\";\n\n// Import only the fallback language files\nimport en from '../public/assets/locale/en.json'\n\nexport default async function() {\n    // 先尝试从 /api/config 获取实时配置（会读取配置文件）\n    let locale = \"en\";\n    try {\n        let config = await (await fetch(\"/api/config\")).json();\n        locale = config.locale ?? \"en\";\n    } catch (e) {\n        // 如果失败，回退到 /api/configLocale（从内存读取）\n        try {\n            let r = await (await fetch(\"/api/configLocale\")).json();\n            locale = r.locale ?? \"en\";\n        } catch (e2) {\n            console.error(\"Failed to get locale config\", e, e2);\n        }\n    }\n    \n    document.querySelector('html').setAttribute('lang', locale);\n    let messages = {\n        en\n    };\n    try {\n        if (locale !== 'en') {\n            let r = await (await fetch(`/assets/locale/${locale}.json`)).json();\n            messages[locale] = r;\n        }\n    } catch (e) {\n        console.error(\"Failed to download translations\", e);\n    }\n    const i18n = createI18n({\n        legacy: false, // 使用 Composition API 模式\n        locale: locale, // set locale\n        fallbackLocale: 'en', // set fallback locale\n        messages: messages,\n        globalInjection: true, // 允许在模板中使用 $t\n        warnHtmlMessage: false, // 禁用 HTML 消息警告（因为我们使用 v-html 来渲染受信任的翻译内容）\n    })\n    return i18n;\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/config.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bs-theme=\"auto\">\n  <head>\n    <%- header %>\n  </head>\n\n  <body id=\"app\" v-cloak>\n    <!-- Vue 应用挂载点 -->\n  </body>\n\n  <script type=\"module\">\n    import { createApp } from 'vue'\n    import { initApp } from './init'\n    import Config from './views/Config.vue'\n\n    const app = createApp(Config)\n    initApp(app)\n  </script>\n</html>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/Advanced.vue",
    "content": "<script setup>\nimport { ref, computed, onMounted, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport PlatformLayout from '../../components/layout/PlatformLayout.vue'\n\nconst { t } = useI18n()\n\nconst props = defineProps(['platform', 'config', 'global_prep_cmd'])\n\nconst config = ref(props.config)\n\n// 检查是否在 Tauri 环境中（通过 inject-script.js 注入）\nconst isTauri = computed(() => {\n  return typeof window !== 'undefined' && window.__TAURI__?.core?.invoke\n})\n\n// 检查是否选择了 WGC\nconst isWGCSelected = computed(() => {\n  return props.platform === 'windows' && config.value.capture === 'wgc'\n})\n\n// 检查是否选择了 AMD Display Capture\nconst isAMDCaptureSelected = computed(() => {\n  return props.platform === 'windows' && config.value.capture === 'amd'\n})\n\n// Sunshine 运行模式状态\nconst isUserMode = ref(false)\nconst isCheckingMode = ref(false)\n\nconst showMessage = (message, type = 'info') => {\n  // 尝试使用 window.showToast（如果可用）\n  if (typeof window.showToast === 'function') {\n    window.showToast(message, type)\n    return\n  }\n\n  // 尝试通过 postMessage 请求父窗口显示消息\n  if (window.parent && window.parent !== window) {\n    try {\n      window.parent.postMessage(\n        {\n          type: 'show-message',\n          message,\n          messageType: type,\n          source: 'sunshine-webui',\n        },\n        '*'\n      )\n      return\n    } catch (e) {\n      console.warn('无法通过 postMessage 发送消息:', e)\n    }\n  }\n\n  // 降级到 alert\n  if (type === 'error') {\n    alert(message)\n  } else {\n    console.info(message)\n  }\n}\n\n// 检查当前 Sunshine 运行模式\nconst checkSunshineMode = async () => {\n  if (!isTauri.value) {\n    return\n  }\n\n  isCheckingMode.value = true\n  try {\n    const result = await window.__TAURI__.core.invoke('is_sunshine_running_in_user_mode')\n    isUserMode.value = result === true\n  } catch (error) {\n    console.error('检查 Sunshine 模式失败:', error)\n    // 如果检查失败，默认假设为服务模式\n    isUserMode.value = false\n  } finally {\n    isCheckingMode.value = false\n  }\n}\n\n// 切换 Sunshine 运行模式\nconst toggleSunshineMode = async () => {\n  if (!isTauri.value) {\n    showMessage(t('config.wgc_control_panel_only'), 'error')\n    return\n  }\n\n  try {\n    const msg = await window.__TAURI__.core.invoke('toggle_sunshine_mode')\n    showMessage(msg || t('config.wgc_mode_switch_started'), 'success')\n\n    // 切换通过 UAC 提升的 PowerShell 在后台执行，需预留：UAC 确认 + net stop + taskkill + 启动。延迟后再检查，并做二次检查以修正中间状态。\n    setTimeout(() => checkSunshineMode(), 6000)\n    setTimeout(() => checkSunshineMode(), 11000)\n  } catch (error) {\n    console.error('切换模式失败:', error)\n    showMessage(t('config.wgc_mode_switch_failed') + ': ' + (error.message || error), 'error')\n  }\n}\n\nonMounted(() => {\n  if (isTauri.value && isWGCSelected.value) {\n    checkSunshineMode()\n  }\n})\n\nwatch(isWGCSelected, (newValue) => {\n  if (newValue && isTauri.value) {\n    checkSunshineMode()\n  }\n})\n</script>\n\n<template>\n  <div class=\"config-page\">\n    <!-- FEC Percentage -->\n    <div class=\"mb-3\">\n      <label for=\"fec_percentage\" class=\"form-label\">{{ $t('config.fec_percentage') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"fec_percentage\" placeholder=\"20\" v-model=\"config.fec_percentage\" />\n      <div class=\"form-text\">{{ $t('config.fec_percentage_desc') }}</div>\n    </div>\n\n    <!-- Min Threads -->\n    <div class=\"mb-3\">\n      <label for=\"min_threads\" class=\"form-label\">{{ $t('config.min_threads') }}</label>\n      <input type=\"number\" class=\"form-control\" id=\"min_threads\" placeholder=\"2\" min=\"1\" v-model=\"config.min_threads\" />\n      <div class=\"form-text\">{{ $t('config.min_threads_desc') }}</div>\n    </div>\n\n    <!-- HEVC Support -->\n    <div class=\"mb-3\">\n      <label for=\"hevc_mode\" class=\"form-label\">{{ $t('config.hevc_mode') }}</label>\n      <select id=\"hevc_mode\" class=\"form-select\" v-model=\"config.hevc_mode\">\n        <option value=\"0\">{{ $t('config.hevc_mode_0') }}</option>\n        <option value=\"1\">{{ $t('config.hevc_mode_1') }}</option>\n        <option value=\"2\">{{ $t('config.hevc_mode_2') }}</option>\n        <option value=\"3\">{{ $t('config.hevc_mode_3') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.hevc_mode_desc') }}</div>\n    </div>\n\n    <!-- AV1 Support -->\n    <div class=\"mb-3\">\n      <label for=\"av1_mode\" class=\"form-label\">{{ $t('config.av1_mode') }}</label>\n      <select id=\"av1_mode\" class=\"form-select\" v-model=\"config.av1_mode\">\n        <option value=\"0\">{{ $t('config.av1_mode_0') }}</option>\n        <option value=\"1\">{{ $t('config.av1_mode_1') }}</option>\n        <option value=\"2\">{{ $t('config.av1_mode_2') }}</option>\n        <option value=\"3\">{{ $t('config.av1_mode_3') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.av1_mode_desc') }}</div>\n    </div>\n\n    <!-- Capture -->\n    <div class=\"mb-3\" v-if=\"platform !== 'macos'\">\n      <label for=\"capture\" class=\"form-label\">{{ $t('config.capture') }}</label>\n      <div class=\"d-flex align-items-center gap-2\">\n        <select id=\"capture\" class=\"form-select flex-grow-1\" v-model=\"config.capture\">\n          <option value=\"\">{{ $t('_common.autodetect') }}</option>\n          <PlatformLayout :platform=\"platform\">\n            <template #linux>\n              <option value=\"nvfbc\">NvFBC</option>\n              <option value=\"wlr\">wlroots</option>\n              <option value=\"kms\">KMS</option>\n              <option value=\"x11\">X11</option>\n            </template>\n            <template #windows>\n              <option value=\"ddx\">Desktop Duplication API</option>\n              <option value=\"wgc\">Windows.Graphics.Capture {{ $t('_common.beta') }}</option>\n              <option value=\"amd\">AMD Display Capture {{ $t('_common.beta') }}</option>\n            </template>\n          </PlatformLayout>\n        </select>\n        <button\n          v-if=\"isWGCSelected && isTauri\"\n          type=\"button\"\n          :class=\"['btn', isUserMode ? 'btn-success' : 'btn-warning']\"\n          style=\"white-space: nowrap\"\n          @click=\"toggleSunshineMode\"\n          :disabled=\"isCheckingMode\"\n          :title=\"\n            isUserMode\n              ? $t('config.wgc_switch_to_service_mode_tooltip')\n              : $t('config.wgc_switch_to_user_mode_tooltip')\n          \"\n        >\n          <i v-if=\"isCheckingMode\" class=\"fas fa-spinner fa-spin me-1\"></i>\n          <i v-else class=\"fas fa-sync-alt me-1\"></i>\n          {{\n            isCheckingMode\n              ? $t('config.wgc_checking_mode')\n              : isUserMode\n                ? $t('config.wgc_switch_to_service_mode')\n                : $t('config.wgc_switch_to_user_mode')\n          }}\n        </button>\n      </div>\n      <div class=\"form-text\">\n        {{ $t('config.capture_desc') }}\n        <span v-if=\"isWGCSelected && isTauri\" :class=\"['d-block mt-1', isUserMode ? 'text-success' : 'text-warning']\">\n          <i :class=\"['me-1', isUserMode ? 'fas fa-check-circle' : 'fas fa-exclamation-triangle']\"></i>\n          <span v-if=\"isCheckingMode\">{{ $t('config.wgc_checking_running_mode') }}</span>\n          <span v-else-if=\"isUserMode\">{{ $t('config.wgc_user_mode_available') }}</span>\n          <span v-else>{{ $t('config.wgc_service_mode_warning') }}</span>\n        </span>\n        <span v-if=\"isAMDCaptureSelected\" class=\"d-block mt-1 text-warning\">\n          <i class=\"fas fa-exclamation-triangle me-1\"></i>\n          {{ $t('config.amd_capture_no_virtual_display') }}\n        </span>\n      </div>\n    </div>\n\n    <!-- Encoder -->\n    <div class=\"mb-3\">\n      <label for=\"encoder\" class=\"form-label\">{{ $t('config.encoder') }}</label>\n      <select id=\"encoder\" class=\"form-select\" v-model=\"config.encoder\">\n        <option value=\"\">{{ $t('_common.autodetect') }}</option>\n        <PlatformLayout :platform=\"platform\">\n          <template #windows>\n            <option value=\"nvenc\">NVIDIA NVENC</option>\n            <option value=\"quicksync\">Intel QuickSync</option>\n            <option value=\"amdvce\">AMD AMF/VCE</option>\n          </template>\n          <template #linux>\n            <option value=\"nvenc\">NVIDIA NVENC</option>\n            <option value=\"vaapi\">VA-API</option>\n          </template>\n          <template #macos>\n            <option value=\"videotoolbox\">VideoToolbox</option>\n          </template>\n        </PlatformLayout>\n        <option value=\"software\">{{ $t('config.encoder_software') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.encoder_desc') }}</div>\n    </div>\n  </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/AudioVideo.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { $tp } from '../../platform-i18n'\nimport { openExternalUrl } from '../../utils/helpers.js'\nimport PlatformLayout from '../../components/layout/PlatformLayout.vue'\nimport AdapterNameSelector from './audiovideo/AdapterNameSelector.vue'\nimport NewDisplayOutputSelector from './audiovideo/NewDisplayOutputSelector.vue'\nimport LegacyDisplayOutputSelector from './audiovideo/LegacyDisplayOutputSelector.vue'\nimport DisplayDeviceOptions from './audiovideo/DisplayDeviceOptions.vue'\nimport ExperimentalFeatures from './audiovideo/ExperimentalFeatures.vue'\nimport DisplayModesSettings from './audiovideo/DisplayModesSettings.vue'\nimport VirtualDisplaySettings from './audiovideo/VirtualDisplaySettings.vue'\nimport Checkbox from '../../components/Checkbox.vue'\n\nconst props = defineProps(['platform', 'config', 'resolutions', 'fps', 'display_mode_remapping', 'min_fps_factor'])\n\nconst { t } = useI18n()\nconst config = ref(props.config)\nconst currentSubTab = ref('display-modes')\nconst showDownloadConfirm = ref(false)\n\nconst handleDownloadVSink = () => {\n  showDownloadConfirm.value = true\n}\n\nconst confirmDownload = async () => {\n  showDownloadConfirm.value = false\n  const url = 'https://download.vb-audio.com/Download_CABLE/VBCABLE_Driver_Pack43.zip'\n  \n  try {\n    await openExternalUrl(url)\n  } catch (error) {\n    console.error('Failed to open URL:', error)\n  }\n}\n\nconst cancelDownload = () => {\n  showDownloadConfirm.value = false\n}\n</script>\n\n<template>\n  <div id=\"audio-video\" class=\"config-page\">\n    <!-- Audio Sink -->\n    <div class=\"mb-3\">\n      <label for=\"audio_sink\" class=\"form-label\">{{ $t('config.audio_sink') }}</label>\n      <input\n        type=\"text\"\n        class=\"form-control\"\n        id=\"audio_sink\"\n        :placeholder=\"$tp('config.audio_sink_placeholder', 'alsa_output.pci-0000_09_00.3.analog-stereo')\"\n        v-model=\"config.audio_sink\"\n      />\n      <div class=\"form-text\">\n        {{ $tp('config.audio_sink_desc') }}<br />\n        <PlatformLayout :platform=\"platform\">\n          <template #windows>\n            <pre>tools\\audio-info.exe</pre>\n          </template>\n          <template #linux>\n            <pre>pacmd list-sinks | grep \"name:\"</pre>\n            <pre>pactl info | grep Source</pre>\n          </template>\n          <template #macos>\n            <a href=\"https://github.com/mattingalls/Soundflower\" target=\"_blank\">Soundflower</a><br />\n            <a href=\"https://github.com/ExistentialAudio/BlackHole\" target=\"_blank\">BlackHole</a>.\n          </template>\n        </PlatformLayout>\n      </div>\n    </div>\n\n    <PlatformLayout :platform=\"platform\">\n      <template #windows>\n        <!-- Virtual Sink -->\n        <div class=\"mb-3\">\n          <label for=\"virtual_sink\" class=\"form-label\">{{ $t('config.virtual_sink') }}</label>\n          <input\n            type=\"text\"\n            class=\"form-control\"\n            id=\"virtual_sink\"\n            :placeholder=\"$t('config.virtual_sink_placeholder')\"\n            v-model=\"config.virtual_sink\"\n          />\n          <div class=\"form-text\">{{ $t('config.virtual_sink_desc') }}</div>\n        </div>\n\n        <!-- Install Steam Audio Drivers -->\n        <div class=\"mb-3\">\n          <label for=\"install_steam_audio_drivers\" class=\"form-label\">{{\n            $t('config.install_steam_audio_drivers')\n          }}</label>\n          <select id=\"install_steam_audio_drivers\" class=\"form-select\" v-model=\"config.install_steam_audio_drivers\">\n            <option value=\"disabled\">{{ $t('_common.disabled') }}</option>\n            <option value=\"enabled\">{{ $t('_common.enabled_def') }}</option>\n          </select>\n          <div class=\"form-text\">{{ $t('config.install_steam_audio_drivers_desc') }}</div>\n        </div>\n      </template>\n    </PlatformLayout>\n\n    <!-- Disable Audio -->\n    <Checkbox\n      class=\"mb-3\"\n      id=\"stream_audio\"\n      locale-prefix=\"config\"\n      v-model=\"config.stream_audio\"\n      default=\"true\"\n    ></Checkbox>\n\n    <!-- Disable Microphone -->\n    <div class=\"mb-3\">\n      <Checkbox\n        id=\"stream_mic\"\n        locale-prefix=\"config\"\n        v-model=\"config.stream_mic\"\n        default=\"true\"\n      ></Checkbox>\n      <div class=\"stream-mic-helper mt-2\">\n        <button\n          type=\"button\"\n          class=\"btn btn-sm btn-primary stream-mic-download-btn\"\n          @click=\"handleDownloadVSink\"\n        >\n          <i class=\"fas fa-download me-1\"></i>\n          {{ $t('_common.download') }}\n        </button>\n        <div class=\"stream-mic-note\">\n          <i class=\"fas fa-info-circle me-2\"></i>\n          <span>{{ $t('config.stream_mic_note') }}</span>\n        </div>\n      </div>\n    </div>\n\n    <AdapterNameSelector :platform=\"platform\" :config=\"config\" />\n\n    <NewDisplayOutputSelector :platform=\"platform\" :config=\"config\" />\n\n    <DisplayDeviceOptions :platform=\"platform\" :config=\"config\" />\n\n    <!-- Display Modes Tab Navigation -->\n    <div class=\"mb-3\">\n      <ul class=\"nav nav-tabs\">\n        <li class=\"nav-item\">\n          <a\n            class=\"nav-link\"\n            :class=\"{ active: currentSubTab === 'display-modes' }\"\n            href=\"#\"\n            @click.prevent=\"currentSubTab = 'display-modes'\"\n          >\n            {{ $t('config.display_modes') || 'Display Modes' }}\n          </a>\n        </li>\n        <li class=\"nav-item\">\n          <a\n            class=\"nav-link\"\n            :class=\"{ active: currentSubTab === 'virtual-display' }\"\n            href=\"#\"\n            @click.prevent=\"currentSubTab = 'virtual-display'\"\n          >\n            {{ $t('config.virtual_display') || 'Virtual Display' }}\n          </a>\n        </li>\n      </ul>\n\n      <!-- Display Modes Tab Content -->\n      <div class=\"tab-content\">\n        <DisplayModesSettings\n          v-if=\"currentSubTab === 'display-modes'\"\n          :platform=\"platform\"\n          :config=\"config\"\n          :min_fps_factor=\"min_fps_factor\"\n        />\n        \n        <!-- Virtual Display Tab Content -->\n        <VirtualDisplaySettings\n          v-if=\"currentSubTab === 'virtual-display'\"\n          :platform=\"platform\"\n          :config=\"config\"\n          :resolutions=\"resolutions\"\n          :fps=\"fps\"\n        />\n      </div>\n    </div>\n\n    <ExperimentalFeatures :platform=\"platform\" :config=\"config\" :display_mode_remapping=\"display_mode_remapping\" />\n\n    <!-- 下载确认对话框 -->\n    <Teleport to=\"body\">\n      <Transition name=\"fade\">\n        <div v-if=\"showDownloadConfirm\" class=\"download-confirm-overlay\" @click.self=\"cancelDownload\">\n          <div class=\"download-confirm-modal\">\n            <div class=\"download-confirm-header\">\n              <h5>\n                <i class=\"fas fa-external-link-alt me-2\"></i>{{ $t('_common.download') }}\n              </h5>\n              <button class=\"btn-close\" @click=\"cancelDownload\"></button>\n            </div>\n            <div class=\"download-confirm-body\">\n              <p>{{ $t('config.stream_mic_download_confirm') }}</p>\n            </div>\n            <div class=\"download-confirm-footer\">\n              <button type=\"button\" class=\"btn btn-secondary\" @click=\"cancelDownload\">{{ $t('_common.cancel') }}</button>\n              <button type=\"button\" class=\"btn btn-primary\" @click=\"confirmDownload\">\n                <i class=\"fas fa-download me-1\"></i>{{ $t('_common.download') }}\n              </button>\n            </div>\n          </div>\n        </div>\n      </Transition>\n    </Teleport>\n  </div>\n</template>\n\n<style scoped>\n.nav-tabs {\n  border-bottom: 1px solid rgba(0, 0, 0, 0.1);\n  margin-bottom: 1rem;\n}\n\n.nav-tabs .nav-link {\n  border: none;\n  border-bottom: 2px solid transparent;\n  color: var(--bs-secondary-color);\n  padding: 0.75rem 1.5rem;\n  transition: all 0.3s ease;\n}\n\n.nav-tabs .nav-link:hover {\n  border-bottom-color: var(--bs-primary);\n  color: var(--bs-primary);\n}\n\n.nav-tabs .nav-link.active {\n  color: var(--bs-primary);\n  background-color: transparent;\n  border-bottom-color: var(--bs-primary);\n  font-weight: 600;\n}\n\n.tab-content {\n  padding-top: 1rem;\n}\n\n.stream-mic-helper {\n  display: flex;\n  align-items: center;\n  gap: 1rem;\n  flex-wrap: wrap;\n  padding: 0.75rem;\n  background: var(--bs-secondary-bg);\n  border-radius: 8px;\n  border: 1px solid var(--bs-border-color);\n}\n\n.stream-mic-download-btn {\n  white-space: nowrap;\n  flex-shrink: 0;\n  order: -1;\n}\n\n.stream-mic-note {\n  display: flex;\n  align-items: center;\n  color: var(--bs-secondary-color);\n  font-size: 0.875rem;\n  flex: 1;\n  min-width: 200px;\n\n  i {\n    color: var(--bs-info);\n    font-size: 1rem;\n  }\n}\n\n[data-bs-theme='dark'] .stream-mic-helper {\n  background: rgba(255, 255, 255, 0.05);\n  border-color: rgba(255, 255, 255, 0.1);\n}\n\n/* Download Confirm Modal - teleported to body, styles must not be scoped */\n</style>\n\n<style>\n.download-confirm-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 100vw;\n  height: 100vh;\n  margin: 0;\n  background: var(--overlay-bg, rgba(0, 0, 0, 0.7));\n  backdrop-filter: blur(8px);\n  z-index: 9999;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: var(--spacing-lg, 20px);\n  overflow: hidden;\n\n  [data-bs-theme='light'] & {\n    background: rgba(0, 0, 0, 0.5);\n  }\n}\n\n.download-confirm-modal {\n  background: var(--modal-bg, rgba(30, 30, 50, 0.95));\n  border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.2));\n  border-radius: var(--border-radius-xl, 12px);\n  width: 100%;\n  max-width: 500px;\n  display: flex;\n  flex-direction: column;\n  backdrop-filter: blur(20px);\n  box-shadow: var(--shadow-xl, 0 25px 50px rgba(0, 0, 0, 0.5));\n  animation: modalSlideUp 0.3s ease;\n\n  [data-bs-theme='light'] & {\n    background: rgba(255, 255, 255, 0.95);\n    border: 1px solid rgba(0, 0, 0, 0.15);\n    box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);\n  }\n}\n\n.download-confirm-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 1.25rem 1.5rem;\n  border-bottom: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  [data-bs-theme='light'] & {\n    border-bottom-color: rgba(0, 0, 0, 0.1);\n  }\n\n  h5 {\n    margin: 0;\n    font-size: 1.125rem;\n    font-weight: 600;\n    color: var(--bs-body-color);\n    display: flex;\n    align-items: center;\n\n    i {\n      color: var(--bs-primary);\n    }\n  }\n\n  .btn-close {\n    background: none;\n    border: none;\n    font-size: 1.5rem;\n    color: var(--bs-secondary-color);\n    cursor: pointer;\n    padding: 0;\n    width: 1.5rem;\n    height: 1.5rem;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    opacity: 0.6;\n    transition: opacity 0.2s;\n\n    &:hover {\n      opacity: 1;\n    }\n\n    &::before {\n      content: '×';\n    }\n  }\n}\n\n.download-confirm-body {\n  padding: 1.5rem;\n  color: var(--bs-body-color);\n\n  p {\n    margin: 0;\n    line-height: 1.6;\n  }\n}\n\n.download-confirm-footer {\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n  gap: 0.75rem;\n  padding: 1.25rem 1.5rem;\n  border-top: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  [data-bs-theme='light'] & {\n    border-top-color: rgba(0, 0, 0, 0.1);\n  }\n}\n\n@keyframes modalSlideUp {\n  from {\n    transform: translateY(20px);\n    opacity: 0;\n  }\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/ContainerEncoders.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\nimport NvidiaNvencEncoder from './encoders/NvidiaNvencEncoder.vue'\nimport IntelQuickSyncEncoder from './encoders/IntelQuickSyncEncoder.vue'\nimport AmdAmfEncoder from './encoders/AmdAmfEncoder.vue'\nimport VideotoolboxEncoder from './encoders/VideotoolboxEncoder.vue'\nimport SoftwareEncoder from './encoders/SoftwareEncoder.vue'\n\nconst props = defineProps([\n  'platform',\n  'config',\n  'currentTab'\n])\n\nconst config = ref(props.config)\n</script>\n\n<template>\n\n  <!-- NVIDIA NVENC Encoder Tab -->\n  <NvidiaNvencEncoder\n      v-if=\"currentTab === 'nv'\"\n      :platform=\"platform\"\n      :config=\"config\"\n  />\n\n  <!-- Intel QuickSync Encoder Tab -->\n  <IntelQuickSyncEncoder\n      v-if=\"currentTab === 'qsv'\"\n      :platform=\"platform\"\n      :config=\"config\"\n  />\n\n  <!-- AMD AMF Encoder Tab -->\n  <AmdAmfEncoder\n      v-if=\"currentTab === 'amd'\"\n      :platform=\"platform\"\n      :config=\"config\"\n  />\n\n  <!-- VideoToolbox Encoder Tab -->\n  <VideotoolboxEncoder\n      v-if=\"currentTab === 'vt'\"\n      :platform=\"platform\"\n      :config=\"config\"\n  />\n\n  <!-- Software Encoder Tab -->\n  <SoftwareEncoder\n      v-if=\"currentTab === 'sw'\"\n      :platform=\"platform\"\n      :config=\"config\"\n  />\n\n</template>\n\n<style scoped>\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/Files.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\n\nconst props = defineProps([\n  'platform',\n  'config'\n])\n\nconst config = ref(props.config)\n</script>\n\n<template>\n  <div id=\"files\" class=\"config-page\">\n    <!-- Apps File -->\n    <div class=\"mb-3\">\n      <label for=\"file_apps\" class=\"form-label\">{{ $t('config.file_apps') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"file_apps\" placeholder=\"apps.json\" v-model=\"config.file_apps\" />\n      <div class=\"form-text\">{{ $t('config.file_apps_desc') }}</div>\n    </div>\n\n    <!-- Credentials File -->\n    <div class=\"mb-3\">\n      <label for=\"credentials_file\" class=\"form-label\">{{ $t('config.credentials_file') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"credentials_file\" placeholder=\"sunshine_state.json\" v-model=\"config.credentials_file\" />\n      <div class=\"form-text\">{{ $t('config.credentials_file_desc') }}</div>\n    </div>\n\n    <!-- Log Path -->\n    <div class=\"mb-3\">\n      <label for=\"log_path\" class=\"form-label\">{{ $t('config.log_path') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"log_path\" placeholder=\"sunshine.log\" v-model=\"config.log_path\" />\n      <div class=\"form-text\">{{ $t('config.log_path_desc') }}</div>\n    </div>\n\n    <!-- Private Key -->\n    <div class=\"mb-3\">\n      <label for=\"pkey\" class=\"form-label\">{{ $t('config.pkey') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"pkey\" placeholder=\"/dir/pkey.pem\" v-model=\"config.pkey\" />\n      <div class=\"form-text\">{{ $t('config.pkey_desc') }}</div>\n    </div>\n\n    <!-- Certificate -->\n    <div class=\"mb-3\">\n      <label for=\"cert\" class=\"form-label\">{{ $t('config.cert') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"cert\" placeholder=\"/dir/cert.pem\" v-model=\"config.cert\" />\n      <div class=\"form-text\">{{ $t('config.cert_desc') }}</div>\n    </div>\n\n    <!-- State File -->\n    <div class=\"mb-3\">\n      <label for=\"file_state\" class=\"form-label\">{{ $t('config.file_state') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"file_state\" placeholder=\"sunshine_state.json\"\n             v-model=\"config.file_state\" />\n      <div class=\"form-text\">{{ $t('config.file_state_desc') }}</div>\n    </div>\n\n  </div>\n</template>\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/General.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\nimport Checkbox from '../../components/Checkbox.vue'\nimport CommandTable from '../../components/CommandTable.vue'\n\nconst props = defineProps({\n  platform: String,\n  config: Object,\n  globalPrepCmd: Array,\n})\n\nconst config = ref(props.config)\nconst globalPrepCmd = ref(props.globalPrepCmd)\n\nfunction addCmd() {\n  let template = {\n    do: '',\n    undo: '',\n  }\n\n  if (props.platform === 'windows') {\n    template = { ...template, elevated: false }\n  }\n  globalPrepCmd.value.push(template)\n}\n\nfunction removeCmd(index) {\n  globalPrepCmd.value.splice(index, 1)\n}\n\nfunction handleCommandOrderChanged(newOrder) {\n  // 更新命令顺序\n  globalPrepCmd.value.splice(0, globalPrepCmd.value.length, ...newOrder)\n}\n</script>\n\n<template>\n  <div id=\"general\" class=\"config-page\">\n    <!-- Locale -->\n    <div class=\"mb-3\">\n      <label for=\"locale\" class=\"form-label\">{{ $t('config.locale') }}</label>\n      <select id=\"locale\" class=\"form-select\" v-model=\"config.locale\">\n        <option value=\"bg\">Български (Bulgarian)</option>\n        <option value=\"cs\">Čeština (Czech)</option>\n        <option value=\"de\">Deutsch (German)</option>\n        <option value=\"en\">English</option>\n        <option value=\"en_GB\">English, UK</option>\n        <option value=\"en_US\">English, US</option>\n        <option value=\"es\">Español (Spanish)</option>\n        <option value=\"fr\">Français (French)</option>\n        <option value=\"it\">Italiano (Italian)</option>\n        <option value=\"ja\">日本語 (Japanese)</option>\n        <option value=\"pt\">Português (Portuguese)</option>\n        <option value=\"ru\">Русский (Russian)</option>\n        <option value=\"sv\">svenska (Swedish)</option>\n        <option value=\"tr\">Türkçe (Turkish)</option>\n        <option value=\"zh\">简体中文 (Chinese Simplified)</option>\n        <option value=\"zh_TW\">繁體中文 (Chinese Traditional)</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.locale_desc') }}</div>\n    </div>\n\n    <!-- Sunshine Name -->\n    <div class=\"mb-3\">\n      <label for=\"sunshine_name\" class=\"form-label\">{{ $t('config.sunshine_name') }}</label>\n      <input\n        type=\"text\"\n        class=\"form-control\"\n        id=\"sunshine_name\"\n        placeholder=\"Sunshine\"\n        v-model=\"config.sunshine_name\"\n      />\n      <div class=\"form-text\">{{ $t('config.sunshine_name_desc') }}</div>\n    </div>\n\n    <!-- Log Level -->\n    <div class=\"mb-3\">\n      <label for=\"min_log_level\" class=\"form-label\">{{ $t('config.log_level') }}</label>\n      <select id=\"min_log_level\" class=\"form-select\" v-model=\"config.min_log_level\">\n        <option value=\"0\">{{ $t('config.log_level_0') }}</option>\n        <option value=\"1\">{{ $t('config.log_level_1') }}</option>\n        <option value=\"2\">{{ $t('config.log_level_2') }}</option>\n        <option value=\"3\">{{ $t('config.log_level_3') }}</option>\n        <option value=\"4\">{{ $t('config.log_level_4') }}</option>\n        <option value=\"5\">{{ $t('config.log_level_5') }}</option>\n        <option value=\"6\">{{ $t('config.log_level_6') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.log_level_desc') }}</div>\n    </div>\n\n    <!-- Global Prep Commands -->\n    <div class=\"mb-3\">\n      <label class=\"form-label\">{{ $t('config.global_prep_cmd') }}</label>\n      <div class=\"form-text\">{{ $t('config.global_prep_cmd_desc') }}</div>\n      <CommandTable\n        class=\"mt-3\"\n        :commands=\"globalPrepCmd\"\n        :platform=\"platform\"\n        type=\"prep\"\n        @add-command=\"addCmd\"\n        @remove-command=\"removeCmd\"\n        @order-changed=\"handleCommandOrderChanged\"\n      />\n    </div>\n\n    <!-- Notify Pre-Releases -->\n    <Checkbox\n      class=\"mb-3\"\n      id=\"notify_pre_releases\"\n      locale-prefix=\"config\"\n      v-model=\"config.notify_pre_releases\"\n      default=\"false\"\n    ></Checkbox>\n\n    <!-- Enable system tray -->\n    <Checkbox\n      class=\"mb-3\"\n      id=\"system_tray\"\n      locale-prefix=\"config\"\n      v-model=\"config.system_tray\"\n      default=\"true\"\n    ></Checkbox>\n\n    <!-- Sleep Mode -->\n    <div class=\"mb-3\">\n      <label for=\"sleep_mode\" class=\"form-label\">{{ $t('config.sleep_mode') }}</label>\n      <select id=\"sleep_mode\" class=\"form-select\" v-model=\"config.sleep_mode\">\n        <option value=\"0\">{{ $t('config.sleep_mode_suspend') }}</option>\n        <option value=\"1\">{{ $t('config.sleep_mode_hibernate') }}</option>\n        <option value=\"2\">{{ $t('config.sleep_mode_away') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.sleep_mode_desc') }}</div>\n    </div>\n  </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/Inputs.vue",
    "content": "<script setup>\nimport { ref, computed, onMounted } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport PlatformLayout from '../../components/layout/PlatformLayout.vue'\n\nconst { t } = useI18n()\n\nconst props = defineProps([\n  'platform',\n  'config'\n])\n\nconst config = ref(props.config)\n\n// Tauri 环境下的 vmouse 驱动管理\nconst isTauri = ref(false)\nconst vmouseStatus = ref({ installed: false, running: false, status_text: '' })\nconst vmouseLoading = ref(false)\nconst vmouseOperating = ref(false)\n\nonMounted(async () => {\n  if (window.isTauri && window.vmouseDriver) {\n    isTauri.value = true\n    await refreshVmouseStatus()\n  }\n})\n\nasync function refreshVmouseStatus() {\n  vmouseLoading.value = true\n  try {\n    vmouseStatus.value = await window.vmouseDriver.getStatus()\n  } catch { /* ignore */ }\n  vmouseLoading.value = false\n}\n\nasync function installVmouse() {\n  if (!confirm(t('config.vmouse_confirm_install'))) return\n  vmouseOperating.value = true\n  try {\n    await window.vmouseDriver.install()\n    setTimeout(() => refreshVmouseStatus(), 2000)\n  } catch (e) {\n    alert(String(e))\n  }\n  vmouseOperating.value = false\n}\n\nasync function uninstallVmouse() {\n  if (!confirm(t('config.vmouse_confirm_uninstall'))) return\n  vmouseOperating.value = true\n  try {\n    await window.vmouseDriver.uninstall()\n    setTimeout(() => refreshVmouseStatus(), 2000)\n  } catch (e) {\n    alert(String(e))\n  }\n  vmouseOperating.value = false\n}\n\nconst vmouseDotClass = computed(() => {\n  if (vmouseStatus.value.running) return 'dot-active'\n  if (vmouseStatus.value.installed) return 'dot-warning'\n  return 'dot-inactive'\n})\n\nconst vmouseStatusLabel = computed(() => {\n  if (vmouseStatus.value.running) return t('config.vmouse_status_running')\n  if (vmouseStatus.value.installed) return t('config.vmouse_status_installed')\n  return t('config.vmouse_status_not_installed')\n})\n</script>\n\n<template>\n  <div id=\"input\" class=\"config-page\">\n    <!-- Enable Gamepad Input -->\n    <div class=\"mb-3\">\n      <div class=\"form-check form-switch\">\n        <input class=\"form-check-input\" type=\"checkbox\" id=\"controller\" \n               v-model=\"config.controller\" true-value=\"enabled\" false-value=\"disabled\">\n        <label class=\"form-check-label\" for=\"controller\">\n          {{ $t('config.controller') }}\n        </label>\n      </div>\n      <div class=\"form-text\">{{ $t('config.controller_desc') }}</div>\n    </div>\n\n    <!-- Emulated Gamepad Type -->\n    <div class=\"mb-3\" v-if=\"config.controller === 'enabled' && platform !== 'macos'\">\n      <label for=\"gamepad\" class=\"form-label\">{{ $t('config.gamepad') }}</label>\n      <select id=\"gamepad\" class=\"form-select\" v-model=\"config.gamepad\">\n        <option value=\"auto\">{{ $t('_common.auto') }}</option>\n\n        <PlatformLayout :platform=\"platform\">\n          <template #linux>\n            <option value=\"ds5\">{{ $t(\"config.gamepad_ds5\") }}</option>\n            <option value=\"switch\">{{ $t(\"config.gamepad_switch\") }}</option>\n            <option value=\"xone\">{{ $t(\"config.gamepad_xone\") }}</option>\n          </template>\n          \n          <template #windows>\n            <option value=\"ds4\">{{ $t('config.gamepad_ds4') }}</option>\n            <option value=\"x360\">{{ $t('config.gamepad_x360') }}</option>\n          </template>\n        </PlatformLayout>\n      </select>\n      <div class=\"form-text\">{{ $t('config.gamepad_desc') }}</div>\n    </div>\n\n    <div class=\"accordion\" v-if=\"config.gamepad === 'ds4'\">\n      <div class=\"accordion-item\">\n        <h2 class=\"accordion-header\">\n          <button class=\"accordion-button\" type=\"button\" data-bs-toggle=\"collapse\"\n                  data-bs-target=\"#panelsStayOpen-collapseOne\">\n            {{ $t('config.gamepad_ds4_manual') }}\n          </button>\n        </h2>\n        <div id=\"panelsStayOpen-collapseOne\" class=\"accordion-collapse collapse show\"\n             aria-labelledby=\"panelsStayOpen-headingOne\">\n          <div class=\"accordion-body\">\n            <div>\n              <label for=\"ds4_back_as_touchpad_click\" class=\"form-label\">{{ $t('config.ds4_back_as_touchpad_click') }}</label>\n              <select id=\"ds4_back_as_touchpad_click\" class=\"form-select\"\n                      v-model=\"config.ds4_back_as_touchpad_click\">\n                <option value=\"disabled\">{{ $t('_common.disabled') }}</option>\n                <option value=\"enabled\">{{ $t('_common.enabled_def') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.ds4_back_as_touchpad_click_desc') }}</div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"accordion\" v-if=\"config.controller === 'enabled' && config.gamepad === 'auto' && platform === 'windows'\">\n      <div class=\"accordion-item\">\n        <h2 class=\"accordion-header\">\n          <button class=\"accordion-button\" type=\"button\" data-bs-toggle=\"collapse\"\n                  data-bs-target=\"#panelsStayOpen-collapseOne\">\n            {{ $t('config.gamepad_auto') }}\n          </button>\n        </h2>\n        <div id=\"panelsStayOpen-collapseOne\" class=\"accordion-collapse collapse show\"\n             aria-labelledby=\"panelsStayOpen-headingOne\">\n          <div class=\"accordion-body\">\n            <div class=\"mb-3\">\n              <div class=\"form-check form-switch\">\n                <input class=\"form-check-input\" type=\"checkbox\" id=\"motion_as_ds4\" \n                       v-model=\"config.motion_as_ds4\" true-value=\"enabled\" false-value=\"disabled\">\n                <label class=\"form-check-label\" for=\"motion_as_ds4\">\n                  {{ $t('config.motion_as_ds4') }}\n                </label>\n              </div>\n              <div class=\"form-text\">{{ $t('config.motion_as_ds4_desc') }}</div>\n            </div>\n            <div class=\"mb-3\">\n              <div class=\"form-check form-switch\">\n                <input class=\"form-check-input\" type=\"checkbox\" id=\"touchpad_as_ds4\" \n                       v-model=\"config.touchpad_as_ds4\" true-value=\"enabled\" false-value=\"disabled\">\n                <label class=\"form-check-label\" for=\"touchpad_as_ds4\">\n                  {{ $t('config.touchpad_as_ds4') }}\n                </label>\n              </div>\n              <div class=\"form-text\">{{ $t('config.touchpad_as_ds4_desc') }}</div>\n            </div>\n            <div class=\"mb-3\">\n              <div class=\"form-check form-switch\">\n                <input class=\"form-check-input\" type=\"checkbox\" id=\"enable_dsu_server\" \n                       v-model=\"config.enable_dsu_server\" true-value=\"enabled\" false-value=\"disabled\">\n                <label class=\"form-check-label\" for=\"enable_dsu_server\">\n                  {{ $t('config.enable_dsu_server') }}\n                </label>\n              </div>\n              <div class=\"form-text\">{{ $t('config.enable_dsu_server_desc') }}</div>\n            </div>\n            <div class=\"mb-3\" v-if=\"config.enable_dsu_server === 'enabled'\">\n              <label for=\"dsu_server_port\" class=\"form-label\">{{ $t('config.dsu_server_port') }}</label>\n              <input type=\"number\" class=\"form-control\" id=\"dsu_server_port\" placeholder=\"26760\"\n                     v-model=\"config.dsu_server_port\" min=\"1024\" max=\"65535\" />\n              <div class=\"form-text\">{{ $t('config.dsu_server_port_desc') }}</div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Home/Guide Button Emulation Timeout -->\n    <div class=\"mb-3\" v-if=\"config.controller === 'enabled'\">\n      <label for=\"back_button_timeout\" class=\"form-label\">{{ $t('config.back_button_timeout') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"back_button_timeout\" placeholder=\"-1\"\n             v-model=\"config.back_button_timeout\" />\n      <div class=\"form-text\">{{ $t('config.back_button_timeout_desc') }}</div>\n    </div>\n\n    <!-- Enable Keyboard Input -->\n    <hr>\n    <div class=\"mb-3\">\n      <div class=\"form-check form-switch\">\n        <input class=\"form-check-input\" type=\"checkbox\" id=\"keyboard\" \n               v-model=\"config.keyboard\" true-value=\"enabled\" false-value=\"disabled\">\n        <label class=\"form-check-label\" for=\"keyboard\">\n          {{ $t('config.keyboard') }}\n        </label>\n      </div>\n      <div class=\"form-text\">{{ $t('config.keyboard_desc') }}</div>\n    </div>\n\n    <!-- Key Repeat Delay-->\n    <div class=\"mb-3\" v-if=\"config.keyboard === 'enabled' && platform === 'windows'\">\n      <label for=\"key_repeat_delay\" class=\"form-label\">{{ $t('config.key_repeat_delay') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"key_repeat_delay\" placeholder=\"500\"\n             v-model=\"config.key_repeat_delay\" />\n      <div class=\"form-text\">{{ $t('config.key_repeat_delay_desc') }}</div>\n    </div>\n\n    <!-- Key Repeat Frequency-->\n    <div class=\"mb-3\" v-if=\"config.keyboard === 'enabled' && platform === 'windows'\">\n      <label for=\"key_repeat_frequency\" class=\"form-label\">{{ $t('config.key_repeat_frequency') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"key_repeat_frequency\" placeholder=\"24.9\"\n             v-model=\"config.key_repeat_frequency\" />\n      <div class=\"form-text\">{{ $t('config.key_repeat_frequency_desc') }}</div>\n    </div>\n\n    <!-- Always send scancodes -->\n    <div class=\"mb-3\" v-if=\"config.keyboard === 'enabled' && platform === 'windows'\">\n      <div class=\"form-check form-switch\">\n        <input class=\"form-check-input\" type=\"checkbox\" id=\"always_send_scancodes\" \n               v-model=\"config.always_send_scancodes\" true-value=\"enabled\" false-value=\"disabled\">\n        <label class=\"form-check-label\" for=\"always_send_scancodes\">\n          {{ $t('config.always_send_scancodes') }}\n        </label>\n      </div>\n      <div class=\"form-text\">{{ $t('config.always_send_scancodes_desc') }}</div>\n    </div>\n\n    <!-- Mapping Key AltRight to Key Windows -->\n    <div class=\"mb-3\" v-if=\"config.keyboard === 'enabled'\">\n      <div class=\"form-check form-switch\">\n        <input class=\"form-check-input\" type=\"checkbox\" id=\"key_rightalt_to_key_win\" \n               v-model=\"config.key_rightalt_to_key_win\" true-value=\"enabled\" false-value=\"disabled\">\n        <label class=\"form-check-label\" for=\"key_rightalt_to_key_win\">\n          {{ $t('config.key_rightalt_to_key_win') }}\n        </label>\n      </div>\n      <div class=\"form-text\">{{ $t('config.key_rightalt_to_key_win_desc') }}</div>\n    </div>\n\n    <!-- Enable Mouse Input -->\n    <hr>\n    <div class=\"mb-3\">\n      <div class=\"form-check form-switch\">\n        <input class=\"form-check-input\" type=\"checkbox\" id=\"mouse\" \n               v-model=\"config.mouse\" true-value=\"enabled\" false-value=\"disabled\">\n        <label class=\"form-check-label\" for=\"mouse\">\n          {{ $t('config.mouse') }}\n        </label>\n      </div>\n      <div class=\"form-text\">{{ $t('config.mouse_desc') }}</div>\n    </div>\n\n    <!-- High resolution scrolling support -->\n    <div class=\"mb-3\" v-if=\"config.mouse === 'enabled'\">\n      <div class=\"form-check form-switch\">\n        <input class=\"form-check-input\" type=\"checkbox\" id=\"high_resolution_scrolling\" \n               v-model=\"config.high_resolution_scrolling\" true-value=\"enabled\" false-value=\"disabled\">\n        <label class=\"form-check-label\" for=\"high_resolution_scrolling\">\n          {{ $t('config.high_resolution_scrolling') }}\n        </label>\n      </div>\n      <div class=\"form-text\">{{ $t('config.high_resolution_scrolling_desc') }}</div>\n    </div>\n\n    <!-- Native pen/touch support -->\n    <div class=\"mb-3\" v-if=\"config.mouse === 'enabled'\">\n      <div class=\"form-check form-switch\">\n        <input class=\"form-check-input\" type=\"checkbox\" id=\"native_pen_touch\" \n               v-model=\"config.native_pen_touch\" true-value=\"enabled\" false-value=\"disabled\">\n        <label class=\"form-check-label\" for=\"native_pen_touch\">\n          {{ $t('config.native_pen_touch') }}\n        </label>\n      </div>\n      <div class=\"form-text\">{{ $t('config.native_pen_touch_desc') }}</div>\n    </div>\n\n    <!-- Virtual mouse driver -->\n    <div class=\"mb-3\" v-if=\"config.mouse === 'enabled' && platform === 'windows'\">\n      <div class=\"form-check form-switch\">\n        <input class=\"form-check-input\" type=\"checkbox\" id=\"virtual_mouse\"\n               v-model=\"config.virtual_mouse\" true-value=\"enabled\" false-value=\"disabled\">\n        <label class=\"form-check-label\" for=\"virtual_mouse\">\n          {{ $t('config.virtual_mouse') }}\n          <span class=\"badge bg-warning text-dark ms-1\" style=\"font-size: 0.7em; vertical-align: middle;\">{{ $t('config.experimental') }}</span>\n        </label>\n      </div>\n      <div class=\"form-text\">{{ $t('config.virtual_mouse_desc') }}</div>\n\n      <!-- Tauri 环境：驱动管理面板 -->\n      <div v-if=\"isTauri\" class=\"vmouse-panel mt-2\">\n        <div class=\"vmouse-panel-header\">\n          <div class=\"vmouse-status-indicator\">\n            <span class=\"vmouse-dot\" :class=\"vmouseDotClass\"></span>\n            <span class=\"vmouse-status-label\">{{ vmouseStatusLabel }}</span>\n          </div>\n          <button class=\"vmouse-refresh-btn\" @click=\"refreshVmouseStatus\"\n                  :disabled=\"vmouseLoading\" :title=\"$t('config.vmouse_refresh')\">\n            <i class=\"fas fa-sync-alt\" :class=\"{ 'fa-spin': vmouseLoading }\"></i>\n          </button>\n        </div>\n        <div class=\"vmouse-panel-body\">\n          <button v-if=\"!vmouseStatus.installed\" class=\"vmouse-action-btn vmouse-install-btn\"\n                  @click=\"installVmouse\" :disabled=\"vmouseOperating\">\n            <span v-if=\"vmouseOperating\" class=\"vmouse-spinner\"></span>\n            <i v-else class=\"fas fa-download\"></i>\n            <span>{{ vmouseOperating ? $t('config.vmouse_installing') : $t('config.vmouse_install') }}</span>\n          </button>\n          <button v-else class=\"vmouse-action-btn vmouse-uninstall-btn\"\n                  @click=\"uninstallVmouse\" :disabled=\"vmouseOperating\">\n            <span v-if=\"vmouseOperating\" class=\"vmouse-spinner\"></span>\n            <i v-else class=\"fas fa-trash-alt\"></i>\n            <span>{{ vmouseOperating ? $t('config.vmouse_uninstalling') : $t('config.vmouse_uninstall') }}</span>\n          </button>\n        </div>\n      </div>\n\n      <!-- 非 Tauri 环境：显示提示信息 -->\n      <div v-else class=\"vmouse-helper mt-2\">\n        <i class=\"fas fa-info-circle me-1 text-info\"></i>\n        <span>{{ $t('config.vmouse_note') }}</span>\n      </div>\n    </div>\n\n    <!-- Draw mouse cursor in AMF -->\n    <div class=\"mb-3\">\n      <div class=\"form-check form-switch\">\n        <input class=\"form-check-input\" type=\"checkbox\" id=\"amf_draw_mouse_cursor\" \n               v-model=\"config.amf_draw_mouse_cursor\">\n        <label class=\"form-check-label\" for=\"amf_draw_mouse_cursor\">\n          {{ $t('config.amf_draw_mouse_cursor') }}\n        </label>\n      </div>\n      <div class=\"form-text\">{{ $t('config.amf_draw_mouse_cursor_desc') }}</div>\n    </div>\n\n  </div>\n</template>\n\n<style scoped>\n/* 非 Tauri 环境的提示信息 */\n.vmouse-helper {\n  display: flex;\n  align-items: center;\n  padding: 0.5rem 0.75rem;\n  background: var(--bs-secondary-bg);\n  border-radius: 8px;\n  border: 1px solid var(--bs-border-color);\n  color: var(--bs-secondary-color);\n  font-size: 0.85rem;\n}\n\n[data-bs-theme='dark'] .vmouse-helper {\n  background: rgba(255, 255, 255, 0.05);\n  border-color: rgba(255, 255, 255, 0.1);\n}\n\n/* 驱动管理面板 */\n.vmouse-panel {\n  border-radius: 10px;\n  border: 1px solid var(--bs-border-color);\n  overflow: hidden;\n  transition: border-color 0.2s ease;\n}\n\n.vmouse-panel:hover {\n  border-color: var(--bs-primary);\n}\n\n[data-bs-theme='dark'] .vmouse-panel {\n  border-color: rgba(255, 255, 255, 0.1);\n}\n\n[data-bs-theme='dark'] .vmouse-panel:hover {\n  border-color: rgba(var(--bs-primary-rgb), 0.5);\n}\n\n/* 面板头部 */\n.vmouse-panel-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 0.6rem 0.85rem;\n  background: var(--bs-tertiary-bg);\n}\n\n[data-bs-theme='dark'] .vmouse-panel-header {\n  background: rgba(255, 255, 255, 0.04);\n}\n\n/* 状态指示器 */\n.vmouse-status-indicator {\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n}\n\n.vmouse-dot {\n  width: 8px;\n  height: 8px;\n  border-radius: 50%;\n  flex-shrink: 0;\n}\n\n.vmouse-dot.dot-active {\n  background: #22c55e;\n  box-shadow: 0 0 6px rgba(34, 197, 94, 0.5);\n  animation: vmouse-pulse 2s ease-in-out infinite;\n}\n\n.vmouse-dot.dot-warning {\n  background: #f59e0b;\n  box-shadow: 0 0 6px rgba(245, 158, 11, 0.4);\n}\n\n.vmouse-dot.dot-inactive {\n  background: #9ca3af;\n}\n\n@keyframes vmouse-pulse {\n  0%, 100% { opacity: 1; }\n  50% { opacity: 0.5; }\n}\n\n.vmouse-status-label {\n  font-size: 0.8rem;\n  font-weight: 500;\n  color: var(--bs-body-color);\n  opacity: 0.85;\n}\n\n/* 刷新按钮 */\n.vmouse-refresh-btn {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 28px;\n  height: 28px;\n  border: none;\n  border-radius: 6px;\n  background: transparent;\n  color: var(--bs-secondary-color);\n  cursor: pointer;\n  transition: all 0.15s ease;\n}\n\n.vmouse-refresh-btn:hover:not(:disabled) {\n  background: var(--bs-secondary-bg);\n  color: var(--bs-body-color);\n}\n\n.vmouse-refresh-btn:disabled {\n  opacity: 0.4;\n  cursor: not-allowed;\n}\n\n/* 面板操作区 */\n.vmouse-panel-body {\n  padding: 0.6rem 0.85rem;\n}\n\n/* 操作按钮 */\n.vmouse-action-btn {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.4rem;\n  padding: 0.35rem 0.85rem;\n  border: 1px solid transparent;\n  border-radius: 6px;\n  font-size: 0.8rem;\n  font-weight: 500;\n  cursor: pointer;\n  transition: all 0.15s ease;\n}\n\n.vmouse-action-btn:disabled {\n  opacity: 0.65;\n  cursor: not-allowed;\n}\n\n.vmouse-install-btn {\n  background: var(--bs-primary);\n  color: #fff;\n  border-color: var(--bs-primary);\n}\n\n.vmouse-install-btn:hover:not(:disabled) {\n  filter: brightness(1.1);\n}\n\n.vmouse-uninstall-btn {\n  background: transparent;\n  color: var(--bs-danger);\n  border-color: var(--bs-danger);\n}\n\n.vmouse-uninstall-btn:hover:not(:disabled) {\n  background: var(--bs-danger);\n  color: #fff;\n}\n\n/* 操作中 spinner */\n.vmouse-spinner {\n  width: 14px;\n  height: 14px;\n  border: 2px solid currentColor;\n  border-top-color: transparent;\n  border-radius: 50%;\n  animation: vmouse-spin 0.6s linear infinite;\n}\n\n@keyframes vmouse-spin {\n  to { transform: rotate(360deg); }\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/Network.vue",
    "content": "<script setup>\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nconst props = defineProps([\n  'platform',\n  'config'\n])\n\nconst { t } = useI18n()\n\nconst defaultMoonlightPort = 47989\n\nconst config = ref(props.config)\nconst effectivePort = computed(() => +config.value?.port ?? defaultMoonlightPort)\nconst showCurlModal = ref(false)\nconst copied = ref(false)\n\nconst curlCommand = computed(() => {\n  if (!config.value.webhook_url) {\n    return ''\n  }\n  \n  const url = config.value.webhook_url\n  const payload = JSON.stringify({\n    msgtype: 'text',\n    text: {\n      content: 'Hello, Sunshine Foundation Webhook'\n    }\n  })\n  \n  // 转义 JSON 中的双引号，以便在双引号字符串中使用\n  const escapedPayload = payload.replace(/\"/g, '\\\\\"')\n  \n  return `curl -X POST \"${url}\" -H \"Content-Type: application/json\" -d \"${escapedPayload}\"`\n})\n\nconst showCurlCommand = () => {\n  showCurlModal.value = true\n  copied.value = false\n}\n\nconst closeCurlModal = () => {\n  showCurlModal.value = false\n  copied.value = false\n}\n\nconst copyCurlCommand = async () => {\n  try {\n    await navigator.clipboard.writeText(curlCommand.value)\n    copied.value = true\n    setTimeout(() => {\n      copied.value = false\n    }, 2000)\n  } catch (error) {\n    // 降级方案：使用传统方法\n    const textArea = document.createElement('textarea')\n    textArea.value = curlCommand.value\n    textArea.style.position = 'fixed'\n    textArea.style.opacity = '0'\n    document.body.appendChild(textArea)\n    textArea.select()\n    try {\n      document.execCommand('copy')\n      copied.value = true\n      setTimeout(() => {\n        copied.value = false\n      }, 2000)\n    } catch (err) {\n      alert(t('config.webhook_curl_copy_failed') || '复制失败，请手动选择并复制')\n    }\n    document.body.removeChild(textArea)\n  }\n}\n\nconst testWebhook = async () => {\n  if (!config.value.webhook_url) {\n    alert(t('config.webhook_test_url_required'))\n    return\n  }\n\n  try {\n    new URL(config.value.webhook_url)\n  } catch (error) {\n    alert(t('config.webhook_test_failed') + ': Invalid URL format')\n    return\n  }\n\n  try {\n    const testPayload = JSON.stringify({\n      msgtype: 'text',\n      text: {\n        content: 'Sunshine Webhook Test - This is a test message from Sunshine configuration page'\n      }\n    })\n\n    const controller = new AbortController()\n    const timeout = parseInt(config.value.webhook_timeout) || 1000\n    const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n    try {\n      const response = await fetch(config.value.webhook_url, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: testPayload,\n        signal: controller.signal\n      })\n\n      clearTimeout(timeoutId)\n\n      if (response.ok) {\n        alert(t('config.webhook_test_success'))\n        return\n      } else {\n        throw new Error(`HTTP ${response.status}`)\n      }\n    } catch (corsError) {\n      clearTimeout(timeoutId)\n\n      if (corsError.name === 'TypeError' && corsError.message.includes('Failed to fetch')) {\n        const noCorsController = new AbortController()\n        const noCorsTimeoutId = setTimeout(() => noCorsController.abort(), timeout)\n\n        try {\n          await fetch(config.value.webhook_url, {\n            method: 'POST',\n            mode: 'no-cors',\n            headers: {\n              'Content-Type': 'application/json',\n            },\n            body: testPayload,\n            signal: noCorsController.signal\n          })\n\n          clearTimeout(noCorsTimeoutId)\n          alert(t('config.webhook_test_success') + '\\n\\n' + t('config.webhook_test_success_cors_note'))\n        } catch (noCorsError) {\n          clearTimeout(noCorsTimeoutId)\n          if (noCorsError.name === 'AbortError') {\n            throw new Error(`Request timeout (${timeout}ms)`)\n          }\n          throw noCorsError\n        }\n      } else if (corsError.name === 'AbortError') {\n        throw new Error(`Request timeout (${timeout}ms)`)\n      } else {\n        throw corsError\n      }\n    }\n  } catch (error) {\n    if (error.name === 'AbortError' || error.message.includes('timeout')) {\n      const timeout = parseInt(config.value.webhook_timeout) || 1000\n      alert(t('config.webhook_test_failed') + `: Request timeout (${timeout}ms)`)\n    } else {\n      alert(`${t('config.webhook_test_failed')}: ${error.message || 'Unknown error'}\\n\\n${t('config.webhook_test_failed_note')}`)\n    }\n  }\n}\n</script>\n\n<template>\n  <div id=\"network\" class=\"config-page\">\n    <!-- UPnP -->\n    <div class=\"mb-3\">\n      <label for=\"upnp\" class=\"form-label\">{{ $t('config.upnp') }}</label>\n      <select id=\"upnp\" class=\"form-select\" v-model=\"config.upnp\">\n        <option value=\"disabled\">{{ $t('_common.disabled_def') }}</option>\n        <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.upnp_desc') }}</div>\n    </div>\n\n    <!-- Address family -->\n    <div class=\"mb-3\">\n      <label for=\"address_family\" class=\"form-label\">{{ $t('config.address_family') }}</label>\n      <select id=\"address_family\" class=\"form-select\" v-model=\"config.address_family\">\n        <option value=\"ipv4\">{{ $t('config.address_family_ipv4') }}</option>\n        <option value=\"both\">{{ $t('config.address_family_both') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.address_family_desc') }}</div>\n    </div>\n\n    <!-- Bind address -->\n    <div class=\"mb-3\">\n      <label for=\"bind_address\" class=\"form-label\">{{ $t('config.bind_address') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"bind_address\" v-model=\"config.bind_address\" />\n      <div class=\"form-text\">{{ $t('config.bind_address_desc') }}</div>\n    </div>\n\n    <!-- Port family -->\n    <div class=\"mb-3\">\n      <label for=\"port\" class=\"form-label\">{{ $t('config.port') }}</label>\n      <input type=\"number\" min=\"1029\" max=\"65514\" class=\"form-control\" id=\"port\" :placeholder=\"defaultMoonlightPort\"\n             v-model=\"config.port\" />\n      <div class=\"form-text\">{{ $t('config.port_desc') }}</div>\n      <div class=\"alert alert-danger\" v-if=\"(+effectivePort - 5) < 1024\">\n        <i class=\"fa-solid fa-xl fa-triangle-exclamation\"></i> {{ $t('config.port_alert_1') }}\n      </div>\n      <div class=\"alert alert-danger\" v-if=\"(+effectivePort + 21) > 65535\">\n        <i class=\"fa-solid fa-xl fa-triangle-exclamation\"></i> {{ $t('config.port_alert_2') }}\n      </div>\n      <table class=\"table\">\n        <thead>\n        <tr>\n          <th scope=\"col\">{{ $t('config.port_protocol') }}</th>\n          <th scope=\"col\">{{ $t('config.port_port') }}</th>\n          <th scope=\"col\">{{ $t('config.port_note') }}</th>\n        </tr>\n        </thead>\n        <tbody>\n        <tr>\n          <!-- HTTPS -->\n          <td>{{ $t('config.port_tcp') }}</td>\n          <td>{{+effectivePort - 5}}</td>\n          <td></td>\n        </tr>\n        <tr>\n          <!-- HTTP -->\n          <td>{{ $t('config.port_tcp') }}</td>\n          <td>{{+effectivePort}}</td>\n          <td>\n            <div class=\"alert alert-primary\" role=\"alert\" v-if=\"+effectivePort !== defaultMoonlightPort\">\n              <i class=\"fa-solid fa-xl fa-circle-info\"></i> {{ $t('config.port_http_port_note') }}\n            </div>\n          </td>\n        </tr>\n        <tr>\n          <!-- Web UI -->\n          <td>{{ $t('config.port_tcp') }}</td>\n          <td>{{+effectivePort + 1}}</td>\n          <td>{{ $t('config.port_web_ui') }}</td>\n        </tr>\n        <tr>\n          <!-- RTSP -->\n          <td>{{ $t('config.port_tcp') }}</td>\n          <td>{{+effectivePort + 21}}</td>\n          <td></td>\n        </tr>\n        <tr>\n          <!-- Video, Control, Audio, Mic -->\n          <td>{{ $t('config.port_udp') }}</td>\n          <td>{{+effectivePort + 9}} - {{+effectivePort + 12}}</td>\n          <td></td>\n        </tr>\n        </tbody>\n      </table>\n      <div class=\"alert alert-warning\" v-if=\"config.origin_web_ui_allowed === 'wan'\">\n        <i class=\"fa-solid fa-xl fa-triangle-exclamation\"></i> {{ $t('config.port_warning') }}\n      </div>\n    </div>\n\n    <!-- Origin Web UI Allowed -->\n    <div class=\"mb-3\">\n      <label for=\"origin_web_ui_allowed\" class=\"form-label\">{{ $t('config.origin_web_ui_allowed') }}</label>\n      <select id=\"origin_web_ui_allowed\" class=\"form-select\" v-model=\"config.origin_web_ui_allowed\">\n        <option value=\"pc\">{{ $t('config.origin_web_ui_allowed_pc') }}</option>\n        <option value=\"lan\">{{ $t('config.origin_web_ui_allowed_lan') }}</option>\n        <option value=\"wan\">{{ $t('config.origin_web_ui_allowed_wan') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.origin_web_ui_allowed_desc') }}</div>\n    </div>\n\n    <!-- External IP -->\n    <div class=\"mb-3\">\n      <label for=\"external_ip\" class=\"form-label\">{{ $t('config.external_ip') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"external_ip\" placeholder=\"123.456.789.12\" v-model=\"config.external_ip\" />\n      <div class=\"form-text\">{{ $t('config.external_ip_desc') }}</div>\n    </div>\n\n    <!-- LAN Encryption Mode -->\n    <div class=\"mb-3\">\n      <label for=\"lan_encryption_mode\" class=\"form-label\">{{ $t('config.lan_encryption_mode') }}</label>\n      <select id=\"lan_encryption_mode\" class=\"form-select\" v-model=\"config.lan_encryption_mode\">\n        <option value=\"0\">{{ $t('_common.disabled_def') }}</option>\n        <option value=\"1\">{{ $t('config.lan_encryption_mode_1') }}</option>\n        <option value=\"2\">{{ $t('config.lan_encryption_mode_2') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.lan_encryption_mode_desc') }}</div>\n    </div>\n\n    <!-- WAN Encryption Mode -->\n    <div class=\"mb-3\">\n      <label for=\"wan_encryption_mode\" class=\"form-label\">{{ $t('config.wan_encryption_mode') }}</label>\n      <select id=\"wan_encryption_mode\" class=\"form-select\" v-model=\"config.wan_encryption_mode\">\n        <option value=\"0\">{{ $t('_common.disabled') }}</option>\n        <option value=\"1\">{{ $t('config.wan_encryption_mode_1') }}</option>\n        <option value=\"2\">{{ $t('config.wan_encryption_mode_2') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.wan_encryption_mode_desc') }}</div>\n    </div>\n\n    <!-- CLOSE VERIFY SAFE -->\n    <div class=\"mb-3\">\n      <label for=\"close_verify_safe\" class=\"form-label\">{{ $t('config.close_verify_safe') }}</label>\n      <select id=\"close_verify_safe\" class=\"form-select\" v-model=\"config.close_verify_safe\">\n        <option value=\"disabled\">{{ $t('_common.disabled_def') }}</option>\n        <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.close_verify_safe_desc') }}</div>\n    </div>\n\n    <!-- MDNS BROADCAST -->\n    <div class=\"mb-3\">\n      <label for=\"mdns_broadcast\" class=\"form-label\">{{ $t('config.mdns_broadcast') }}</label>\n      <select id=\"mdns_broadcast\" class=\"form-select\" v-model=\"config.mdns_broadcast\">\n        <option value=\"disabled\">{{ $t('_common.disabled') }}</option>\n        <option value=\"enabled\">{{ $t('_common.enabled_def') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.mdns_broadcast_desc') }}</div>\n    </div>\n\n    <!-- Ping Timeout -->\n    <div class=\"mb-3\">\n      <label for=\"ping_timeout\" class=\"form-label\">{{ $t('config.ping_timeout') }}</label>\n      <input type=\"text\" class=\"form-control\" id=\"ping_timeout\" placeholder=\"10000\" v-model=\"config.ping_timeout\" />\n      <div class=\"form-text\">{{ $t('config.ping_timeout_desc') }}</div>\n    </div>\n\n    <!-- Webhook Settings -->\n    <div class=\"accordion\">\n      <div class=\"accordion-item\">\n        <h2 class=\"accordion-header\">\n          <button class=\"accordion-button\" type=\"button\" data-bs-toggle=\"collapse\"\n                  data-bs-target=\"#webhook-collapse\">\n            {{ $t('config.webhook_group') }}\n          </button>\n        </h2>\n        <div id=\"webhook-collapse\" class=\"accordion-collapse collapse show\">\n          <div class=\"accordion-body\">\n            <!-- Webhook Enable -->\n            <div class=\"mb-3\">\n              <label for=\"webhook_enabled\" class=\"form-label\">{{ $t('config.webhook_enabled') }}</label>\n              <select id=\"webhook_enabled\" class=\"form-select\" v-model=\"config.webhook_enabled\">\n                <option value=\"disabled\">{{ $t('_common.disabled_def') }}</option>\n                <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.webhook_enabled_desc') }}</div>\n            </div>\n\n            <!-- Webhook URL -->\n            <div class=\"mb-3\" v-if=\"config.webhook_enabled === 'enabled'\">\n              <label for=\"webhook_url\" class=\"form-label\">{{ $t('config.webhook_url') }}</label>\n              <div class=\"input-group\">\n                <input type=\"url\" class=\"form-control\" id=\"webhook_url\" placeholder=\"https://example.com/webhook\" v-model=\"config.webhook_url\" />\n                <button class=\"btn btn-outline-info\" type=\"button\" @click=\"testWebhook\" :disabled=\"!config.webhook_url || config.webhook_enabled !== 'enabled'\">\n                  <i class=\"fas fa-paper-plane me-1\"></i>{{ $t('config.webhook_test') }}\n                </button>\n                <button class=\"btn btn-outline-info\" type=\"button\" @click=\"showCurlCommand\" :disabled=\"!config.webhook_url || config.webhook_enabled !== 'enabled'\">\n                  <i class=\"fas fa-terminal me-1\"></i>{{ $t('config.webhook_curl_command') }}\n                </button>\n              </div>\n              <div class=\"form-text\">{{ $t('config.webhook_url_desc') }}</div>\n            </div>\n\n            <!-- Skip SSL Verify -->\n            <div class=\"mb-3\" v-if=\"config.webhook_enabled === 'enabled'\">\n              <label for=\"webhook_skip_ssl_verify\" class=\"form-label\">{{ $t('config.webhook_skip_ssl_verify') }}</label>\n              <select id=\"webhook_skip_ssl_verify\" class=\"form-select\" v-model=\"config.webhook_skip_ssl_verify\">\n                <option value=\"disabled\">{{ $t('_common.disabled_def') }}</option>\n                <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.webhook_skip_ssl_verify_desc') }}</div>\n            </div>\n\n            <!-- Webhook Timeout -->\n            <div class=\"mb-3\" v-if=\"config.webhook_enabled === 'enabled'\">\n              <label for=\"webhook_timeout\" class=\"form-label\">{{ $t('config.webhook_timeout') }}</label>\n              <input type=\"number\" min=\"100\" max=\"5000\" class=\"form-control\" id=\"webhook_timeout\" placeholder=\"1000\" v-model=\"config.webhook_timeout\" />\n              <div class=\"form-text\">{{ $t('config.webhook_timeout_desc') }}</div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n  </div>\n\n  <!-- Curl Command Modal -->\n  <Transition name=\"fade\">\n    <div v-if=\"showCurlModal\" class=\"curl-command-overlay\" @click.self=\"closeCurlModal\">\n      <div class=\"curl-command-modal\">\n        <div class=\"curl-command-header\">\n          <h5>\n            <i class=\"fas fa-terminal me-2\"></i>{{ $t('config.webhook_curl_command') || 'Curl 命令' }}\n          </h5>\n          <button class=\"btn-close\" @click=\"closeCurlModal\"></button>\n        </div>\n        <div class=\"curl-command-body\">\n          <p class=\"text-muted mb-3\">{{ $t('config.webhook_curl_command_desc') || '复制以下命令到终端中执行，可以测试 webhook 是否正常工作：' }}</p>\n          <div class=\"curl-command-container\">\n            <pre class=\"curl-command\" id=\"curlCommandText\">{{ curlCommand }}</pre>\n          </div>\n          <div class=\"alert alert-info mt-3\" v-if=\"copied\">\n            <i class=\"fas fa-check-circle me-2\"></i>{{ $t('_common.copied') || '已复制到剪贴板' }}\n          </div>\n        </div>\n        <div class=\"curl-command-footer\">\n          <button class=\"copy-btn\" @click=\"copyCurlCommand\" type=\"button\">\n            <i class=\"fas fa-copy me-1\"></i>{{ $t('_common.copy') }}\n          </button>\n          <button type=\"button\" class=\"btn btn-secondary\" @click=\"closeCurlModal\">{{ $t('_common.close') || '关闭' }}</button>\n        </div>\n      </div>\n    </div>\n  </Transition>\n</template>\n\n<style scoped>\n/* Curl Command Modal - 使用 ScanResultModal 样式 */\n.curl-command-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 100vw;\n  height: 100vh;\n  margin: 0;\n  background: var(--overlay-bg, rgba(0, 0, 0, 0.7));\n  backdrop-filter: blur(8px);\n  z-index: 9999;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: var(--spacing-lg, 20px);\n  overflow: hidden;\n  \n  [data-bs-theme='light'] & {\n    background: rgba(0, 0, 0, 0.5);\n  }\n}\n\n.curl-command-modal {\n  background: var(--modal-bg, rgba(30, 30, 50, 0.95));\n  border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.2));\n  border-radius: var(--border-radius-xl, 12px);\n  width: 100%;\n  max-width: 700px;\n  max-height: 80vh;\n  display: flex;\n  flex-direction: column;\n  backdrop-filter: blur(20px);\n  box-shadow: var(--shadow-xl, 0 25px 50px rgba(0, 0, 0, 0.5));\n  animation: modalSlideUp 0.3s ease;\n  \n  [data-bs-theme='light'] & {\n    background: rgba(255, 255, 255, 0.95);\n    border: 1px solid rgba(0, 0, 0, 0.15);\n    box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);\n  }\n}\n\n@keyframes modalSlideUp {\n  from {\n    transform: translateY(20px);\n    opacity: 0;\n  }\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n.curl-command-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-bottom: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  h5 {\n    margin: 0;\n    color: var(--text-primary, #fff);\n    font-size: var(--font-size-lg, 1.1rem);\n    font-weight: 600;\n    display: flex;\n    align-items: center;\n    gap: var(--spacing-sm, 8px);\n  }\n  \n  [data-bs-theme='light'] & {\n    border-bottom: 1px solid rgba(0, 0, 0, 0.1);\n    \n    h5 {\n      color: #000000;\n    }\n  }\n}\n\n.curl-command-body {\n  padding: var(--spacing-lg, 24px);\n  overflow-y: auto;\n  flex: 1;\n  color: var(--text-primary, #fff);\n  \n  p, span, div {\n    color: var(--text-primary, #fff);\n  }\n  \n  .text-muted {\n    color: rgba(255, 255, 255, 0.6);\n  }\n  \n  .alert {\n    color: var(--text-primary, #fff);\n  }\n\n  .alert-info {\n    background: rgba(23, 162, 184, 0.2);\n    border-color: rgba(23, 162, 184, 0.5);\n  }\n  \n  [data-bs-theme='light'] & {\n    color: #000000;\n    \n    p, span, div {\n      color: #000000;\n    }\n    \n    .text-muted {\n      color: rgba(0, 0, 0, 0.6);\n    }\n    \n    .alert {\n      color: #000000;\n    }\n\n    .alert-info {\n      background: rgba(23, 162, 184, 0.15);\n      border-color: rgba(23, 162, 184, 0.4);\n    }\n  }\n}\n\n.curl-command-footer {\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  gap: 10px;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-top: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n  \n  [data-bs-theme='light'] & {\n    border-top: 1px solid rgba(0, 0, 0, 0.1);\n  }\n}\n\n.curl-command-container {\n  position: relative;\n  background-color: rgba(255, 255, 255, 0.05);\n  border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n  border-radius: 4px;\n  padding: 15px;\n  \n  [data-bs-theme='light'] & {\n    background-color: rgba(248, 249, 250, 0.8);\n    border: 1px solid rgba(0, 0, 0, 0.15);\n  }\n}\n\n.curl-command {\n  margin: 0;\n  padding: 0;\n  font-family: 'Courier New', monospace;\n  font-size: 0.9rem;\n  color: var(--text-primary, #fff);\n  background: transparent;\n  border: none;\n  white-space: pre-wrap;\n  word-break: break-all;\n  overflow-x: auto;\n  max-height: 300px;\n  overflow-y: auto;\n  \n  [data-bs-theme='light'] & {\n    color: #000000;\n  }\n}\n\n\n/* Vue 过渡动画 */\n.fade-enter-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/audiovideo/AdapterNameSelector.vue",
    "content": "<script setup>\nimport { ref, computed } from 'vue'\nimport { $tp } from '../../../platform-i18n'\nimport PlatformLayout from '../../../components/layout/PlatformLayout.vue'\n\nconst props = defineProps([\n  'platform',\n  'config'\n])\n\nconst config = ref(props.config)\n\n// 按 name 去重，同一名称只保留一项（保持首次出现顺序）\nconst uniqueAdapters = computed(() => {\n  const list = config.value?.adapters ?? []\n  const seen = new Set()\n  return list.filter((a) => {\n    const name = a?.name ?? ''\n    if (seen.has(name)) return false\n    seen.add(name)\n    return true\n  })\n})\n</script>\n\n<template>\n  <div class=\"mb-3\" v-if=\"platform !== 'macos'\">\n    <label for=\"adapter_name\" class=\"form-label\">{{ $t('config.adapter_name') }}</label>\n    <PlatformLayout :platform=\"platform\">\n      <template #windows>\n        <select id=\"adapter_name\" class=\"form-select\" v-model=\"config.adapter_name\">\n          <option value=\"\">{{ $t(\"_common.autodetect\") }}</option>\n          <option v-for=\"(adapter, index) in uniqueAdapters\" :value=\"adapter.name\" :key=\"index\">\n            {{ adapter.name }}\n          </option>\n        </select>\n      </template>\n      <template #linux>\n        <input type=\"text\" class=\"form-control\" id=\"adapter_name\"\n           :placeholder=\"$tp('config.adapter_name_placeholder', '/dev/dri/renderD128')\"\n           v-model=\"config.adapter_name\" />\n      </template>\n    </PlatformLayout>\n    <div class=\"form-text\">\n      <PlatformLayout :platform=\"platform\">\n        <template #windows>\n          {{ $t('config.adapter_name_desc_windows') }}<br>\n        </template>\n        <template #linux>\n          {{ $t('config.adapter_name_desc_linux_1') }}<br>\n          <pre>ls /dev/dri/renderD*  # {{ $t('config.adapter_name_desc_linux_2') }}</pre>\n          <pre>\n              vainfo --display drm --device /dev/dri/renderD129 | \\\n                grep -E \"((VAProfileH264High|VAProfileHEVCMain|VAProfileHEVCMain10).*VAEntrypointEncSlice)|Driver version\"\n            </pre>\n          {{ $t('config.adapter_name_desc_linux_3') }}<br>\n          <i>VAProfileH264High   : VAEntrypointEncSlice</i>\n        </template>\n      </PlatformLayout>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\nimport { $tp } from '../../../platform-i18n'\nimport PlatformLayout from '../../../components/layout/PlatformLayout.vue'\nimport Checkbox from '../../../components/Checkbox.vue'\n\nconst props = defineProps({\n  platform: String,\n  config: Object,\n})\n\nconst config = ref(props.config)\nconst display_mode_remapping = ref(props.display_mode_remapping || [])\n\n// TODO: Sample for use in PR #2032\nfunction getRemappingType() {\n  // Assuming here that at least one setting is set to \"automatic\"\n  if (config.value.resolution_change !== 'automatic') {\n    return 'refresh_rate_only'\n  }\n  if (config.value.refresh_rate_change !== 'automatic') {\n    return 'resolution_only'\n  }\n  return ''\n}\n\nfunction addRemapping(type) {\n  let template = {\n    type: type,\n    received_resolution: '',\n    received_fps: '',\n    final_resolution: '',\n    final_refresh_rate: '',\n  }\n\n  display_mode_remapping.value.push(template)\n}\n</script>\n\n<template>\n  <PlatformLayout :platform=\"platform\">\n    <template #windows>\n      <div class=\"mb-3 accordion\">\n        <div class=\"accordion-item\">\n          <h2 class=\"accordion-header\" id=\"panelsStayOpen-headingOne\">\n            <button\n              class=\"accordion-button\"\n              type=\"button\"\n              data-bs-toggle=\"collapse\"\n              data-bs-target=\"#panelsStayOpen-collapseOne\"\n            >\n              {{ $tp('config.display_device_options') }}\n            </button>\n          </h2>\n          <div\n            id=\"panelsStayOpen-collapseOne\"\n            class=\"accordion-collapse collapse show\"\n            aria-labelledby=\"panelsStayOpen-headingOne\"\n          >\n            <div class=\"accordion-body\">\n              <div class=\"mb-3\">\n                <label class=\"form-label\">\n                  {{ $tp('config.display_device_options_note') }}\n                </label>\n                <div class=\"form-text\">\n                  <p style=\"white-space: pre-line\">{{ $tp('config.display_device_options_note_desc') }}</p>\n                </div>\n              </div>\n\n              <!-- Display device preparation -->\n              <div class=\"mb-3\">\n                <label for=\"display_device_prep\" class=\"form-label\">\n                  {{ $tp('config.display_device_prep') }}\n                </label>\n                <select id=\"display_device_prep\" class=\"form-select\" v-model=\"config.display_device_prep\">\n                  <option value=\"no_operation\">{{ $tp('config.display_device_prep_no_operation') }}</option>\n                  <option value=\"ensure_active\">{{ $tp('config.display_device_prep_ensure_active') }}</option>\n                  <option value=\"ensure_primary\">{{ $tp('config.display_device_prep_ensure_primary') }}</option>\n                  <option value=\"ensure_secondary\">{{ $tp('config.display_device_prep_ensure_secondary') }}</option>\n                  <option value=\"ensure_only_display\">\n                    {{ $tp('config.display_device_prep_ensure_only_display') }}\n                  </option>\n                </select>\n                <div class=\"form-text\" v-if=\"config.display_device_prep\">\n                  {{ $tp('config.display_device_prep_' + config.display_device_prep + '_desc') }}\n                </div>\n              </div>\n\n              <!-- Resolution change -->\n              <div class=\"mb-3\">\n                <label for=\"resolution_change\" class=\"form-label\">\n                  {{ $tp('config.resolution_change') }}\n                </label>\n                <select id=\"resolution_change\" class=\"form-select\" v-model=\"config.resolution_change\">\n                  <option value=\"no_operation\">{{ $tp('config.resolution_change_no_operation') }}</option>\n                  <option value=\"automatic\">{{ $tp('config.resolution_change_automatic') }}</option>\n                  <option value=\"manual\">{{ $tp('config.resolution_change_manual') }}</option>\n                </select>\n                <div\n                  class=\"form-text\"\n                  v-if=\"config.resolution_change === 'automatic' || config.resolution_change === 'manual'\"\n                >\n                  {{ $tp('config.resolution_change_ogs_desc') }}\n                </div>\n\n                <!-- Manual resolution -->\n                <div class=\"mt-2 ps-4\" v-if=\"config.resolution_change === 'manual'\">\n                  <div class=\"form-text\">\n                    {{ $tp('config.resolution_change_manual_desc') }}\n                  </div>\n                  <input\n                    type=\"text\"\n                    class=\"form-control\"\n                    id=\"manual_resolution\"\n                    placeholder=\"2560x1440\"\n                    v-model=\"config.manual_resolution\"\n                  />\n                </div>\n              </div>\n\n              <!-- Refresh rate change -->\n              <div class=\"mb-3\">\n                <label for=\"refresh_rate_change\" class=\"form-label\">\n                  {{ $tp('config.refresh_rate_change') }}\n                </label>\n                <select id=\"refresh_rate_change\" class=\"form-select\" v-model=\"config.refresh_rate_change\">\n                  <option value=\"no_operation\">{{ $tp('config.refresh_rate_change_no_operation') }}</option>\n                  <option value=\"automatic\">{{ $tp('config.refresh_rate_change_automatic') }}</option>\n                  <option value=\"manual\">{{ $tp('config.refresh_rate_change_manual_desc') }}</option>\n                </select>\n\n                <!-- Manual refresh rate -->\n                <div class=\"mt-2 ps-4\" v-if=\"config.refresh_rate_change === 'manual'\">\n                  <div class=\"form-text\">\n                    {{ $tp('config.refresh_rate_change_manual_desc') }}\n                  </div>\n                  <input\n                    type=\"text\"\n                    class=\"form-control\"\n                    id=\"manual_refresh_rate\"\n                    placeholder=\"59.95\"\n                    v-model=\"config.manual_refresh_rate\"\n                  />\n                </div>\n              </div>\n\n              <!-- HDR preparation -->\n              <div class=\"mb-3\">\n                <label for=\"hdr_prep\" class=\"form-label\">\n                  {{ $tp('config.hdr_prep') }}\n                </label>\n                <select id=\"hdr_prep\" class=\"form-select\" v-model=\"config.hdr_prep\">\n                  <option value=\"no_operation\">{{ $tp('config.hdr_prep_no_operation') }}</option>\n                  <option value=\"automatic\">{{ $tp('config.hdr_prep_automatic') }}</option>\n                </select>\n              </div>\n\n              <Checkbox\n                class=\"mb-3\"\n                id=\"hdr_luminance_analysis\"\n                locale-prefix=\"config\"\n                v-model=\"config.hdr_luminance_analysis\"\n                default=\"true\"\n              ></Checkbox>\n            </div>\n          </div>\n        </div>\n      </div>\n    </template>\n    <template #linux> </template>\n    <template #macos> </template>\n  </PlatformLayout>\n</template>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/audiovideo/DisplayModesSettings.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\nimport { $tp } from '../../../platform-i18n'\nimport PlatformLayout from '../../../components/layout/PlatformLayout.vue'\n\nconst props = defineProps([\n  'platform',\n  'config',\n])\n\nconst config = ref(props.config)\n</script>\n\n<template>\n  <div class=\"mb-3\">\n\n    <!--max_bitrate-->\n    <div class=\"mb-3\">\n      <label for=\"max_bitrate\" class=\"form-label\">{{ $t(\"config.max_bitrate\") }}</label>\n      <input type=\"number\" class=\"form-control\" id=\"max_bitrate\" placeholder=\"0\" v-model=\"config.max_bitrate\" />\n      <div class=\"form-text\">{{ $t(\"config.max_bitrate_desc\") }}</div>\n    </div>\n\n    <!-- Variable Refresh Rate -->\n    <div class=\"mb-3\">\n      <div class=\"form-check form-switch\">\n        <input class=\"form-check-input\" type=\"checkbox\" id=\"variable_refresh_rate\" v-model=\"config.variable_refresh_rate\" true-value=\"enabled\" false-value=\"disabled\" />\n        <label class=\"form-check-label\" for=\"variable_refresh_rate\">\n          {{ $t(\"config.variable_refresh_rate\") }}\n        </label>\n      </div>\n      <div class=\"form-text\">{{ $t(\"config.variable_refresh_rate_desc\") }}</div>\n    </div>\n\n    <!-- Minimum FPS Target -->\n    <div class=\"mb-3\">\n      <label for=\"minimum_fps_target\" class=\"form-label\">{{ $t(\"config.minimum_fps_target\") }}</label>\n      <input type=\"number\" class=\"form-control\" id=\"minimum_fps_target\" placeholder=\"0\" v-model.number=\"config.minimum_fps_target\" min=\"0\" max=\"1000\" />\n      <div class=\"form-text\">{{ $t(\"config.minimum_fps_target_desc\") }}</div>\n    </div>\n\n  </div>\n</template>\n\n<style scoped>\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/audiovideo/ExperimentalFeatures.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { $tp } from '../../../platform-i18n'\nimport PlatformLayout from '../../../components/layout/PlatformLayout.vue'\nimport Checkbox from '../../../components/Checkbox.vue'\n\nconst props = defineProps({\n  platform: String,\n  config: Object,\n  display_mode_remapping: Array,\n})\n\nconst { t } = useI18n()\nconst config = ref(props.config)\nconst display_mode_remapping = ref(props.display_mode_remapping || [])\n\nfunction getRemappingType() {\n  if (config.value.resolution_change !== 'automatic') {\n    return 'refresh_rate_only'\n  }\n  if (config.value.refresh_rate_change !== 'automatic') {\n    return 'resolution_only'\n  }\n  return ''\n}\n\nfunction addRemapping(type) {\n  display_mode_remapping.value.push({\n    type: type,\n    received_resolution: '',\n    received_fps: '',\n    final_resolution: '',\n    final_refresh_rate: '',\n  })\n}\n</script>\n\n<template>\n  <PlatformLayout :platform=\"platform\">\n    <template #windows>\n      <div class=\"mb-3 accordion\">\n        <div class=\"accordion-item\">\n          <h2 class=\"accordion-header\">\n            <button\n              class=\"accordion-button collapsed\"\n              type=\"button\"\n              data-bs-toggle=\"collapse\"\n              data-bs-target=\"#experimental-features-collapse\"\n            >\n              {{ $t('config.experimental_features') }}\n            </button>\n          </h2>\n          <div\n            id=\"experimental-features-collapse\"\n            class=\"accordion-collapse collapse\"\n          >\n            <div class=\"accordion-body\">\n              <!-- Capture Target -->\n              <div class=\"mb-3\">\n                <label for=\"capture_target\" class=\"form-label\">{{ $t('config.capture_target') }}</label>\n                <select id=\"capture_target\" class=\"form-select\" v-model=\"config.capture_target\">\n                  <option value=\"display\">{{ $t('config.capture_target_display') }}</option>\n                  <option value=\"window\">{{ $t('config.capture_target_window') }}</option>\n                </select>\n                <div class=\"form-text\">{{ $t('config.capture_target_desc') }}</div>\n              </div>\n\n              <!-- Window Title (only shown when capture_target is window) -->\n              <div class=\"mb-3\" v-if=\"config.capture_target === 'window'\">\n                <label for=\"window_title\" class=\"form-label\">{{ $t('config.window_title') }}</label>\n                <input\n                  type=\"text\"\n                  class=\"form-control\"\n                  id=\"window_title\"\n                  :placeholder=\"$t('config.window_title_placeholder')\"\n                  v-model=\"config.window_title\"\n                />\n                <div class=\"form-text\">{{ $t('config.window_title_desc') }}</div>\n              </div>\n\n              <!-- WGC Disable Secure Desktop -->\n              <Checkbox\n                class=\"mb-3\"\n                id=\"wgc_disable_secure_desktop\"\n                locale-prefix=\"config\"\n                v-model=\"config.wgc_disable_secure_desktop\"\n                default=\"false\"\n              ></Checkbox>\n\n              <!-- Display Mode Remapping -->\n              <div\n                class=\"mb-3\"\n                v-if=\"config.resolution_change === 'automatic' || config.refresh_rate_change === 'automatic'\"\n              >\n                <label class=\"form-label\">\n                  {{ $tp('config.display_mode_remapping') }}\n                </label>\n                <div class=\"d-flex flex-column\">\n                  <div class=\"form-text\">\n                    <p style=\"white-space: pre-line\">{{ $tp('config.display_mode_remapping_desc') }}</p>\n                    <p v-if=\"getRemappingType() === ''\" style=\"white-space: pre-line\">\n                      {{ $tp('config.display_mode_remapping_default_mode_desc') }}\n                    </p>\n                    <p v-if=\"getRemappingType() === 'resolution_only'\" style=\"white-space: pre-line\">\n                      {{ $tp('config.display_mode_remapping_resolution_only_mode_desc') }}\n                    </p>\n                  </div>\n\n                  <table\n                    class=\"table\"\n                    v-if=\"display_mode_remapping.filter((value) => value.type === getRemappingType()).length > 0\"\n                  >\n                    <thead>\n                      <tr>\n                        <th scope=\"col\" v-if=\"getRemappingType() !== 'refresh_rate_only'\">\n                          {{ $tp('config.display_mode_remapping_received_resolution') }}\n                        </th>\n                        <th scope=\"col\" v-if=\"getRemappingType() !== 'resolution_only'\">\n                          {{ $tp('config.display_mode_remapping_received_fps') }}\n                        </th>\n                        <th scope=\"col\" v-if=\"getRemappingType() !== 'refresh_rate_only'\">\n                          {{ $tp('config.display_mode_remapping_final_resolution') }}\n                        </th>\n                        <th scope=\"col\" v-if=\"getRemappingType() !== 'resolution_only'\">\n                          {{ $tp('config.display_mode_remapping_final_refresh_rate') }}\n                        </th>\n                        <th scope=\"col\"></th>\n                      </tr>\n                    </thead>\n                    <tbody>\n                      <tr v-for=\"(c, i) in display_mode_remapping\" :key=\"i\">\n                        <template v-if=\"c.type === '' && c.type === getRemappingType()\">\n                          <td>\n                            <input\n                              type=\"text\"\n                              class=\"form-control monospace\"\n                              v-model=\"c.received_resolution\"\n                              :placeholder=\"`1920x1080 (${$t('config.display_mode_remapping_optional')})`\"\n                            />\n                          </td>\n                          <td>\n                            <input\n                              type=\"text\"\n                              class=\"form-control monospace\"\n                              v-model=\"c.received_fps\"\n                              :placeholder=\"`60 (${$t('config.display_mode_remapping_optional')})`\"\n                            />\n                          </td>\n                          <td>\n                            <input\n                              type=\"text\"\n                              class=\"form-control monospace\"\n                              v-model=\"c.final_resolution\"\n                              :placeholder=\"`2560x1440 (${$t('config.display_mode_remapping_optional')})`\"\n                            />\n                          </td>\n                          <td>\n                            <input\n                              type=\"text\"\n                              class=\"form-control monospace\"\n                              v-model=\"c.final_refresh_rate\"\n                              :placeholder=\"`119.95 (${$t('config.display_mode_remapping_optional')})`\"\n                            />\n                          </td>\n                          <td>\n                            <button class=\"btn btn-danger\" @click=\"display_mode_remapping.splice(i, 1)\">\n                              <i class=\"fas fa-trash\"></i>\n                            </button>\n                          </td>\n                        </template>\n                        <template v-if=\"c.type === 'resolution_only' && c.type === getRemappingType()\">\n                          <td>\n                            <input\n                              type=\"text\"\n                              class=\"form-control monospace\"\n                              v-model=\"c.received_resolution\"\n                              placeholder=\"1920x1080\"\n                            />\n                          </td>\n                          <td>\n                            <input\n                              type=\"text\"\n                              class=\"form-control monospace\"\n                              v-model=\"c.final_resolution\"\n                              placeholder=\"2560x1440\"\n                            />\n                          </td>\n                          <td>\n                            <button class=\"btn btn-danger\" @click=\"display_mode_remapping.splice(i, 1)\">\n                              <i class=\"fas fa-trash\"></i>\n                            </button>\n                          </td>\n                        </template>\n                        <template v-if=\"c.type === 'refresh_rate_only' && c.type === getRemappingType()\">\n                          <td>\n                            <input\n                              type=\"text\"\n                              class=\"form-control monospace\"\n                              v-model=\"c.received_fps\"\n                              placeholder=\"60\"\n                            />\n                          </td>\n                          <td>\n                            <input\n                              type=\"text\"\n                              class=\"form-control monospace\"\n                              v-model=\"c.final_refresh_rate\"\n                              placeholder=\"119.95\"\n                            />\n                          </td>\n                          <td>\n                            <button class=\"btn btn-danger\" @click=\"display_mode_remapping.splice(i, 1)\">\n                              <i class=\"fas fa-trash\"></i>\n                            </button>\n                          </td>\n                        </template>\n                      </tr>\n                    </tbody>\n                  </table>\n                  <button\n                    class=\"ms-0 mt-2 btn btn-success\"\n                    style=\"margin: 0 auto\"\n                    @click=\"addRemapping(getRemappingType())\"\n                  >\n                    &plus; Add\n                  </button>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </template>\n    <template #linux> </template>\n    <template #macos> </template>\n  </PlatformLayout>\n</template>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/audiovideo/LegacyDisplayOutputSelector.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\nimport { $tp } from '../../../platform-i18n'\nimport PlatformLayout from '../../../components/layout/PlatformLayout.vue'\n\nconst props = defineProps([\n  'platform',\n  'config'\n])\n\nconst config = ref(props.config)\nconst outputNamePlaceholder = (props.platform === 'windows') ? '\\\\\\\\.\\\\DISPLAY1' : '0'\n</script>\n\n<template>\n  <div class=\"mb-3\">\n    <label for=\"output_name\" class=\"form-label\">{{ $tp('config.output_name') }}</label>\n    <input type=\"text\" class=\"form-control\" id=\"output_name\" :placeholder=\"outputNamePlaceholder\"\n           v-model=\"config.output_name\"/>\n    <div class=\"form-text\">\n      {{ $tp('config.output_name_desc') }}<br>\n      <PlatformLayout :platform=\"platform\">\n        <template #windows>\n          <pre>tools\\dxgi-info.exe</pre>\n        </template>\n        <template #linux>\n            <pre style=\"white-space: pre-line;\">\n              Info: Detecting displays\n              Info: Detected display: DVI-D-0 (id: 0) connected: false\n              Info: Detected display: HDMI-0 (id: 1) connected: true\n              Info: Detected display: DP-0 (id: 2) connected: true\n              Info: Detected display: DP-1 (id: 3) connected: false\n              Info: Detected display: DVI-D-1 (id: 4) connected: false\n            </pre>\n        </template>\n        <template #macos>\n            <pre style=\"white-space: pre-line;\">\n              Info: Detecting displays\n              Info: Detected display: Monitor-0 (id: 3) connected: true\n              Info: Detected display: Monitor-1 (id: 2) connected: true\n            </pre>\n        </template>\n      </PlatformLayout>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/audiovideo/NewDisplayOutputSelector.vue",
    "content": "<script setup>\nimport { ref, computed } from \"vue\";\nimport { $tp } from \"../../../platform-i18n\";\nimport PlatformLayout from \"../../../components/layout/PlatformLayout.vue\";\n\nconst props = defineProps([\"platform\", \"config\", \"displays\"]);\n\nconst config = ref(props.config);\n// const outputNamePlaceholder =\n//   props.platform === \"windows\"\n//     ? \"{de9bb7e2-186e-505b-9e93-f48793333810}\"\n//     : \"4531345\";\n\n// Check if VDD mode is enabled (output_name is 'ZakoHDR')\nconst isVddMode = computed(() => {\n  return config.value.output_name === 'ZakoHDR'\n});\n\n// \"DISPLAY NAME: \\\\\\\\.\\\\DISPLAY1\\nFRIENDLY NAME: F32D80U\\nDEVICE STATE: PRIMARY\\nHDR STATE: ENABLED\"\nconst displayDevices = computed(() => {\n  const devices = config.value.display_devices;\n  if (!Array.isArray(devices)) {\n    return [];\n  }\n  return devices.map(({ device_id, data = \"\" }) => ({\n    id: device_id,\n    name: data\n      .replace(\n        /.*?(DISPLAY\\d+)?\\nFRIENDLY NAME: (.*[^\\n])*?\\n.*\\n.*/g,\n        \"$2 ($1)\"\n      )\n      .replace(\"()\", \"\"),\n  }));\n});\n</script>\n\n<template>\n  <div class=\"mb-3\">\n    <label for=\"output_name\" class=\"form-label\">{{\n      $t(\"config.output_name_windows\")\n    }}</label>\n    <select id=\"output_name\" class=\"form-select\" v-model=\"config.output_name\">\n      <option value=\"\">{{ $t(\"_common.autodetect\") }}</option>\n      <option value=\"ZakoHDR\"> 就是要用虚拟显示器～ </option>\n      <option\n        v-for=\"device in displayDevices\"\n        :value=\"device.id\"\n        :key=\"device.id\"\n      >\n        {{ device.name }}\n      </option>\n    </select>\n    <div class=\"form-text\">\n      <p style=\"white-space: pre-line\">{{ $tp(\"config.output_name_desc\") }}</p>\n      <PlatformLayout :platform=\"platform\">\n        <template #windows></template>\n        <template #linux> </template>\n        <template #macos> </template>\n      </PlatformLayout>\n    </div>\n  </div>\n\n  <!-- VDD mode: Reuse VDD for all clients (only shown in VDD mode, Windows only) -->\n  <div class=\"mb-3 form-check\" v-if=\"isVddMode && platform === 'windows'\">\n    <input\n      type=\"checkbox\"\n      class=\"form-check-input\"\n      id=\"vdd_reuse\"\n      v-model=\"config.vdd_reuse\"\n      true-value=\"enabled\"\n      false-value=\"disabled\"\n    />\n    <label class=\"form-check-label\" for=\"vdd_reuse\">\n      {{ $tp('config.vdd_reuse') }}\n    </label>\n    <div class=\"form-text\">\n      {{ $tp('config.vdd_reuse_desc') }}\n    </div>\n  </div>\n\n  <div class=\"mb-3\" v-if=\"platform === 'linux' || platform === 'macos'\">\n    <label for=\"output_name\" class=\"form-label\">{{\n      $t(\"config.output_name_unix\")\n    }}</label>\n    <input\n      type=\"text\"\n      class=\"form-control\"\n      id=\"output_name\"\n      placeholder=\"0\"\n      v-model=\"config.output_name\"\n    />\n    <div class=\"form-text\">\n      {{ $t(\"config.output_name_desc_unix\") }}<br />\n      <br />\n      <pre style=\"white-space: pre-line\" v-if=\"platform === 'linux'\">\n              Info: Detecting displays\n              Info: Detected display: DVI-D-0 (id: 0) connected: false\n              Info: Detected display: HDMI-0 (id: 1) connected: true\n              Info: Detected display: DP-0 (id: 2) connected: true\n              Info: Detected display: DP-1 (id: 3) connected: false\n              Info: Detected display: DVI-D-1 (id: 4) connected: false\n            </pre\n      >\n      <pre style=\"white-space: pre-line\" v-if=\"platform === 'macos'\">\n              Info: Detecting displays\n              Info: Detected display: Monitor-0 (id: 3) connected: true\n              Info: Detected display: Monitor-1 (id: 2) connected: true\n            </pre\n      >\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/audiovideo/VirtualDisplaySettings.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\n\nconst props = defineProps({\n  platform: String,\n  config: Object,\n  resolutions: Array,\n  fps: Array,\n})\n\nconst resolutions = ref(props.resolutions)\nconst fps = ref(props.fps)\n\nconst resIn = ref('')\nconst fpsIn = ref('')\n\nconst MAX_RESOLUTIONS = 25\nconst MAX_FPS = 5\nconst MIN_FPS_VALUE = 1\nconst MAX_FPS_VALUE = 480\n\nfunction addResolution() {\n  if (resIn.value && resolutions.value.length < MAX_RESOLUTIONS) {\n    resolutions.value.push(resIn.value)\n    resIn.value = ''\n  }\n}\n\nfunction removeResolution(index) {\n  resolutions.value.splice(index, 1)\n}\n\nfunction validateFps(value) {\n  // 支持整数和小数格式（如 60, 59.94, 29.97）\n  const pattern = /^\\d+(\\.\\d+)?$/\n  if (!pattern.test(value)) {\n    return false\n  }\n  const fpsValue = parseFloat(value)\n  return fpsValue >= MIN_FPS_VALUE && fpsValue <= MAX_FPS_VALUE\n}\n\nfunction addFps() {\n  const value = fpsIn.value.trim()\n  if (!value) {\n    fpsIn.value = ''\n    return\n  }\n  \n  if (!validateFps(value)) {\n    // 验证失败，清空输入但不显示错误（由HTML5 pattern验证处理）\n    fpsIn.value = ''\n    return\n  }\n  \n  const fpsValue = parseFloat(value)\n  if (fpsValue >= MIN_FPS_VALUE && fpsValue <= MAX_FPS_VALUE && fps.value.length < MAX_FPS) {\n    if (!fps.value.includes(value)) {\n      fps.value.push(value)\n    }\n  }\n  fpsIn.value = ''\n}\n\nfunction removeFps(index) {\n  fps.value.splice(index, 1)\n}\n</script>\n\n<template>\n  <div class=\"virtual-display-settings\">\n    <!-- Advertised Resolutions -->\n    <div class=\"settings-section\">\n      <div class=\"section-header\">\n        <i class=\"fas fa-desktop section-icon\"></i>\n        <label class=\"section-title\">{{ $t('config.resolutions') }}</label>\n      </div>\n      <div class=\"tags-container\">\n        <transition-group name=\"tag-fade\" tag=\"div\" class=\"tags-wrapper\">\n          <div class=\"tag-item\" v-for=\"(r, i) in resolutions\" :key=\"r + i\">\n            <span class=\"tag-text\">{{ r }}</span>\n            <button class=\"tag-remove\" @click=\"removeResolution(i)\" :title=\"$t('_common.remove')\">\n              <i class=\"fas fa-times\"></i>\n            </button>\n          </div>\n        </transition-group>\n        <div v-if=\"resolutions.length === 0\" class=\"empty-hint\">\n          {{ $t('config.no_resolutions') || 'No resolutions added' }}\n        </div>\n      </div>\n      <form @submit.prevent=\"addResolution\" class=\"add-form\">\n        <input\n          type=\"text\"\n          v-model=\"resIn\"\n          required\n          pattern=\"[0-9]+x[0-9]+\"\n          class=\"form-control add-input\"\n          placeholder=\"1920x1080\"\n        />\n        <button v-if=\"resolutions.length < MAX_RESOLUTIONS\" class=\"btn btn-success add-btn\" type=\"submit\">\n          <i class=\"fas fa-plus\"></i>\n        </button>\n      </form>\n      <div class=\"limit-hint\" v-if=\"resolutions.length >= MAX_RESOLUTIONS\">\n        {{ $t('config.max_resolutions_reached') || 'Maximum resolutions reached' }}\n      </div>\n    </div>\n\n    <!-- Advertised FPS -->\n    <div class=\"settings-section\">\n      <div class=\"section-header\">\n        <i class=\"fas fa-tachometer-alt section-icon\"></i>\n        <label class=\"section-title\">{{ $t('config.fps') }}</label>\n      </div>\n      <div class=\"tags-container\">\n        <transition-group name=\"tag-fade\" tag=\"div\" class=\"tags-wrapper\">\n          <div class=\"tag-item tag-fps\" v-for=\"(f, i) in fps\" :key=\"f + i\">\n            <span class=\"tag-text\">{{ f }} FPS</span>\n            <button class=\"tag-remove\" @click=\"removeFps(i)\" :title=\"$t('_common.remove')\">\n              <i class=\"fas fa-times\"></i>\n            </button>\n          </div>\n        </transition-group>\n        <div v-if=\"fps.length === 0\" class=\"empty-hint\">\n          {{ $t('config.no_fps') || 'No FPS values added' }}\n        </div>\n      </div>\n      <form @submit.prevent=\"addFps\" class=\"add-form\">\n        <input\n          type=\"text\"\n          v-model=\"fpsIn\"\n          required\n          pattern=\"\\d+(\\.\\d+)?\"\n          class=\"form-control add-input add-input-fps\"\n          placeholder=\"例如: 120 或 119.88\"\n        />\n        <button v-if=\"fps.length < MAX_FPS\" class=\"btn btn-success add-btn\" type=\"submit\">\n          <i class=\"fas fa-plus\"></i>\n        </button>\n      </form>\n      <div class=\"limit-hint\" v-if=\"fps.length >= MAX_FPS\">\n        {{ $t('config.max_fps_reached') || 'Maximum FPS values reached' }}\n      </div>\n    </div>\n\n    <div class=\"form-text description-text\">\n      <i class=\"fas fa-info-circle\"></i>\n      {{ $t('config.res_fps_desc') }}\n    </div>\n  </div>\n</template>\n\n<style scoped>\n.virtual-display-settings {\n  padding: 1rem 0;\n}\n\n.settings-section {\n  background: var(--bs-body-bg);\n  border: 1px solid var(--bs-border-color);\n  border-radius: 8px;\n  padding: 1rem;\n  margin-bottom: 1rem;\n}\n\n.section-header {\n  display: flex;\n  align-items: center;\n  margin-bottom: 0.75rem;\n}\n\n.section-icon {\n  color: var(--bs-primary);\n  margin-right: 0.5rem;\n  font-size: 1rem;\n}\n\n.section-title {\n  font-weight: 600;\n  font-size: 0.95rem;\n  color: var(--bs-heading-color);\n  margin: 0;\n}\n\n.tags-container {\n  min-height: 40px;\n  margin-bottom: 0.75rem;\n}\n\n.tags-wrapper {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 0.5rem;\n}\n\n.tag-item {\n  display: inline-flex;\n  align-items: center;\n  background: linear-gradient(135deg, var(--bs-primary) 0%, var(--bs-info) 100%);\n  color: white;\n  padding: 0.35rem 0.5rem 0.35rem 0.75rem;\n  border-radius: 20px;\n  font-size: 0.85rem;\n  font-weight: 500;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n  transition: all 0.2s ease;\n}\n\n.tag-item:hover {\n  transform: translateY(-1px);\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);\n}\n\n.tag-fps {\n  background: linear-gradient(135deg, var(--bs-success) 0%, var(--bs-teal, #20c997) 100%);\n}\n\n.tag-text {\n  margin-right: 0.5rem;\n  font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;\n}\n\n.tag-remove {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 20px;\n  height: 20px;\n  border: none;\n  background: rgba(255, 255, 255, 0.2);\n  color: white;\n  border-radius: 50%;\n  cursor: pointer;\n  transition: all 0.2s ease;\n  padding: 0;\n  font-size: 0.7rem;\n}\n\n.tag-remove:hover {\n  background: rgba(255, 255, 255, 0.4);\n  transform: scale(1.1);\n}\n\n.empty-hint {\n  color: var(--bs-secondary-color);\n  font-size: 0.85rem;\n  font-style: italic;\n  padding: 0.5rem 0;\n}\n\n.add-form {\n  display: flex;\n  align-items: center;\n  gap: 0;\n}\n\n.add-input {\n  width: 140px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;\n}\n\n.add-input-fps {\n  width: 80px;\n}\n\n.add-btn {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n  padding: 0.375rem 0.75rem;\n}\n\n.limit-hint {\n  color: var(--bs-warning);\n  font-size: 0.8rem;\n  margin-top: 0.5rem;\n}\n\n.description-text {\n  display: flex;\n  align-items: flex-start;\n  gap: 0.5rem;\n  padding: 0.75rem;\n  background: var(--bs-tertiary-bg);\n  border-radius: 6px;\n  margin-top: 0.5rem;\n}\n\n.description-text i {\n  color: var(--bs-info);\n  margin-top: 0.15rem;\n}\n\n/* Transition animations */\n.tag-fade-enter-active,\n.tag-fade-leave-active {\n  transition: all 0.3s ease;\n}\n\n.tag-fade-enter-from,\n.tag-fade-leave-to {\n  opacity: 0;\n  transform: scale(0.8);\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/encoders/AmdAmfEncoder.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\n\nconst props = defineProps(['platform', 'config'])\n\nconst config = ref(props.config)\n</script>\n\n<template>\n  <div id=\"amd-amf-encoder\" class=\"config-page\">\n    <!-- AMF Usage -->\n    <div class=\"mb-3\">\n      <label for=\"amd_usage\" class=\"form-label\">{{ $t('config.amd_usage') }}</label>\n      <select id=\"amd_usage\" class=\"form-select\" v-model=\"config.amd_usage\">\n        <option value=\"transcoding\">{{ $t('config.amd_usage_transcoding') }}</option>\n        <option value=\"webcam\">{{ $t('config.amd_usage_webcam') }}</option>\n        <option value=\"lowlatency_high_quality\">{{ $t('config.amd_usage_lowlatency_high_quality') }}</option>\n        <option value=\"lowlatency\">{{ $t('config.amd_usage_lowlatency') }}</option>\n        <option value=\"ultralowlatency\">{{ $t('config.amd_usage_ultralowlatency') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.amd_usage_desc') }}</div>\n    </div>\n\n    <!-- AMD Rate Control group options -->\n    <div class=\"accordion mb-3\">\n      <div class=\"accordion-item\">\n        <h2 class=\"accordion-header\">\n          <button\n            class=\"accordion-button collapsed\"\n            type=\"button\"\n            data-bs-toggle=\"collapse\"\n            data-bs-target=\"#panelsStayOpen-collapseOne\"\n          >\n            {{ $t('config.amd_rc_group') }}\n          </button>\n        </h2>\n        <div\n          id=\"panelsStayOpen-collapseOne\"\n          class=\"accordion-collapse collapse\"\n          aria-labelledby=\"panelsStayOpen-headingOne\"\n        >\n          <div class=\"accordion-body\">\n            <!-- AMF Rate Control -->\n            <div class=\"mb-3\">\n              <label for=\"amd_rc\" class=\"form-label\">{{ $t('config.amd_rc') }}</label>\n              <select id=\"amd_rc\" class=\"form-select\" v-model=\"config.amd_rc\">\n                <option value=\"cbr\">{{ $t('config.amd_rc_cbr') }}</option>\n                <option value=\"cqp\">{{ $t('config.amd_rc_cqp') }}</option>\n                <option value=\"vbr_latency\">{{ $t('config.amd_rc_vbr_latency') }}</option>\n                <option value=\"vbr_peak\">{{ $t('config.amd_rc_vbr_peak') }}</option>\n                <option value=\"qvbr\">{{ $t('config.amd_rc_qvbr') }}</option>\n                <option value=\"hqvbr\">{{ $t('config.amd_rc_hqvbr') }}</option>\n                <option value=\"hqcbr\">{{ $t('config.amd_rc_hqcbr') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.amd_rc_desc') }}</div>\n            </div>\n\n            <!-- AMF HRD Enforcement -->\n            <div class=\"mb-3\">\n              <label for=\"amd_enforce_hrd\" class=\"form-label\">{{ $t('config.amd_enforce_hrd') }}</label>\n              <select id=\"amd_enforce_hrd\" class=\"form-select\" v-model=\"config.amd_enforce_hrd\">\n                <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n                <option value=\"disabled\">{{ $t('_common.disabled_def') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.amd_enforce_hrd_desc') }}</div>\n            </div>\n\n            <!-- AMF QVBR Quality Level -->\n            <div class=\"mb-3\" v-if=\"config.amd_rc === 'qvbr'\">\n              <label for=\"amd_qvbr_quality\" class=\"form-label\">\n                {{ $t('config.amd_qvbr_quality') }}: {{ config.amd_qvbr_quality || 23 }}\n              </label>\n              <input\n                type=\"range\"\n                class=\"form-range\"\n                id=\"amd_qvbr_quality\"\n                min=\"1\"\n                max=\"51\"\n                step=\"1\"\n                v-model=\"config.amd_qvbr_quality\"\n              />\n              <div class=\"form-text\">{{ $t('config.amd_qvbr_quality_desc') }}</div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- AMF Quality group options -->\n    <div class=\"accordion mb-3\">\n      <div class=\"accordion-item\">\n        <h2 class=\"accordion-header\">\n          <button\n            class=\"accordion-button collapsed\"\n            type=\"button\"\n            data-bs-toggle=\"collapse\"\n            data-bs-target=\"#panelsStayOpen-collapseTwo\"\n          >\n            {{ $t('config.amd_quality_group') }}\n          </button>\n        </h2>\n        <div\n          id=\"panelsStayOpen-collapseTwo\"\n          class=\"accordion-collapse collapse\"\n          aria-labelledby=\"panelsStayOpen-headingTwo\"\n        >\n          <div class=\"accordion-body\">\n            <!-- AMF Quality -->\n            <div class=\"mb-3\">\n              <label for=\"amd_quality\" class=\"form-label\">{{ $t('config.amd_quality') }}</label>\n              <select id=\"amd_quality\" class=\"form-select\" v-model=\"config.amd_quality\">\n                <option value=\"speed\">{{ $t('config.amd_quality_speed') }}</option>\n                <option value=\"balanced\">{{ $t('config.amd_quality_balanced') }}</option>\n                <option value=\"quality\">{{ $t('config.amd_quality_quality') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.amd_quality_desc') }}</div>\n            </div>\n\n            <!-- AMD Preanalysis -->\n            <div class=\"mb-3\">\n              <label for=\"amd_preanalysis\" class=\"form-label\">{{ $t('config.amd_preanalysis') }}</label>\n              <select id=\"amd_preanalysis\" class=\"form-select\" v-model=\"config.amd_preanalysis\">\n                <option value=\"disabled\">{{ $t('_common.disabled_def') }}</option>\n                <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.amd_preanalysis_desc') }}</div>\n            </div>\n\n            <!-- AMD VBAQ -->\n            <div class=\"mb-3\">\n              <label for=\"amd_vbaq\" class=\"form-label\">{{ $t('config.amd_vbaq') }}</label>\n              <select id=\"amd_vbaq\" class=\"form-select\" v-model=\"config.amd_vbaq\">\n                <option value=\"disabled\">{{ $t('_common.disabled') }}</option>\n                <option value=\"enabled\">{{ $t('_common.enabled_def') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.amd_vbaq_desc') }}</div>\n            </div>\n\n            <!-- AMF Coder (H264) -->\n            <div class=\"mb-3\">\n              <label for=\"amd_coder\" class=\"form-label\">{{ $t('config.amd_coder') }}</label>\n              <select id=\"amd_coder\" class=\"form-select\" v-model=\"config.amd_coder\">\n                <option value=\"auto\">{{ $t('config.ffmpeg_auto') }}</option>\n                <option value=\"cabac\">{{ $t('config.coder_cabac') }}</option>\n                <option value=\"cavlc\">{{ $t('config.coder_cavlc') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.amd_coder_desc') }}</div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Slices Per Frame -->\n    <div class=\"mb-3\" v-if=\"platform === 'windows'\">\n      <label for=\"amd_slices_per_frame\" class=\"form-label\">{{ $t('config.amd_slices_per_frame') }}</label>\n      <select id=\"amd_slices_per_frame\" class=\"form-select\" v-model=\"config.amd_slices_per_frame\">\n        <option value=\"0\">{{ $t('config.amd_slices_per_frame_auto') }}</option>\n        <option value=\"1\">1</option>\n        <option value=\"2\">2</option>\n        <option value=\"3\">3</option>\n        <option value=\"4\">4</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.amd_slices_per_frame_desc') }}</div>\n    </div>\n  </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/encoders/IntelQuickSyncEncoder.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\n\nconst props = defineProps([\n  'platform',\n  'config',\n])\n\nconst config = ref(props.config)\n</script>\n\n<template>\n  <div id=\"intel-quicksync-encoder\" class=\"config-page\">\n    <!-- QuickSync Preset -->\n    <div class=\"mb-3\">\n      <label for=\"qsv_preset\" class=\"form-label\">{{ $t('config.qsv_preset') }}</label>\n      <select id=\"qsv_preset\" class=\"form-select\" v-model=\"config.qsv_preset\">\n        <option value=\"veryfast\">{{ $t('config.qsv_preset_veryfast') }}</option>\n        <option value=\"faster\">{{ $t('config.qsv_preset_faster') }}</option>\n        <option value=\"fast\">{{ $t('config.qsv_preset_fast') }}</option>\n        <option value=\"medium\">{{ $t('config.qsv_preset_medium') }}</option>\n        <option value=\"slow\">{{ $t('config.qsv_preset_slow') }}</option>\n        <option value=\"slower\">{{ $t('config.qsv_preset_slower') }}</option>\n        <option value=\"slowest\">{{ $t('config.qsv_preset_slowest') }}</option>\n      </select>\n    </div>\n\n    <!-- QuickSync Coder (H264) -->\n    <div class=\"mb-3\">\n      <label for=\"qsv_coder\" class=\"form-label\">{{ $t('config.qsv_coder') }}</label>\n      <select id=\"qsv_coder\" class=\"form-select\" v-model=\"config.qsv_coder\">\n        <option value=\"auto\">{{ $t('config.ffmpeg_auto') }}</option>\n        <option value=\"cabac\">{{ $t('config.coder_cabac') }}</option>\n        <option value=\"cavlc\">{{ $t('config.coder_cavlc') }}</option>\n      </select>\n    </div>\n\n    <!-- Allow Slow HEVC Encoding -->\n    <div class=\"mb-3\">\n      <label for=\"qsv_slow_hevc\" class=\"form-label\">{{ $t('config.qsv_slow_hevc') }}</label>\n      <select id=\"qsv_slow_hevc\" class=\"form-select\" v-model=\"config.qsv_slow_hevc\">\n        <option value=\"disabled\">{{ $t('_common.disabled_def') }}</option>\n        <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.qsv_slow_hevc_desc') }}</div>\n    </div>\n\n  </div>\n</template>\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/encoders/NvidiaNvencEncoder.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\n\nconst props = defineProps([\n  'platform',\n  'config',\n])\n\nconst config = ref(props.config)\n</script>\n\n<template>\n  <div id=\"nvidia-nvenc-encoder\" class=\"config-page\">\n    <!-- Performance preset -->\n    <div class=\"mb-3\">\n      <label for=\"nvenc_preset\" class=\"form-label\">{{ $t('config.nvenc_preset') }}</label>\n      <select id=\"nvenc_preset\" class=\"form-select\" v-model=\"config.nvenc_preset\">\n        <option value=\"1\">P1 {{ $t('config.nvenc_preset_1') }}</option>\n        <option value=\"2\">P2</option>\n        <option value=\"3\">P3</option>\n        <option value=\"4\">P4</option>\n        <option value=\"5\">P5</option>\n        <option value=\"6\">P6</option>\n        <option value=\"7\">P7 {{ $t('config.nvenc_preset_7') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.nvenc_preset_desc') }}</div>\n    </div>\n\n    <!-- Two-pass mode -->\n    <div class=\"mb-3\">\n      <label for=\"nvenc_twopass\" class=\"form-label\">{{ $t('config.nvenc_twopass') }}</label>\n      <select id=\"nvenc_twopass\" class=\"form-select\" v-model=\"config.nvenc_twopass\">\n        <option value=\"disabled\">{{ $t('config.nvenc_twopass_disabled') }}</option>\n        <option value=\"quarter_res\">{{ $t('config.nvenc_twopass_quarter_res') }}</option>\n        <option value=\"full_res\">{{ $t('config.nvenc_twopass_full_res') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.nvenc_twopass_desc') }}</div>\n    </div>\n\n    <!-- Spatial AQ -->\n    <div class=\"mb-3\">\n      <label for=\"nvenc_spatial_aq\" class=\"form-label\">{{ $t('config.nvenc_spatial_aq') }}</label>\n      <select id=\"nvenc_spatial_aq\" class=\"form-select\" v-model=\"config.nvenc_spatial_aq\">\n        <option value=\"disabled\">{{ $t('config.nvenc_spatial_aq_disabled') }}</option>\n        <option value=\"enabled\">{{ $t('config.nvenc_spatial_aq_enabled') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.nvenc_spatial_aq_desc') }}</div>\n    </div>\n\n    <!-- Temporal AQ -->\n    <!-- <div class=\"mb-3\">\n      <label for=\"nvenc_temporal_aq\" class=\"form-label\">{{ $t('config.nvenc_temporal_aq') }}</label>\n      <select id=\"nvenc_temporal_aq\" class=\"form-select\" v-model=\"config.nvenc_temporal_aq\">\n        <option value=\"disabled\">{{ $t('_common.disabled_def') }}</option>\n        <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.nvenc_temporal_aq_desc') }}</div>\n    </div> -->\n\n    <!-- Lookahead depth -->\n    <!-- <div class=\"mb-3\">\n      <label for=\"nvenc_lookahead_depth\" class=\"form-label\">{{ $t('config.nvenc_lookahead_depth') }}</label>\n      <input type=\"number\" min=\"0\" max=\"32\" class=\"form-control\" id=\"nvenc_lookahead_depth\" placeholder=\"0\"\n             v-model.number=\"config.nvenc_lookahead_depth\" />\n      <div class=\"form-text\">{{ $t('config.nvenc_lookahead_depth_desc') }}</div>\n    </div> -->\n\n    <!-- Lookahead level -->\n    <!-- <div class=\"mb-3\" v-if=\"config.nvenc_lookahead_depth > 0\">\n      <label for=\"nvenc_lookahead_level\" class=\"form-label\">{{ $t('config.nvenc_lookahead_level') }}</label>\n      <select id=\"nvenc_lookahead_level\" class=\"form-select\" v-model=\"config.nvenc_lookahead_level\">\n        <option value=\"disabled\">{{ $t('config.nvenc_lookahead_level_disabled') }}</option>\n        <option value=\"0\">{{ $t('config.nvenc_lookahead_level_0') }}</option>\n        <option value=\"1\">{{ $t('config.nvenc_lookahead_level_1') }}</option>\n        <option value=\"2\">{{ $t('config.nvenc_lookahead_level_2') }}</option>\n        <option value=\"3\">{{ $t('config.nvenc_lookahead_level_3') }}</option>\n        <option value=\"autoselect\">{{ $t('config.nvenc_lookahead_level_autoselect') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.nvenc_lookahead_level_desc') }}</div>\n    </div> -->\n\n    <!-- Temporal filter -->\n    <!-- <div class=\"mb-3\">\n      <label for=\"nvenc_temporal_filter\" class=\"form-label\">{{ $t('config.nvenc_temporal_filter') }}</label>\n      <select id=\"nvenc_temporal_filter\" class=\"form-select\" v-model=\"config.nvenc_temporal_filter\">\n        <option value=\"disabled\">{{ $t('_common.disabled_def') }}</option>\n        <option value=\"0\">{{ $t('config.nvenc_temporal_filter_disabled') }}</option>\n        <option value=\"4\">{{ $t('config.nvenc_temporal_filter_4') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.nvenc_temporal_filter_desc') }}</div>\n    </div> -->\n\n    <!-- Rate control mode -->\n    <div class=\"mb-3\">\n      <label for=\"nvenc_rate_control\" class=\"form-label\">{{ $t('config.nvenc_rate_control') }}</label>\n      <select id=\"nvenc_rate_control\" class=\"form-select\" v-model=\"config.nvenc_rate_control\">\n        <option value=\"cbr\">{{ $t('config.nvenc_rate_control_cbr') }}</option>\n        <option value=\"vbr\">{{ $t('config.nvenc_rate_control_vbr') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.nvenc_rate_control_desc') }}</div>\n    </div>\n\n    <!-- Target quality (VBR mode only) -->\n    <div class=\"mb-3\" v-if=\"config.nvenc_rate_control === 'vbr'\">\n      <label for=\"nvenc_target_quality\" class=\"form-label\">{{ $t('config.nvenc_target_quality') }}</label>\n      <input type=\"number\" min=\"0\" max=\"63\" class=\"form-control\" id=\"nvenc_target_quality\" placeholder=\"0\"\n             v-model.number=\"config.nvenc_target_quality\" />\n      <div class=\"form-text\">{{ $t('config.nvenc_target_quality_desc') }}</div>\n    </div>\n\n    <!-- Single-frame VBV/HRD percentage increase -->\n    <div class=\"mb-3\">\n      <label for=\"nvenc_vbv_increase\" class=\"form-label\">{{ $t('config.nvenc_vbv_increase') }}</label>\n      <input type=\"number\" min=\"0\" max=\"400\" class=\"form-control\" id=\"nvenc_vbv_increase\" placeholder=\"0\"\n             v-model=\"config.nvenc_vbv_increase\" />\n      <div class=\"form-text\">\n        {{ $t('config.nvenc_vbv_increase_desc') }}<br>\n        <br>\n        <a href=\"https://en.wikipedia.org/wiki/Video_buffering_verifier\">VBV/HRD</a>\n      </div>\n    </div>\n\n    <!-- Miscellaneous options -->\n    <div class=\"accordion\">\n      <div class=\"accordion-item\">\n        <h2 class=\"accordion-header\">\n          <button class=\"accordion-button\" type=\"button\" data-bs-toggle=\"collapse\"\n                  data-bs-target=\"#panelsStayOpen-collapseOne\">\n            {{ $t('config.misc') }}\n          </button>\n        </h2>\n        <div id=\"panelsStayOpen-collapseOne\" class=\"accordion-collapse collapse show\"\n             aria-labelledby=\"panelsStayOpen-headingOne\">\n          <div class=\"accordion-body\">\n            <!-- NVENC Realtime HAGS priority -->\n            <div class=\"mb-3\" v-if=\"platform === 'windows'\">\n              <label for=\"nvenc_realtime_hags\" class=\"form-label\">{{ $t('config.nvenc_realtime_hags') }}</label>\n              <select id=\"nvenc_realtime_hags\" class=\"form-select\" v-model=\"config.nvenc_realtime_hags\">\n                <option value=\"disabled\">{{ $t('_common.disabled') }}</option>\n                <option value=\"enabled\">{{ $t('_common.enabled_def') }}</option>\n              </select>\n              <div class=\"form-text\">\n                {{ $t('config.nvenc_realtime_hags_desc') }}<br>\n                <br>\n                <a href=\"https://devblogs.microsoft.com/directx/hardware-accelerated-gpu-scheduling/\">HAGS</a>\n              </div>\n            </div>\n\n            <!-- Split frame encoding -->\n            <div class=\"mb-3\" v-if=\"platform === 'windows'\">\n              <label for=\"nvenc_split_encode\" class=\"form-label\">{{ $t('config.nvenc_split_encode') }}</label>\n              <select id=\"nvenc_split_encode\" class=\"form-select\" v-model=\"config.nvenc_split_encode\">\n                <option value=\"disabled\">{{ $t('_common.disabled') }}</option>\n                <option value=\"driver_decides\">{{ $t('config.nvenc_split_encode_driver_decides_def') }}</option>\n                <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n                <option value=\"two_strips\">{{ $t('config.nvenc_split_encode_two_strips') }}</option>\n                <option value=\"three_strips\">{{ $t('config.nvenc_split_encode_three_strips') }}</option>\n                <option value=\"four_strips\">{{ $t('config.nvenc_split_encode_four_strips') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.nvenc_split_encode_desc') }}</div>\n            </div>\n\n            <!-- Prefer lower encoding latency over power savings -->\n            <div class=\"mb-3\" v-if=\"platform === 'windows'\">\n              <label for=\"nvenc_latency_over_power\" class=\"form-label\">{{ $t('config.nvenc_latency_over_power') }}</label>\n              <select id=\"nvenc_latency_over_power\" class=\"form-select\" v-model=\"config.nvenc_latency_over_power\">\n                <option value=\"disabled\">{{ $t('_common.disabled') }}</option>\n                <option value=\"enabled\">{{ $t('_common.enabled_def') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.nvenc_latency_over_power_desc') }}</div>\n            </div>\n\n            <!-- Present OpenGL/Vulkan on top of DXGI -->\n            <div class=\"mb-3\" v-if=\"platform === 'windows'\">\n              <label for=\"nvenc_opengl_vulkan_on_dxgi\" class=\"form-label\">{{ $t('config.nvenc_opengl_vulkan_on_dxgi') }}</label>\n              <select id=\"nvenc_opengl_vulkan_on_dxgi\" class=\"form-select\" v-model=\"config.nvenc_opengl_vulkan_on_dxgi\">\n                <option value=\"disabled\">{{ $t('_common.disabled') }}</option>\n                <option value=\"enabled\">{{ $t('_common.enabled_def') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.nvenc_opengl_vulkan_on_dxgi_desc') }}</div>\n            </div>\n\n            <!-- NVENC H264 CAVLC -->\n            <div>\n              <label for=\"nvenc_h264_cavlc\" class=\"form-label\">{{ $t('config.nvenc_h264_cavlc') }}</label>\n              <select id=\"nvenc_h264_cavlc\" class=\"form-select\" v-model=\"config.nvenc_h264_cavlc\">\n                <option value=\"disabled\">{{ $t('_common.disabled_def') }}</option>\n                <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n              </select>\n              <div class=\"form-text\">{{ $t('config.nvenc_h264_cavlc_desc') }}</div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/encoders/SoftwareEncoder.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\n\nconst props = defineProps([\n  'platform',\n  'config'\n])\n\nconst config = ref(props.config)\n</script>\n\n<template>\n  <div id=\"software-encoder\" class=\"config-page\">\n    <div class=\"mb-3\">\n      <label for=\"sw_preset\" class=\"form-label\">{{ $t('config.sw_preset') }}</label>\n      <select id=\"sw_preset\" class=\"form-select\" v-model=\"config.sw_preset\">\n        <option value=\"ultrafast\">{{ $t('config.sw_preset_ultrafast') }}</option>\n        <option value=\"superfast\">{{ $t('config.sw_preset_superfast') }}</option>\n        <option value=\"veryfast\">{{ $t('config.sw_preset_veryfast') }}</option>\n        <option value=\"faster\">{{ $t('config.sw_preset_faster') }}</option>\n        <option value=\"fast\">{{ $t('config.sw_preset_fast') }}</option>\n        <option value=\"medium\">{{ $t('config.sw_preset_medium') }}</option>\n        <option value=\"slow\">{{ $t('config.sw_preset_slow') }}</option>\n        <option value=\"slower\">{{ $t('config.sw_preset_slower') }}</option>\n        <option value=\"veryslow\">{{ $t('config.sw_preset_veryslow') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.sw_preset_desc') }}</div>\n    </div>\n\n    <div class=\"mb-3\">\n      <label for=\"sw_tune\" class=\"form-label\">{{ $t('config.sw_tune') }}</label>\n      <select id=\"sw_tune\" class=\"form-select\" v-model=\"config.sw_tune\">\n        <option value=\"film\">{{ $t('config.sw_tune_film') }}</option>\n        <option value=\"animation\">{{ $t('config.sw_tune_animation') }}</option>\n        <option value=\"grain\">{{ $t('config.sw_tune_grain') }}</option>\n        <option value=\"stillimage\">{{ $t('config.sw_tune_stillimage') }}</option>\n        <option value=\"fastdecode\">{{ $t('config.sw_tune_fastdecode') }}</option>\n        <option value=\"zerolatency\">{{ $t('config.sw_tune_zerolatency') }}</option>\n      </select>\n      <div class=\"form-text\">{{ $t('config.sw_tune_desc') }}</div>\n    </div>\n\n    <!-- Quantization Parameter (QP) -->\n    <div class=\"mb-3\">\n      <label for=\"qp\" class=\"form-label\">{{ $t('config.qp') }}</label>\n      <input type=\"number\" class=\"form-control\" id=\"qp\" placeholder=\"28\" v-model=\"config.qp\" />\n      <div class=\"form-text\">{{ $t('config.qp_desc') }}</div>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/configs/tabs/encoders/VideotoolboxEncoder.vue",
    "content": "<script setup>\nimport { ref } from 'vue'\n\nconst props = defineProps([\n  'platform',\n  'config',\n])\n\nconst config = ref(props.config)\n</script>\n\n<template>\n  <div id=\"videotoolbox-encoder\" class=\"config-page\">\n    <!-- Presets -->\n    <div class=\"mb-3\">\n      <label for=\"vt_coder\" class=\"form-label\">{{ $t('config.vt_coder') }}</label>\n      <select id=\"vt_coder\" class=\"form-select\" v-model=\"config.vt_coder\">\n        <option value=\"auto\">{{ $t('config.ffmpeg_auto') }}</option>\n        <option value=\"cabac\">{{ $t('config.coder_cabac') }}</option>\n        <option value=\"cavlc\">{{ $t('config.coder_cavlc') }}</option>\n      </select>\n    </div>\n    <div class=\"mb-3\">\n      <label for=\"vt_software\" class=\"form-label\">{{ $t('config.vt_software') }}</label>\n      <select id=\"vt_software\" class=\"form-select\" v-model=\"config.vt_software\">\n        <option value=\"auto\">{{ $t('_common.auto') }}</option>\n        <option value=\"disabled\">{{ $t('_common.disabled') }}</option>\n        <option value=\"allowed\">{{ $t('config.vt_software_allowed') }}</option>\n        <option value=\"forced\">{{ $t('config.vt_software_forced') }}</option>\n      </select>\n    </div>\n    <div class=\"mb-3\">\n      <label for=\"vt_realtime\" class=\"form-label\">{{ $t('config.vt_realtime') }}</label>\n      <select id=\"vt_realtime\" class=\"form-select\" v-model=\"config.vt_realtime\">\n        <option value=\"enabled\">{{ $t('_common.enabled') }}</option>\n        <option value=\"disabled\">{{ $t('_common.disabled') }}</option>\n      </select>\n    </div>\n  </div>\n</template>\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/fonts/fonts.css",
    "content": "/* Indie Flower - latin-ext */\n@font-face {\n  font-family: 'Indie Flower';\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  src: url('./indie-flower-400-latin-ext.woff2') format('woff2');\n  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,\n    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,\n    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* Indie Flower - latin */\n@font-face {\n  font-family: 'Indie Flower';\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  src: url('./indie-flower-400-latin.woff2') format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,\n    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n    U+FEFF, U+FFFD;\n}\n\n/* Kalam 300 - latin-ext */\n@font-face {\n  font-family: 'Kalam';\n  font-style: normal;\n  font-weight: 300;\n  font-display: swap;\n  src: url('./kalam-300-latin-ext.woff2') format('woff2');\n  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,\n    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,\n    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* Kalam 300 - latin */\n@font-face {\n  font-family: 'Kalam';\n  font-style: normal;\n  font-weight: 300;\n  font-display: swap;\n  src: url('./kalam-300-latin.woff2') format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,\n    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n    U+FEFF, U+FFFD;\n}\n/* Kalam 400 - latin-ext */\n@font-face {\n  font-family: 'Kalam';\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  src: url('./kalam-400-latin-ext.woff2') format('woff2');\n  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,\n    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,\n    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* Kalam 400 - latin */\n@font-face {\n  font-family: 'Kalam';\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  src: url('./kalam-400-latin.woff2') format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,\n    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n    U+FEFF, U+FFFD;\n}\n/* Kalam 700 - latin-ext */\n@font-face {\n  font-family: 'Kalam';\n  font-style: normal;\n  font-weight: 700;\n  font-display: swap;\n  src: url('./kalam-700-latin-ext.woff2') format('woff2');\n  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,\n    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,\n    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* Kalam 700 - latin */\n@font-face {\n  font-family: 'Kalam';\n  font-style: normal;\n  font-weight: 700;\n  font-display: swap;\n  src: url('./kalam-700-latin.woff2') format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,\n    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n    U+FEFF, U+FFFD;\n}\n\n/* Patrick Hand - latin-ext */\n@font-face {\n  font-family: 'Patrick Hand';\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  src: url('./patrick-hand-400-latin-ext.woff2') format('woff2');\n  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,\n    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,\n    U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* Patrick Hand - latin */\n@font-face {\n  font-family: 'Patrick Hand';\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  src: url('./patrick-hand-400-latin.woff2') format('woff2');\n  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,\n    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n    U+FEFF, U+FFFD;\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bs-theme=\"auto\">\n  <head>\n    <%- header %>\n  </head>\n\n  <body id=\"app\" v-cloak>\n    <!-- Vue 应用挂载点 -->\n  </body>\n\n  <script type=\"module\">\n    import { createApp } from 'vue'\n    import { initApp } from './init'\n    import Home from './views/Home.vue'\n    import { initFirebase } from './config/firebase.js'\n\n    console.log('Hello, Sunshine!')\n\n    // 初始化Firebase Analytics\n    initFirebase()\n\n    // 创建应用实例\n    const app = createApp(Home)\n\n    // 初始化应用（包含 i18n 等）\n    initApp(app)\n  </script>\n</html>\n"
  },
  {
    "path": "src_assets/common/assets/web/init.js",
    "content": "import i18n from './config/i18n.js'\n\n// must import even if not implicitly using here\n// https://github.com/aurelia/skeleton-navigation/issues/894\n// https://discourse.aurelia.io/t/bootstrap-import-bootstrap-breaks-dropdown-menu-in-navbar/641/9\n// 导入 Bootstrap 并手动设置到全局对象（ES 模块不会自动注册到 window）\nimport * as bootstrap from 'bootstrap'\n\n// 将 Bootstrap 设置到全局对象，以便在组件中使用\nif (typeof window !== 'undefined') {\n  window.bootstrap = bootstrap\n}\n\nexport function initApp(app, config) {\n    //Wait for locale initialization, then render\n    i18n().then(i18n => {\n        app.use(i18n);\n        app.provide('i18n', i18n.global)\n        app.mount('#app');\n        if (config) {\n            config(app)\n        }\n    });\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/password.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bs-theme=\"auto\">\n  <head>\n    <%- header %>\n  </head>\n\n  <body id=\"app\" v-cloak>\n    <!-- Vue 应用挂载点 -->\n  </body>\n\n  <script type=\"module\">\n    import { createApp } from 'vue'\n    import { initApp } from './init'\n    import Password from './views/Password.vue'\n\n    const app = createApp(Password)\n    initApp(app)\n  </script>\n</html>\n"
  },
  {
    "path": "src_assets/common/assets/web/pin.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bs-theme=\"auto\">\n  <head>\n    <%- header %>\n  </head>\n\n  <body id=\"app\" v-cloak>\n    <!-- Vue 应用挂载点 -->\n  </body>\n\n  <script type=\"module\">\n    import { createApp } from 'vue'\n    import { initApp } from './init'\n    import Pin from './views/Pin.vue'\n\n    const app = createApp(Pin)\n    initApp(app)\n  </script>\n</html>\n"
  },
  {
    "path": "src_assets/common/assets/web/platform-i18n.js",
    "content": "import {inject} from 'vue'\n\nclass PlatformMessageI18n {\n    /**\n     * @param {string} platform\n     */\n    constructor(platform) {\n        this.platform = platform\n    }\n\n    /**\n     * @param {string} key\n     * @param {string} platform identifier\n     * @return {string} key with platform identifier\n     */\n    getPlatformKey(key, platform) {\n        return key + '_' + platform\n    }\n\n    /**\n     * @param {string} key\n     * @param {string?} defaultMsg\n     * @return {string} translated message or defaultMsg if provided\n     */\n    getMessageUsingPlatform(key, defaultMsg) {\n        const realKey = this.getPlatformKey(key, this.platform)\n        const i18n = inject('i18n')\n        let message = i18n.t(realKey)\n\n        if (message !== realKey) {\n            // We got a message back, return early\n            return message\n        }\n        \n        // If on Windows, we don't fallback to unix, so return early\n        if (this.platform === 'windows') {\n            return defaultMsg ? defaultMsg : message\n        }\n        \n        // there's no message for key, check for unix version\n        const unixKey = this.getPlatformKey(key, 'unix')\n        message = i18n.t(unixKey)\n\n        if (message === unixKey && defaultMsg) {\n            // there's no message for unix key, return defaultMsg\n            return defaultMsg\n        }\n        return message\n    }\n}\n\n/**\n * @param {string?} platform\n * @return {PlatformMessageI18n} instance\n */\nexport function usePlatformI18n(platform) {\n    if (!platform) {\n        platform = inject('platform').value\n    }\n\n    if (!platform) {\n        throw 'platform argument missing'\n    }\n\n    return inject(\n        'platformMessage',\n        () => new PlatformMessageI18n(platform),\n        true\n    )\n}\n\n/**\n * @param {string} key\n * @param {string?} defaultMsg\n * @return {string} translated message or defaultMsg if provided\n */\nexport function $tp(key, defaultMsg) {\n    const pm = usePlatformI18n()\n    return pm.getMessageUsingPlatform(key, defaultMsg)\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/css/sunshine.css",
    "content": "/* Hide pages while localization is loading */\n[v-cloak] {\n    display: none;\n}\n\n[data-bs-theme=dark] .element {\n    color: var(--bs-primary-text-emphasis);\n    background-color: var(--bs-primary-bg-subtle);\n}\n\n@media (prefers-color-scheme: dark) {\n    .element {\n        color: var(--bs-primary-text-emphasis);\n        background-color: var(--bs-primary-bg-subtle);\n    }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/bg.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Прилагане\",\n    \"auto\": \"Автоматично\",\n    \"autodetect\": \"Автоматично откриване (препоръчително)\",\n    \"beta\": \"(бета)\",\n    \"cancel\": \"Отказ\",\n    \"close\": \"Затвори\",\n    \"copied\": \"Копирано в клипборда\",\n    \"copy\": \"Копирай\",\n    \"delete\": \"Изтриване\",\n    \"description\": \"Описание\",\n    \"disabled\": \"Изключено\",\n    \"disabled_def\": \"Изключено (по подразбиране)\",\n    \"dismiss\": \"Отхвърляне\",\n    \"do_cmd\": \"Команда преди стартиране\",\n    \"download\": \"Изтегли\",\n    \"edit\": \"Редактиране\",\n    \"elevated\": \"Изпълнение като администратор\",\n    \"enabled\": \"Включено\",\n    \"enabled_def\": \"Включено (по подразбиране)\",\n    \"error\": \"Грешка!\",\n    \"no_changes\": \"Няма промени\",\n    \"note\": \"Забележка:\",\n    \"password\": \"Парола\",\n    \"remove\": \"Премахване\",\n    \"run_as\": \"Стартиране като администратор\",\n    \"save\": \"Запазване\",\n    \"see_more\": \"Вижте повече\",\n    \"success\": \"Успешно!\",\n    \"undo_cmd\": \"Команда след приключване\",\n    \"username\": \"Потребителско име\",\n    \"warning\": \"Внимание!\"\n  },\n  \"apps\": {\n    \"actions\": \"Действия\",\n    \"add_cmds\": \"Добавяне на команди\",\n    \"add_new\": \"Добавяне на ново\",\n    \"advanced_options\": \"Разширени опции\",\n    \"app_name\": \"Име на приложението\",\n    \"app_name_desc\": \"Име на приложението, както се показва в Moonlight\",\n    \"applications_desc\": \"Приложенията се опресняват при рестартиране на клиента\",\n    \"applications_title\": \"Приложения\",\n    \"auto_detach\": \"Продължаване на предаването, ако приложението се затвори бързо\",\n    \"auto_detach_desc\": \"По този начин ще се направи опит за автоматично разпознаване на приложения от тип „стартираща програма“, които се затварят бързо след като стартират друга програма или на друго свое копие. Когато се засече такова, то се третира като разкачено приложение.\",\n    \"basic_info\": \"Основна информация\",\n    \"cmd\": \"Команда\",\n    \"cmd_desc\": \"Основното приложение, което да се стартира. Ако е празно, няма да се стартира никакво приложение.\",\n    \"cmd_examples_title\": \"Често срещани примери:\",\n    \"cmd_note\": \"Ако пътят до изпълнимия файл на командата съдържа интервали, трябва да го заградите с кавички.\",\n    \"cmd_prep_desc\": \"Списък с команди, които да се изпълняват преди/след това приложение. Ако някоя от подготвителните команди се провали, стартирането на приложението се прекъсва.\",\n    \"cmd_prep_name\": \"Подготвителни команди\",\n    \"command_settings\": \"Настройки на команди\",\n    \"covers_found\": \"Намерени обложки\",\n    \"delete\": \"Изтриване\",\n    \"delete_confirm\": \"Are you sure you want to delete \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Разкачени команди\",\n    \"detached_cmds_add\": \"Добавяне на разкачена команда\",\n    \"detached_cmds_desc\": \"Списък с команди, които да се изпълняват във фонов режим.\",\n    \"detached_cmds_note\": \"Ако пътят до изпълнимия файл на командата съдържа интервали, трябва да го заградите с кавички.\",\n    \"detached_cmds_remove\": \"Премахване на разкачена команда\",\n    \"edit\": \"Редактиране\",\n    \"env_app_id\": \"Идентификатор на приложението\",\n    \"env_app_name\": \"Име на приложението\",\n    \"env_client_audio_config\": \"Конфигурацията на звука, поискана от клиента (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"Клиентът е заявил опцията за оптимизиране на играта за оптимално поточно предаване (true/false)\",\n    \"env_client_fps\": \"Заявените от клиента кадри/сек (целочислена стойност)\",\n    \"env_client_gcmap\": \"Заявената маска за контролера, във формат на битова маска (целочислена стойност)\",\n    \"env_client_hdr\": \"HDR е включен от клиента (true/false)\",\n    \"env_client_height\": \"Височината, заявена от клиента (целочислена стойност)\",\n    \"env_client_host_audio\": \"Клиентът е поискал звука да се изпълнява на отдалечения компютър (true/false)\",\n    \"env_client_name\": \"Приятелско име на клиента (низ)\",\n    \"env_client_width\": \"Ширината, заявена от клиента (целочислена стойност)\",\n    \"env_displayplacer_example\": \"Пример за автоматизиране на резолюцията чрез displayplacer:\",\n    \"env_qres_example\": \"Пример за автоматизиране на резолюцията чрез QRes:\",\n    \"env_qres_path\": \"Път до qres\",\n    \"env_var_name\": \"Име на променливата\",\n    \"env_vars_about\": \"Относно променливите на средата\",\n    \"env_vars_desc\": \"Всички команди получават тези променливи на средата по подразбиране:\",\n    \"env_xrandr_example\": \"Пример за автоматизиране на резолюцията чрез Xrandr:\",\n    \"exit_timeout\": \"Време за изчакване при затваряне\",\n    \"exit_timeout_desc\": \"Брой секунди за изчакване на всички процеси на приложението да завършат самостоятелно, когато бъде изпратена заявка за затваряне. Ако не е зададено, по подразбиране се изчаква до 5 секунди. Ако е зададена стойност 0, приложението ще бъде прекратено незабавно.\",\n    \"file_selector_not_initialized\": \"Селекторът на файлове не е инициализиран\",\n    \"find_cover\": \"Търсене на обложка\",\n    \"form_invalid\": \"Моля, проверете задължителните полета\",\n    \"form_valid\": \"Валидно приложение\",\n    \"global_prep_desc\": \"Включване/изключване на изпълнението на глобалните подготвителни команди за това приложение.\",\n    \"global_prep_name\": \"Глобални команди за подготовка\",\n    \"image\": \"Изображение\",\n    \"image_desc\": \"Пътят до иконката/картинката/изображението на приложението, което ще бъде изпратено на клиента. Изображението трябва да е файл във формата PNG. Ако не е зададено, Sunshine ще изпрати стандартно изображение.\",\n    \"image_settings\": \"Настройки на изображението\",\n    \"loading\": \"Зареждане…\",\n    \"menu_cmd_actions\": \"Действия\",\n    \"menu_cmd_add\": \"Добави команда в менюто\",\n    \"menu_cmd_command\": \"Команда\",\n    \"menu_cmd_desc\": \"След конфигуриране тези команди ще бъдат видими в менюто за връщане на клиента, позволявайки бързо изпълнение на специфични операции без прекъсване на потока, като стартиране на помощни програми.\\nПример: Показвано име - Затвори компютъра; Команда - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Показвано име\",\n    \"menu_cmd_drag_sort\": \"Плъзни за сортиране\",\n    \"menu_cmd_name\": \"Команди в менюто\",\n    \"menu_cmd_placeholder_command\": \"Команда\",\n    \"menu_cmd_placeholder_display_name\": \"Показвано име\",\n    \"menu_cmd_placeholder_execute\": \"Изпълни команда\",\n    \"menu_cmd_placeholder_undo\": \"Отмени команда\",\n    \"menu_cmd_remove_menu\": \"Премахни команда от менюто\",\n    \"menu_cmd_remove_prep\": \"Премахни команда за подготовка\",\n    \"mouse_mode\": \"Режим на мишката\",\n    \"mouse_mode_auto\": \"Авто (Глобална настройка)\",\n    \"mouse_mode_desc\": \"Изберете метода за въвеждане на мишка за това приложение. Авто използва глобалната настройка, Виртуална мишка използва HID драйвер, SendInput използва Windows API.\",\n    \"mouse_mode_sendinput\": \"SendInput (Windows API)\",\n    \"mouse_mode_vmouse\": \"Виртуална мишка\",\n    \"name\": \"Име\",\n    \"output_desc\": \"Файлът, в който се запазва изходът (текстовия поток) от командата. Ако не е посочен, изходът се игнорира.\",\n    \"output_name\": \"Изход\",\n    \"run_as_desc\": \"Това може да е необходимо за някои приложения, които изискват администраторски права, за да работят правилно.\",\n    \"scan_result_add_all\": \"Добави всички\",\n    \"scan_result_edit_title\": \"Добави и редактирай\",\n    \"scan_result_filter_all\": \"Всички\",\n    \"scan_result_filter_epic_title\": \"Epic Games игри\",\n    \"scan_result_filter_executable\": \"Изпълним\",\n    \"scan_result_filter_executable_title\": \"Изпълним файл\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxy игри\",\n    \"scan_result_filter_script\": \"Скрипт\",\n    \"scan_result_filter_script_title\": \"Пакетен/Команден скрипт\",\n    \"scan_result_filter_shortcut\": \"Преки път\",\n    \"scan_result_filter_shortcut_title\": \"Преки път\",\n    \"scan_result_filter_steam_title\": \"Steam игри\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Игра\",\n    \"scan_result_games_only\": \"Само игри\",\n    \"scan_result_matched\": \"Съвпадения: {count}\",\n    \"scan_result_no_apps\": \"Не са намерени приложения за добавяне\",\n    \"scan_result_no_matches\": \"Не са намерени съвпадащи приложения\",\n    \"scan_result_quick_add_title\": \"Бързо добавяне\",\n    \"scan_result_remove_title\": \"Премахни от списъка\",\n    \"scan_result_search_placeholder\": \"Търсене на име на приложение, команда или път...\",\n    \"scan_result_show_all\": \"Покажи всички\",\n    \"scan_result_title\": \"Резултати от сканирането\",\n    \"scan_result_try_different_keywords\": \"Опитайте да използвате различни ключови думи за търсене\",\n    \"scan_result_type_batch\": \"Пакетен\",\n    \"scan_result_type_command\": \"Команден скрипт\",\n    \"scan_result_type_executable\": \"Изпълним файл\",\n    \"scan_result_type_shortcut\": \"Преки път\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Търсене на приложения...\",\n    \"select\": \"Избери\",\n    \"test_menu_cmd\": \"Тест на команда\",\n    \"test_menu_cmd_empty\": \"Командата не може да бъде празна\",\n    \"test_menu_cmd_executing\": \"Изпълнение на команда...\",\n    \"test_menu_cmd_failed\": \"Неуспешно изпълнение на команда\",\n    \"test_menu_cmd_success\": \"Командата е изпълнена успешно!\",\n    \"use_desktop_image\": \"Използване на текущия тапет на работния плот\",\n    \"wait_all\": \"Поточното предаване да продължава, докато всички процеси на приложението не се затворят\",\n    \"wait_all_desc\": \"По този начин поточното предаване ще продължи, докато всички процеси, стартирани от приложението, не завършат изпълнението си. Ако няма отметка, предаването ще спре, когато първоначалният процес на приложението завърши, дори и да има други процеси от приложението, които все още работят.\",\n    \"working_dir\": \"Работна директория\",\n    \"working_dir_desc\": \"Работната директория, която да се подаде на процеса. Някои приложения, например, използват работната директория, за да търсят конфигурационни файлове. Ако не е зададена, по подразбиране ще се ползва директорията, в която се намира командата.\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Име на устройството\",\n    \"adapter_name_desc_linux_1\": \"Ръчно задаване на графичния процесор, който да се използва за прихващане на екрана.\",\n    \"adapter_name_desc_linux_2\": \"за намиране на всички устройства, които могат да използват VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Заменете ``renderD129`` с устройството върнато от по-горната команда, за да видите името и възможностите на устройството. За да бъде поддържано от Sunshine, то трябва задължително да има поне:\",\n    \"adapter_name_desc_windows\": \"Ръчно задаване на графичния процесор, който да се използва за прихващане на екрана. Ако не е зададено, графичният процесор се избира автоматично. Силно препоръчваме да оставите това поле празно, за да се направи автоматичен избор! Забележка: към този графичен процесор трябва да има свързан и включен екран. Съответните стойности могат да бъдат намерени чрез следната команда:\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Ако е инсталирана най-новата версия на виртуалния дисплей, тя може автоматично да се свърже с GPU връзката\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Добавяне\",\n    \"address_family\": \"Вид адреси\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Задайте вида адреси, използвани от Sunshine\",\n    \"address_family_ipv4\": \"Само IPv4\",\n    \"always_send_scancodes\": \"Винаги да се пращат скан-кодове\",\n    \"always_send_scancodes_desc\": \"Изпращането на скан-кодове подобрява съвместимостта с игри и приложения, но може да доведе до неправилно разчетени входни сигнали от клавиатурата при някои клиенти, ако не се ползва клавиатурна подредба съвместима с английски (САЩ). Включете това, ако въвеждането от клавиатурата изобщо не работи в някои приложения. Изключете го, ако клавишите на клиента пращат грешни входни сигнали към отдалечения компютър.\",\n    \"amd_coder\": \"Кодиране чрез AMF (H264)\",\n    \"amd_coder_desc\": \"Позволява избирането на ентропия при кодирането, така че да се даде приоритет на качеството или скростта на кодиране. Само за H.264.\",\n    \"amd_enforce_hrd\": \"Принудителна настройка на декодера с хипотетични справки (HRD) на AMF\",\n    \"amd_enforce_hrd_desc\": \"Увеличава ограниченията за контрол на скоростта, така че да се спазват изискванията на модела на HRD. Това значително намалява превишаването на идеалната побитова скорост, но може да предизвика дефекти при кодирането или влошаване на качеството при определени видео карти.\",\n    \"amd_preanalysis\": \"Предварителен анализ на AMF\",\n    \"amd_preanalysis_desc\": \"Дава възможност за предварителен анализ на контрола на скоростта, който може да повиши качеството за сметка на увеличено забавяне на кодирането.\",\n    \"amd_quality\": \"Качество на AMF\",\n    \"amd_quality_balanced\": \"балансирано – балансирано (по подразбиране)\",\n    \"amd_quality_desc\": \"По този начин се контролира балансът между скоростта и качеството на кодиране.\",\n    \"amd_quality_group\": \"Настройки за качеството на AMF\",\n    \"amd_quality_quality\": \"качество – предпочитане на качеството\",\n    \"amd_quality_speed\": \"скорост – предпочитане на скоростта\",\n    \"amd_qvbr_quality\": \"Ниво на качество AMF QVBR\",\n    \"amd_qvbr_quality_desc\": \"Ниво на качество за режим на управление на битрейт QVBR. Обхват: 1-51 (по-ниско = по-добро качество). По подразбиране: 23. Прилага се само когато управлението на битрейт е зададено на 'qvbr'.\",\n    \"amd_rc\": \"Управление на скоростта на AMF\",\n    \"amd_rc_cbr\": \"cbr – постоянна побитова скорост (препоръчва се, ако HRD е включено)\",\n    \"amd_rc_cqp\": \"cqp – режим на постоянно qp\",\n    \"amd_rc_desc\": \"Това задава метода за управление на скоростта, така че да се гарантира, че няма да се превишава целевата побитова скорост на клиента. „cqp“ не е подходящ метод за подсигуряване на определената побитова скорост, а другите възможности (с изключение на „vbr_latency“) зависят от Принудителната настройка на HRD, която да помогне с ограничаването на превишаванията на побитовата скорост.\",\n    \"amd_rc_group\": \"Настройки за управление на скоростта на AMF\",\n    \"amd_rc_hqcbr\": \"hqcbr -- високо качество постоянен битрейт\",\n    \"amd_rc_hqvbr\": \"hqvbr -- високо качество променлив битрейт\",\n    \"amd_rc_qvbr\": \"qvbr -- променлив битрейт с качество (използва ниво на качество QVBR)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency – променлива побитова скорост, ограничена от забавянето (препоръчва се, ако HRD е изключен; по подразбиране)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak – променлива побитова скорост, ограничена от максимумите\",\n    \"amd_usage\": \"Използване на AMF\",\n    \"amd_usage_desc\": \"С тази настройка се задава основният профил на кодиране. Всички настройки по-долу нагласят подмножество от профила на използване, но има зададени и допълнителни скрити настройки, които не могат да бъдат променени другаде.\",\n    \"amd_usage_lowlatency\": \"lowlatency ниско забавяне (най-бързо)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality – ниско забавяне, високо качество (бързо)\",\n    \"amd_usage_transcoding\": \"transcoding – транскодиране (най-бавно)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency – ултра ниско забавяне (най-бързо; по подразбиране)\",\n    \"amd_usage_webcam\": \"webcam – уеб камера (бавно)\",\n    \"amd_vbaq\": \"Базирано на вариации адаптивно квантуване на AMF (VBAQ)\",\n    \"amd_vbaq_desc\": \"Човешкото зрение обикновено е по-малко чувствително към дефекти в места с много текстури. В режима VBAQ дисперсията на пикселите се използва за разпознаване на сложността на пространствените текстури, което позволява на кодирането да заделя повече битове за по-гладките области. Включването на тази функция води до подобряване на субективното визуално качество при някои видове съдържание.\",\n    \"amf_draw_mouse_cursor\": \"Рисуване на прост курсор при използване на метод за запис AMF\",\n    \"amf_draw_mouse_cursor_desc\": \"В някои случаи, използването на запис чрез AMF няма да покаже показалеца на мишката. Включването на тази опция ще нарисува прост показалец на мишката на екрана. Забележка: Позицията на показалеца на мишката ще се обновява само когато има обновление на екрана със съдържание, така че в сценарии извън игри, като например на работния плот, може да наблюдавате забавено движение на показалеца на мишката.\",\n    \"apply_note\": \"Натиснете „Прилагане“, за да рестартирате Sunshine и да приложите промените. Това ще прекрати всички текущи сесии.\",\n    \"audio_sink\": \"Звуков изход\",\n    \"audio_sink_desc_linux\": \"Името на звуковия изход, използван за връщане на звука. Ако не е зададено, pulseaudio ще избере мониторното устройство по подразбиране. Можете да намерите името на звуковия изход като използвате някоя от тези команди:\",\n    \"audio_sink_desc_macos\": \"Името на звуковия изход, използван за връщане на звука. Sunshine може да получи достъп само до микрофоните в macOS, поради системни ограничения. За поточно предаване на системен звук ще Ви трябва помощта на Soundflower или BlackHole.\",\n    \"audio_sink_desc_windows\": \"Ръчно задаване на конкретно звуково устройство за прихващане. Ако не е зададено, устройството се избира автоматично. Силно препоръчително е да оставите това поле празно, за да използвате автоматичния избор на устройство! Ако имате няколко звукови устройства с еднакви имена, може да научите идентификаторите им чрез следната команда:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Високоговорители (звуково устройство с високо качество)\",\n    \"av1_mode\": \"Поддръжка на AV1\",\n    \"av1_mode_0\": \"Sunshine ще обявява поддръжката на AV1 въз основа на възможностите за кодиране (препоръчително)\",\n    \"av1_mode_1\": \"Sunshine няма да обявява поддръжката на AV1\",\n    \"av1_mode_2\": \"Sunshine ще обявява поддръжката на AV1 Main 8-битов профил\",\n    \"av1_mode_3\": \"Sunshine ще обявява поддръжката на AV1 Main 8-битов и 10-битов (HDR) профили\",\n    \"av1_mode_desc\": \"Позволява на клиента да поиска видео поток с кодиране AV1 Main 8-битов или 10-битов. Кодирането на AV1 натоварва повече процесора, така че включването на тази настройка може да намали производителността при използване на софтуерно кодиране.\",\n    \"back_button_timeout\": \"Време на изчакване за симулиране на бутона Home/Guide\",\n    \"back_button_timeout_desc\": \"Ако бутонът Back/Select се задържи натиснат за определения брой милисекунди, се симулира натискане на бутона Home/Guide. Ако е зададена стойност < 0 (по подразбиране), задържането на бутона Back/Select няма да симулира натискането на бутона Home/Guide.\",\n    \"bind_address\": \"Адрес за свързване (тестова функция)\",\n    \"bind_address_desc\": \"Задайте конкретния IP адрес, към който Sunshine ще се свърже. Ако е оставено празно, Sunshine ще се свърже към всички налични адреси.\",\n    \"capture\": \"Принудително използване на определен метод на прихващане\",\n    \"capture_desc\": \"В автоматичния режим Sunshine ще използва първия, който работи. NvFBC изисква коригирани драйвери на nvidia.\",\n    \"capture_target\": \"Цел на запис\",\n    \"capture_target_desc\": \"Изберете типа на целта за запис. Когато изберете 'Прозорец', можете да записвате конкретен прозорец на приложение (като софтуер за интерполация на кадри с изкуствен интелект) вместо целия дисплей.\",\n    \"capture_target_display\": \"Дисплей\",\n    \"capture_target_window\": \"Прозорец\",\n    \"cert\": \"Сертификат\",\n    \"cert_desc\": \"Сертификатът, който да се ползва за сдвояване на уеб интерфейса и клиента Moonlight. За по-добра съвместимост той трябва да има публичен ключ от вида RSA-2048.\",\n    \"channels\": \"Максимален брой свързани клиенти\",\n    \"channels_desc_1\": \"Sunshine може да позволи една сесия за поточно предаване да бъде споделена с множество клиенти едновременно.\",\n    \"channels_desc_2\": \"Някои методи за хардуерно кодиране могат да имат ограничения, които намаляват производителността при излъчване на повече от един поток.\",\n    \"close_verify_safe\": \"Безопасно удостоверяване, съвместимо с стари клиенти\",\n    \"close_verify_safe_desc\": \"Старите клиенти може да не могат да се свържат с Sunshine, моля, изключете тази опция или актуализирайте клиента\",\n    \"coder_cabac\": \"cabac – контекстно-адаптивно двоично аритметично кодиране – по-високо качество\",\n    \"coder_cavlc\": \"cavlc – контекстно адаптивно кодиране с променлива дължина – по-бързо декодиране\",\n    \"configuration\": \"Настройки\",\n    \"controller\": \"Управление чрез контролер\",\n    \"controller_desc\": \"Позволява на клиентите да управляват отдалечения компютър с контролер\",\n    \"credentials_file\": \"Файл с удостоверителни данни\",\n    \"credentials_file_desc\": \"Съхраняване на потребителското име/парола отделно от файла за състоянието на Sunshine.\",\n    \"display_device_options_note_desc_windows\": \"Windows запазва различни настройки на дисплея за всяка комбинация от текущо активни дисплеи.\\nСлед това Sunshine прилага промени към дисплей(и), принадлежащ(и) към такава дисплейна комбинация.\\nАко изключите устройство, което е било активно, когато Sunshine е приложил настройките, промените не могат да бъдат\\nвърнати обратно, освен ако комбинацията не може да бъде активирана отново до момента, в който Sunshine се опита да върне промените!\",\n    \"display_device_options_note_windows\": \"Бележка относно начина на прилагане на настройките\",\n    \"display_device_options_windows\": \"Опции на устройството за дисплей\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Активира дисплея, ако не е вече активен\",\n    \"display_device_prep_ensure_active_windows\": \"Активиране на дисплея автоматично\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Деактивира всички други дисплеи и активира само посочения\",\n    \"display_device_prep_ensure_only_display_windows\": \"Деактивиране на други дисплеи и активиране само на посочения дисплей\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Активира дисплея и го задава като основен\",\n    \"display_device_prep_ensure_primary_windows\": \"Активиране на дисплея автоматично и задаването му като основен дисплей\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Използва само виртуалния дисплей за вторично разширено стрийминг\",\n    \"display_device_prep_ensure_secondary_windows\": \"Стрийминг на вторичен дисплей (само виртуален дисплей)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Без промени в състоянието на дисплея; потребителят трябва сам да се увери, че дисплеят е готов\",\n    \"display_device_prep_no_operation_windows\": \"Изключено\",\n    \"display_device_prep_windows\": \"Подготовка на дисплея\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Трябва да бъдат посочени поне една „получена“ и една „крайна“ стойност.\\nПразно поле в раздел „получена“ означава „съвпадение с всяка“. Празно поле в раздел „крайна“ означава „запазване на получената стойност“.\\nМожете да съпоставите конкретна стойност на кадъра/сек с конкретна резолюция, ако желаете...\\n\\nЗабележка: ако опцията „Оптимизиране на настройките на играта“ не е включена в клиента Moonlight, се игнорират редовете, съдържащи стойности на резолюцията.\",\n    \"display_mode_remapping_desc_windows\": \"Посочете как конкретна резолюция и/или честота на опресняване трябва да бъдат пренасочени към други стойности.\\nМожете да стриймвате с по-ниска резолюция, докато рендирате с по-висока резолюция на хоста за ефект на суперсемплиране.\\nИли можете да стриймвате с по-високи кадри/сек, докато ограничавате хоста до по-ниската честота на опресняване.\\nСъвпадението се извършва отгоре надолу. След като записът съвпадне, другите вече не се проверяват, но все пак се валидират.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Крайна честота на опресняване\",\n    \"display_mode_remapping_final_resolution_windows\": \"Крайна резолюция\",\n    \"display_mode_remapping_optional\": \"по избор\",\n    \"display_mode_remapping_received_fps_windows\": \"Получени кадри/сек\",\n    \"display_mode_remapping_received_resolution_windows\": \"Получена резолюция\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Забележка: ако опцията „Оптимизиране на настройките на играта“ не е включена в клиента Moonlight, пренасочването е деактивирано.\",\n    \"display_mode_remapping_windows\": \"Пренасочване на режимите на дисплея\",\n    \"display_modes\": \"Режими на дисплея\",\n    \"ds4_back_as_touchpad_click\": \"Симулиране на бутона Back/Select чрез натискане на сензорния панел\",\n    \"ds4_back_as_touchpad_click_desc\": \"При принудително симулиране на контролер DualShock 4, да се симулира натискането на бутона Back/Select чрез натискане на сензорния панел\",\n    \"dsu_server_port\": \"Порт на DSU сървър\",\n    \"dsu_server_port_desc\": \"Порт за слушане на DSU сървър (по подразбиране 26760). Sunshine ще действа като DSU сървър за получаване на клиентски връзки и изпращане на данни за движение. Активирайте DSU сървъра във вашия клиент (Yuzu, Ryujinx и др.) и задайте адрес на DSU сървъра (127.0.0.1) и порт (26760)\",\n    \"enable_dsu_server\": \"Активиране на DSU сървър\",\n    \"enable_dsu_server_desc\": \"Активиране на DSU сървър за получаване на клиентски връзки и изпращане на данни за движение\",\n    \"encoder\": \"Принудително използване на определен метод на кодиране\",\n    \"encoder_desc\": \"Принудително използване на определен метод на кодиране. Ако не е зададено, Sunshine ще избере най-добрия наличен вариант. Забележка: ако използвате Уиндоус и изберете хардуерно кодиране, то трябва да се поддържа от видео картата, към която е свързан екранът.\",\n    \"encoder_software\": \"Софтуерно\",\n    \"experimental\": \"Експериментално\",\n    \"experimental_features\": \"Експериментални функции\",\n    \"external_ip\": \"Външен IP адрес\",\n    \"external_ip_desc\": \"Ако не е зададен външен IP адрес, Sunshine автоматично ще открие какъв е той\",\n    \"fec_percentage\": \"Процент на FEC\",\n    \"fec_percentage_desc\": \"Процент на пакетите за коригиране на грешки от всеки пакет данни за всеки видео кадър. По-високите стойности могат да коригират по-голяма загуба на мрежови пакети, но за сметка на увеличаване на данните предавани по мрежата.\",\n    \"ffmpeg_auto\": \"auto – нека ffmpeg реши (по подразбиране)\",\n    \"file_apps\": \"Файл с приложения\",\n    \"file_apps_desc\": \"Файлът, в който се съхраняват настройките на приложенията в Sunshine.\",\n    \"file_state\": \"Файл за състоянието\",\n    \"file_state_desc\": \"Файлът, в който се съхранява текущото състояние на Sunshine\",\n    \"fps\": \"Обявени кадри/сек\",\n    \"gamepad\": \"Симулиран вид контролер\",\n    \"gamepad_auto\": \"Опции за автоматичен избор\",\n    \"gamepad_desc\": \"Изберете какъв вид контролер да бъде симулиран на отдалечения компютър\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"Опции за избор на DS4\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Ръчни настройки за DS4\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Подготвителни команди\",\n    \"global_prep_cmd_desc\": \"Настройване на списък с команди, които да се изпълняват преди или след дадено приложение. Ако някоя от посочените подготвителни команди се провали, процесът на стартиране на приложението ще бъде прекъснат.\",\n    \"hdr_luminance_analysis\": \"Динамични HDR метаданни (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Активира анализ на яркостта на GPU за кадър и инжектира динамични метаданни HDR10+ (ST 2094-40) и HDR Vivid (CUVA) в кодирания поток. Осигурява подсказки за тонално картографиране за кадър за поддържани дисплеи. Добавя малко GPU натоварване (~0,5-1,5ms/кадър при високи резолюции). Деактивирайте при спадове на кадровата честота с HDR.\",\n    \"hdr_prep_automatic_windows\": \"Включване/изключване на HDR режима според заявката на клиента\",\n    \"hdr_prep_no_operation_windows\": \"Изключено\",\n    \"hdr_prep_windows\": \"Промяна на състоянието на HDR\",\n    \"hevc_mode\": \"Поддръжка на HEVC\",\n    \"hevc_mode_0\": \"Sunshine ще обявява поддръжката на HEVC въз основа на възможностите за кодиране (препоръчително)\",\n    \"hevc_mode_1\": \"Sunshine няма да обявява поддръжката на HEVC\",\n    \"hevc_mode_2\": \"Sunshine ще обявява поддръжката на HEVC, профил Main\",\n    \"hevc_mode_3\": \"Sunshine ще обявява поддръжката на HEVC, профили Main и Main10 (HDR)\",\n    \"hevc_mode_desc\": \"Позволява на клиента да поиска видео поток с кодиране HEVC Main или HEVC Main10. Кодирането на HEVC натоварва повече процесора, така че включването на тази настройка може да намали производителността при използване на софтуерно кодиране.\",\n    \"high_resolution_scrolling\": \"Поддръжка на превъртане с висока резолюция\",\n    \"high_resolution_scrolling_desc\": \"Когато това е включено, Sunshine просто ще препредава командите за превъртане на колелцето на мишката с висока резолюция, идващи от клиенти използващи Moonlight. Може да е по-добре това да бъде изключено за по-старите приложения, в които превъртането може да мести съдържанието твърде бързо, ако събитията за превъртане са с висока резолюция.\",\n    \"install_steam_audio_drivers\": \"Инсталиране на аудио драйверите на Steam\",\n    \"install_steam_audio_drivers_desc\": \"Ако Steam е инсталиран, това автоматично ще инсталира и драйвера за поточно предаване на звук на Steam, чрез който може да се поддържа 5.1/7.1 обемен звук, както и да се заглушава звука на отдалечения компютър.\",\n    \"key_repeat_delay\": \"Забавяне на повторението на клавишите\",\n    \"key_repeat_delay_desc\": \"Колко бързо да започва повтарянето на симулираното натискане на клавишите, при задържането им в натиснато положение. Това е първоначалното закъснение в милисекунди преди да за почне повторението на клавишите.\",\n    \"key_repeat_frequency\": \"Честота на повтаряне на клавишите\",\n    \"key_repeat_frequency_desc\": \"Колко често да се извършват симулирани натискания на клавишите в секунда, при задържане на клавишите в натиснато положение. Стойността тук може да бъде и нецяло число.\",\n    \"key_rightalt_to_key_win\": \"Map Right Alt key to Windows key\",\n    \"key_rightalt_to_key_win_desc\": \"Възможно е натискането на клавиша Windows да не може да бъде изпратено към сървъра от Moonlight. В тези случаи може да настроите Sunshine да мисли, че десният Alt е клавишът Windows.\",\n    \"key_rightalt_to_key_windows\": \"Map Right Alt key to Windows key\",\n    \"keyboard\": \"Управление чрез клавиатура\",\n    \"keyboard_desc\": \"Позволява на клиентите да управляват отдалечения компютър с клавиатура\",\n    \"lan_encryption_mode\": \"Режим на шифроване в LAN\",\n    \"lan_encryption_mode_1\": \"Включено за поддържаните клиенти\",\n    \"lan_encryption_mode_2\": \"Задължително за всички клиенти\",\n    \"lan_encryption_mode_desc\": \"Това определя дали да се използва шифроване при излъчване в локалната мрежа. Шифроването може да намали производителността на излъчването, особено при не особено мощни сървъри и клиенти.\",\n    \"locale\": \"Език\",\n    \"locale_desc\": \"Език на потребителския интерфейс на Sunshine.\",\n    \"log_level\": \"Ниво на съобщенията в журнала\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"Info\",\n    \"log_level_3\": \"Warning\",\n    \"log_level_4\": \"Error\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"Нищо\",\n    \"log_level_desc\": \"Минималното ниво на съобщения в журнала, извеждан на стандартния изход\",\n    \"log_path\": \"Път до журналния файл\",\n    \"log_path_desc\": \"Файлът, в който се запазва текущия журнал на Sunshine.\",\n    \"max_bitrate\": \"Максимална побитова скорост\",\n    \"max_bitrate_desc\": \"Максималната побитова скорост (в Kbps), с която да се кодира потокът. Ако е зададена стойност 0, винаги ще се използва скоростта, поискана от Moonlight.\",\n    \"max_fps_reached\": \"Достигнати максимални стойности на FPS\",\n    \"max_resolutions_reached\": \"Достигнат максимален брой резолюции\",\n    \"mdns_broadcast\": \"Намиране на този компютър в локалната мрежа\",\n    \"mdns_broadcast_desc\": \"Ако е активирана тази опция, Sunshine ще позволи на други устройства да намират този компютър автоматично. Moonlight трябва да бъде настроен да намира този компютър автоматично в локалната мрежа.\",\n    \"min_threads\": \"Минимален брой нишки на процесора\",\n    \"min_threads_desc\": \"Увеличаването на стойността леко намалява ефективността на кодирането, но компромисът обикновено си заслужава, тъй като ще се използват повече процесорни ядра за кодиране. Идеалната стойност е най-ниската възможна стойност, при която кодирането може да се извършва надеждно при желаните от настройки за излъчване и използваният хардуер.\",\n    \"minimum_fps_target\": \"Целеви минимални кадри/сек\",\n    \"minimum_fps_target_desc\": \"Минимални кадри/сек за поддържане при кодиране (0 = автоматично, около половината от кадрите/сек на потока; 1-1000 = минимални кадри/сек за поддържане). Когато е активирана променлива честота на опресняване, тази настройка се игнорира, ако е зададена на 0.\",\n    \"misc\": \"Други настройки\",\n    \"motion_as_ds4\": \"Симулиране на контролер DS4, ако контролерът на клиента разполага с поддръжка на сензори за движение\",\n    \"motion_as_ds4_desc\": \"Ако е изключено, сензорите за движение няма да се вземат предвид при избора на вида контролер.\",\n    \"mouse\": \"Управление чрез мишка\",\n    \"mouse_desc\": \"Позволява на клиентите да управляват отдалечения компютър с мишка\",\n    \"native_pen_touch\": \"Собствена поддръжка на писалка/докосване\",\n    \"native_pen_touch_desc\": \"Когато е включено, Sunshine просто ще препредава командите идващи от писалка/докосване както са получени от клиентите използващи Moonlight. Може да е по-добре това да бъде изключено за по-старите приложения, които няма собствена поддръжка на писалка/докосване.\",\n    \"no_fps\": \"Не са добавени стойности на FPS\",\n    \"no_resolutions\": \"Не са добавени резолюции\",\n    \"notify_pre_releases\": \"Известия за предварителни версии\",\n    \"notify_pre_releases_desc\": \"Дали да бъдете уведомявани за нови предварителни версии на Sunshine, преди превръщането им в официални\",\n    \"nvenc_h264_cavlc\": \"Предпочитане на CAVLC пред CABAC за H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Опростен вариант за ентропия при кодирането. CAVLC се нуждае от около 10% повече побитова скорост за същото качество. Има смисъл само за много стари декодиращи устройства.\",\n    \"nvenc_latency_over_power\": \"Предпочитане на по-малкото забавяне пред икономията на енергия\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine изисква от графичния процесор да работи на максималната си тактова честота по време на излъчване, за да намали забавянето при кодирането. Изключването на тази настройка не се препоръчва, тъй като това може да доведе до значително увеличаване на закъснението при кодиране.\",\n    \"nvenc_lookahead_depth\": \"Дълбочина на прогнозиране\",\n    \"nvenc_lookahead_depth_desc\": \"Брой кадри за прогнозиране по време на кодиране (0-32). Прогнозирането подобрява качеството на кодиране, особено в сложни сцени, като осигурява по-добра оценка на движението и разпределение на битрейта. По-високите стойности подобряват качеството, но увеличават латентността на кодирането. Задайте на 0, за да изключите прогнозирането. Изисква NVENC SDK 13.0 (1202) или по-нов.\",\n    \"nvenc_lookahead_level\": \"Ниво на прогнозиране\",\n    \"nvenc_lookahead_level_0\": \"Ниво 0 (най-ниско качество, най-бързо)\",\n    \"nvenc_lookahead_level_1\": \"Ниво 1\",\n    \"nvenc_lookahead_level_2\": \"Ниво 2\",\n    \"nvenc_lookahead_level_3\": \"Ниво 3 (най-високо качество, най-бавно)\",\n    \"nvenc_lookahead_level_autoselect\": \"Автоматичен избор (шофьорът избира оптималното ниво)\",\n    \"nvenc_lookahead_level_desc\": \"Ниво на качество на прогнозирането. По-високите нива подобряват качеството за сметка на производителността. Тази опция влиза в сила само когато lookahead_depth е по-голямо от 0. Изисква NVENC SDK 13.0 (1202) или по-нов.\",\n    \"nvenc_lookahead_level_disabled\": \"Изключено (същото като ниво 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Изчертаване на OpenGL/Vulkan върху DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine не може да прихваща с пълна честота на кадрите програми, реализирани с OpenGL или Vulkan, работещи на цял екран, освен ако не се изчертават върху DXGI. Това е генерална системна настройка, която ще бъде върната в първоначалното си състояние при затваряне на процеса на Sunshine.\",\n    \"nvenc_preset\": \"Настройка за производителност\",\n    \"nvenc_preset_1\": \"(най-бързо, по подразбиране)\",\n    \"nvenc_preset_7\": \"(най-бавно)\",\n    \"nvenc_preset_desc\": \"По-големите числа подобряват компресията (качеството при дадена побитова скорост) за сметка на увеличено забавяне при кодирането. Препоръчително е това да се променя само ако има ограничение от мрежата или декодера. В противен случай подобен ефект може да се постигне чрез увеличаване на побитовата скорост.\",\n    \"nvenc_rate_control\": \"Режим на контрол на скоростта\",\n    \"nvenc_rate_control_cbr\": \"CBR (Постоянна побитова скорост) - Ниска латентност\",\n    \"nvenc_rate_control_desc\": \"Изберете режим на контрол на скоростта. CBR (Постоянна побитова скорост) осигурява фиксиран битрейт за стрийминг с ниска латентност. VBR (Променлива побитова скорост) позволява битрейтът да варира в зависимост от сложността на сцената, осигурявайки по-добро качество за сложни сцени за сметка на променлив битрейт.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Променлива побитова скорост) - По-добро качество\",\n    \"nvenc_realtime_hags\": \"Използване на реално-времеви приоритет при хардуерно-ускореното планиране на задачите на графичния процесор (HAGS)\",\n    \"nvenc_realtime_hags_desc\": \"В момента драйверите на NVIDIA могат да засичат при кодиране, ако HAGS е включено, използва се реално-времеви приоритет и използването на видео паметта е близо до максимума. Изключването на тази опция понижава приоритета до „висок“, заобикаляйки засичането за сметка на намалена производителност на прихващане на екрана, когато графичният процесор е силно натоварен.\",\n    \"nvenc_spatial_aq\": \"Пространствено AQ\",\n    \"nvenc_spatial_aq_desc\": \"Използване на по-високи стойности на QP за по-простите области във видеото. Препоръчително е това да бъде включено, когато се излъчва с по-ниска побитова скорост.\",\n    \"nvenc_spatial_aq_disabled\": \"Изключено (по-бързо, по подразбиране)\",\n    \"nvenc_spatial_aq_enabled\": \"Включено (по-бавно)\",\n    \"nvenc_split_encode\": \"Разделено кодиране на кадри\",\n    \"nvenc_split_encode_desc\": \"Разделяне на кодирането на всеки видео кадър върху множество NVENC хардуерни единици. Значително намалява латентността на кодирането с минимално влошаване на ефективността на компресията. Тази опция се игнорира, ако вашият графичен процесор има една NVENC единица.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Шофьорът решава (по подразбиране)\",\n    \"nvenc_split_encode_four_strips\": \"Правене на разделяне на 4 ленти (изисква 4+ NVENC двигателя)\",\n    \"nvenc_split_encode_three_strips\": \"Правене на разделяне на 3 ленти (изисква 3+ NVENC двигателя)\",\n    \"nvenc_split_encode_two_strips\": \"Правене на разделяне на 2 ленти (изисква 2+ NVENC двигателя)\",\n    \"nvenc_target_quality\": \"Целево качество (VBR режим)\",\n    \"nvenc_target_quality_desc\": \"Ниво на целевото качество за VBR режим (0-51 за H.264/HEVC, 0-63 за AV1). По-ниски стойности = по-високо качество. Задайте на 0 за автоматичен избор на качество. Използва се само когато режимът на контрол на скоростта е VBR.\",\n    \"nvenc_temporal_aq\": \"Времево адаптивно квантуване\",\n    \"nvenc_temporal_aq_desc\": \"Активиране на времево адаптивно квантуване. Времевото AQ оптимизира квантуването във времето, осигурявайки по-добро разпределение на битрейта и подобрено качество в сцени с движение. Тази функция работи съвместно с пространственото AQ и изисква активиране на прогнозиране (lookahead_depth > 0). Изисква NVENC SDK 13.0 (1202) или по-нов.\",\n    \"nvenc_temporal_filter\": \"Времеви филтър\",\n    \"nvenc_temporal_filter_4\": \"Ниво 4 (максимална сила)\",\n    \"nvenc_temporal_filter_desc\": \"Сила на времевото филтриране, прилагана преди кодирането. Времевият филтър намалява шума и подобрява ефективността на компресията, особено за естествено съдържание. По-високите нива осигуряват по-добро намаляване на шума, но могат да доведат до леко размазване. Изисква NVENC SDK 13.0 (1202) или по-нов. Забележка: Изисква frameIntervalP >= 5, несъвместимо с zeroReorderDelay или стерео MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Изключено (без времево филтриране)\",\n    \"nvenc_twopass\": \"Режим на две преминавания\",\n    \"nvenc_twopass_desc\": \"Добавя предварителна стъпка на кодиране. Това позволява да се открият повече вектори на движение, да се разпредели по-добре побитовата скорост в рамките на кадъра, както и да се спазват по-стриктно ограниченията на побитовата скорост. Изключването не се препоръчва, тъй като това може да доведе до периодично превишаване на зададената побитова скорост и последваща загуба на пакети.\",\n    \"nvenc_twopass_disabled\": \"Изключено (най-бързо, не се препоръчва)\",\n    \"nvenc_twopass_full_res\": \"Пълна резолюция (по-бавно)\",\n    \"nvenc_twopass_quarter_res\": \"Четвърт резолюция (по-бързо, по подразбиране)\",\n    \"nvenc_vbv_increase\": \"Процентно увеличение на VBV/HRD в един кадър\",\n    \"nvenc_vbv_increase_desc\": \"По подразбиране Sunshine използва VBV/HRD в един кадър, което означава, че размерът на всеки кодиран видео кадър не се очаква да превишава желаната побитова скорост, разделена на желаната честота на кадрите. Отслабването на това ограничение може да бъде от полза и да действа като променлива побитова скорост с ниско забавяне, но същевременно може да доведе до загуба на пакети, ако мрежата не разполага с достатъчен буфер, за да се справи с пиковете на побитова скорост. Максимално допустимата стойност е 400, което съответства на 5 пъти увеличен максимален размер на кодирания видео кадър.\",\n    \"origin_web_ui_allowed\": \"Разрешение за достъп до уеб интерфейса\",\n    \"origin_web_ui_allowed_desc\": \"Определя от къде може да се ползва уеб интерфейсът. Това не отменя нуждата от въвеждане на потребителско име и парола.\",\n    \"origin_web_ui_allowed_lan\": \"Само устройства в локалната мрежа имат достъп до уеб интерфейса\",\n    \"origin_web_ui_allowed_pc\": \"Само компютърът, на който работи Sunshine, има достъп до уеб интерфейса\",\n    \"origin_web_ui_allowed_wan\": \"Всеки има достъп до уеб интерфейса\",\n    \"output_name_desc_unix\": \"По време на стартирането на Sunshine би трябвало да видите списък с откритите екрани. Забележка: трябва да използвате стойността на идентификатора в скобите. По-долу е даден пример – действителният екран може да бъде намерен в раздела за Отстраняване на проблеми.\",\n    \"output_name_desc_windows\": \"Ръчно задаване на идентификатор на екран, който да се ползва за прихващане на картината. Ако не е зададено, ще се използва основният екран. Забележка: ако сте посочили конкретна видео карта по-горе, този екран трябва да е свързан към същата. По време на стартирането на Sunshine би трябвало да видите списък с откритите екрани. По-долу е даден пример – действителният екран може да бъде намерен в раздела за Отстраняване на проблеми.\",\n    \"output_name_unix\": \"Номер на екрана\",\n    \"output_name_windows\": \"Идентификатора на екрана\",\n    \"ping_timeout\": \"Време за изчакване на отговор\",\n    \"ping_timeout_desc\": \"Продължителност от време в милисекунди, през което да се изчаква за получаване на данни от Moonlight, преди да се прекрати потокът\",\n    \"pkey\": \"Частен ключ\",\n    \"pkey_desc\": \"Частният ключ, използван за уеб интерфейса и при сдвояване с клиента на Moonlight. За най-добра съвместимост е добре това да бъде частен ключ от вида RSA-2048.\",\n    \"port\": \"Порт\",\n    \"port_alert_1\": \"Sunshine не може да използва портове с номера по-малки 1024!\",\n    \"port_alert_2\": \"Не могат да се ползват портове с номера по-големи от 65535!\",\n    \"port_desc\": \"Задаване на групата от портове, които да се използват от Sunshine\",\n    \"port_http_port_note\": \"Използвайте този порт, за да се свържете с Moonlight.\",\n    \"port_note\": \"Бележка\",\n    \"port_port\": \"Порт\",\n    \"port_protocol\": \"Протокол\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Даването на достъп до уеб интерфейса от Интернет представлява риск за сигурността! Действате на своя отговорност!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Параметър на квантуване (QP)\",\n    \"qp_desc\": \"Някои устройства може да не поддържат постоянна побитова скорост на предаване. За тях вместо това се използва параметъра на кватуване. По-високите стойности означават по-голяма компресия, но и по-ниско качество.\",\n    \"qsv_coder\": \"Кодиране чрез QuickSync (H264)\",\n    \"qsv_preset\": \"Настройка на QuickSync\",\n    \"qsv_preset_fast\": \"бързо (ниско качество)\",\n    \"qsv_preset_faster\": \"по-бързо (по-ниско качество)\",\n    \"qsv_preset_medium\": \"средно (по подразбиране)\",\n    \"qsv_preset_slow\": \"бавно (добро качество)\",\n    \"qsv_preset_slower\": \"по-бавно (по-добро качество)\",\n    \"qsv_preset_slowest\": \"най-бавно (най-добро качество)\",\n    \"qsv_preset_veryfast\": \"най-бързо (най-ниско качество)\",\n    \"qsv_slow_hevc\": \"Разрешаване на бавното кодиране чрез HEVC\",\n    \"qsv_slow_hevc_desc\": \"Това може да даде възможност за кодиране чрез HEVC при ползване на по-стари графични процесори на Intel, за сметка на по-голямо използване на графичния процесор и по-ниска производителност.\",\n    \"refresh_rate_change_automatic_windows\": \"Използване на стойността на кадрите/сек предоставена от клиента\",\n    \"refresh_rate_change_manual_desc_windows\": \"Въведете честотата на опресняване за използване\",\n    \"refresh_rate_change_manual_windows\": \"Използване на ръчно въведена честота на опресняване\",\n    \"refresh_rate_change_no_operation_windows\": \"Изключено\",\n    \"refresh_rate_change_windows\": \"Промяна на кадрите/сек\",\n    \"res_fps_desc\": \"Режимите на дисплея, обявени от Sunshine. Някои версии на Moonlight, като Moonlight-nx (Switch), разчитат на тези списъци, за да гарантират, че исканите резолюции и кадри/сек се поддържат. Тази настройка не променя начина, по който потокът на екрана се изпраща към Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Използване на резолюцията предоставена от клиента\",\n    \"resolution_change_manual_desc_windows\": \"Опцията „Оптимизиране на настройките на играта“ трябва да е включена в клиента Moonlight, за да работи това.\",\n    \"resolution_change_manual_windows\": \"Използване на ръчно въведена резолюция\",\n    \"resolution_change_no_operation_windows\": \"Изключено\",\n    \"resolution_change_ogs_desc_windows\": \"Опцията „Оптимизиране на настройките на играта“ трябва да е включена в клиента Moonlight, за да работи това.\",\n    \"resolution_change_windows\": \"Промяна на резолюцията\",\n    \"resolutions\": \"Обявени резолюции\",\n    \"restart_note\": \"Sunshine се рестартира, за да се приложат промените.\",\n    \"sleep_mode\": \"Режим на заспиване\",\n    \"sleep_mode_away\": \"Режим на отсъствие (Дисплей изключен, незабавно събуждане)\",\n    \"sleep_mode_desc\": \"Контролира какво се случва, когато клиентът изпрати команда за заспиване. Приспиване (S3): традиционен сън, ниска консумация, но изисква WOL за събуждане. Хибернация (S4): запис на диск, много ниска консумация. Режим на отсъствие: дисплеят се изключва, но системата продължава да работи за незабавно събуждане — идеален за сървъри за стрийминг на игри.\",\n    \"sleep_mode_hibernate\": \"Хибернация (S4)\",\n    \"sleep_mode_suspend\": \"Приспиване (S3)\",\n    \"stream_audio\": \"Активиране на предаване на звука\",\n    \"stream_audio_desc\": \"Изключете тази опция, за да спрете предаването на звука.\",\n    \"stream_mic\": \"Активиране на микрофонен поток\",\n    \"stream_mic_desc\": \"Изключете тази опция, за да спрете микрофонния поток.\",\n    \"stream_mic_download_btn\": \"Изтегли виртуален микрофон\",\n    \"stream_mic_download_confirm\": \"Ще бъдете пренасочени към страницата за изтегляне на виртуален микрофон. Продължи?\",\n    \"stream_mic_note\": \"Тази функция изисква инсталиране на виртуален микрофон\",\n    \"sunshine_name\": \"Име на Sunshine\",\n    \"sunshine_name_desc\": \"Името, показвано в Moonlight за този сървър. Ако не е посочено, се използва името на компютъра.\",\n    \"sw_preset\": \"Настройка на софтуерното кодиране\",\n    \"sw_preset_desc\": \"Оптимизиране на баланса между скоростта на кодиране (брой кодирани кадри в секунда) и ефективността на компресиране (качество за бит в битовия поток). Стандартната стойност е „супер бързо“.\",\n    \"sw_preset_fast\": \"бързо\",\n    \"sw_preset_faster\": \"по-бързо\",\n    \"sw_preset_medium\": \"средно\",\n    \"sw_preset_slow\": \"бавно\",\n    \"sw_preset_slower\": \"по-бавно\",\n    \"sw_preset_superfast\": \"супер бързо (по подразбиране)\",\n    \"sw_preset_ultrafast\": \"ултра бързо\",\n    \"sw_preset_veryfast\": \"много бързо\",\n    \"sw_preset_veryslow\": \"много бавно\",\n    \"sw_tune\": \"Фина настройка на софтуерното кодиране\",\n    \"sw_tune_animation\": \"animation – подходящо за анимационни филми; използва по-висока степен на деблокиране и повече референтни кадри\",\n    \"sw_tune_desc\": \"Опции за фина настройка, които се прилагат след зададената по-горе настройка. Стандартната стойност е „zerolatency“.\",\n    \"sw_tune_fastdecode\": \"fastdecode – позволява по-бързо декодиране чрез изключване на определени филтри\",\n    \"sw_tune_film\": \"film – подходящо за висококачествено филмово съдържание; намалява деблокирането\",\n    \"sw_tune_grain\": \"grain – запазва зърнестата структура типична за по-стари филмови материали\",\n    \"sw_tune_stillimage\": \"stillimage – подходящо за съдържание с малко движение, подобно на презентация\",\n    \"sw_tune_zerolatency\": \"zerolatency – подходящо за бързо кодиране и предаване с ниско забавяне (по подразбиране)\",\n    \"system_tray\": \"Активиране на системния трей\",\n    \"system_tray_desc\": \"Дали да се активира системният трей. Ако е активирано, Sunshine ще показва икона в системния трей и може да се управлява от там.\",\n    \"touchpad_as_ds4\": \"Симулиране на контролер DS4, ако контролерът на клиента разполага със сензорен панел\",\n    \"touchpad_as_ds4_desc\": \"Ако е изключено, сензорният панел няма да се взема предвид при избора на вида контролер.\",\n    \"unsaved_changes_tooltip\": \"Имате незапазени промени. Кликнете, за да запазите.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Автоматично настройване на пренасочването на портове за излъчване през Интернет\",\n    \"variable_refresh_rate\": \"Променлива честота на опресняване (VRR)\",\n    \"variable_refresh_rate_desc\": \"Позволяване на скоростта на кадрите на видеопотока да съвпада със скоростта на рендиране на кадрите за поддръжка на VRR. Когато е активирано, кодирането се извършва само когато са налични нови кадри, което позволява на потока да следва действителната скорост на рендиране на кадрите.\",\n    \"vdd_reuse_desc_windows\": \"Когато е активирано, всички клиенти ще споделят един и същ VDD (Virtual Display Device). Когато е деактивирано (по подразбиране), всеки клиент получава собствен VDD. Активирайте за по-бързо превключване между клиенти, но имайте предвид, че всички клиенти ще споделят едни и същи настройки на дисплея.\",\n    \"vdd_reuse_windows\": \"Използване на един и същ VDD за всички клиенти\",\n    \"virtual_display\": \"Виртуален дисплей\",\n    \"virtual_mouse\": \"Драйвер за виртуална мишка\",\n    \"virtual_mouse_desc\": \"При активиране Sunshine ще използва драйвера Zako Virtual Mouse (ако е инсталиран) за симулиране на въвеждане с мишка на ниво HID. Позволява на игри с Raw Input да получават събития на мишката. При деактивиране или липса на драйвер се използва SendInput.\",\n    \"virtual_sink\": \"Виртуален изход\",\n    \"virtual_sink_desc\": \"Ръчно задаване на виртуално звуково устройство, което да се ползва. Ако не е зададено, устройството се избира автоматично. Силно препоръчително е да оставите това поле празно, за да използвате автоматичния избор на устройство!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Инсталиране на драйвер за виртуална мишка?\",\n    \"vmouse_confirm_uninstall\": \"Деинсталиране на драйвер за виртуална мишка?\",\n    \"vmouse_install\": \"Инсталиране на драйвер\",\n    \"vmouse_installing\": \"Инсталира се...\",\n    \"vmouse_note\": \"Драйверът за виртуална мишка изисква отделна инсталация. Използвайте контролния панел на Sunshine за инсталиране или управление на драйвера.\",\n    \"vmouse_refresh\": \"Обновяване на състоянието\",\n    \"vmouse_status_installed\": \"Инсталиран (неактивен)\",\n    \"vmouse_status_not_installed\": \"Не е инсталиран\",\n    \"vmouse_status_running\": \"Работи\",\n    \"vmouse_uninstall\": \"Деинсталиране на драйвер\",\n    \"vmouse_uninstalling\": \"Деинсталира се...\",\n    \"vt_coder\": \"Кодиране на VideoToolbox\",\n    \"vt_realtime\": \"Кодиране в реално време на VideoToolbox\",\n    \"vt_software\": \"Софтуерно кодиране на VideoToolbox\",\n    \"vt_software_allowed\": \"Разрешено\",\n    \"vt_software_forced\": \"Принудително\",\n    \"wan_encryption_mode\": \"Режим на шифроване в WAN\",\n    \"wan_encryption_mode_1\": \"Включено за поддържаните клиенти (по подразбиране)\",\n    \"wan_encryption_mode_2\": \"Задължително за всички клиенти\",\n    \"wan_encryption_mode_desc\": \"Това определя дали да се използва шифроване при излъчване през Интернет. Шифроването може да намали производителността на излъчването, особено при не особено мощни сървъри и клиенти.\",\n    \"webhook_curl_command\": \"Команда\",\n    \"webhook_curl_command_desc\": \"Копирайте следната команда във вашия терминал, за да тествате дали webhook работи правилно:\",\n    \"webhook_curl_copy_failed\": \"Копирането не бе успешно, моля изберете и копирайте ръчно\",\n    \"webhook_enabled\": \"Уведомления Webhook\",\n    \"webhook_enabled_desc\": \"Когато е активирано, Sunshine ще изпраща уведомления за събития на посочения Webhook URL\",\n    \"webhook_group\": \"Настройки за уведомления Webhook\",\n    \"webhook_skip_ssl_verify\": \"Пропускане на проверката на SSL сертификат\",\n    \"webhook_skip_ssl_verify_desc\": \"Пропускане на проверката на SSL сертификат за HTTPS връзки, само за тестване или самоподписани сертификати\",\n    \"webhook_test\": \"Тест\",\n    \"webhook_test_failed\": \"Webhook тестът се провали\",\n    \"webhook_test_failed_note\": \"Забележка: Моля, проверете дали URL адресът е правилен или проверете конзолата на браузъра за повече информация.\",\n    \"webhook_test_success\": \"Webhook тестът е успешен!\",\n    \"webhook_test_success_cors_note\": \"Забележка: Поради ограниченията на CORS, статусът на отговора на сървъра не може да бъде потвърден.\\nЗаявката е изпратена. Ако webhook е конфигуриран правилно, съобщението трябва да е доставено.\\n\\nПредложение: Проверете раздела Мрежа в инструментите за разработчици на вашия браузър за детайли на заявката.\",\n    \"webhook_test_url_required\": \"Моля, първо въведете Webhook URL\",\n    \"webhook_timeout\": \"Таймаут на заявката\",\n    \"webhook_timeout_desc\": \"Таймаут на заявките за Webhook в милисекунди, диапазон 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"URL за получаване на уведомления за събития, поддържа HTTP/HTTPS протоколи\",\n    \"wgc_checking_mode\": \"Проверка...\",\n    \"wgc_checking_running_mode\": \"Проверка на режима на работа...\",\n    \"wgc_control_panel_only\": \"Тази функция е достъпка само в контролния панел на Sunshine\",\n    \"wgc_mode_switch_failed\": \"Неуспешна смяна на режима\",\n    \"wgc_mode_switch_started\": \"Започната е смяна на режима. Ако се появи подкана от UAC, моля, кликнете върху „Да“, за да потвърдите.\",\n    \"wgc_service_mode_warning\": \"Записът чрез WGC изисква работа в потребителски режим. Ако в момента работите в режим на услуга, моля, кликнете върху бутона по-горе, за да превключите към потребителски режим.\",\n    \"wgc_switch_to_service_mode\": \"Превключване към режим на услуга\",\n    \"wgc_switch_to_service_mode_tooltip\": \"В момента работи в потребителски режим. Кликнете, за да превключите към режим на услуга.\",\n    \"wgc_switch_to_user_mode\": \"Превключване към потребителски режим\",\n    \"wgc_switch_to_user_mode_tooltip\": \"Записът чрез WGC изисква работа в потребителски режим. Кликнете върху този бутон, за да превключите към потребителски режим.\",\n    \"wgc_user_mode_available\": \"В момента работи в потребителски режим. Записът чрез WGC е наличен.\",\n    \"window_title\": \"Заглавие на прозореца\",\n    \"window_title_desc\": \"Заглавието на прозореца за запис (частично съвпадение, без значение от малки/главни букви). Ако е оставено празно, името на текущото стартирано приложение ще бъде използвано автоматично.\",\n    \"window_title_placeholder\": \"напр., Име на приложение\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine е сървър за собствено поточно предаване на игри, предназначен за ползване с Moonlight.\",\n    \"download\": \"Сваляне\",\n    \"installed_version_not_stable\": \"Използвате предварителна версия на Sunshine. Възможно е да се сблъскате с различни видове проблеми. Моля, съобщавайте за всички проблеми, които срещате. Благодарим, че помагате да направим Sunshine по-добър софтуер!\",\n    \"loading_latest\": \"Зареждане на последната версия…\",\n    \"new_pre_release\": \"Има нова версия предварителна версия!\",\n    \"new_stable\": \"Има нова стабилна версия!\",\n    \"startup_errors\": \"<b>Внимание!</b> Sunshine засече тези грешки по време на стартиране. <b>НАИСТИНА Е ПРЕПОРЪЧИТЕЛНО</b> да ги отстраните, преди да започнете излъчването.\",\n    \"update_download_confirm\": \"На път сте да отворите страницата за изтегляне на актуализации във вашия браузър. Продължи?\",\n    \"version_dirty\": \"Благодарим, че помогнахте да направим Sunshine по-добър софтуер!\",\n    \"version_latest\": \"Използвате най-новата версия на Sunshine\",\n    \"view_logs\": \"Преглед на журнали\",\n    \"welcome\": \"Здравейте от Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Приложения\",\n    \"configuration\": \"Настройки\",\n    \"home\": \"Начало\",\n    \"password\": \"Промяна на паролата\",\n    \"pin\": \"ПИН\",\n    \"theme_auto\": \"Автоматично\",\n    \"theme_dark\": \"Тъмна\",\n    \"theme_light\": \"Светла\",\n    \"toggle_theme\": \"Цветова схема\",\n    \"troubleshoot\": \"Отстраняване на проблеми\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Потвърждаване на паролата\",\n    \"current_creds\": \"Текущи данни за идентификация\",\n    \"new_creds\": \"Нови данни за идентификация\",\n    \"new_username_desc\": \"Ако не бъде посочено, потребителското име няма да бъде променено\",\n    \"password_change\": \"Промяна на паролата\",\n    \"success_msg\": \"Паролата е променена успешно! Тази страница скоро ще се презареди и браузърът ще поиска да въведете новите данни за идентификация.\"\n  },\n  \"pin\": {\n    \"actions\": \"Действия\",\n    \"cancel_editing\": \"Отказ от редактиране\",\n    \"client_name\": \"Име\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Потвърди изтриване\",\n    \"delete_client\": \"Изтриване на клиент\",\n    \"delete_confirm_message\": \"Наистина ли искате да изтриете <strong>{name}</strong>?\",\n    \"delete_warning\": \"Това действие не може да бъде отменено.\",\n    \"device_name\": \"Име на устройството\",\n    \"device_size\": \"Размер на устройството\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Голямо - TV\",\n    \"device_size_medium\": \"Средно - Таблет\",\n    \"device_size_small\": \"Малко - Телефон\",\n    \"edit_client_settings\": \"Редактиране на настройките на клиента\",\n    \"hdr_profile\": \"HDR профил\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Зареждане...\",\n    \"loading_clients\": \"Зареждане на клиенти...\",\n    \"modify_in_gui\": \"Моля, променете в графичния интерфейс\",\n    \"none\": \"-- Няма --\",\n    \"or_manual_pin\": \"или въведете PIN ръчно\",\n    \"pair_failure\": \"Неуспешно сдвояване. Проверете дали ПИН кодът е въведен правилно.\",\n    \"pair_success\": \"Успешно сдвояване! Проверете Moonlight, за да продължите.\",\n    \"pin_pairing\": \"Сдвояване чрез ПИН код\",\n    \"qr_expires_in\": \"Изтича след\",\n    \"qr_generate\": \"Генерирай QR код\",\n    \"qr_paired_success\": \"Успешно сдвояване!\",\n    \"qr_pairing\": \"Сдвояване чрез QR код\",\n    \"qr_pairing_desc\": \"Генерирайте QR код за бързо сдвояване. Сканирайте го с клиента Moonlight за автоматично сдвояване.\",\n    \"qr_pairing_warning\": \"Експериментална функция. Ако сдвояването не успее, моля използвайте ръчното въвеждане на PIN по-долу. Забележка: Тази функция работи само в LAN.\",\n    \"qr_refresh\": \"Обновяване на QR код\",\n    \"remove_paired_devices_desc\": \"Премахнете вашите сдвоени устройства.\",\n    \"save_changes\": \"Запазване на промените\",\n    \"save_failed\": \"Неуспешно запазване на настройките на клиента. Моля, опитайте отново.\",\n    \"save_or_cancel_first\": \"Моля, първо запазете или отменете редактирането\",\n    \"send\": \"Изпращане\",\n    \"unknown_client\": \"Неизвестен клиент\",\n    \"unpair_all_confirm\": \"Наистина ли искате да премахнете сдвояването на всички клиенти? Това действие не може да бъде отменено.\",\n    \"unsaved_changes\": \"Незаписани промени\",\n    \"warning_msg\": \"Уверете се, че имате достъп до клиента, с който сдвоявате сървъра. Този софтуер може да предостави пълен контрол върху компютъра, затова внимавайте!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android препоръчан\",\n    \"client_downloads\": \"Изтегляне на клиенти\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"Дискусии в GitHub\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Присъединете се към общността\",\n    \"join_group_desc\": \"Получете помощ и споделете опит\",\n    \"legal\": \"Правни въпроси\",\n    \"legal_desc\": \"Използвайки този софтуер, Вие се съгласявате с правилата и условията в следните документи.\",\n    \"license\": \"Лиценз\",\n    \"lizardbyte_website\": \"Уеб сайт на LizardByte\",\n    \"official_website\": \"Official Website\",\n    \"official_website_title\": \"AlkaidLab - Официален уебсайт\",\n    \"open_source\": \"Отворен код\",\n    \"open_source_desc\": \"Star & Fork за подкрепа на проекта\",\n    \"quick_start\": \"Бързо начало\",\n    \"resources\": \"Ресурси\",\n    \"resources_desc\": \"Ресурси за Sunshine!\",\n    \"third_party_desc\": \"Известия за компоненти на трети страни\",\n    \"third_party_moonlight\": \"Приятелски връзки\",\n    \"third_party_notice\": \"Забележка относно ползването на имената на трети страни\",\n    \"tutorial\": \"Ръководство\",\n    \"tutorial_desc\": \"Подробно ръководство за конфигуриране и използване\",\n    \"view_license\": \"Преглед на пълния лиценз\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Virtual Display\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"Go to Repository\",\n    \"harmony_modal_desc\": \"For HarmonyOS NEXT Moonlight, please search for Moonlight V+ in the HarmonyOS App Store\",\n    \"harmony_modal_link_notice\": \"This link will redirect to the project repository\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"секунди\",\n    \"restart_desc\": \"Конфигурацията е запазена. Sunshine се рестартира за прилагане на настройките на дисплея.\",\n    \"restart_go_now\": \"Отиди сега\",\n    \"restart_title\": \"Рестартиране на Sunshine\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"Основните настройки вече са активни. Можете веднага да започнете стрийминг с клиент Moonlight!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"Базовият дисплей (VDD) е вграденият интелигентен виртуален дисплей на Sunshine Foundation, поддържащ произволна резолюция, кадрова честота и HDR оптимизация. Той е предпочитаният избор за стрийминг с изключен екран и стрийминг на разширен дисплей.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Осигуряване на активиране\",\n    \"step3_ensure_active_desc\": \"Активира дисплея, ако не е вече активен\",\n    \"step3_ensure_only_display\": \"Осигуряване на единствен дисплей\",\n    \"step3_ensure_only_display_desc\": \"Деактивира всички други дисплеи и активира само посочения (препоръчително)\",\n    \"step3_ensure_primary\": \"Осигуряване на основен дисплей\",\n    \"step3_ensure_primary_desc\": \"Активира дисплея и го задава като основен\",\n    \"step3_ensure_secondary\": \"Вторичен стрийминг\",\n    \"step3_ensure_secondary_desc\": \"Използва само виртуалния дисплей за вторично разширено стрийминг\",\n    \"step3_no_operation\": \"Без действие\",\n    \"step3_no_operation_desc\": \"Без промени в състоянието на дисплея; потребителят трябва сам да се увери, че дисплеят е готов\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Разширени\",\n    \"amd\": \"AMD AMF Енкодер\",\n    \"av\": \"Аудио/Видео\",\n    \"encoders\": \"Енкодери\",\n    \"files\": \"Конфигурационни файлове\",\n    \"general\": \"Общи\",\n    \"input\": \"Вход\",\n    \"network\": \"Мрежа\",\n    \"nv\": \"NVIDIA NVENC Енкодер\",\n    \"qsv\": \"Intel QuickSync Енкодер\",\n    \"sw\": \"Софтуерен Енкодер\",\n    \"vaapi\": \"VAAPI Енкодер\",\n    \"vt\": \"VideoToolbox Енкодер\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Анализира се...\",\n    \"ai_analyzing_logs\": \"Анализиране на журналите, моля изчакайте...\",\n    \"ai_config\": \"AI конфигурация\",\n    \"ai_copy_result\": \"Копиране\",\n    \"ai_diagnosis\": \"AI диагностика\",\n    \"ai_diagnosis_title\": \"AI диагностика на журнали\",\n    \"ai_error\": \"Анализът не успя\",\n    \"ai_key_local\": \"API ключът се съхранява само локално и никога не се качва\",\n    \"ai_model\": \"Модел\",\n    \"ai_provider\": \"Доставчик\",\n    \"ai_reanalyze\": \"Повторен анализ\",\n    \"ai_result\": \"Резултат от диагностика\",\n    \"ai_retry\": \"Повтори\",\n    \"ai_start_diagnosis\": \"Стартиране на диагностика\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Ако трябва незабавно да затворите Sunshine, можете да използвате тази функция. Имайте предвид, че ще трябва ръчно да го стартирате отново след затваряне.\",\n    \"boom_sunshine_success\": \"Sunshine е затворен\",\n    \"confirm_boom\": \"Наистина ли искате да излезете?\",\n    \"confirm_boom_desc\": \"Така че наистина искате да излезете? Е, не мога да ви спра, продължете и кликнете отново\",\n    \"confirm_logout\": \"Потвърдете излизане?\",\n    \"confirm_logout_desc\": \"Ще трябва отново да въведете паролата си за достъп до уеб интерфейса.\",\n    \"copy_config\": \"Копиране на конфигурация\",\n    \"copy_config_error\": \"Грешка при копиране на конфигурация\",\n    \"copy_config_success\": \"Конфигурацията е копирана в клипборда!\",\n    \"copy_logs\": \"Копиране на журнали\",\n    \"download_logs\": \"Сваляне на журнали\",\n    \"force_close\": \"Принудително затваряне\",\n    \"force_close_desc\": \"Ако Moonlight се оплаква, че дадено приложение работи в момента, принудителното затваряне на това приложение би трябвало да реши проблема.\",\n    \"force_close_error\": \"Грешка при затваряне на приложението\",\n    \"force_close_success\": \"Приложението е затворено успешно!\",\n    \"ignore_case\": \"Игнориране на големи и малки букви\",\n    \"logout\": \"Изход\",\n    \"logout_desc\": \"Изход от системата. Може да се наложи да се въведе отново потребителското име и парола.\",\n    \"logout_localhost_tip\": \"Текущата среда не изисква вход; изходът няма да покаже поискването за парола.\",\n    \"logs\": \"Журнал\",\n    \"logs_desc\": \"Разгледайте съобщенията в журнала на Sunshine\",\n    \"logs_find\": \"Търсене…\",\n    \"match_contains\": \"Съдържа\",\n    \"match_exact\": \"Точно съвпада\",\n    \"match_regex\": \"Регулярен израз\",\n    \"reopen_setup_wizard\": \"Отваряне отново на майстора за настройка\",\n    \"reopen_setup_wizard_desc\": \"Отваряне отново на страницата на майстора за настройка, за да се конфигурират отново началните настройки.\",\n    \"reopen_setup_wizard_error\": \"Грешка при отваряне отново на майстора за настройка\",\n    \"reset_display_device_desc_windows\": \"Ако Sunshine е заседнал при опит за възстановяване на променените настройки на устройството за дисплей, можете да нулирате настройките и да възстановите състоянието на дисплея ръчно.\\nТова може да се случи по различни причини: устройството вече не е налично, беше свързано към друг порт и т.н.\",\n    \"reset_display_device_error_windows\": \"Грешка при нулиране на постоянността!\",\n    \"reset_display_device_success_windows\": \"Успешно нулиране на постоянността!\",\n    \"reset_display_device_windows\": \"Нулиране на паметта на дисплея\",\n    \"restart_sunshine\": \"Рестартиране на Sunshine\",\n    \"restart_sunshine_desc\": \"Ако Sunshine не работи правилно, можете да опитате да го рестартирате. Това ще прекрати всички текущи сесии.\",\n    \"restart_sunshine_success\": \"Sunshine се рестартира\",\n    \"troubleshooting\": \"Отстраняване на проблеми\",\n    \"unpair_all\": \"Премахване на сдвояването с всички клиенти\",\n    \"unpair_all_error\": \"Грешка при премахването на сдвояванията\",\n    \"unpair_all_success\": \"Премахнато е сдвояването с всички устройства.\",\n    \"unpair_desc\": \"Премахване на всички сдвоени устройства. Устройствата, чието сдвояване бъде премахнато, докато имат активна сесия, ще останат свързани, но няма да могат да започнат нови сесии или да възобновят вече започнати такива.\",\n    \"unpair_single_no_devices\": \"Няма сдвоени устройства.\",\n    \"unpair_single_success\": \"Въпреки това едно или повече устройства може все още да са в активна сесия. Използвайте бутона „Принудително затваряне“ по-горе, за да прекратите всички активни сесии.\",\n    \"unpair_single_unknown\": \"Неизвестен клиент\",\n    \"unpair_title\": \"Премахване на сдвояването с устройствата\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Потвърждаване на паролата\",\n    \"create_creds\": \"Преди да започнете, трябва да си създадете ново потребителско име и парола за достъп до уеб интерфейса.\",\n    \"create_creds_alert\": \"Данните по-долу ще са необходими за достъп до уеб интерфейса на Sunshine. Пазете ги, тъй като няма да ги видите отново!\",\n    \"creds_local_only\": \"Вашите идентификационни данни се съхраняват локално офлайн и никога не се качват на сървър.\",\n    \"error\": \"Грешка!\",\n    \"greeting\": \"Добре дошли в Sunshine Foundation!\",\n    \"hide_password\": \"Скриване на парола\",\n    \"login\": \"Вписване\",\n    \"network_error\": \"Мрежова грешка, моля проверете връзката\",\n    \"password\": \"Парола\",\n    \"password_match\": \"Паролите съвпадат\",\n    \"password_mismatch\": \"Паролите не съвпадат\",\n    \"server_error\": \"Грешка на сървъра\",\n    \"show_password\": \"Показване на парола\",\n    \"success\": \"Успешно!\",\n    \"username\": \"Потребителско име\",\n    \"welcome_success\": \"Тази страница скоро ще се презареди и браузърът ще поиска да въведете новите данни за идентификация\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/cs.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Použít\",\n    \"auto\": \"Automaticky\",\n    \"autodetect\": \"Automatická detekce (doporučeno)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"Zrušit\",\n    \"close\": \"Zavřít\",\n    \"copied\": \"Zkopírováno do schránky\",\n    \"copy\": \"Kopírovat\",\n    \"delete\": \"Smazat\",\n    \"description\": \"Description\",\n    \"disabled\": \"Zakázáno\",\n    \"disabled_def\": \"Zakázáno (výchozí)\",\n    \"dismiss\": \"Odmítnout\",\n    \"do_cmd\": \"Provést příkaz\",\n    \"download\": \"Stáhnout\",\n    \"edit\": \"Upravit\",\n    \"elevated\": \"Zvýšené\",\n    \"enabled\": \"Povoleno\",\n    \"enabled_def\": \"Povoleno (výchozí)\",\n    \"error\": \"Chyba!\",\n    \"no_changes\": \"Žádné změny\",\n    \"note\": \"Pozn.:\",\n    \"password\": \"Heslo\",\n    \"remove\": \"Odebrat\",\n    \"run_as\": \"Spustit jako správce\",\n    \"save\": \"Uložit\",\n    \"see_more\": \"Zobrazit více\",\n    \"success\": \"Úspěch!\",\n    \"undo_cmd\": \"Vrátit příkaz\",\n    \"username\": \"Uživatelské jméno\",\n    \"warning\": \"Varování!\"\n  },\n  \"apps\": {\n    \"actions\": \"Akce\",\n    \"add_cmds\": \"Přidat příkazy\",\n    \"add_new\": \"Přidat nový\",\n    \"advanced_options\": \"Pokročilé možnosti\",\n    \"app_name\": \"Název aplikace\",\n    \"app_name_desc\": \"Název aplikace, jak je uveden v aplikaci Moonlight\",\n    \"applications_desc\": \"Aplikace se obnovují pouze při restartování klienta\",\n    \"applications_title\": \"Aplikace\",\n    \"auto_detach\": \"Pokračovat ve streamování, pokud se aplikace rychle ukončí\",\n    \"auto_detach_desc\": \"Pokusí se automaticky detekovat aplikace typu launcher, které se po spuštění jiného programu nebo jeho instance rychle zavřou. Pokud je detekována aplikace typu launcher, je považována za odpojenou aplikaci.\",\n    \"basic_info\": \"Základní informace\",\n    \"cmd\": \"Příkaz\",\n    \"cmd_desc\": \"Hlavní aplikace ke spuštění. Pokud je prázdná, nebude spuštěna žádná aplikace.\",\n    \"cmd_examples_title\": \"Běžné příklady:\",\n    \"cmd_note\": \"Pokud cesta ke spustitelnému příkazu obsahuje mezery, musíte ji uvést v uvozovkách.\",\n    \"cmd_prep_desc\": \"Seznam příkazů, které mají být spuštěny před/po této aplikaci. Pokud některý z přípravných příkazů selže, spuštění aplikace se přeruší.\",\n    \"cmd_prep_name\": \"Příprava příkazů\",\n    \"command_settings\": \"Nastavení příkazů\",\n    \"covers_found\": \"Nalezené obaly\",\n    \"delete\": \"Vymazat\",\n    \"delete_confirm\": \"Are you sure you want to delete \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Oddělené příkazy\",\n    \"detached_cmds_add\": \"Přidat oddělený příkaz\",\n    \"detached_cmds_desc\": \"Seznam příkazů, které mají být spuštěny na pozadí.\",\n    \"detached_cmds_note\": \"Pokud cesta k spustitelnému příkazu obsahuje mezery, musíte ji vložit do uvozovek.\",\n    \"detached_cmds_remove\": \"Odstranit oddělený příkaz\",\n    \"edit\": \"Upravit\",\n    \"env_app_id\": \"ID aplikace\",\n    \"env_app_name\": \"Název aplikace\",\n    \"env_client_audio_config\": \"Nastavení zvuku požadované klientem (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"Klient požádal o možnost optimalizovat hru pro optimální streamování (true/false)\",\n    \"env_client_fps\": \"FPS požadované klientem (int)\",\n    \"env_client_gcmap\": \"Požadovaná maska gamepadu ve formátu bitset/bitfield (int)\",\n    \"env_client_hdr\": \"HDR je povoleno klientem (true/false)\",\n    \"env_client_height\": \"Výška požadovaná klientem (int)\",\n    \"env_client_host_audio\": \"Klient si vyžádal hostitelský zvuk (true/false)\",\n    \"env_client_name\": \"Přívětivé jméno klienta (řetězec)\",\n    \"env_client_width\": \"Šířka požadovaná klientem (int)\",\n    \"env_displayplacer_example\": \"Příklad - displayplacer pro automatizaci rozlišení:\",\n    \"env_qres_example\": \"Příklad – QR pro automatizaci rozlišení:\",\n    \"env_qres_path\": \"qres cesta\",\n    \"env_var_name\": \"Název Var\",\n    \"env_vars_about\": \"O proměnných prostředí\",\n    \"env_vars_desc\": \"Ve výchozím nastavení získávají tyto proměnné prostředí všechny příkazy:\",\n    \"env_xrandr_example\": \"Příklad - Xrandr pro automatizaci rozlišení:\",\n    \"exit_timeout\": \"Časový limit pro ukončení\",\n    \"exit_timeout_desc\": \"Počet sekund, po které se čeká na elegantní ukončení všech procesů aplikace, když je požadováno ukončení. Pokud není nastaveno, výchozí je čekat až 5 sekund. Je-li nastavena hodnota 0, aplikace bude ukončena okamžitě.\",\n    \"file_selector_not_initialized\": \"Výběr souboru není inicializován\",\n    \"find_cover\": \"Najít obal\",\n    \"form_invalid\": \"Prosím zkontrolujte povinná pole\",\n    \"form_valid\": \"Platná aplikace\",\n    \"global_prep_desc\": \"Povolit/zakázat provádění globálních předběžných příkazů pro tuto aplikaci.\",\n    \"global_prep_name\": \"Globální předběžné příkazy\",\n    \"image\": \"Obrázek\",\n    \"image_desc\": \"Cesta k ikoně/obrázku aplikace, který bude odeslán klientovi. Obrázek musí být ve formátu PNG. Pokud není nastaven, Sunshine odešle výchozí obrázek schránky.\",\n    \"image_settings\": \"Nastavení obrázku\",\n    \"loading\": \"Načítám...\",\n    \"menu_cmd_actions\": \"Akce\",\n    \"menu_cmd_add\": \"Přidat příkaz do menu\",\n    \"menu_cmd_command\": \"Příkaz\",\n    \"menu_cmd_desc\": \"Po konfiguraci budou tyto příkazy viditelné v návratovém menu klienta, což umožní rychlé provedení specifických operací bez přerušení streamu, například spuštění pomocných programů.\\nPříklad: Zobrazované jméno - Zavřít počítač; Příkaz - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Zobrazované jméno\",\n    \"menu_cmd_drag_sort\": \"Přetáhněte pro řazení\",\n    \"menu_cmd_name\": \"Příkazy menu\",\n    \"menu_cmd_placeholder_command\": \"Příkaz\",\n    \"menu_cmd_placeholder_display_name\": \"Zobrazované jméno\",\n    \"menu_cmd_placeholder_execute\": \"Spustit příkaz\",\n    \"menu_cmd_placeholder_undo\": \"Vrátit příkaz zpět\",\n    \"menu_cmd_remove_menu\": \"Odebrat příkaz z menu\",\n    \"menu_cmd_remove_prep\": \"Odebrat příkaz přípravy\",\n    \"mouse_mode\": \"Režim myši\",\n    \"mouse_mode_auto\": \"Auto (Globální nastavení)\",\n    \"mouse_mode_desc\": \"Vyberte metodu vstupu myši pro tuto aplikaci. Auto používá globální nastavení, Virtuální myš používá HID ovladač, SendInput používá Windows API.\",\n    \"mouse_mode_sendinput\": \"SendInput (Windows API)\",\n    \"mouse_mode_vmouse\": \"Virtuální myš\",\n    \"name\": \"Název\",\n    \"output_desc\": \"Soubor, kde je uložen výstup příkazu, pokud není zadán, výstup je ignorován\",\n    \"output_name\": \"Výstup\",\n    \"run_as_desc\": \"To může být nutné u některých aplikací, které ke správnému běhu vyžadují oprávnění správce.\",\n    \"scan_result_add_all\": \"Přidat vše\",\n    \"scan_result_edit_title\": \"Přidat a upravit\",\n    \"scan_result_filter_all\": \"Vše\",\n    \"scan_result_filter_epic_title\": \"Hry Epic Games\",\n    \"scan_result_filter_executable\": \"Spustitelný\",\n    \"scan_result_filter_executable_title\": \"Spustitelný soubor\",\n    \"scan_result_filter_gog_title\": \"Hry GOG Galaxy\",\n    \"scan_result_filter_script\": \"Skript\",\n    \"scan_result_filter_script_title\": \"Dávkový/Příkazový skript\",\n    \"scan_result_filter_shortcut\": \"Zástupce\",\n    \"scan_result_filter_shortcut_title\": \"Zástupce\",\n    \"scan_result_filter_steam_title\": \"Hry Steam\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Hra\",\n    \"scan_result_games_only\": \"Pouze hry\",\n    \"scan_result_matched\": \"Shody: {count}\",\n    \"scan_result_no_apps\": \"Nebyly nalezeny žádné aplikace k přidání\",\n    \"scan_result_no_matches\": \"Nebyly nalezeny žádné odpovídající aplikace\",\n    \"scan_result_quick_add_title\": \"Rychlé přidání\",\n    \"scan_result_remove_title\": \"Odebrat ze seznamu\",\n    \"scan_result_search_placeholder\": \"Hledat název aplikace, příkaz nebo cestu...\",\n    \"scan_result_show_all\": \"Zobrazit vše\",\n    \"scan_result_title\": \"Výsledky skenování\",\n    \"scan_result_try_different_keywords\": \"Zkuste použít jiná klíčová slova pro vyhledávání\",\n    \"scan_result_type_batch\": \"Dávkový\",\n    \"scan_result_type_command\": \"Příkazový skript\",\n    \"scan_result_type_executable\": \"Spustitelný soubor\",\n    \"scan_result_type_shortcut\": \"Zástupce\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Hledat aplikace...\",\n    \"select\": \"Vybrat\",\n    \"test_menu_cmd\": \"Testovat příkaz\",\n    \"test_menu_cmd_empty\": \"Příkaz nemůže být prázdný\",\n    \"test_menu_cmd_executing\": \"Provádění příkazu...\",\n    \"test_menu_cmd_failed\": \"Provedení příkazu selhalo\",\n    \"test_menu_cmd_success\": \"Příkaz byl úspěšně proveden!\",\n    \"use_desktop_image\": \"Použít aktuální tapetu plochy\",\n    \"wait_all\": \"Pokračujte ve streamování, dokud se neukončí všechny procesy aplikace\",\n    \"wait_all_desc\": \"Streamování bude pokračovat, dokud nebudou ukončeny všechny procesy spuštěné aplikací. Pokud není zaškrtnuto, streamování se zastaví, jakmile se ukončí počáteční proces aplikace, i když ostatní procesy aplikace stále běží.\",\n    \"working_dir\": \"Pracovní adresář\",\n    \"working_dir_desc\": \"Pracovní adresář, který má být předán procesu. Některé aplikace například používají pracovní adresář k vyhledávání konfiguračních souborů. Pokud není nastaven, bude Sunshine implicitně nastaven na nadřazený adresář příkazu\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Název adaptéru\",\n    \"adapter_name_desc_linux_1\": \"Ruční zadání GPU, která se má použít pro snímání.\",\n    \"adapter_name_desc_linux_2\": \"najít všechna zařízení schopná VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Nahraďte ``renderD129`` zařízením z výše uvedeného seznamu, abyste vypsali název a schopnosti zařízení. Aby bylo zařízení Sunshine podporováno, musí mít minimálně:\",\n    \"adapter_name_desc_windows\": \"Ruční zadání GPU, která se má použít pro snímání. Pokud není nastaveno, je GPU vybrána automaticky. Důrazně doporučujeme ponechat toto pole prázdné, chcete-li použít automatický výběr GPU! Poznámka: Toto GPU musí mít připojený a zapnutý displej. Příslušné hodnoty lze zjistit pomocí následujícího příkazu:\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Pokud je nainstalována nejnovější verze virtuálního displeje, může se automaticky přidružit k vazbě GPU\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Přidat\",\n    \"address_family\": \"Rodina adres\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Nastavení rodiny adres, kterou používá Sunshine\",\n    \"address_family_ipv4\": \"Pouze IPv4\",\n    \"always_send_scancodes\": \"Vždy odesílat Scancody\",\n    \"always_send_scancodes_desc\": \"Odesílání scancodes zvyšuje kompatibilitu s hrami a aplikacemi, ale může mít za následek nesprávný vstup na klávesnici od určitých klientů, kteří nepoužívají americké anglické rozložení klávesnice. Povolit, pokud vstup klávesnice v některých aplikacích vůbec nefunguje. Zakázat, pokud klíče klienta generují nesprávný vstup na hostitele.\",\n    \"amd_coder\": \"AMF kodér (H264)\",\n    \"amd_coder_desc\": \"Umožňuje vybrat entropie kódování pro upřednostnění kvality nebo rychlosti kódování. Pouze H.264.\",\n    \"amd_enforce_hrd\": \"Vymáhání hypotetického referenčního dekodéru (HRD) AMF\",\n    \"amd_enforce_hrd_desc\": \"Zvyšuje omezení regulace rychlosti, aby splňovala požadavky modelu HRD. To výrazně snižuje bitrate přetečení, ale může způsobit kódování artefaktů nebo nižší kvalitu na některých kartách.\",\n    \"amd_preanalysis\": \"Předběžná analýza AMF\",\n    \"amd_preanalysis_desc\": \"To umožňuje předběžnou analýzu kontroly sazeb, která může zvýšit kvalitu na úkor zvýšené latence kódování.\",\n    \"amd_quality\": \"Kvalita AMF\",\n    \"amd_quality_balanced\": \"vyrovnané -- vyvážené (výchozí)\",\n    \"amd_quality_desc\": \"Tím se řídí rovnováha mezi rychlostí kódování a kvalitou.\",\n    \"amd_quality_group\": \"Nastavení kvality AMF\",\n    \"amd_quality_quality\": \"kvalita -- preferovat kvalitu\",\n    \"amd_quality_speed\": \"Rychlost -- preferovat rychlost\",\n    \"amd_qvbr_quality\": \"Úroveň kvality AMF QVBR\",\n    \"amd_qvbr_quality_desc\": \"Úroveň kvality pro režim řízení bitrate QVBR. Rozsah: 1-51 (nižší = lepší kvalita). Výchozí: 23. Platí pouze když je řízení bitrate nastaveno na 'qvbr'.\",\n    \"amd_rc\": \"Ovládání AMF\",\n    \"amd_rc_cbr\": \"cbr -- konstantní bitrate (doporučujeme pokud je HRD zapnuto)\",\n    \"amd_rc_cqp\": \"cqp -- konstantní qp režim\",\n    \"amd_rc_desc\": \"Tím se řídí metoda řízení rychlosti, aby se zajistilo, že nepřekročíme cílový datový tok klienta. 'cqp' není vhodné pro cílování datového toku a ostatní možnosti kromě 'vbr_latency' závisí na HRD Enforcement, které pomáhají omezit přetečení datového toku.\",\n    \"amd_rc_group\": \"Nastavení řízení AMF\",\n    \"amd_rc_hqcbr\": \"hqcbr -- vysokokvalitní konstantní bitrate\",\n    \"amd_rc_hqvbr\": \"hqvbr -- vysokokvalitní variabilní bitrate\",\n    \"amd_rc_qvbr\": \"qvbr -- kvalitní variabilní bitrate (používá úroveň kvality QVBR)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- proměnný datový tok s omezenou latencí (doporučeno, pokud je HRD vypnuta; výchozí)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- maximální nastavená proměnná bitrate\",\n    \"amd_usage\": \"Využití AMF\",\n    \"amd_usage_desc\": \"Nastaví základní profil kódování. Všechny níže uvedené možnosti přepíší podmnožinu uživatelského profilu, ale existují další skrytá nastavení, která nelze nastavit jinde.\",\n    \"amd_usage_lowlatency\": \"lowlatency - nízká latence (nejrychlejší)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - nízká latence, vysoká kvalita (rychlá)\",\n    \"amd_usage_transcoding\": \"překódování -- překódování (nejpomalejší)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatence - ultra nízká latence (nejrychlejší; výchozí)\",\n    \"amd_usage_webcam\": \"webová kamera -- webová kamera (pomalá)\",\n    \"amd_vbaq\": \"AMF adaptivní kvantifikace založená na rozptylu (VBAQ)\",\n    \"amd_vbaq_desc\": \"Lidský vizuální systém je obvykle méně citlivý na artefakty ve vysoce strukturovaných oblastech. V režimu VBAQ se rozptyl pixelů používá k označení složitosti prostorových textur, což umožňuje enkodéru přidělit více bitů do plynulejších oblastí. Povolení této funkce vede ke zlepšení subjektivní vizuální kvality s určitým obsahem.\",\n    \"amf_draw_mouse_cursor\": \"Vykreslit jednoduchý kurzor myši při použití metody snímání AMF\",\n    \"amf_draw_mouse_cursor_desc\": \"V některých případech použití snímání AMF nezobrazí ukazatel myši. Povolení této možnosti vykreslí na obrazovce jednoduchý ukazatel myši. Poznámka: Pozice kurzoru myši bude aktualizována pouze při aktualizaci obrazovky s obsahem, takže ve scénářích mimo hru, například na ploše, můžete pozorovat trhaný pohyb kurzoru myši.\",\n    \"apply_note\": \"Kliknutím na tlačítko \\\"Použít\\\" restartujte Sunshine a aplikujte změny. Tím se ukončí všechny spuštěné relace.\",\n    \"audio_sink\": \"Zvukový výřez\",\n    \"audio_sink_desc_linux\": \"Název skluzu zvuku, který se používá pro zvukovou smyčku. Pokud tuto proměnnou nevyberete, pulseaudio zvolí výchozí zařízení pro monitor. Název sklízení zvuku můžete najít pomocí některého příkazu:\",\n    \"audio_sink_desc_macos\": \"Název zvukového výřezu používaný pro Audio Loopback. Sunshine má přístup pouze k mikrofonům na macOS kvůli systémovým omezením. Pro streamování zvuku systému pomocí Soundflower nebo BlackHole.\",\n    \"audio_sink_desc_windows\": \"Ručně zadejte konkrétní zvukové zařízení pro zachycení. Pokud je zařízení odstaveno, je vybráno automaticky. Důrazně doporučujeme ponechat toto pole prázdné pro automatický výběr zařízení! Pokud máte více zvukových zařízení se stejnými jmény, můžete získat ID zařízení pomocí následujícího příkazu:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Reproduktory (High Definition Audio Device)\",\n    \"av1_mode\": \"AV1 podpora\",\n    \"av1_mode_0\": \"Sunshine bude inzerovat podporu AV1 na základě schopností kodéru (doporučeno)\",\n    \"av1_mode_1\": \"Sunshine nebude inzerovat podporu AV1\",\n    \"av1_mode_2\": \"Sunshine bude inzerovat podporu hlavního 8bitového profilu AV1\",\n    \"av1_mode_3\": \"Sunshine bude inzerovat podporu hlavních 8bitových a 10bitových profilů AV1 (HDR)\",\n    \"av1_mode_desc\": \"Umožňuje klientovi požádat o AV1 hlavní 8-bitové nebo 10-bitové video streamy. AV1 je intenzivnější na CPU kódování, takže díky tomu může dojít ke snížení výkonu při používání kódování softwaru.\",\n    \"back_button_timeout\": \"Časový limit emulace tlačítka Domů/Návod\",\n    \"back_button_timeout_desc\": \"Pokud je tlačítko Zpět/Výběr podrženo pro zadaný počet milisekund, je emulováno stisknutí tlačítka Domů/Průvodce. Je-li nastaveno na hodnotu < 0 (výchozí), podržením tlačítka Zpět/Výběr nebude tlačítko Domů/Průvodce emulováno.\",\n    \"bind_address\": \"Adresa vazby (testovací funkce)\",\n    \"bind_address_desc\": \"Nastavte konkrétní IP adresu, na kterou se Sunshine naváže. Pokud necháte prázdné, Sunshine se naváže na všechny dostupné adresy.\",\n    \"capture\": \"Vynutit specifickou metodu snímání\",\n    \"capture_desc\": \"V automatickém režimu bude Sunshine používat ten první, který funguje. NvFBC vyžaduje upravené nvidia ovladače.\",\n    \"capture_target\": \"Cíl snímání\",\n    \"capture_target_desc\": \"Vyberte typ cíle pro snímání. Při výběru 'Okno' můžete snímat konkrétní okno aplikace (jako je software pro interpolaci snímků AI) namísto celého displeje.\",\n    \"capture_target_display\": \"Displej\",\n    \"capture_target_window\": \"Okno\",\n    \"cert\": \"Osvědčení\",\n    \"cert_desc\": \"Certifikát používaný pro párování webových UI a Moonlight klientů. Pro nejlepší kompatibilitu by měl mít veřejný klíč RSA-2048.\",\n    \"channels\": \"Maximální počet připojených klientů\",\n    \"channels_desc_1\": \"Sluneční svár může umožnit sdílení jediné streamovací relace s více klienty současně.\",\n    \"channels_desc_2\": \"Některé hardwarové enkodéry mohou mít omezení, která snižují výkon s více streamy.\",\n    \"close_verify_safe\": \"Bezpečná ověření kompatibilní s klienty starší\",\n    \"close_verify_safe_desc\": \"Starší klienti nemusí být schopni se připojit k Sunshine, prosím zakázat tuto volbu nebo aktualizovat klienta\",\n    \"coder_cabac\": \"cabac -- kontextové binární aritmetické kódování – vyšší kvalita\",\n    \"coder_cavlc\": \"cavlc -- kontextové adaptivní kódování variabilní délky - rychlejší dekódování\",\n    \"configuration\": \"Konfigurace\",\n    \"controller\": \"Enable Gamepad Input\",\n    \"controller_desc\": \"Umožňuje hostům ovládat hostitelský systém pomocí gamepad / controller\",\n    \"credentials_file\": \"Soubor pověření\",\n    \"credentials_file_desc\": \"Uložit uživatelské jméno/heslo odděleně od souboru se stavem Sunshine.\",\n    \"display_device_options_note_desc_windows\": \"Windows ukládá různá nastavení zobrazení pro každou kombinaci aktuálně aktivních displejů.\\nSunshine poté aplikuje změny na displej(e) patřící do takové kombinace displejů.\\nPokud odpojíte zařízení, které bylo aktivní, když Sunshine aplikoval nastavení, změny nelze\\nvrátit zpět, dokud nebude kombinace znovu aktivována v době, kdy se Sunshine pokusí vrátit změny!\",\n    \"display_device_options_note_windows\": \"Poznámka k tomu, jak jsou nastavení aplikována\",\n    \"display_device_options_windows\": \"Možnosti zobrazovacího zařízení\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Aktivuje displej, pokud není již aktivní\",\n    \"display_device_prep_ensure_active_windows\": \"Automaticky aktivovat displej\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Deaktivuje všechny ostatní displeje a aktivuje pouze zadaný\",\n    \"display_device_prep_ensure_only_display_windows\": \"Deaktivovat ostatní displeje a aktivovat pouze zadaný displej\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Aktivuje displej a nastaví jej jako hlavní\",\n    \"display_device_prep_ensure_primary_windows\": \"Automaticky aktivovat displej a nastavit jej jako hlavní\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Používá pouze virtuální displej pro sekundární rozšířený streaming\",\n    \"display_device_prep_ensure_secondary_windows\": \"Streaming sekundárního displeje (pouze virtuální displej)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Žádné změny stavu displeje; uživatel musí sám zajistit, že displej je připraven\",\n    \"display_device_prep_no_operation_windows\": \"Zakázáno\",\n    \"display_device_prep_windows\": \"Příprava displeje\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Musí být zadána alespoň jedna \\\"přijatá\\\" a jedna \\\"konečná\\\" hodnota.\\nPrázdné pole v sekci \\\"přijatá\\\" znamená \\\"shoda s čímkoli\\\". Prázdné pole v sekci \\\"konečná\\\" znamená \\\"zachovat přijatou hodnotu\\\".\\nMůžete přiřadit konkrétní hodnotu FPS ke konkrétnímu rozlišení, pokud si to přejete...\\n\\nPoznámka: pokud není v klientovi Moonlight povolena možnost \\\"Optimalizovat nastavení hry\\\", řádky obsahující hodnoty rozlišení jsou ignorovány.\",\n    \"display_mode_remapping_desc_windows\": \"Uveďte, jak se má konkrétní rozlišení a/nebo obnovovací frekvence přemapovat na jiné hodnoty.\\nMůžete streamovat v nižším rozlišení, zatímco na hostiteli renderovat ve vyšším rozlišení pro efekt supersamplingu.\\nNebo můžete streamovat s vyšším FPS, zatímco omezíte hostitele na nižší obnovovací frekvenci.\\nShoda se provádí shora dolů. Jakmile je záznam spárován, ostatní se již nekontrolují, ale stále se ověřují.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Konečná obnovovací frekvence\",\n    \"display_mode_remapping_final_resolution_windows\": \"Konečné rozlišení\",\n    \"display_mode_remapping_optional\": \"volitelné\",\n    \"display_mode_remapping_received_fps_windows\": \"Přijaté FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"Přijaté rozlišení\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Poznámka: pokud není v klientovi Moonlight povolena možnost \\\"Optimalizovat nastavení hry\\\", přemapování je zakázáno.\",\n    \"display_mode_remapping_windows\": \"Přemapovat režimy zobrazení\",\n    \"display_modes\": \"Režimy zobrazení\",\n    \"ds4_back_as_touchpad_click\": \"Mapa zpátky/Vyberte pro klepnutí na Touchpad\",\n    \"ds4_back_as_touchpad_click_desc\": \"Při vynucení emulace DS4 mapa zpět/Vyberte na Touchpad kliknutí\",\n    \"dsu_server_port\": \"Port serveru DSU\",\n    \"dsu_server_port_desc\": \"Naslouchací port serveru DSU (výchozí 26760). Sunshine bude fungovat jako server DSU pro příjem připojení klientů a odesílání dat o pohybu. Povolte server DSU ve svém klientovi (Yuzu, Ryujinx atd.) a nastavte adresu serveru DSU (127.0.0.1) a port (26760)\",\n    \"enable_dsu_server\": \"Povolit server DSU\",\n    \"enable_dsu_server_desc\": \"Povolit server DSU pro příjem připojení klientů a odesílání dat o pohybu\",\n    \"encoder\": \"Vynutit specifický enkodér\",\n    \"encoder_desc\": \"Vynutit konkrétní enkodér, jinak Sunshine vybere nejlepší dostupnou možnost. Poznámka: Pokud zadáte hardwarový enkodér v systému Windows, musí odpovídat GPU, kde je displej připojen.\",\n    \"encoder_software\": \"Programové vybavení\",\n    \"experimental\": \"Experimentální\",\n    \"experimental_features\": \"Experimentální funkce\",\n    \"external_ip\": \"Externí IP\",\n    \"external_ip_desc\": \"Pokud není zadána žádná externí IP adresa, Sluneční server automaticky rozpozná externí IP adresu\",\n    \"fec_percentage\": \"Procento FEC\",\n    \"fec_percentage_desc\": \"Procento chyb při opravě paketů na datových paketech v každém video snímku. Vyšší hodnoty mohou opravit větší ztrátu síťových paketů, ale za cenu zvýšení využití šířky pásma.\",\n    \"ffmpeg_auto\": \"auto -- nechat rozhodnutí ffmpeg (výchozí)\",\n    \"file_apps\": \"Soubor aplikací\",\n    \"file_apps_desc\": \"Soubor, kde jsou uloženy aktuální aplikace Sunshine.\",\n    \"file_state\": \"Státní soubor\",\n    \"file_state_desc\": \"Soubor, ve kterém je uložen aktuální stav sunshine\",\n    \"fps\": \"Inzerované FPS\",\n    \"gamepad\": \"Emulovaný typ hry\",\n    \"gamepad_auto\": \"Možnosti automatického výběru\",\n    \"gamepad_desc\": \"Vyberte typ gamepadu, který chcete emulovat na hostiteli\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"Možnosti výběru DS4\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Možnosti manuálního DS4\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Příprava příkazů\",\n    \"global_prep_cmd_desc\": \"Konfigurace seznamu příkazů, které mají být provedeny před spuštěním jakékoli aplikace nebo po ní. Pokud některý z určených příkazů neuspěje, proces spuštění aplikace bude přerušen.\",\n    \"hdr_luminance_analysis\": \"Dynamická HDR metadata (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Povolí analýzu luminance GPU na snímek a vkládá dynamická metadata HDR10+ (ST 2094-40) a HDR Vivid (CUVA) do kódovaného proudu. Poskytuje per-snímkové tone-mappingové rady pro podporované displeje. Přidává mírnou zátěž GPU (~0,5-1,5ms/snímek při vysokých rozlišeních). Zakažte při poklesech snímkové frekvence s HDR.\",\n    \"hdr_prep_automatic_windows\": \"Zapnout/vypnout režim HDR podle požadavku klienta\",\n    \"hdr_prep_no_operation_windows\": \"Zakázáno\",\n    \"hdr_prep_windows\": \"Změna stavu HDR\",\n    \"hevc_mode\": \"Podpora HEVC\",\n    \"hevc_mode_0\": \"Sunshine bude propagovat podporu pro HEVC na základě možností enkodéru (doporučeno)\",\n    \"hevc_mode_1\": \"Sluneční síť nebude propagovat podporu HEVC\",\n    \"hevc_mode_2\": \"Sluneční svaz bude propagovat podporu hlavního profilu HEVC\",\n    \"hevc_mode_3\": \"Sluneční svaz bude propagovat podporu profilů HEVC Main a Main10 (HDR)\",\n    \"hevc_mode_desc\": \"Umožňuje klientovi vyžádat si HEVC Main nebo HEVC Main10 video streamy. HEVC je intenzivnější na CPU kódování, takže povolení může snížit výkon při používání kódování softwaru.\",\n    \"high_resolution_scrolling\": \"Podpora rolování s vysokým rozlišením\",\n    \"high_resolution_scrolling_desc\": \"Pokud je povoleno, sunshine projde událostmi posunu s vysokým rozlišením od klientů Moonight. To může být užitečné pro vypnutí starších aplikací, které se posouvají příliš rychle při posunu s vysokým rozlišením.\",\n    \"install_steam_audio_drivers\": \"Nainstalujte Steam Audio Drivers\",\n    \"install_steam_audio_drivers_desc\": \"Pokud je Steam nainstalován, tak se automaticky nainstaluje ovladač Steam Streaming Speakers pro podporu 5.1/7.1 prostorového zvuku a ztlumení zvuku.\",\n    \"key_repeat_delay\": \"Zpoždění opakování klíče\",\n    \"key_repeat_delay_desc\": \"Ovládejte, jak rychle se budou klíče opakovat. Počáteční zpoždění v milisekundách před opakováním klíčů.\",\n    \"key_repeat_frequency\": \"Frekvence opakování klíče\",\n    \"key_repeat_frequency_desc\": \"Jak často se klíče opakují každou vteřinu. Tato konfigurovatelná volba podporuje desetinná místa.\",\n    \"key_rightalt_to_key_win\": \"Map Right Alt key to Windows key\",\n    \"key_rightalt_to_key_win_desc\": \"Může být možné, že z Moonlight nemůžete přímo odeslat klíč pro Windows. V těchto případech může být užitečné udělat sunshine si myslet, že klíč pravý Alt je klíč pro Windows\",\n    \"key_rightalt_to_key_windows\": \"Map Right Alt key to Windows key\",\n    \"keyboard\": \"Povolit vstup klávesnice\",\n    \"keyboard_desc\": \"Umožňuje hostům ovládat hostitelský systém pomocí klávesnice\",\n    \"lan_encryption_mode\": \"Režim šifrování LAN\",\n    \"lan_encryption_mode_1\": \"Povoleno pro podporované klienty\",\n    \"lan_encryption_mode_2\": \"Vyžadováno pro všechny klienty\",\n    \"lan_encryption_mode_desc\": \"Určuje, kdy bude šifrování použito při streamování přes místní síť. Šifrování může snížit výkon streamování, zejména u méně výkonných hostitelů a klientů.\",\n    \"locale\": \"Místní prostředí\",\n    \"locale_desc\": \"Lokální prostředí používané pro uživatelské rozhraní Sunshine.\",\n    \"log_level\": \"Úroveň logu\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"Info\",\n    \"log_level_3\": \"Varování\",\n    \"log_level_4\": \"Chyba\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"Nic\",\n    \"log_level_desc\": \"Minimální úroveň logu vytištěná pro standardizaci\",\n    \"log_path\": \"Cesta k logu\",\n    \"log_path_desc\": \"Soubor s aktuálními protokoly sunshine jsou uloženy.\",\n    \"max_bitrate\": \"Maximální bitrate\",\n    \"max_bitrate_desc\": \"Maximální bitrate (v Kbps) zakódovaný sunshine streamem. Je-li nastaveno na 0, bude vždy používat bitrate požadovaný Moonlight.\",\n    \"max_fps_reached\": \"Dosaženy maximální hodnoty FPS\",\n    \"max_resolutions_reached\": \"Dosažen maximální počet rozlišení\",\n    \"mdns_broadcast\": \"Najít tento počítač v místní síti\",\n    \"mdns_broadcast_desc\": \"Pokud je tato volba povolena, Sunshine umožní ostatním zařízením automaticky najít tento počítač. Moonlight musí být také nakonfigurován, aby automaticky najít tento počítač v místní síti.\",\n    \"min_threads\": \"Minimální počet CPU vláken\",\n    \"min_threads_desc\": \"Zvýšení hodnoty mírně snižuje efektivitu kódování, ale tento kompromis obvykle stojí za to získat více jader CPU pro kódování. Ideální hodnota je nejnižší hodnota, která může spolehlivě kódovat v požadovaném nastavení streamu do vašeho hardwaru.\",\n    \"minimum_fps_target\": \"Minimální cílové FPS\",\n    \"minimum_fps_target_desc\": \"Minimální FPS k udržení při kódování (0 = auto, přibližně polovina streamovaného FPS; 1-1000 = minimální FPS k udržení). Pokud je povolena variabilní obnovovací frekvence, toto nastavení je ignorováno, pokud je nastaveno na 0.\",\n    \"misc\": \"Různé možnosti\",\n    \"motion_as_ds4\": \"Emulovat DS4 gamepad pokud jsou přítomny snímače pohybu\",\n    \"motion_as_ds4_desc\": \"Je-li zakázáno, nebudou při výběru typu gamepadu brány v úvahu snímače pohybu.\",\n    \"mouse\": \"Povolit vstup myši\",\n    \"mouse_desc\": \"Umožňuje hostům ovládat systém pomocí myši\",\n    \"native_pen_touch\": \"Nativní Peněžení/Dotkněte se podpory\",\n    \"native_pen_touch_desc\": \"Pokud je povoleno, Sunshine projde nativní per/dotyk od klientů Moonlight událostí. To může být užitečné pro vypnutí starších aplikací bez nativní podpory pen/dotyku.\",\n    \"no_fps\": \"Nebyly přidány žádné hodnoty FPS\",\n    \"no_resolutions\": \"Nebyla přidána žádná rozlišení\",\n    \"notify_pre_releases\": \"Oznámení před vydáním\",\n    \"notify_pre_releases_desc\": \"Zda mají být informovány o nových předběžných verzích Sunshine\",\n    \"nvenc_h264_cavlc\": \"Preferovat CAVLC před CABAC v H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Jednoduchá forma entropizace. CAVLC potřebuje asi o 10 % více bitratu ve stejné kvalitě. Je relevantní pouze pro opravdu staré dekódování zařízení.\",\n    \"nvenc_latency_over_power\": \"Preferovat nižší latenci kódování před úsporami energie\",\n    \"nvenc_latency_over_power_desc\": \"Sluneční požadavky vyžadují maximální rychlost GPU hodin při streamování, aby se snížila latence kódování. Vypnutí se nedoporučuje, protože to může vést k výraznému zvýšení latence kódování.\",\n    \"nvenc_lookahead_depth\": \"Hloubka předpovědi (Lookahead)\",\n    \"nvenc_lookahead_depth_desc\": \"Počet snímků pro předpověď během kódování (0-32). Lookahead zlepšuje kvalitu kódování, zejména ve složitých scénách, poskytováním lepšího odhadu pohybu a distribuce datového toku. Vyšší hodnoty zlepšují kvalitu, ale zvyšují latenci kódování. Nastavte na 0 pro vypnutí lookahead. Vyžaduje NVENC SDK 13.0 (1202) nebo novější.\",\n    \"nvenc_lookahead_level\": \"Úroveň předpovědi (Lookahead)\",\n    \"nvenc_lookahead_level_0\": \"Úroveň 0 (nejnižší kvalita, nejrychlejší)\",\n    \"nvenc_lookahead_level_1\": \"Úroveň 1\",\n    \"nvenc_lookahead_level_2\": \"Úroveň 2\",\n    \"nvenc_lookahead_level_3\": \"Úroveň 3 (nejvyšší kvalita, nejpomalejší)\",\n    \"nvenc_lookahead_level_autoselect\": \"Automatický výběr (nechat ovladač vybrat optimální úroveň)\",\n    \"nvenc_lookahead_level_desc\": \"Úroveň kvality Lookahead. Vyšší úrovně zlepšují kvalitu na úkor výkonu. Tato možnost se projeví pouze v případě, že je lookahead_depth větší než 0. Vyžaduje NVENC SDK 13.0 (1202) nebo novější.\",\n    \"nvenc_lookahead_level_disabled\": \"Zakázáno (stejné jako úroveň 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Současný OpenGL/Vulkan nad DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sluneční neschopný zachytit programy OpenGL a Vulkan při plném snímku, pokud nejsou přítomny na vrcholu DXGI. Toto je systémové nastavení, které je vráceno při ukončení slunečního programu.\",\n    \"nvenc_preset\": \"Předvolba výkonu\",\n    \"nvenc_preset_1\": \"(nejrychlejší, výchozí)\",\n    \"nvenc_preset_7\": \"(nejmenší)\",\n    \"nvenc_preset_desc\": \"Vyšší čísla zlepšují kompresi (kvalita při dané bitové rychlosti) za cenu zvýšeného zpoždění kódování. Změnu doporučujeme pouze v případě, že je omezena sítí nebo dekodérem, jinak lze dosáhnout podobného efektu zvýšením bitrate.\",\n    \"nvenc_rate_control\": \"Režim řízení toku\",\n    \"nvenc_rate_control_cbr\": \"CBR (Konstantní datový tok) - Nízká latence\",\n    \"nvenc_rate_control_desc\": \"Vyberte režim řízení toku. CBR (Konstantní datový tok) poskytuje fixní datový tok pro streamování s nízkou latencí. VBR (Variabilní datový tok) umožňuje měnit datový tok na základě složitosti scény, což poskytuje lepší kvalitu pro složité scény za cenu proměnlivého datového toku.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Variabilní datový tok) - Lepší kvalita\",\n    \"nvenc_realtime_hags\": \"Použít prioritu v reálném čase v hardwarově akcelerovaném plánování\",\n    \"nvenc_realtime_hags_desc\": \"V současné době mohou ovladače NVIDIA zmrazit v enkodéru, pokud je HAGS povoleno, je použita priorita v reálném čase a využití VRAM je blízko maximu. Zakázání této možnosti snižuje prioritu na vysokou úroveň, vyhýbá se zmrazení za cenu snížení výkonu zachytávání při vysoké zátěži.\",\n    \"nvenc_spatial_aq\": \"Spatial AQ\",\n    \"nvenc_spatial_aq_desc\": \"Přiřadit vyšší hodnoty QP plochým oblastem videa. Doporučeno povolit při streamování při nižších bitech.\",\n    \"nvenc_spatial_aq_disabled\": \"Zakázáno (rychlejší, výchozí)\",\n    \"nvenc_spatial_aq_enabled\": \"Povoleno (pomalejší)\",\n    \"nvenc_split_encode\": \"Rozdělené kódování snímků\",\n    \"nvenc_split_encode_desc\": \"Rozdělit kódování každého snímku videa na více hardwarových jednotek NVENC. Výrazně snižuje latenci kódování s minimálním dopadem na efektivitu komprese. Tato možnost je ignorována, pokud má vaše GPU pouze jednu jednotku NVENC.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Rozhodne ovladač (výchozí)\",\n    \"nvenc_split_encode_four_strips\": \"Vynutit rozdělení na 4 pruhy (vyžaduje 4+ NVENC enginy)\",\n    \"nvenc_split_encode_three_strips\": \"Vynutit rozdělení na 3 pruhy (vyžaduje 3+ NVENC enginy)\",\n    \"nvenc_split_encode_two_strips\": \"Vynutit rozdělení na 2 pruhy (vyžaduje 2+ NVENC enginy)\",\n    \"nvenc_target_quality\": \"Cílová kvalita (režim VBR)\",\n    \"nvenc_target_quality_desc\": \"Úroveň cílové kvality pro režim VBR (0-51 pro H.264/HEVC, 0-63 pro AV1). Nižší hodnoty = vyšší kvalita. Nastavte na 0 pro automatický výběr kvality. Používá se pouze v případě, že je režim řízení toku VBR.\",\n    \"nvenc_temporal_aq\": \"Časová adaptivní kvantizace\",\n    \"nvenc_temporal_aq_desc\": \"Povolit časovou adaptivní kvantizaci. Časová AQ optimalizuje kvantizaci v čase, což poskytuje lepší distribuci datového toku a lepší kvalitu v pohyblivých scénách. Tato funkce funguje společně s prostorovou AQ a vyžaduje povolení lookahead (lookahead_depth > 0). Vyžaduje NVENC SDK 13.0 (1202) nebo novější.\",\n    \"nvenc_temporal_filter\": \"Časový filtr\",\n    \"nvenc_temporal_filter_4\": \"Úroveň 4 (maximální síla)\",\n    \"nvenc_temporal_filter_desc\": \"Síla časového filtrování aplikovaná před kódováním. Časový filtr snižuje šum a zlepšuje efektivitu komprese, zejména u přirozeného obsahu. Vyšší úrovně poskytují lepší redukci šumu, ale mohou způsobit mírné rozmazání. Vyžaduje NVENC SDK 13.0 (1202) nebo novější. Poznámka: Vyžaduje frameIntervalP >= 5, nekompatibilní se zeroReorderDelay nebo stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Zakázáno (žádné časové filtrování)\",\n    \"nvenc_twopass\": \"Režim obousměrného průjezdu\",\n    \"nvenc_twopass_desc\": \"Přidá předběžné kódování. To umožňuje detekovat více vektorů pohybu, lépe distribuovat bitrate napříč rámcem a přesněji dodržovat limity bitratu. Vypnutí se nedoporučuje, protože to může způsobit občasné překročení bitratu a následnou ztrátu paketů.\",\n    \"nvenc_twopass_disabled\": \"Zakázáno (nejrychlejší, nedoporučeno)\",\n    \"nvenc_twopass_full_res\": \"Úplné rozlišení (pomalejší)\",\n    \"nvenc_twopass_quarter_res\": \"Čtvrtletní rozlišení (rychlejší, výchozí)\",\n    \"nvenc_vbv_increase\": \"Zvýšení procenta jednoho snímku VBV/HRD\",\n    \"nvenc_vbv_increase_desc\": \"Ve výchozím nastavení používá sluneční záření jednosnímkový VBV/HRD, což znamená, že se neočekává, že by žádná velikost zakódovaného video snímku překročila požadovanou bitrate dělenou požadovanou frekvencí snímku. zmírnění tohoto omezení může být prospěšné a fungovat jako variabilní bitrate s nízkou latencí, ale může také vést ke ztrátě paketů, pokud síť nemá mezipaměnnou mezipaměť pro zvládání výkyvů bitratů. Maximální přípustná hodnota je 400, což odpovídá 5x zvýšenému limitu horní velikosti zakódovaného video snímku.\",\n    \"origin_web_ui_allowed\": \"Origin Web UI povoleno\",\n    \"origin_web_ui_allowed_desc\": \"Původ adresy vzdáleného koncového bodu, které není odepřen přístup k webovému uživatelskému rozhraní\",\n    \"origin_web_ui_allowed_lan\": \"Přístup k webovému uživatelskému rozhraní mohou mít pouze uživatelé LAN\",\n    \"origin_web_ui_allowed_pc\": \"Pouze localhost může přistupovat k webovému rozhraní\",\n    \"origin_web_ui_allowed_wan\": \"Kdokoli může přistupovat k webovému rozhraní\",\n    \"output_name_desc_unix\": \"Při spuštění pomocí slunečního svitu byste měli vidět seznam rozpoznaných displejů. Poznámka: Je třeba použít id hodnotu uvnitř závorky. Níže je příklad; skutečný výstup lze nalézt v záložce řešení problémů.\",\n    \"output_name_desc_windows\": \"Ručně zadejte id zobrazovacího zařízení pro zachycení. Pokud je odpojen, primární obrazovka je zachycena. Poznámka: Pokud jste zadali GPU výše, musí být tento displej připojen k grafické kartě. Při spouštění přes Sunshine byste měli vidět seznam detekovaných displejů. Níže je příklad; skutečný výstup lze nalézt v záložce Řešení problémů.\",\n    \"output_name_unix\": \"Zobrazit číslo\",\n    \"output_name_windows\": \"Zobrazit ID zařízení\",\n    \"ping_timeout\": \"Časový limit Ping\",\n    \"ping_timeout_desc\": \"Jak dlouho čekat v milisekundách na data z Měsíčního světla před vypnutím proudu\",\n    \"pkey\": \"Soukromý klíč\",\n    \"pkey_desc\": \"Soukromý klíč používaný pro párování webových UI a Moonlight klientů. Pro nejlepší kompatibilitu by měl být soukromý klíč RSA-2048.\",\n    \"port\": \"Přístav\",\n    \"port_alert_1\": \"Sluneční svaz nemůže používat přístavy pod 1024!\",\n    \"port_alert_2\": \"Přístavy nad 65535 nejsou k dispozici!\",\n    \"port_desc\": \"Nastavte rodinu přístavů používaných sunshine\",\n    \"port_http_port_note\": \"Použijte tento port pro připojení k měsíčnímu světlu.\",\n    \"port_note\": \"Poznámka\",\n    \"port_port\": \"Přístav\",\n    \"port_protocol\": \"Protocol\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Vystavení webového uživatelského rozhraní na internet je bezpečnostní riziko! Pokračujte na vlastní nebezpečí!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Parametr kvantizace\",\n    \"qp_desc\": \"Některá zařízení nemusí podporovat Constant Bit Rate. Pro tato zařízení se místo toho používá QP. Vyšší hodnota znamená větší kompresi, ale menší kvalitu.\",\n    \"qsv_coder\": \"Kodér QuickSync (H264)\",\n    \"qsv_preset\": \"QuickSync Preset\",\n    \"qsv_preset_fast\": \"rychlá (nízká kvalita)\",\n    \"qsv_preset_faster\": \"rychlejší (nižší kvalita)\",\n    \"qsv_preset_medium\": \"střední (výchozí)\",\n    \"qsv_preset_slow\": \"pomalý (dobrá kvalita)\",\n    \"qsv_preset_slower\": \"pomalejší (lepší kvalita)\",\n    \"qsv_preset_slowest\": \"nejpomalejší (nejlepší kvalita)\",\n    \"qsv_preset_veryfast\": \"nejrychlejší (nejnižší jakost)\",\n    \"qsv_slow_hevc\": \"Povolit pomalé kódování HEVC\",\n    \"qsv_slow_hevc_desc\": \"To může povolit HEVC kódování na starších Intel GPU, za cenu vyšší spotřeby GPU a horšího výkonu.\",\n    \"refresh_rate_change_automatic_windows\": \"Použít hodnotu FPS poskytnutou klientem\",\n    \"refresh_rate_change_manual_desc_windows\": \"Zadejte obnovovací frekvenci, která se má použít\",\n    \"refresh_rate_change_manual_windows\": \"Použít ručně zadanou obnovovací frekvenci\",\n    \"refresh_rate_change_no_operation_windows\": \"Zakázáno\",\n    \"refresh_rate_change_windows\": \"Změna FPS\",\n    \"res_fps_desc\": \"Režimy zobrazení inzerované Sunshine. Některé verze Moonlight, jako například Moonlight-nx (Switch), spoléhají na tyto seznamy, aby zajistily, že požadovaná rozlišení a fps jsou podporovány. Toto nastavení nemění způsob odesílání obrazu do Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Použít rozlišení poskytnuté klientem\",\n    \"resolution_change_manual_desc_windows\": \"Pro funkčnost této možnosti musí být v klientovi Moonlight povolena volba \\\"Optimalizovat nastavení hry\\\".\",\n    \"resolution_change_manual_windows\": \"Použít ručně zadané rozlišení\",\n    \"resolution_change_no_operation_windows\": \"Zakázáno\",\n    \"resolution_change_ogs_desc_windows\": \"Pro funkčnost této možnosti musí být v klientovi Moonlight povolena volba \\\"Optimalizovat nastavení hry\\\".\",\n    \"resolution_change_windows\": \"Změna rozlišení\",\n    \"resolutions\": \"Inzerovaná rozlišení\",\n    \"restart_note\": \"Sluneční brýle se restartuje a aplikuje změny.\",\n    \"sleep_mode\": \"Režim spánku\",\n    \"sleep_mode_away\": \"Režim nepřítomnosti (Displej vypnutý, okamžité probuzení)\",\n    \"sleep_mode_desc\": \"Řídí, co se stane, když klient odešle příkaz ke spánku. Uspání (S3): tradiční spánek, nízká spotřeba, ale vyžaduje WOL k probuzení. Hibernace (S4): uloží na disk, velmi nízká spotřeba. Režim nepřítomnosti: displej se vypne, ale systém běží dál pro okamžité probuzení - ideální pro servery herního streamování.\",\n    \"sleep_mode_hibernate\": \"Hibernace (S4)\",\n    \"sleep_mode_suspend\": \"Uspání (S3)\",\n    \"stream_audio\": \"Povolit streamování zvuku\",\n    \"stream_audio_desc\": \"Zakažte tuto možnost, abyste zastavili streamování zvuku.\",\n    \"stream_mic\": \"Povolit streamování mikrofonu\",\n    \"stream_mic_desc\": \"Zakažte tuto možnost, abyste zastavili streamování mikrofonu.\",\n    \"stream_mic_download_btn\": \"Stáhnout virtuální mikrofon\",\n    \"stream_mic_download_confirm\": \"Budete přesměrováni na stránku ke stažení virtuálního mikrofonu. Pokračovat?\",\n    \"stream_mic_note\": \"Tato funkce vyžaduje instalaci virtuálního mikrofonu\",\n    \"sunshine_name\": \"Sluneční jméno\",\n    \"sunshine_name_desc\": \"Jméno zobrazené podle měsíčního světla. Není-li zadáno, použije se hostname počítače\",\n    \"sw_preset\": \"SW přednastavení\",\n    \"sw_preset_desc\": \"Optimalizujte kompromis mezi rychlostí kódování (kódované snímky za sekundu) a efektivitou komprese (kvalita na bit v bitovém toku). Výchozí nastavení je superrychlé.\",\n    \"sw_preset_fast\": \"rychlá\",\n    \"sw_preset_faster\": \"rychleji\",\n    \"sw_preset_medium\": \"střední\",\n    \"sw_preset_slow\": \"pomalu\",\n    \"sw_preset_slower\": \"pomalejší\",\n    \"sw_preset_superfast\": \"superfast (výchozí)\",\n    \"sw_preset_ultrafast\": \"ultrafast\",\n    \"sw_preset_veryfast\": \"veryfast\",\n    \"sw_preset_veryslow\": \"veryslow\",\n    \"sw_tune\": \"SW melodie\",\n    \"sw_tune_animation\": \"animace -- dobré pro karikatury; používá vyšší deblokovací a více referenčních rámců\",\n    \"sw_tune_desc\": \"Vyladění možností, které jsou aplikovány po předvolbě. Výchozí nastavení je nula.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- umožňuje rychlejší dekódování vypnutím určitých filtrů\",\n    \"sw_tune_film\": \"film -- používá pro vysoce kvalitní filmový obsah; snižuje odblokování\",\n    \"sw_tune_grain\": \"zrno – zachovává strukturu zrn ve starém, zrním materiálu\",\n    \"sw_tune_stillimage\": \"stillimage -- dobré pro slideshow-like obsah\",\n    \"sw_tune_zerolatency\": \"nulová latence -- dobrá pro rychlé kódování a nízká latence streamování (výchozí)\",\n    \"system_tray\": \"Povolit systémovou lištu\",\n    \"system_tray_desc\": \"Zda povolit systémovou lištu. Pokud je povoleno, Sunshine zobrazí ikonu v systémové liště a lze ji odtud ovládat.\",\n    \"touchpad_as_ds4\": \"Emulovat DS4 gamepad pokud klient nahlásí přítomnost touchpadu\",\n    \"touchpad_as_ds4_desc\": \"Je-li zakázáno, během výběru typu gamepadu nebude brána v úvahu přítomnost touchpadu.\",\n    \"unsaved_changes_tooltip\": \"Máte neuložené změny. Klikněte pro uložení.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Automaticky konfigurovat přesměrování portů pro streamování přes Internet\",\n    \"variable_refresh_rate\": \"Variabilní obnovovací frekvence (VRR)\",\n    \"variable_refresh_rate_desc\": \"Povolit framerate video streamu, aby odpovídal framerate renderování pro podporu VRR. Je-li povoleno, kódování probíhá pouze tehdy, když jsou k dispozici nové snímky, což umožňuje streamu sledovat skutečný framerate renderování.\",\n    \"vdd_reuse_desc_windows\": \"Když je povoleno, všichni klienti budou sdílet stejný VDD (Virtual Display Device). Když je zakázáno (výchozí), každý klient získá vlastní VDD. Povolte toto pro rychlejší přepínání klientů, ale mějte na paměti, že všichni klienti budou sdílet stejná nastavení zobrazení.\",\n    \"vdd_reuse_windows\": \"Použít stejný VDD pro všechny klienty\",\n    \"virtual_display\": \"Virtuální displej\",\n    \"virtual_mouse\": \"Ovladač virtuální myši\",\n    \"virtual_mouse_desc\": \"Když je povoleno, Sunshine použije ovladač Zako Virtual Mouse (pokud je nainstalován) k simulaci vstupu myši na úrovni HID. Umožňuje hrám s Raw Input přijímat události myši. Když je zakázáno nebo ovladač není nainstalován, přepne na SendInput.\",\n    \"virtual_sink\": \"Virtuální šinek\",\n    \"virtual_sink_desc\": \"Ručně zadejte virtuální audio zařízení, které má být použito. Pokud je zařízení odstaveno, je vybráno automaticky. Důrazně doporučujeme ponechat toto pole prázdné pro automatický výběr zařízení!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Nainstalovat ovladač virtuální myši?\",\n    \"vmouse_confirm_uninstall\": \"Odinstalovat ovladač virtuální myši?\",\n    \"vmouse_install\": \"Instalovat ovladač\",\n    \"vmouse_installing\": \"Instalace...\",\n    \"vmouse_note\": \"Ovladač virtuální myši vyžaduje samostatnou instalaci. Použijte ovládací panel Sunshine k instalaci nebo správě ovladače.\",\n    \"vmouse_refresh\": \"Obnovit stav\",\n    \"vmouse_status_installed\": \"Nainstalován (neaktivní)\",\n    \"vmouse_status_not_installed\": \"Nenainstalován\",\n    \"vmouse_status_running\": \"Běží\",\n    \"vmouse_uninstall\": \"Odinstalovat ovladač\",\n    \"vmouse_uninstalling\": \"Odinstalace...\",\n    \"vt_coder\": \"VideoToolbox Coder\",\n    \"vt_realtime\": \"Video Toolbox v reálném čase enkódování\",\n    \"vt_software\": \"Softwarové kódování video nástrojů\",\n    \"vt_software_allowed\": \"Povoleno\",\n    \"vt_software_forced\": \"Vynucené\",\n    \"wan_encryption_mode\": \"Režim šifrování WAN\",\n    \"wan_encryption_mode_1\": \"Povoleno pro podporované klienty (výchozí)\",\n    \"wan_encryption_mode_2\": \"Vyžadováno pro všechny klienty\",\n    \"wan_encryption_mode_desc\": \"Určuje, kdy bude šifrování použito při streamování přes internet. Šifrování může snížit streamovací výkon, zejména u méně výkonných hostitelů a klientů.\",\n    \"webhook_curl_command\": \"Příkaz\",\n    \"webhook_curl_command_desc\": \"Zkopírujte následující příkaz do terminálu a otestujte, zda webhook funguje správně:\",\n    \"webhook_curl_copy_failed\": \"Kopírování selhalo, prosím vyberte a zkopírujte ručně\",\n    \"webhook_enabled\": \"Oznámení Webhook\",\n    \"webhook_enabled_desc\": \"Když je povoleno, Sunshine bude odesílat oznámení o událostech na zadanou Webhook URL\",\n    \"webhook_group\": \"Nastavení oznámení Webhook\",\n    \"webhook_skip_ssl_verify\": \"Přeskočit ověření SSL certifikátu\",\n    \"webhook_skip_ssl_verify_desc\": \"Přeskočit ověření SSL certifikátu pro HTTPS připojení, pouze pro testování nebo samopodepsané certifikáty\",\n    \"webhook_test\": \"Test\",\n    \"webhook_test_failed\": \"Webhook test selhal\",\n    \"webhook_test_failed_note\": \"Poznámka: Zkontrolujte, zda je URL správná, nebo zkontrolujte konzoli prohlížeče pro více informací.\",\n    \"webhook_test_success\": \"Webhook test úspěšný!\",\n    \"webhook_test_success_cors_note\": \"Poznámka: Kvůli omezením CORS nelze potvrdit stav odpovědi serveru.\\nPožadavek byl odeslán. Pokud je webhook správně nakonfigurován, zpráva by měla být doručena.\\n\\nNávrh: Zkontrolujte kartu Síť v nástrojích pro vývojáře vašeho prohlížeče pro podrobnosti o požadavku.\",\n    \"webhook_test_url_required\": \"Nejprve zadejte Webhook URL\",\n    \"webhook_timeout\": \"Timeout požadavku\",\n    \"webhook_timeout_desc\": \"Timeout pro webhook požadavky v milisekundách, rozmezí 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"URL pro příjem oznámení o událostech, podporuje protokoly HTTP/HTTPS\",\n    \"wgc_checking_mode\": \"Kontrola...\",\n    \"wgc_checking_running_mode\": \"Kontrola režimu běhu...\",\n    \"wgc_control_panel_only\": \"Tato funkce je dostupná pouze v ovládacím panelu Sunshine\",\n    \"wgc_mode_switch_failed\": \"Přepnutí režimu selhalo\",\n    \"wgc_mode_switch_started\": \"Přepnutí režimu zahájeno. Pokud se objeví výzva UAC, klikněte prosím na 'Ano' pro potvrzení.\",\n    \"wgc_service_mode_warning\": \"Snímání WGC vyžaduje spuštění v uživatelském režimu. Pokud aktuálně běžíte v režimu služby, klikněte na tlačítko výše pro přepnutí do uživatelského režimu.\",\n    \"wgc_switch_to_service_mode\": \"Přepnout do režimu služby\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Aktuálně běží v uživatelském režimu. Klikněte pro přepnutí do režimu služby.\",\n    \"wgc_switch_to_user_mode\": \"Přepnout do uživatelského režimu\",\n    \"wgc_switch_to_user_mode_tooltip\": \"Snímání WGC vyžaduje spuštění v uživatelském režimu. Klikněte na toto tlačítko pro přepnutí do uživatelského režimu.\",\n    \"wgc_user_mode_available\": \"Aktuálně běží v uživatelském režimu. Snímání WGC je dostupné.\",\n    \"window_title\": \"Titulek okna\",\n    \"window_title_desc\": \"Titulek okna k zachycení (částečná shoda, nerozlišuje velká/malá písmena). Pokud je prázdné, použije se automaticky název aktuálně běžící aplikace.\",\n    \"window_title_placeholder\": \"např. Název aplikace\"\n  },\n  \"index\": {\n    \"description\": \"Sluneční stream je hostitelem pro Měsíční světlo.\",\n    \"download\": \"Stáhnout\",\n    \"installed_version_not_stable\": \"Používáte předverzi Sunshine. Můžete zaznamenat chyby nebo jiné problémy. Prosím nahlaste všechny problémy, se kterými se setkáváte. Děkujeme, že jste pomohli udělat sunshine lepší software!\",\n    \"loading_latest\": \"Načítání nejnovější verze...\",\n    \"new_pre_release\": \"Je k dispozici nová verze před-vydání!\",\n    \"new_stable\": \"K dispozici je nová stabilní verze!\",\n    \"startup_errors\": \"<b>Pozor!</b> Sunshine detekoval tyto chyby během spuštění. <b>STRONGLY RECOMMEND</b> je opraví před vysíláním.\",\n    \"update_download_confirm\": \"Chystáte se otevřít stránku stahování aktualizací v prohlížeči. Pokračovat?\",\n    \"version_dirty\": \"Děkujeme, že jste pomohli udělat sunshine lepší software!\",\n    \"version_latest\": \"Používáte nejnovější verzi Sunshine\",\n    \"view_logs\": \"Zobrazit protokoly\",\n    \"welcome\": \"Ahoj, sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Aplikace\",\n    \"configuration\": \"Konfigurace\",\n    \"home\": \"Domů\",\n    \"password\": \"Změnit heslo\",\n    \"pin\": \"PIN\",\n    \"theme_auto\": \"Automaticky\",\n    \"theme_dark\": \"Tmavý\",\n    \"theme_light\": \"Světlý\",\n    \"toggle_theme\": \"Téma\",\n    \"troubleshoot\": \"Řešení problémů\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Potvrzení hesla\",\n    \"current_creds\": \"Aktuální přihlašovací údaje\",\n    \"new_creds\": \"Nové přihlašovací údaje\",\n    \"new_username_desc\": \"Pokud není zadáno, uživatelské jméno se nezmění\",\n    \"password_change\": \"Změna hesla\",\n    \"success_msg\": \"Heslo bylo úspěšně změněno! Tato stránka se brzy obnoví, váš prohlížeč vás požádá o nové přihlašovací údaje.\"\n  },\n  \"pin\": {\n    \"actions\": \"Akce\",\n    \"cancel_editing\": \"Zrušit úpravu\",\n    \"client_name\": \"Jméno\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Potvrdit smazání\",\n    \"delete_client\": \"Smazat klienta\",\n    \"delete_confirm_message\": \"Opravdu chcete smazat <strong>{name}</strong>?\",\n    \"delete_warning\": \"Tuto akci nelze vrátit zpět.\",\n    \"device_name\": \"Název zařízení\",\n    \"device_size\": \"Velikost zařízení\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Velké - TV\",\n    \"device_size_medium\": \"Střední - Tablet\",\n    \"device_size_small\": \"Malé - Telefon\",\n    \"edit_client_settings\": \"Upravit nastavení klienta\",\n    \"hdr_profile\": \"HDR profil\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Načítání...\",\n    \"loading_clients\": \"Načítání klientů...\",\n    \"modify_in_gui\": \"Prosím upravte v grafickém rozhraní\",\n    \"none\": \"-- Žádný --\",\n    \"or_manual_pin\": \"nebo zadejte PIN ručně\",\n    \"pair_failure\": \"Spárování se nezdařilo: Zkontrolujte, zda je PIN správně zadán\",\n    \"pair_success\": \"Úspěch! Prosím, zkontrolujte Moonlight pro pokračování\",\n    \"pin_pairing\": \"PIN Pairing\",\n    \"qr_expires_in\": \"Vyprší za\",\n    \"qr_generate\": \"Vygenerovat QR kód\",\n    \"qr_paired_success\": \"Spárování úspěšné!\",\n    \"qr_pairing\": \"Párování QR kódem\",\n    \"qr_pairing_desc\": \"Vygenerujte QR kód pro rychlé párování. Naskenujte ho klientem Moonlight pro automatické spárování.\",\n    \"qr_pairing_warning\": \"Experimentální funkce. Pokud párování selže, použijte ruční párování PINem níže. Poznámka: Tato funkce funguje pouze v LAN.\",\n    \"qr_refresh\": \"Obnovit QR kód\",\n    \"remove_paired_devices_desc\": \"Odstraňte vaše spárovaná zařízení.\",\n    \"save_changes\": \"Uložit změny\",\n    \"save_failed\": \"Nepodařilo se uložit nastavení klienta. Prosím zkuste to znovu.\",\n    \"save_or_cancel_first\": \"Nejprve prosím uložte nebo zrušte úpravu\",\n    \"send\": \"Poslat\",\n    \"unknown_client\": \"Neznámý klient\",\n    \"unpair_all_confirm\": \"Opravdu chcete zrušit spárování všech klientů? Tuto akci nelze vrátit zpět.\",\n    \"unsaved_changes\": \"Neuložené změny\",\n    \"warning_msg\": \"Ujistěte se, že máte přístup k klientovi, se kterým spárujete. Tento software může poskytnout vašemu počítači úplnou kontrolu, takže buďte opatrní!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android doporučeno\",\n    \"client_downloads\": \"Stažení klientů\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"GitHub Discussions\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Přidat se ke komunitě\",\n    \"join_group_desc\": \"Získejte pomoc a sdílejte zkušenosti\",\n    \"legal\": \"Právní předpisy\",\n    \"legal_desc\": \"Pokračováním v používání tohoto softwaru souhlasíte s podmínkami v následujících dokumentech.\",\n    \"license\": \"Licence\",\n    \"lizardbyte_website\": \"Webové stránky LizardByte\",\n    \"official_website\": \"Official Website\",\n    \"official_website_title\": \"AlkaidLab - Oficiální stránky\",\n    \"open_source\": \"Otevřený zdrojový kód\",\n    \"open_source_desc\": \"Star & Fork pro podporu projektu\",\n    \"quick_start\": \"Rychlý start\",\n    \"resources\": \"Zdroje\",\n    \"resources_desc\": \"Zdroje pro sunnity!\",\n    \"third_party_desc\": \"Oznámení o komponentech třetích stran\",\n    \"third_party_moonlight\": \"Přátelské odkazy\",\n    \"third_party_notice\": \"Oznámení třetí strany\",\n    \"tutorial\": \"Návod\",\n    \"tutorial_desc\": \"Podrobný průvodce konfigurací a používáním\",\n    \"view_license\": \"Zobrazit úplnou licenci\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Virtual Display\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"Go to Repository\",\n    \"harmony_modal_desc\": \"For HarmonyOS NEXT Moonlight, please search for Moonlight V+ in the HarmonyOS App Store\",\n    \"harmony_modal_link_notice\": \"This link will redirect to the project repository\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"sekund\",\n    \"restart_desc\": \"Konfigurace uložena. Sunshine se restartuje pro aplikování nastavení displeje.\",\n    \"restart_go_now\": \"Jít nyní\",\n    \"restart_title\": \"Restartování Sunshine\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"Základní nastavení je nyní aktivní. Můžete ihned začít streamovat pomocí klienta Moonlight!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"Základní displej (VDD) je vestavěný inteligentní virtuální displej Sunshine Foundation, který podporuje libovolné rozlišení, snímkovou frekvenci a optimalizaci HDR. Je preferovanou volbou pro streaming s vypnutou obrazovkou a streaming na rozšířený displej.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Zajistit aktivaci\",\n    \"step3_ensure_active_desc\": \"Aktivuje displej, pokud není již aktivní\",\n    \"step3_ensure_only_display\": \"Zajistit jediný displej\",\n    \"step3_ensure_only_display_desc\": \"Deaktivuje všechny ostatní displeje a aktivuje pouze zadaný (doporučeno)\",\n    \"step3_ensure_primary\": \"Zajistit hlavní displej\",\n    \"step3_ensure_primary_desc\": \"Aktivuje displej a nastaví jej jako hlavní\",\n    \"step3_ensure_secondary\": \"Sekundární streaming\",\n    \"step3_ensure_secondary_desc\": \"Používá pouze virtuální displej pro sekundární rozšířený streaming\",\n    \"step3_no_operation\": \"Žádná operace\",\n    \"step3_no_operation_desc\": \"Žádné změny stavu displeje; uživatel musí sám zajistit, že displej je připraven\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Pokročilé\",\n    \"amd\": \"AMD AMF Kodér\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Kodéry\",\n    \"files\": \"Konfigurační soubory\",\n    \"general\": \"Obecné\",\n    \"input\": \"Vstup\",\n    \"network\": \"Síť\",\n    \"nv\": \"NVIDIA NVENC Kodér\",\n    \"qsv\": \"Intel QuickSync Kodér\",\n    \"sw\": \"Softwarový kodér\",\n    \"vaapi\": \"VAAPI Kodér\",\n    \"vt\": \"VideoToolbox Kodér\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analyzuji...\",\n    \"ai_analyzing_logs\": \"Analyzuji logy, prosím čekejte...\",\n    \"ai_config\": \"AI konfigurace\",\n    \"ai_copy_result\": \"Kopírovat\",\n    \"ai_diagnosis\": \"AI diagnostika\",\n    \"ai_diagnosis_title\": \"AI diagnostika logů\",\n    \"ai_error\": \"Analýza selhala\",\n    \"ai_key_local\": \"API klíč je uložen pouze lokálně a nikdy není nahrán\",\n    \"ai_model\": \"Model\",\n    \"ai_provider\": \"Poskytovatel\",\n    \"ai_reanalyze\": \"Znovu analyzovat\",\n    \"ai_result\": \"Výsledek diagnostiky\",\n    \"ai_retry\": \"Opakovat\",\n    \"ai_start_diagnosis\": \"Zahájit diagnostiku\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Pokud potřebujete okamžitě vypnout Sunshine, můžete použít tuto funkci. Všimněte si, že budete muset ručně spustit znovu po vypnutí.\",\n    \"boom_sunshine_success\": \"Sunshine byl vypnut\",\n    \"confirm_boom\": \"Opravdu chcete ukončit?\",\n    \"confirm_boom_desc\": \"Takže opravdu chcete ukončit? No, nemůžu vás zastavit, pokračujte a klikněte znovu\",\n    \"confirm_logout\": \"Confirm logout?\",\n    \"confirm_logout_desc\": \"You will need to enter your password again to access the web UI.\",\n    \"copy_config\": \"Kopírovat konfiguraci\",\n    \"copy_config_error\": \"Nepodařilo se zkopírovat konfiguraci\",\n    \"copy_config_success\": \"Konfigurace zkopírována do schránky!\",\n    \"copy_logs\": \"Copy logs\",\n    \"download_logs\": \"Download logs\",\n    \"force_close\": \"Vynutit zavření\",\n    \"force_close_desc\": \"Pokud si Měsíc stěžuje na právě spuštěnou aplikaci, vynucené zavření aplikace by mělo problém vyřešit.\",\n    \"force_close_error\": \"Chyba při zavírání aplikace\",\n    \"force_close_success\": \"Aplikace úspěšně uzavřena!\",\n    \"ignore_case\": \"Ignorovat velikost písmen\",\n    \"logout\": \"Odhlásit se\",\n    \"logout_desc\": \"Odhlášení. Možná se budete muset znovu přihlásit.\",\n    \"logout_localhost_tip\": \"Aktuální prostředí nevyžaduje přihlášení; odhlášení nevyvolá výzvu k zadání hesla.\",\n    \"logs\": \"Logy\",\n    \"logs_desc\": \"Podívejte se na logy nahrané sunshine\",\n    \"logs_find\": \"Najít...\",\n    \"match_contains\": \"Obsahuje\",\n    \"match_exact\": \"Přesně\",\n    \"match_regex\": \"Regulární výraz\",\n    \"reopen_setup_wizard\": \"Znovu otevřít průvodce nastavením\",\n    \"reopen_setup_wizard_desc\": \"Znovu otevřít stránku průvodce nastavením pro opětovnou konfiguraci počátečních nastavení.\",\n    \"reopen_setup_wizard_error\": \"Chyba při opětovném otevření průvodce nastavením\",\n    \"reset_display_device_desc_windows\": \"Pokud Sunshine uvázl při pokusu o obnovení změněných nastavení zobrazovacího zařízení, můžete nastavení resetovat a pokračovat v ruční obnově stavu displeje.\\nK tomu může dojít z různých důvodů: zařízení již není k dispozici, bylo připojeno k jinému portu atd.\",\n    \"reset_display_device_error_windows\": \"Chyba při resetování trvalosti!\",\n    \"reset_display_device_success_windows\": \"Úspěšné resetování trvalosti!\",\n    \"reset_display_device_windows\": \"Resetovat paměť displeje\",\n    \"restart_sunshine\": \"Restartovat Sunshine\",\n    \"restart_sunshine_desc\": \"Pokud sluneční svit nefunguje správně, můžete jej zkusit restartovat. To ukončí všechny spuštěné relace.\",\n    \"restart_sunshine_success\": \"Sunshine se restartuje\",\n    \"troubleshooting\": \"Řešení problémů\",\n    \"unpair_all\": \"Zrušit spárování vše\",\n    \"unpair_all_error\": \"Chyba při nepárování\",\n    \"unpair_all_success\": \"Všechna zařízení nejsou spárována.\",\n    \"unpair_desc\": \"Odstranit spárovaná zařízení. Jednotlivě nespárovaná zařízení s aktivní relací zůstanou připojena, ale nemohou spustit nebo obnovit relaci.\",\n    \"unpair_single_no_devices\": \"Neexistují žádná spárovaná zařízení.\",\n    \"unpair_single_success\": \"Zařízení však mohou být stále v aktivní relaci. Použijte tlačítko 'Vynutit zavření' pro ukončení všech otevřených relací.\",\n    \"unpair_single_unknown\": \"Neznámý klient\",\n    \"unpair_title\": \"Zrušit párování\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Potvrdit heslo\",\n    \"create_creds\": \"Před spuštěním potřebujeme, abyste vytvořili nové uživatelské jméno a heslo pro přístup k webovému uživatelskému rozhraní.\",\n    \"create_creds_alert\": \"Níže uvedené přihlašovací údaje jsou potřebné pro přístup k webovému rozhraní Sunshine. Uchovávejte je v bezpečí, protože je už nikdy nebudete vidět!\",\n    \"creds_local_only\": \"Vaše přihlašovací údaje jsou uloženy lokálně offline a nikdy nebudou nahrány na žádný server.\",\n    \"error\": \"Chyba!\",\n    \"greeting\": \"Vítejte v Sunshine Foundation!\",\n    \"hide_password\": \"Skrýt heslo\",\n    \"login\": \"Přihlásit se\",\n    \"network_error\": \"Chyba sítě, zkontrolujte připojení\",\n    \"password\": \"Heslo\",\n    \"password_match\": \"Hesla se shodují\",\n    \"password_mismatch\": \"Hesla se neshodují\",\n    \"server_error\": \"Chyba serveru\",\n    \"show_password\": \"Zobrazit heslo\",\n    \"success\": \"Úspěch!\",\n    \"username\": \"Uživatelské jméno\",\n    \"welcome_success\": \"Tato stránka se brzy obnoví, váš prohlížeč vás požádá o nové přihlašovací údaje.\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/de.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Übernehmen\",\n    \"auto\": \"Automatisch\",\n    \"autodetect\": \"AutoDetection (empfohlen)\",\n    \"beta\": \"(Beta)\",\n    \"cancel\": \"Abbrechen\",\n    \"close\": \"Schließen\",\n    \"copied\": \"In Zwischenablage kopiert\",\n    \"copy\": \"Kopieren\",\n    \"delete\": \"Löschen\",\n    \"description\": \"Description\",\n    \"disabled\": \"Deaktiviert\",\n    \"disabled_def\": \"Deaktiviert (Standard)\",\n    \"dismiss\": \"Verwerfen\",\n    \"do_cmd\": \"Befehl ausführen\",\n    \"download\": \"Herunterladen\",\n    \"edit\": \"Bearbeiten\",\n    \"elevated\": \"Erhöhte\",\n    \"enabled\": \"Aktiviert\",\n    \"enabled_def\": \"Aktiviert (Standard)\",\n    \"error\": \"Fehler!\",\n    \"no_changes\": \"Keine Änderungen\",\n    \"note\": \"Hinweis:\",\n    \"password\": \"Passwort\",\n    \"remove\": \"Entfernen\",\n    \"run_as\": \"Als Admin ausführen\",\n    \"save\": \"Speichern\",\n    \"see_more\": \"Mehr ansehen\",\n    \"success\": \"Erfolgreich!\",\n    \"undo_cmd\": \"Befehl rückgängig machen\",\n    \"username\": \"Benutzername\",\n    \"warning\": \"Warnung!\"\n  },\n  \"apps\": {\n    \"actions\": \"Aktionen\",\n    \"add_cmds\": \"Befehle hinzufügen\",\n    \"add_new\": \"Neu hinzufügen\",\n    \"advanced_options\": \"Erweiterte Optionen\",\n    \"app_name\": \"Anwendungsname\",\n    \"app_name_desc\": \"Anwendungsname, wie auf Mondlicht gezeigt\",\n    \"applications_desc\": \"Anwendungen werden nur beim Neustart des Clients aktualisiert\",\n    \"applications_title\": \"Anwendungen\",\n    \"auto_detach\": \"Streaming fortsetzen, wenn die Anwendung schnell beendet wird\",\n    \"auto_detach_desc\": \"Dies wird versuchen, automatisch Apps vom Launcher-Typ zu erkennen, die sich nach dem Start eines anderen Programms oder einer Instanz von sich selbst schnell schließen. Wenn eine Anwendung vom Launcher-Typ erkannt wird, wird sie als abgetrennte App behandelt.\",\n    \"basic_info\": \"Grundinformationen\",\n    \"cmd\": \"Befehl\",\n    \"cmd_desc\": \"Die zu startende Hauptanwendung. Wenn leer wird keine Anwendung gestartet.\",\n    \"cmd_examples_title\": \"Häufige Beispiele:\",\n    \"cmd_note\": \"Wenn der Pfad zum ausführbaren Kommando Leerzeichen enthält, müssen Sie ihn in Anführungszeichen einfügen.\",\n    \"cmd_prep_desc\": \"Eine Liste von Befehlen, die vor oder nach dieser Anwendung ausgeführt werden sollen. Wenn einer der Prep-Befehle fehlschlägt, wird das Starten der Anwendung abgebrochen.\",\n    \"cmd_prep_name\": \"Befehlsvorbereitungen\",\n    \"command_settings\": \"Befehls-Einstellungen\",\n    \"covers_found\": \"Cover gefunden\",\n    \"delete\": \"Löschen\",\n    \"delete_confirm\": \"Möchten Sie \\\"{name}\\\" wirklich löschen?\",\n    \"detached_cmds\": \"Getrennte Befehle\",\n    \"detached_cmds_add\": \"Separiertes Kommando hinzufügen\",\n    \"detached_cmds_desc\": \"Eine Liste von Befehlen, die im Hintergrund ausgeführt werden sollen.\",\n    \"detached_cmds_note\": \"Wenn der Pfad zum ausführbaren Kommando Leerzeichen enthält, müssen Sie ihn in Anführungszeichen einfügen.\",\n    \"detached_cmds_remove\": \"Losgelösten Befehl entfernen\",\n    \"edit\": \"Bearbeiten\",\n    \"env_app_id\": \"App-ID\",\n    \"env_app_name\": \"App-Name\",\n    \"env_client_audio_config\": \"Die vom Client angeforderte Audio-Konfiguration (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"Der Client hat die Option angefordert, das Spiel für ein optimales Streaming zu optimieren (true/false)\",\n    \"env_client_fps\": \"Das vom Client angeforderte FPS (int)\",\n    \"env_client_gcmap\": \"Die angeforderte Gamepadmaske im Bitset/Bitfield Format (int)\",\n    \"env_client_hdr\": \"HDR ist vom Client aktiviert (true/false)\",\n    \"env_client_height\": \"Die vom Client angeforderte Höhe (int)\",\n    \"env_client_host_audio\": \"Der Client hat Host-Audio angefordert (true/falsch)\",\n    \"env_client_name\": \"Anzeigename des Clients (Zeichenfolge)\",\n    \"env_client_width\": \"Die vom Client angeforderte Breite (int)\",\n    \"env_displayplacer_example\": \"Beispiel - displayplacer für die Automatisierung der Auflösung:\",\n    \"env_qres_example\": \"Beispiel - QRes für die Automatisierung der Auflösung:\",\n    \"env_qres_path\": \"qres Pfad\",\n    \"env_var_name\": \"Var Name\",\n    \"env_vars_about\": \"Über Umgebungsvariablen\",\n    \"env_vars_desc\": \"Alle Befehle erhalten diese Umgebungsvariablen standardmäßig:\",\n    \"env_xrandr_example\": \"Beispiel - Xrandr für die Auflösungsautomatisierung:\",\n    \"exit_timeout\": \"Timeout beenden\",\n    \"exit_timeout_desc\": \"Anzahl der Sekunden, die gewartet werden, bis alle App-Prozesse anmutig beendet werden, wenn sie beendet werden. Wenn nicht gesetzt, ist die Standardeinstellung bis zu 5 Sekunden. Wenn Null oder ein negativer Wert gesetzt wird, wird die App sofort beendet.\",\n    \"file_selector_not_initialized\": \"Dateiauswahl nicht initialisiert\",\n    \"find_cover\": \"Cover finden\",\n    \"form_invalid\": \"Bitte überprüfen Sie die erforderlichen Felder\",\n    \"form_valid\": \"Gültige Anwendung\",\n    \"global_prep_desc\": \"Aktiviere/Deaktiviere die Ausführung von Global Prep Commands für diese Anwendung.\",\n    \"global_prep_name\": \"Globale Vorbereitungsbefehle\",\n    \"image\": \"Bild\",\n    \"image_desc\": \"Symbol/Bild/Bild/Bildpfad, der an den Client gesendet wird. Das Bild muss eine PNG-Datei sein. Falls nicht gesetzt, sendet Sunshine ein Standardbild-Bild.\",\n    \"image_settings\": \"Bildeinstellungen\",\n    \"loading\": \"Wird geladen...\",\n    \"menu_cmd_actions\": \"Aktionen\",\n    \"menu_cmd_add\": \"Menü-Befehl hinzufügen\",\n    \"menu_cmd_command\": \"Befehl\",\n    \"menu_cmd_desc\": \"Nach der Konfiguration sind diese Befehle im Rückkehr-Menü des Clients sichtbar und ermöglichen die schnelle Ausführung spezifischer Operationen ohne den Stream zu unterbrechen, wie z.B. das Starten von Hilfsprogrammen.\\nBeispiel: Anzeigename - Computer herunterfahren; Befehl - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Anzeigename\",\n    \"menu_cmd_drag_sort\": \"Zum Sortieren ziehen\",\n    \"menu_cmd_name\": \"Menü-Befehle\",\n    \"menu_cmd_placeholder_command\": \"Befehl\",\n    \"menu_cmd_placeholder_display_name\": \"Anzeigename\",\n    \"menu_cmd_placeholder_execute\": \"Befehl ausführen\",\n    \"menu_cmd_placeholder_undo\": \"Befehl rückgängig machen\",\n    \"menu_cmd_remove_menu\": \"Menü-Befehl entfernen\",\n    \"menu_cmd_remove_prep\": \"Vorbereitungsbefehl entfernen\",\n    \"mouse_mode\": \"Mausmodus\",\n    \"mouse_mode_auto\": \"Auto (Globale Einstellung)\",\n    \"mouse_mode_desc\": \"Wählen Sie die Mauseingabemethode für diese Anwendung. Auto verwendet die globale Einstellung, Virtuelle Maus verwendet den HID-Treiber, SendInput verwendet die Windows-API.\",\n    \"mouse_mode_sendinput\": \"SendInput (Windows API)\",\n    \"mouse_mode_vmouse\": \"Virtuelle Maus\",\n    \"name\": \"Name\",\n    \"output_desc\": \"Die Datei, in der die Ausgabe des Befehls gespeichert wird, wenn sie nicht angegeben ist, wird die Ausgabe ignoriert\",\n    \"output_name\": \"Ausgang\",\n    \"run_as_desc\": \"Dies kann für einige Anwendungen notwendig sein, die Administratorrechte benötigen, um ordnungsgemäß zu funktionieren.\",\n    \"scan_result_add_all\": \"Alle hinzufügen\",\n    \"scan_result_edit_title\": \"Hinzufügen und bearbeiten\",\n    \"scan_result_filter_all\": \"Alle\",\n    \"scan_result_filter_epic_title\": \"Epic Games-Spiele\",\n    \"scan_result_filter_executable\": \"Ausführbar\",\n    \"scan_result_filter_executable_title\": \"Ausführbare Datei\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxy-Spiele\",\n    \"scan_result_filter_script\": \"Skript\",\n    \"scan_result_filter_script_title\": \"Batch/Befehls-Skript\",\n    \"scan_result_filter_shortcut\": \"Verknüpfung\",\n    \"scan_result_filter_shortcut_title\": \"Verknüpfung\",\n    \"scan_result_filter_steam_title\": \"Steam-Spiele\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Spiel\",\n    \"scan_result_games_only\": \"Nur Spiele\",\n    \"scan_result_matched\": \"Gefunden: {count}\",\n    \"scan_result_no_apps\": \"Keine Anwendungen zum Hinzufügen gefunden\",\n    \"scan_result_no_matches\": \"Keine passenden Anwendungen gefunden\",\n    \"scan_result_quick_add_title\": \"Schnell hinzufügen\",\n    \"scan_result_remove_title\": \"Aus Liste entfernen\",\n    \"scan_result_search_placeholder\": \"Anwendungsname, Befehl oder Pfad suchen...\",\n    \"scan_result_show_all\": \"Alle anzeigen\",\n    \"scan_result_title\": \"Scan-Ergebnisse\",\n    \"scan_result_try_different_keywords\": \"Versuchen Sie andere Suchbegriffe zu verwenden\",\n    \"scan_result_type_batch\": \"Batch\",\n    \"scan_result_type_command\": \"Befehls-Skript\",\n    \"scan_result_type_executable\": \"Ausführbare Datei\",\n    \"scan_result_type_shortcut\": \"Verknüpfung\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Anwendungen suchen...\",\n    \"select\": \"Auswählen\",\n    \"test_menu_cmd\": \"Befehl testen\",\n    \"test_menu_cmd_empty\": \"Befehl darf nicht leer sein\",\n    \"test_menu_cmd_executing\": \"Befehl wird ausgeführt...\",\n    \"test_menu_cmd_failed\": \"Befehlsausführung fehlgeschlagen\",\n    \"test_menu_cmd_success\": \"Befehl erfolgreich ausgeführt!\",\n    \"use_desktop_image\": \"Aktuelles Desktop-Hintergrundbild verwenden\",\n    \"wait_all\": \"Streaming fortsetzen bis alle App-Prozesse beendet sind\",\n    \"wait_all_desc\": \"Dies wird fortgesetzt, bis alle Prozesse, die von der App gestartet werden, beendet sind. Wenn diese Option deaktiviert ist, wird das Streaming gestoppt, wenn der erste App-Prozess beendet wird, auch wenn andere App-Prozesse noch laufen.\",\n    \"working_dir\": \"Arbeitsverzeichnis\",\n    \"working_dir_desc\": \"Das Arbeitsverzeichnis, das an den Prozess übergeben werden soll. Zum Beispiel verwenden einige Anwendungen das Arbeitsverzeichnis, um nach Konfigurationsdateien zu suchen. Falls nicht gesetzt, wird Sunshine standardmäßig das übergeordnete Verzeichnis des Befehls verwenden\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Adaptername\",\n    \"adapter_name_desc_linux_1\": \"Geben Sie eine GPU für die Aufnahme manuell an.\",\n    \"adapter_name_desc_linux_2\": \"um alle Geräte zu finden, die VAAPI nutzen können\",\n    \"adapter_name_desc_linux_3\": \"Ersetze ``renderD129`` durch das Gerät von oben, um den Namen und die Fähigkeiten des Geräts aufzulisten. Um von Sunshine unterstützt zu werden, muss es zumindest über folgende Punkte verfügen:\",\n    \"adapter_name_desc_windows\": \"Geben Sie manuell eine GPU für die Aufnahme an. Wenn nicht festgelegt, wird die GPU automatisch ausgewählt. Hinweis: Diese GPU muss mit einem eingeschalteten Display verbunden sein. Wenn Ihr Laptop keinen direkten GPU-Ausgang aktivieren kann, stellen Sie dies bitte auf automatisch.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Wenn die neueste Version des virtuellen Displays installiert ist, kann es automatisch mit der GPU-Bindung verknüpft werden\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Neu\",\n    \"address_family\": \"Adressfamilie\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Adressfamilie einstellen, die von Sunshine verwendet wird\",\n    \"address_family_ipv4\": \"Nur IPv4\",\n    \"always_send_scancodes\": \"Scancodes immer senden\",\n    \"always_send_scancodes_desc\": \"Das Senden von Scancodes verbessert die Kompatibilität mit Spielen und Apps, kann aber zu falschen Tastatureingaben von bestimmten Clients führen, die kein amerikanisches Tastaturlayout verwenden. Aktivieren, wenn die Eingabe der Tastatur in bestimmten Anwendungen überhaupt nicht funktioniert. Deaktivieren, wenn Schlüssel auf dem Client die falsche Eingabe auf dem Host generieren.\",\n    \"amd_coder\": \"AMF Coder (H264)\",\n    \"amd_coder_desc\": \"Erlaubt es Ihnen, die Entropy-Kodierung auszuwählen, um die Qualität oder die Kodierungsgeschwindigkeit zu priorisieren. H.264 nur.\",\n    \"amd_enforce_hrd\": \"Hypothetische Referenz-Decodierer (HRD) durchsetzen\",\n    \"amd_enforce_hrd_desc\": \"Steigern Sie die Einschränkungen bei der Ratensteuerung, um die Anforderungen des HRD-Modells zu erfüllen. Dies reduziert die Bitratenüberläufe erheblich, kann jedoch zu Kodierungsartefakten oder zu geringerer Qualität auf bestimmten Karten führen.\",\n    \"amd_preanalysis\": \"AMF-Voranalyse\",\n    \"amd_preanalysis_desc\": \"Dies ermöglicht die Vorabanalyse der Rate, wodurch die Qualität auf Kosten einer erhöhten Encoding-Latenz erhöht werden kann.\",\n    \"amd_quality\": \"AMF-Qualität\",\n    \"amd_quality_balanced\": \"ausgewogen -- Ausgewogen (Standard)\",\n    \"amd_quality_desc\": \"Dies steuert die Balance zwischen Kodierungsgeschwindigkeit und Qualität.\",\n    \"amd_quality_group\": \"AMF Qualitätseinstellungen\",\n    \"amd_quality_quality\": \"Qualität -- Qualität bevorzugen\",\n    \"amd_quality_speed\": \"speed -- bevorzuge Geschwindigkeit\",\n    \"amd_qvbr_quality\": \"AMF QVBR-Qualitätsstufe\",\n    \"amd_qvbr_quality_desc\": \"Qualitätsstufe für den QVBR-Ratensteuerungsmodus. Bereich: 1-51 (niedriger = bessere Qualität). Standard: 23. Gilt nur wenn die Ratensteuerung auf 'qvbr' eingestellt ist.\",\n    \"amd_rc\": \"AMF-Ratensteuerung\",\n    \"amd_rc_cbr\": \"cbr – Konstante Bitrate\",\n    \"amd_rc_cqp\": \"cqp -- Konstanter qp-Modus\",\n    \"amd_rc_desc\": \"Diese steuert die Methode der Ratensteuerung, um sicherzustellen, dass wir nicht das Client-Bitrate Ziel überschreiten. 'cqp' ist nicht geeignet für Bitraten-Targeting, und andere Optionen außer 'vbr_latency' hängen von der Durchsetzung von HRD ab, um Bitraten-Überläufe einzuschränken.\",\n    \"amd_rc_group\": \"AMF Rate Control Einstellungen\",\n    \"amd_rc_hqcbr\": \"hqcbr -- Hohe Qualität konstante Bitrate\",\n    \"amd_rc_hqvbr\": \"hqvbr -- Hohe Qualität variable Bitrate\",\n    \"amd_rc_qvbr\": \"qvbr -- Qualitätsvariable Bitrate (verwendet QVBR-Qualitätsstufe)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- latenzeingeschränkte Bitrate (Standard)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak – eingeschränkte Variablen-Bitrate spitzen\",\n    \"amd_usage\": \"AMF-Nutzung\",\n    \"amd_usage_desc\": \"Dies legt das Basiscodierungsprofil fest. Alle unten dargestellten Optionen werden eine Teilmenge des Nutzungsprofils überschreiben. Es werden jedoch zusätzliche versteckte Einstellungen angewendet, die an anderer Stelle nicht konfiguriert werden können.\",\n    \"amd_usage_lowlatency\": \"niedrige Latenz - niedrige Latenz (schnell)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - niedrige Latenz, hohe Qualität (schnell)\",\n    \"amd_usage_transcoding\": \"transcoding -- Umkodierung (langsamste)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatenz - extrem niedrige Latenz (schnellste)\",\n    \"amd_usage_webcam\": \"webcam -- Webcam (langsam)\",\n    \"amd_vbaq\": \"AMF-Varianz-basierte Adaptive Quantisierung (VBAQ)\",\n    \"amd_vbaq_desc\": \"Das menschliche visuelle System ist in der Regel weniger empfindlich auf Artefakte in stark strukturierten Bereichen. Im VBAQ-Modus wird die Pixelvarianz verwendet, um die Komplexität der räumlichen Texturen anzuzeigen, so dass der Encoder mehr Bits für glättende Bereiche zuweisen kann. Die Aktivierung dieser Funktion führt zu Verbesserungen der subjektiven visuellen Qualität mit einigen Inhalten.\",\n    \"amf_draw_mouse_cursor\": \"Einfachen Cursor zeichnen, wenn AMF-Aufnahmemethode verwendet wird\",\n    \"amf_draw_mouse_cursor_desc\": \"In einigen Fällen wird bei Verwendung der AMF-Aufnahme der Mauszeiger nicht angezeigt. Durch Aktivieren dieser Option wird ein einfacher Mauszeiger auf dem Bildschirm gezeichnet. Hinweis: Die Position des Mauszeigers wird nur aktualisiert, wenn es eine Aktualisierung des Inhaltsbildschirms gibt. Daher können Sie in Nicht-Spiel-Szenarien wie auf dem Desktop eine träge Mauszeigerbewegung beobachten.\",\n    \"apply_note\": \"Klicken Sie auf 'Anwenden', um Sunshine neu zu starten und Änderungen anzuwenden. Dies wird alle laufenden Sitzungen beenden.\",\n    \"audio_sink\": \"Audio Sink\",\n    \"audio_sink_desc_linux\": \"Der Name des Audio-Spüls, der für Audio Loopback verwendet wird. Wenn Sie diese Variable nicht angeben, wählt pulseaudio das Standard-Monitorgerät. Sie können den Namen des Audiospülers mit einem Befehl finden:\",\n    \"audio_sink_desc_macos\": \"Der Name des für Audio Loopback verwendeten Audiosenks kann aufgrund von Systembeschränkungen nur auf Mikrofone auf macOS zugreifen. Zum Streamen von System-Audio mit Soundflower oder BlackHole.\",\n    \"audio_sink_desc_windows\": \"Geben Sie ein bestimmtes Audiogerät für die Aufnahme manuell an. Wenn nicht gesetzt, wird das Gerät automatisch ausgewählt. Wir empfehlen dringend, dieses Feld leer zu lassen, um die automatische Geräteauswahl zu verwenden! Wenn Sie mehrere Audiogeräte mit identischen Namen haben, können Sie die Geräte-ID mit dem folgenden Befehl erhalten:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Lautsprecher (High Definition Audio Device)\",\n    \"av1_mode\": \"AV1 Support\",\n    \"av1_mode_0\": \"Sunshine werbt Unterstützung für AV1 basierend auf Encoder Fähigkeiten (empfohlen)\",\n    \"av1_mode_1\": \"Sunshine werbt keinen Support für AV1\",\n    \"av1_mode_2\": \"Sunshine werbt Unterstützung für AV1 Hauptprofil mit 8-Bit\",\n    \"av1_mode_3\": \"Sunshine werbt Unterstützung für AV1 Hauptprofile mit 8-Bit und 10-Bit (HDR)\",\n    \"av1_mode_desc\": \"Ermöglicht dem Client, AV1 Haupt-8-bit oder 10-bit Video-Streams anzufordern. AV1 ist CPU-intensiver zum Kodieren, daher kann die Aktivierung die Leistung bei der Verwendung von Software Codierung verringern.\",\n    \"back_button_timeout\": \"Timeout für Home/Guide Button Emulation\",\n    \"back_button_timeout_desc\": \"Wenn die Schaltfläche Zurück/Auswählen für die angegebene Anzahl an Millisekunden gedrückt gehalten wird, wird die Taste Home/Guide emuliert. Wenn auf einen Wert < 0 (Standard) gesetzt ist, wird die Home/Guide-Taste nicht nachgeahmt.\",\n    \"bind_address\": \"Bind-Adresse (Testfunktion)\",\n    \"bind_address_desc\": \"Legen Sie die spezifische IP-Adresse fest, an die Sunshine gebunden werden soll. Wenn leer, wird Sunshine an alle verfügbaren Adressen gebunden.\",\n    \"capture\": \"Erzwinge eine bestimmte Aufnahmemethode\",\n    \"capture_desc\": \"Im automatischen Modus wird Sunshine den ersten verwenden, der funktioniert. NvFBC benötigt gepatchte Nvidia-Treiber.\",\n    \"capture_target\": \"Aufnahmeziel\",\n    \"capture_target_desc\": \"Wählen Sie den Typ des Aufnahmeziels aus. Wenn Sie 'Fenster' wählen, können Sie ein bestimmtes Anwendungsfenster (wie z.B. AI Frame Interpolation Software) anstatt des gesamten Displays aufnehmen.\",\n    \"capture_target_display\": \"Anzeige\",\n    \"capture_target_window\": \"Fenster\",\n    \"cert\": \"Zertifikat\",\n    \"cert_desc\": \"Das Zertifikat, das für das Web-UI und Moonlight Client-Paar verwendet wird. Für bestmögliche Kompatibilität sollte dieser einen RSA-2048 öffentlichen Schlüssel haben.\",\n    \"channels\": \"Maximal verbundene Clients\",\n    \"channels_desc_1\": \"Sunshine kann eine einzelne Streaming-Sitzung gleichzeitig mit mehreren Clients teilen.\",\n    \"channels_desc_2\": \"Einige Hardware-Encoder haben möglicherweise Einschränkungen, die die Leistung bei mehreren Streams verringern.\",\n    \"close_verify_safe\": \"Sichere Überprüfung kompatibel mit alten Clients\",\n    \"close_verify_safe_desc\": \"Alte Clients können nicht mit Sunshine verbunden werden, bitte deaktivieren Sie diese Option oder aktualisieren Sie den Client\",\n    \"coder_cabac\": \"cabac -- kontextadaptive binäre arithmetische Kodierung - höhere Qualität\",\n    \"coder_cavlc\": \"cavlc -- kontextadaptive Kodierung variabler Länge - schnellere Dekodierung\",\n    \"configuration\": \"Konfiguration\",\n    \"controller\": \"Enable Gamepad Input\",\n    \"controller_desc\": \"Erlaubt Gästen das Host-System mit einem Gamepad/Controller zu steuern\",\n    \"credentials_file\": \"Anmeldedaten Datei\",\n    \"credentials_file_desc\": \"Speichere Benutzername/Passwort getrennt von Sunshine's Status-Datei.\",\n    \"display_device_options_note_desc_windows\": \"Windows speichert verschiedene Anzeigeeinstellungen für jede Kombination von derzeit aktiven Anzeigen.\\nSunshine wendet dann Änderungen auf eine oder mehrere Anzeigen an, die zu einer solchen Anzeigekombination gehören.\\nWenn Sie ein Gerät trennen, das aktiv war, als Sunshine die Einstellungen anwendete, können die Änderungen nicht\\nrückgängig gemacht werden, es sei denn, die Kombination kann wieder aktiviert werden, wenn Sunshine versucht, Änderungen rückgängig zu machen!\",\n    \"display_device_options_note_windows\": \"Hinweis zur Anwendung der Einstellungen\",\n    \"display_device_options_windows\": \"Anzeigegeräteoptionen\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Aktiviert die Anzeige, wenn sie nicht bereits aktiv ist\",\n    \"display_device_prep_ensure_active_windows\": \"Anzeige automatisch aktivieren\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Deaktiviert alle anderen Anzeigen und aktiviert nur die angegebene Anzeige\",\n    \"display_device_prep_ensure_only_display_windows\": \"Andere Anzeigen deaktivieren und nur die angegebene Anzeige aktivieren\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Aktiviert die Anzeige und legt sie als Hauptanzeige fest\",\n    \"display_device_prep_ensure_primary_windows\": \"Anzeige automatisch aktivieren und als Hauptanzeige festlegen\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Verwendet nur die virtuelle Anzeige für sekundäres erweitertes Streaming\",\n    \"display_device_prep_ensure_secondary_windows\": \"Sekundäres Display-Streaming (nur virtuelles Display)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Keine Änderungen am Anzeigestatus; der Benutzer muss sicherstellen, dass die Anzeige bereit ist\",\n    \"display_device_prep_no_operation_windows\": \"Deaktiviert\",\n    \"display_device_prep_windows\": \"Anzeigevorbereitung\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Mindestens ein \\\"empfangener\\\" und ein \\\"finaler\\\" Wert müssen angegeben werden.\\nLeeres Feld im \\\"empfangenen\\\" Abschnitt bedeutet \\\"beliebig übereinstimmen\\\". Leeres Feld im \\\"finalen\\\" Abschnitt bedeutet \\\"empfangenen Wert beibehalten\\\".\\nSie können einen bestimmten FPS-Wert einer bestimmten Auflösung zuordnen, wenn Sie möchten...\\n\\nHinweis: Wenn die Option \\\"Spieleinstellungen optimieren\\\" im Moonlight-Client nicht aktiviert ist, werden die Zeilen mit Auflösungswerten ignoriert.\",\n    \"display_mode_remapping_desc_windows\": \"Geben Sie an, wie eine bestimmte Auflösung und/oder Bildwiederholrate auf andere Werte neu zugeordnet werden soll.\\nSie können mit niedrigerer Auflösung streamen, während Sie auf dem Host mit höherer Auflösung rendern, um einen Supersampling-Effekt zu erzielen.\\nOder Sie können mit höherem FPS streamen, während Sie den Host auf die niedrigere Bildwiederholrate beschränken.\\nDie Übereinstimmung erfolgt von oben nach unten. Sobald ein Eintrag übereinstimmt, werden andere nicht mehr überprüft, aber dennoch validiert.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Finale Bildwiederholrate\",\n    \"display_mode_remapping_final_resolution_windows\": \"Finale Auflösung\",\n    \"display_mode_remapping_optional\": \"optional\",\n    \"display_mode_remapping_received_fps_windows\": \"Empfangene FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"Empfangene Auflösung\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Hinweis: Wenn die Option \\\"Spieleinstellungen optimieren\\\" im Moonlight-Client nicht aktiviert ist, ist die Neuordnung deaktiviert.\",\n    \"display_mode_remapping_windows\": \"Anzeigemodi neu zuordnen\",\n    \"display_modes\": \"Anzeigemodi\",\n    \"ds4_back_as_touchpad_click\": \"Zum Touchpad-Klick zurück/auswählen\",\n    \"ds4_back_as_touchpad_click_desc\": \"Beim Erzwingen der DS4-Emulation zum Touchpad-Klick zurück/auswählen\",\n    \"dsu_server_port\": \"DSU-Server-Port\",\n    \"dsu_server_port_desc\": \"DSU-Server-Überwachungsport (Standard 26760). Sunshine fungiert als DSU-Server, um Client-Verbindungen zu empfangen und Bewegungsdaten zu senden. Aktivieren Sie den DSU-Server in Ihrem Client (Yuzu, Ryujinx usw.) und setzen Sie die DSU-Server-Adresse (127.0.0.1) und den Port (26760)\",\n    \"enable_dsu_server\": \"DSU-Server aktivieren\",\n    \"enable_dsu_server_desc\": \"DSU-Server aktivieren, um Client-Verbindungen zu empfangen und Bewegungsdaten zu senden\",\n    \"encoder\": \"Erzwinge einen bestimmten Encoder\",\n    \"encoder_desc\": \"Erzwinge einen bestimmten Encoder, sonst wählt Sunshine die beste verfügbare Option. Notiz: Wenn Sie einen Hardwarekodierer unter Windows angeben, muss er mit der GPU übereinstimmen, in der das Display verbunden ist.\",\n    \"encoder_software\": \"Software\",\n    \"experimental\": \"Experimentell\",\n    \"experimental_features\": \"Experimentelle Funktionen\",\n    \"external_ip\": \"Externe IP\",\n    \"external_ip_desc\": \"Wenn keine externe IP-Adresse angegeben ist, erkennt Sunshine automatisch externe IP\",\n    \"fec_percentage\": \"Prozentsatz FEC\",\n    \"fec_percentage_desc\": \"Prozentsatz der Fehlerkorrektur von Paketen pro Datenpaket in jedem Videobild. Höhere Werte können für mehr Netzwerk-Paketverlust korrigieren, aber auf Kosten einer erhöhten Bandbreitennutzung.\",\n    \"ffmpeg_auto\": \"auto -- ffmpeg entscheiden lassen (Standard)\",\n    \"file_apps\": \"App-Datei\",\n    \"file_apps_desc\": \"Die Datei, in der die aktuellen Apps von Sunshine gespeichert werden.\",\n    \"file_state\": \"Zustandsdatei\",\n    \"file_state_desc\": \"Die Datei, in der der aktuelle Zustand von Sunshine gespeichert ist\",\n    \"fps\": \"Angekündigte FPS\",\n    \"gamepad\": \"Emulierter Gamepad-Typ\",\n    \"gamepad_auto\": \"Automatische Auswahloptionen\",\n    \"gamepad_desc\": \"Wähle welche Art von Gamepad emuliert werden soll\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"Manuelle DS4-Optionen\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Manuelle DS4 Optionen\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Befehlsvorbereitungen\",\n    \"global_prep_cmd_desc\": \"Konfigurieren Sie eine Liste von Befehlen, die vor oder nach Ausführung einer Anwendung ausgeführt werden sollen. Wenn eines der angegebenen Vorbereitungsbefehle fehlschlägt, wird der Anwendungsstart abgebrochen.\",\n    \"hdr_luminance_analysis\": \"HDR Dynamische Metadaten (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Aktiviert pro-Frame GPU-Luminanzanalyse und injiziert HDR10+ (ST 2094-40) und HDR Vivid (CUVA) dynamische Metadaten in den kodierten Bitstrom. Bietet pro-Frame Tone-Mapping-Hinweise für unterstützte Displays. Fügt geringen GPU-Overhead hinzu (~0,5-1,5ms/Frame bei hohen Auflösungen). Deaktivieren Sie dies bei Framerate-Einbrüchen mit HDR.\",\n    \"hdr_prep_automatic_windows\": \"HDR-Modus nach Client-Anfrage ein-/ausschalten\",\n    \"hdr_prep_no_operation_windows\": \"Deaktiviert\",\n    \"hdr_prep_windows\": \"HDR-Zustandsänderung\",\n    \"hevc_mode\": \"HEVC Unterstützung\",\n    \"hevc_mode_0\": \"Sunshine werbt Unterstützung für HEVC basierend auf Encoderfähigkeiten (empfohlen)\",\n    \"hevc_mode_1\": \"Sunshine werbt keine Unterstützung für HEVC\",\n    \"hevc_mode_2\": \"Sunshine werbt Unterstützung für das HEVC Hauptprofil\",\n    \"hevc_mode_3\": \"Sunshine werbt Unterstützung für HEVC Haupt- und Main10-Profile (HDR)\",\n    \"hevc_mode_desc\": \"Ermöglicht dem Client, HEVC Main oder HEVC Main10 Videostreams anzufordern. HEVC ist CPU-intensiver zum Kodieren, daher kann dies die Leistung bei der Verwendung von Software-Kodierungen verringern.\",\n    \"high_resolution_scrolling\": \"Unterstützung für hochauflösende Scrolling\",\n    \"high_resolution_scrolling_desc\": \"Wenn aktiviert, durchläuft Sunshine hochauflösende Scroll-Ereignisse von Moonlight-Clients. Dies kann nützlich sein, um ältere Anwendungen zu deaktivieren, die bei hochauflösenden Scroll-Ereignissen zu schnell scrollen.\",\n    \"install_steam_audio_drivers\": \"Steam Audio Treiber installieren\",\n    \"install_steam_audio_drivers_desc\": \"Wenn Steam installiert ist, wird der Steam Streaming Speakers Treiber automatisch installiert, um 5.1/7.1 Surround-Sound zu unterstützen und Host-Audio zu mutieren.\",\n    \"key_repeat_delay\": \"Schlüssel-Wiederholung Verzögerung\",\n    \"key_repeat_delay_desc\": \"Legen Sie fest, wie schnell sich die Tasten wiederholen. Die anfängliche Verzögerung in Millisekunden bevor Sie die Tasten wiederholen.\",\n    \"key_repeat_frequency\": \"Tastendruck-Frequenz\",\n    \"key_repeat_frequency_desc\": \"Wie oft Tasten jede Sekunde wiederholen. Diese konfigurierbare Option unterstützt Dezimalstellen.\",\n    \"key_rightalt_to_key_win\": \"Rechte Alt-Taste auf Windows-Taste zuweisen\",\n    \"key_rightalt_to_key_win_desc\": \"Möglicherweise können Sie den Windows-Schlüssel nicht direkt von Moonlight senden. In diesen Fällen kann es nützlich sein, Sunshine glauben zu lassen, dass die rechte Alt-Taste die Windows-Taste ist\",\n    \"key_rightalt_to_key_windows\": \"Rechter Alt-Taste auf Windows-Taste zuweisen\",\n    \"keyboard\": \"Tastatureingabe aktivieren\",\n    \"keyboard_desc\": \"Erlaubt Gästen das Host-System mit der Tastatur zu steuern\",\n    \"lan_encryption_mode\": \"LAN-Verschlüsselungsmodus\",\n    \"lan_encryption_mode_1\": \"Für unterstützte Clients aktiviert\",\n    \"lan_encryption_mode_2\": \"Benötigt für alle Kunden\",\n    \"lan_encryption_mode_desc\": \"Dies legt fest, wann die Verschlüsselung beim Streaming über Ihr lokales Netzwerk verwendet wird. Verschlüsselung kann die Streaming-Leistung senken, insbesondere auf weniger leistungsfähigen Hosts und Clients.\",\n    \"locale\": \"Lokal\",\n    \"locale_desc\": \"Die Locale, die für die Benutzeroberfläche von Sunshine verwendet wird.\",\n    \"log_level\": \"Log-Level\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"Info\",\n    \"log_level_3\": \"Warnung\",\n    \"log_level_4\": \"Fehler\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"Keine\",\n    \"log_level_desc\": \"Der minimale Log-Level wird auf Standard gedruckt\",\n    \"log_path\": \"Logdateipfad\",\n    \"log_path_desc\": \"Die Datei, in der die aktuellen Logs von Sunshine gespeichert werden.\",\n    \"max_bitrate\": \"Maximale Bitrate\",\n    \"max_bitrate_desc\": \"Die maximale Bitrate (in Kbps), bei der Sunshine den Stream kodiert. Wenn sie auf 0 gesetzt ist, wird sie immer die Bitrate verwenden, die von Mononlight angefordert wird.\",\n    \"max_fps_reached\": \"Maximale FPS-Werte erreicht\",\n    \"max_resolutions_reached\": \"Maximale Anzahl von Auflösungen erreicht\",\n    \"mdns_broadcast\": \"Diesen Computer in der lokalen Netzwerk finden\",\n    \"mdns_broadcast_desc\": \"Wenn diese Option aktiviert ist, wird Sunshine anderen Geräten erlauben, diesen Computer automatisch zu finden. Moonlight muss ebenfalls so konfiguriert sein, dass er diesen Computer automatisch in der lokalen Netzwerk findet.\",\n    \"min_threads\": \"Minimale CPU-Thread-Anzahl\",\n    \"min_threads_desc\": \"Die Erhöhung des Wertes verringert die Encoding-Effizienz, aber der Abgleich lohnt sich in der Regel, mehr CPU-Kerne für die Kodierung zu verwenden. Der ideale Wert ist der niedrigste Wert, der zuverlässig an den gewünschten Streaming-Einstellungen auf Ihrer Hardware kodieren kann.\",\n    \"minimum_fps_target\": \"Minimales FPS-Ziel\",\n    \"minimum_fps_target_desc\": \"Minimales FPS, das beim Kodieren aufrechterhalten werden soll (0 = automatisch, etwa die Hälfte der Stream-FPS; 1-1000 = minimales FPS, das aufrechterhalten werden soll). Wenn die variable Bildwiederholrate aktiviert ist, wird diese Einstellung ignoriert, wenn sie auf 0 gesetzt ist.\",\n    \"misc\": \"Verschiedene Optionen\",\n    \"motion_as_ds4\": \"Ein DS4 Gamepad emulieren, wenn der Client Gamepad Bewegungsmelder meldet\",\n    \"motion_as_ds4_desc\": \"Wenn deaktiviert, werden Bewegungssensor bei der Auswahl des Gamepad-Typs nicht berücksichtigt.\",\n    \"mouse\": \"Maus-Eingabe aktivieren\",\n    \"mouse_desc\": \"Erlaubt Gästen das Host-System mit der Maus zu steuern\",\n    \"native_pen_touch\": \"Native Pen/Touch Unterstützung\",\n    \"native_pen_touch_desc\": \"Wenn aktiviert, durchläuft Sunshine natives Pen / Berühren von Moonlight-Clients. Dies kann nützlich sein, um ältere Anwendungen ohne nativen Stift-/Berührungs-Support zu deaktivieren.\",\n    \"no_fps\": \"Keine FPS-Werte hinzugefügt\",\n    \"no_resolutions\": \"Keine Auflösungen hinzugefügt\",\n    \"notify_pre_releases\": \"Pre-Release-Benachrichtigungen\",\n    \"notify_pre_releases_desc\": \"Ob über neue Versionen von Sunshine benachrichtigt werden soll\",\n    \"nvenc_h264_cavlc\": \"CAVLC gegenüber CABAC in H.264 bevorzugen\",\n    \"nvenc_h264_cavlc_desc\": \"Einfachere Form der Entropy-Codierung. CAVLC benötigt ca. 10% mehr Bitrate für die gleiche Qualität. Nur relevant für wirklich alte Decodierungsgeräte.\",\n    \"nvenc_latency_over_power\": \"Reduzierte Encoding-Latenz gegenüber Energieeinsparungen bevorzugen\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine fordert die maximale GPU-Taktgeschwindigkeit beim Streaming an, um die Encoding-Latenz zu reduzieren. Deaktivieren wird nicht empfohlen, da dies zu einer signifikant erhöhten Encoding-Latenz führen kann.\",\n    \"nvenc_lookahead_depth\": \"Lookahead-Tiefe\",\n    \"nvenc_lookahead_depth_desc\": \"Anzahl der Frames, die während der Kodierung vorausgeschaut werden (0-32). Lookahead verbessert die Kodierungsqualität, insbesondere in komplexen Szenen, durch eine bessere Bewegungsschätzung und Bitratenverteilung. Höhere Werte verbessern die Qualität, erhöhen jedoch die Kodierungslatenz. Auf 0 setzen, um Lookahead zu deaktivieren. Erfordert NVENC SDK 13.0 (1202) oder neuer.\",\n    \"nvenc_lookahead_level\": \"Lookahead-Level\",\n    \"nvenc_lookahead_level_0\": \"Level 0 (niedrigste Qualität, am schnellsten)\",\n    \"nvenc_lookahead_level_1\": \"Level 1\",\n    \"nvenc_lookahead_level_2\": \"Level 2\",\n    \"nvenc_lookahead_level_3\": \"Level 3 (höchste Qualität, am langsamsten)\",\n    \"nvenc_lookahead_level_autoselect\": \"Automatische Auswahl (Treiber wählt optimales Level)\",\n    \"nvenc_lookahead_level_desc\": \"Qualitätsstufe für Lookahead. Höhere Stufen verbessern die Qualität auf Kosten der Leistung. Diese Option ist nur wirksam, wenn lookahead_depth größer als 0 ist. Erfordert NVENC SDK 13.0 (1202) oder neuer.\",\n    \"nvenc_lookahead_level_disabled\": \"Deaktiviert (wie Level 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"OpenGL/Vulkan auf DXGI zeigen\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine kann OpenGL und Vulkan Programme nicht mit voller Bildwiederholrate erfassen, es sei denn, sie sind auf DXGI vorhanden. Dies ist eine systemweite Einstellung, die beim Beenden des Sonnenscheinprogramms rückgängig gemacht wird.\",\n    \"nvenc_preset\": \"Leistungsvorgabe\",\n    \"nvenc_preset_1\": \"(schnellste, Standard)\",\n    \"nvenc_preset_7\": \"(langsamste)\",\n    \"nvenc_preset_desc\": \"Höhere Zahlen verbessern die Komprimierung (Qualität bei der angegebenen Bitrate) auf Kosten einer erhöhten Kodierungslatenz. Wird empfohlen, nur zu ändern, wenn durch Netzwerk oder Decoder begrenzt, sonst kann ein ähnlicher Effekt durch Erhöhung der Bitrate erreicht werden.\",\n    \"nvenc_rate_control\": \"Ratensteuerungsmodus\",\n    \"nvenc_rate_control_cbr\": \"CBR (Konstante Bitrate) - Geringe Latenz\",\n    \"nvenc_rate_control_desc\": \"Wählen Sie den Ratensteuerungsmodus. CBR (Konstante Bitrate) bietet eine feste Bitrate für Streaming mit geringer Latenz. VBR (Variable Bitrate) ermöglicht eine Variation der Bitrate basierend auf der Szenenkomplexität und bietet bei komplexen Szenen eine bessere Qualität auf Kosten einer variablen Bitrate.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Variable Bitrate) - Bessere Qualität\",\n    \"nvenc_realtime_hags\": \"Echtzeit-Priorität in der Hardware-beschleunigten gpu Planung verwenden\",\n    \"nvenc_realtime_hags_desc\": \"Derzeit können NVIDIA-Treiber im Encoder einfrieren, wenn HAGS aktiviert ist, Echtzeit-Priorität verwendet wird und die VRAM-Auslastung fast fast erreicht ist. Die Deaktivierung dieser Option senkt die Priorität auf hoch, indem das Einfrieren auf Kosten einer reduzierten Aufnahmeleistung umgangen wird, wenn die GPU stark belastet ist.\",\n    \"nvenc_spatial_aq\": \"Räumliche AQ\",\n    \"nvenc_spatial_aq_desc\": \"Zuweisen von höheren QP-Werten zu flachen Regionen des Videos. Wird empfohlen zu aktivieren, wenn Streaming mit niedrigeren Bitraten.\",\n    \"nvenc_spatial_aq_disabled\": \"Deaktiviert (schneller, Standard)\",\n    \"nvenc_spatial_aq_enabled\": \"Aktiviert (langsamer)\",\n    \"nvenc_split_encode\": \"Geteilte Frame-Kodierung\",\n    \"nvenc_split_encode_desc\": \"Teilen Sie die Kodierung jedes Videoframes über mehrere NVENC-Hardwareeinheiten auf. Reduziert die Kodierungslatenz erheblich mit einer geringfügigen Komprimierungseffizienzstrafe. Diese Option wird ignoriert, wenn Ihre GPU eine einzelne NVENC-Einheit hat.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Treiber entscheidet (Standard)\",\n    \"nvenc_split_encode_four_strips\": \"Force 4-strip split (requires 4+ NVENC engines)\",\n    \"nvenc_split_encode_three_strips\": \"Force 3-strip split (requires 3+ NVENC engines)\",\n    \"nvenc_split_encode_two_strips\": \"Force 2-strip split (requires 2+ NVENC engines)\",\n    \"nvenc_target_quality\": \"Zielqualität (VBR-Modus)\",\n    \"nvenc_target_quality_desc\": \"Zielqualitätsstufe für den VBR-Modus (0-51 für H.264/HEVC, 0-63 für AV1). Niedrigere Werte = höhere Qualität. Auf 0 setzen für automatische Qualitätsauswahl. Nur verwendet, wenn der Ratensteuerungsmodus VBR ist.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Zwei-Pass-Modus\",\n    \"nvenc_twopass_desc\": \"Fügt vorläufige Kodierungen hinzu. Dies erlaubt es, mehr Bewegungsvektoren zu erkennen, eine bessere Verteilung der Bitrate über den Rahmen und strengere Einhaltung der Bitratengrenzen. Die Deaktivierung ist nicht empfehlenswert, da dies gelegentlich zu Bitraten-Overshoot und anschließendem Paketverlust führen kann.\",\n    \"nvenc_twopass_disabled\": \"Deaktiviert (schnellste, nicht empfohlen)\",\n    \"nvenc_twopass_full_res\": \"Vollständige Auflösung (langsamer)\",\n    \"nvenc_twopass_quarter_res\": \"Viertelauflösung (schneller, Standard)\",\n    \"nvenc_vbv_increase\": \"Prozentsatz Erhöhung des Einzelbild-VBV/HRD\",\n    \"nvenc_vbv_increase_desc\": \"Standardmäßig verwendet Sunshine Einzelbild-VBV/HRD, was bedeutet, dass jegliche kodierte Videobild-Größe nicht voraussichtlich die angeforderte Bitrate überschreiten wird, geteilt durch angeforderte Bildrate. Diese Einschränkung zu lockern, kann vorteilhaft sein und als variable Bitrate mit niedriger Latenz fungieren kann aber auch zu Paketverlusten führen, wenn das Netzwerk keinen Pufferkopf hat, um mit Bitraten-Spitzen umzugehen. Maximal zulässiger Wert ist 400, was einer 5x erhöhten Begrenzung der kodierten Videorahmen.\",\n    \"origin_web_ui_allowed\": \"Ursprungsweb-UI erlaubt\",\n    \"origin_web_ui_allowed_desc\": \"Der Ursprung der Remote-Endpunkt-Adresse, der der Zugriff auf das Web-Interface nicht verweigert wird\",\n    \"origin_web_ui_allowed_lan\": \"Nur LAN-Nutzer können auf Web-UI zugreifen\",\n    \"origin_web_ui_allowed_pc\": \"Nur localhost kann auf Web-UI zugreifen\",\n    \"origin_web_ui_allowed_wan\": \"Jeder kann auf Web-UI zugreifen\",\n    \"output_name_desc_unix\": \"Während des Starts von Sunshine sollten Sie die Liste der erkannten Anzeigen sehen. Hinweis: Sie müssen den Id-Wert innerhalb der Klammer verwenden.\",\n    \"output_name_desc_windows\": \"Legen Sie eine Anzeige für die Aufnahme manuell fest. Wenn diese nicht aktiviert ist, wird die primäre Anzeige aufgenommen. Hinweis: Wenn Sie eine GPU oben angegeben haben, muss diese Anzeige mit dieser GPU verbunden sein. Die entsprechenden Werte finden Sie mit dem folgenden Befehl:\",\n    \"output_name_unix\": \"Anzeigenummer\",\n    \"output_name_windows\": \"Ausgabename\",\n    \"ping_timeout\": \"Ping-Timeout\",\n    \"ping_timeout_desc\": \"Wie lange warten Sie in Millisekunden auf Daten von Mondlicht bevor Sie den Strom herunterfahren\",\n    \"pkey\": \"Privater Schlüssel\",\n    \"pkey_desc\": \"Der private Schlüssel, der für das Web-UI- und Moonlight-Client-Paar verwendet wird. Für bestmögliche Kompatibilität sollte dies ein privater RSA-2048 Schlüssel sein.\",\n    \"port\": \"Port\",\n    \"port_alert_1\": \"Sunshine kann keine Ports unter 1024 benutzen!\",\n    \"port_alert_2\": \"Ports über 65535 sind nicht verfügbar!\",\n    \"port_desc\": \"Legen Sie die Familie der von Sunshine verwendeten Ports fest\",\n    \"port_http_port_note\": \"Benutzen Sie diesen Port, um sich mit Moonlight zu verbinden.\",\n    \"port_note\": \"Notiz\",\n    \"port_port\": \"Port\",\n    \"port_protocol\": \"Protocol\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Das Web-Interface dem Internet zu übergeben, ist ein Sicherheitsrisiko! Fahren Sie auf eigene Gefahr!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Quantifizierungsparameter\",\n    \"qp_desc\": \"Einige Geräte unterstützen möglicherweise keine Constant Bit-Rate. Für diese Geräte wird stattdessen QP verwendet. Höhere Werte bedeuten mehr Kompression, aber weniger Qualität.\",\n    \"qsv_coder\": \"QuickSync Coder (H264)\",\n    \"qsv_preset\": \"QuickSync-Voreinstellung\",\n    \"qsv_preset_fast\": \"schneller (niedrigere Qualität)\",\n    \"qsv_preset_faster\": \"schnellste (niedrigste Qualität)\",\n    \"qsv_preset_medium\": \"medium (Standard)\",\n    \"qsv_preset_slow\": \"langsam (gute Qualität)\",\n    \"qsv_preset_slower\": \"langsamer (bessere Qualität)\",\n    \"qsv_preset_slowest\": \"langsamste (beste Qualität)\",\n    \"qsv_preset_veryfast\": \"schnellste (niedrigste Qualität)\",\n    \"qsv_slow_hevc\": \"Langsame HEVC Encodierung erlauben\",\n    \"qsv_slow_hevc_desc\": \"Dies kann HEVC-Kodierung auf älteren Intel GPUs ermöglichen, auf Kosten einer höheren GPU-Nutzung und schlechteren Performance.\",\n    \"refresh_rate_change_automatic_windows\": \"FPS-Wert vom Client verwenden\",\n    \"refresh_rate_change_manual_desc_windows\": \"Geben Sie die zu verwendende Bildwiederholrate ein\",\n    \"refresh_rate_change_manual_windows\": \"Manuell eingegebene Bildwiederholrate verwenden\",\n    \"refresh_rate_change_no_operation_windows\": \"Deaktiviert\",\n    \"refresh_rate_change_windows\": \"FPS-Änderung\",\n    \"res_fps_desc\": \"Die von Sunshine angekündigten Anzeigemodi. Einige Versionen von Moonlight, wie Moonlight-nx (Switch), verlassen sich auf diese Listen, um sicherzustellen, dass die angeforderten Auflösungen und FPS unterstützt werden. Diese Einstellung ändert nicht, wie der Bildschirmstream an Moonlight gesendet wird.\",\n    \"resolution_change_automatic_windows\": \"Vom Client bereitgestellte Auflösung verwenden\",\n    \"resolution_change_manual_desc_windows\": \"Die Option \\\"Spieleinstellungen optimieren\\\" muss im Moonlight-Client aktiviert sein, damit dies funktioniert.\",\n    \"resolution_change_manual_windows\": \"Manuell eingegebene Auflösung verwenden\",\n    \"resolution_change_no_operation_windows\": \"Deaktiviert\",\n    \"resolution_change_ogs_desc_windows\": \"Die Option \\\"Spieleinstellungen optimieren\\\" muss im Moonlight-Client aktiviert sein, damit dies funktioniert.\",\n    \"resolution_change_windows\": \"Auflösungsänderung\",\n    \"resolutions\": \"Angekündigte Auflösungen\",\n    \"restart_note\": \"Sunshine wird neu gestartet, um Änderungen anzuwenden.\",\n    \"sleep_mode\": \"Schlafmodus\",\n    \"sleep_mode_away\": \"Abwesenheitsmodus (Display aus, sofortiges Aufwachen)\",\n    \"sleep_mode_desc\": \"Steuert, was passiert, wenn der Client einen Schlafbefehl sendet. Standby (S3): traditioneller Schlaf, niedriger Stromverbrauch, erfordert aber WOL zum Aufwachen. Ruhezustand (S4): speichert auf Festplatte, sehr niedriger Stromverbrauch. Abwesenheitsmodus: Display wird ausgeschaltet, System läuft weiter für sofortiges Aufwachen - ideal für Game-Streaming-Server.\",\n    \"sleep_mode_hibernate\": \"Ruhezustand (S4)\",\n    \"sleep_mode_suspend\": \"Standby (S3 Schlaf)\",\n    \"stream_audio\": \"Audio-Streaming aktivieren\",\n    \"stream_audio_desc\": \"Deaktivieren Sie diese Option, um das Audio-Streaming zu stoppen.\",\n    \"stream_mic\": \"Mikrofon-Streaming aktivieren\",\n    \"stream_mic_desc\": \"Deaktivieren Sie diese Option, um das Mikrofon-Streaming zu stoppen.\",\n    \"stream_mic_download_btn\": \"Virtuelles Mikrofon herunterladen\",\n    \"stream_mic_download_confirm\": \"Sie werden zur Download-Seite des virtuellen Mikrofons weitergeleitet. Fortfahren?\",\n    \"stream_mic_note\": \"Diese Funktion erfordert die Installation eines virtuellen Mikrofons\",\n    \"sunshine_name\": \"Sunshine Name\",\n    \"sunshine_name_desc\": \"Der von Mononlight angezeigte Name, falls nicht angegeben, wird der Hostname des PCs verwendet\",\n    \"sw_preset\": \"SW-Voreinstellungen\",\n    \"sw_preset_desc\": \"Optimieren Sie den Abgleich zwischen der Kodierungsgeschwindigkeit (kodierte Frames pro Sekunde) und der Komprimierungseffizienz (Qualität pro Bit im Bitstream). Standard ist überflüssig.\",\n    \"sw_preset_fast\": \"schnell\",\n    \"sw_preset_faster\": \"schneller\",\n    \"sw_preset_medium\": \"mittel\",\n    \"sw_preset_slow\": \"langsam\",\n    \"sw_preset_slower\": \"langsamer\",\n    \"sw_preset_superfast\": \"superschnell (Standard)\",\n    \"sw_preset_ultrafast\": \"extrem schnell\",\n    \"sw_preset_veryfast\": \"veryfast\",\n    \"sw_preset_veryslow\": \"veryslow\",\n    \"sw_tune\": \"SW Tune\",\n    \"sw_tune_animation\": \"animation -- gut für Cartoons; verwendet höhere Deblocking und mehr Referenzrahmen\",\n    \"sw_tune_desc\": \"Einstellmöglichkeiten, die nach der Voreinstellung angewendet werden. Standard ist Null.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- ermöglicht eine schnellere Dekodierung durch Deaktivieren bestimmter Filter\",\n    \"sw_tune_film\": \"film -- verwenden für qualitativ hochwertige Filminhalte; senkt Deblocking\",\n    \"sw_tune_grain\": \"korn -- bewahrt die Kornstruktur im alten, körnigen Filmmaterial\",\n    \"sw_tune_stillimage\": \"stillimage -- gut für slideshow-ähnliche Inhalte\",\n    \"sw_tune_zerolatency\": \"Zerolatency -- gut für schnelle Kodierung und Low-Latency Streaming (Standard)\",\n    \"system_tray\": \"Systemleiste aktivieren\",\n    \"system_tray_desc\": \"Ob die Systemleiste aktiviert werden soll. Wenn aktiviert, zeigt Sunshine ein Symbol in der Systemleiste an und kann von der Systemleiste aus gesteuert werden.\",\n    \"touchpad_as_ds4\": \"Ein DS4 Gamepad emulieren, wenn der Client Gamepad meldet, dass ein Touchpad vorhanden ist\",\n    \"touchpad_as_ds4_desc\": \"Wenn deaktiviert, wird das Touchpad-Vorhandensein bei der Auswahl des Gamepad-Typs nicht berücksichtigt.\",\n    \"unsaved_changes_tooltip\": \"Sie haben ungespeicherte Änderungen. Klicken Sie zum Speichern.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Portweiterleitung für Streaming über das Internet automatisch konfigurieren\",\n    \"variable_refresh_rate\": \"Variable Bildwiederholrate (VRR)\",\n    \"variable_refresh_rate_desc\": \"Erlauben Sie, dass die Bildwiederholrate des Videostreams mit der Render-Bildwiederholrate für VRR-Unterstützung übereinstimmt. Wenn aktiviert, erfolgt die Kodierung nur, wenn neue Frames verfügbar sind, sodass der Stream der tatsächlichen Render-Bildwiederholrate folgen kann.\",\n    \"vdd_reuse_desc_windows\": \"Wenn aktiviert, teilen sich alle Clients dasselbe VDD (Virtual Display Device). Wenn deaktiviert (Standard), erhält jeder Client sein eigenes VDD. Aktivieren Sie dies für schnelleres Client-Wechseln, aber beachten Sie, dass alle Clients dieselben Anzeigeeinstellungen teilen.\",\n    \"vdd_reuse_windows\": \"Dasselbe VDD für alle Clients verwenden\",\n    \"virtual_display\": \"Virtuelle Anzeige\",\n    \"virtual_mouse\": \"Virtueller Maustreiber\",\n    \"virtual_mouse_desc\": \"Wenn aktiviert, verwendet Sunshine den Zako Virtual Mouse-Treiber (falls installiert) zur Simulation der Mauseingabe auf HID-Ebene. Dies ermöglicht Spielen mit Raw Input den Empfang von Mausereignissen. Wenn deaktiviert oder Treiber nicht installiert, wird auf SendInput zurückgegriffen.\",\n    \"virtual_sink\": \"Virtueller Sink\",\n    \"virtual_sink_desc\": \"Legen Sie ein virtuelles Audiogerät manuell fest. Wenn nicht gesetzt, wird das Gerät automatisch ausgewählt. Wir empfehlen dringend, dieses Feld leer zu lassen, um die automatische Geräteauswahl zu verwenden!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Virtuellen Maustreiber installieren?\",\n    \"vmouse_confirm_uninstall\": \"Virtuellen Maustreiber deinstallieren?\",\n    \"vmouse_install\": \"Treiber installieren\",\n    \"vmouse_installing\": \"Wird installiert...\",\n    \"vmouse_note\": \"Der virtuelle Maustreiber erfordert eine separate Installation. Bitte verwenden Sie das Sunshine-Kontrollpanel zur Installation oder Verwaltung des Treibers.\",\n    \"vmouse_refresh\": \"Status aktualisieren\",\n    \"vmouse_status_installed\": \"Installiert (nicht aktiv)\",\n    \"vmouse_status_not_installed\": \"Nicht installiert\",\n    \"vmouse_status_running\": \"Wird ausgeführt\",\n    \"vmouse_uninstall\": \"Treiber deinstallieren\",\n    \"vmouse_uninstalling\": \"Wird deinstalliert...\",\n    \"vt_coder\": \"VideoToolbox-Coder\",\n    \"vt_realtime\": \"VideoToolbox-Echtzeit-Kodierung\",\n    \"vt_software\": \"VideoToolbox-Software-Kodierung\",\n    \"vt_software_allowed\": \"Zulässig\",\n    \"vt_software_forced\": \"Erzwungen\",\n    \"wan_encryption_mode\": \"WAN-Verschlüsselungsmodus\",\n    \"wan_encryption_mode_1\": \"Aktiviert für unterstützte Clients (Standard)\",\n    \"wan_encryption_mode_2\": \"Benötigt für alle Kunden\",\n    \"wan_encryption_mode_desc\": \"Dies legt fest, wann Verschlüsselung beim Streaming über das Internet verwendet wird. Verschlüsselung kann die Streaming-Leistung senken, insbesondere auf weniger leistungsfähigen Hosts und Clients.\",\n    \"webhook_curl_command\": \"Befehl\",\n    \"webhook_curl_command_desc\": \"Kopieren Sie den folgenden Befehl in Ihr Terminal, um zu testen, ob der Webhook ordnungsgemäß funktioniert:\",\n    \"webhook_curl_copy_failed\": \"Kopieren fehlgeschlagen, bitte manuell auswählen und kopieren\",\n    \"webhook_enabled\": \"Webhook-Benachrichtigungen\",\n    \"webhook_enabled_desc\": \"Wenn aktiviert, sendet Sunshine Ereignisbenachrichtigungen an die angegebene Webhook-URL\",\n    \"webhook_group\": \"Webhook-Benachrichtigungseinstellungen\",\n    \"webhook_skip_ssl_verify\": \"SSL-Zertifikatsverifizierung überspringen\",\n    \"webhook_skip_ssl_verify_desc\": \"SSL-Zertifikatsverifizierung für HTTPS-Verbindungen überspringen, nur für Tests oder selbstsignierte Zertifikate\",\n    \"webhook_test\": \"Testen\",\n    \"webhook_test_failed\": \"Webhook-Test fehlgeschlagen\",\n    \"webhook_test_failed_note\": \"Hinweis: Bitte überprüfen Sie, ob die URL korrekt ist, oder überprüfen Sie die Browser-Konsole für weitere Informationen.\",\n    \"webhook_test_success\": \"Webhook-Test erfolgreich!\",\n    \"webhook_test_success_cors_note\": \"Hinweis: Aufgrund von CORS-Beschränkungen kann der Server-Antwortstatus nicht bestätigt werden.\\nDie Anfrage wurde gesendet. Wenn der Webhook korrekt konfiguriert ist, sollte die Nachricht zugestellt worden sein.\\n\\nVorschlag: Überprüfen Sie die Registerkarte Netzwerk in den Entwicklertools Ihres Browsers für Anfragedetails.\",\n    \"webhook_test_url_required\": \"Bitte geben Sie zuerst eine Webhook-URL ein\",\n    \"webhook_timeout\": \"Anfrage-Timeout\",\n    \"webhook_timeout_desc\": \"Timeout für Webhook-Anfragen in Millisekunden, Bereich 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"Die URL zum Empfangen von Ereignisbenachrichtigungen, unterstützt HTTP/HTTPS-Protokolle\",\n    \"wgc_checking_mode\": \"Überprüfe Modus...\",\n    \"wgc_checking_running_mode\": \"Überprüfe Ausführungsmodus...\",\n    \"wgc_control_panel_only\": \"Diese Funktion ist nur im Sunshine Control Panel verfügbar\",\n    \"wgc_mode_switch_failed\": \"Moduswechsel fehlgeschlagen\",\n    \"wgc_mode_switch_started\": \"Moduswechsel initiiert. Wenn ein UAC-Fenster erscheint, klicken Sie bitte auf 'Ja', um zu bestätigen.\",\n    \"wgc_service_mode_warning\": \"WGC-Aufnahme erfordert die Ausführung im Benutzermodus. Wenn Sie derzeit im Dienstmodus ausgeführt werden, klicken Sie bitte auf die Schaltfläche oben, um in den Benutzermodus zu wechseln.\",\n    \"wgc_switch_to_service_mode\": \"In den Service-Modus wechseln\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Derzeit im Benutzermodus ausgeführt. Klicken Sie hier, um in den Dienstmodus zu wechseln.\",\n    \"wgc_switch_to_user_mode\": \"In den Benutzermodus wechseln\",\n    \"wgc_switch_to_user_mode_tooltip\": \"WGC-Aufnahme erfordert die Ausführung im Benutzermodus. Klicken Sie auf diese Schaltfläche, um in den Benutzermodus zu wechseln.\",\n    \"wgc_user_mode_available\": \"Derzeit im Benutzermodus ausgeführt. WGC-Aufnahme ist verfügbar.\",\n    \"window_title\": \"Fenstertitel\",\n    \"window_title_desc\": \"Der Titel des aufzunehmenden Fensters (teilweise Übereinstimmung, Groß-/Kleinschreibung wird nicht beachtet). Wenn leer, wird der Name der aktuell laufenden Anwendung automatisch verwendet.\",\n    \"window_title_placeholder\": \"z.B. Anwendungsname\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine ist ein selbst gehosteter Game-Stream-Host für Moonlight.\",\n    \"download\": \"Download\",\n    \"installed_version_not_stable\": \"Sie verwenden eine Vor-Release-Version von Sunshine. Sie können Fehler oder andere Probleme haben. Bitte melde alle Probleme, auf die du triffst. Danke, dass du dabei geholfen hast, Sunshine zu einer besseren Software zu machen!\",\n    \"loading_latest\": \"Lade neueste Version...\",\n    \"new_pre_release\": \"Eine neue Pre-Release Version ist verfügbar!\",\n    \"new_stable\": \"Eine neue Stable Version ist verfügbar!\",\n    \"startup_errors\": \"<b>Achtung!</b> Sunshine erkannte diese Fehler während des Starts. Wir <b>STRONGLY EMPFOHLEN</b> beheben sie vor dem Streaming.\",\n    \"update_download_confirm\": \"Die Update-Downloadseite wird in Ihrem Browser geöffnet. Fortfahren?\",\n    \"version_dirty\": \"Vielen Dank, dass Sie dazu beigetragen haben, Sunshine zu einer besseren Software zu machen!\",\n    \"version_latest\": \"Du verwendest die neueste Version von Sunshine\",\n    \"view_logs\": \"Protokolle anzeigen\",\n    \"welcome\": \"Hallo, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Anwendungen\",\n    \"configuration\": \"Konfiguration\",\n    \"home\": \"Zuhause\",\n    \"password\": \"Passwort ändern\",\n    \"pin\": \"Pin\",\n    \"theme_auto\": \"Auto\",\n    \"theme_dark\": \"Dunkel\",\n    \"theme_light\": \"Hell\",\n    \"toggle_theme\": \"Thema\",\n    \"troubleshoot\": \"Fehlerbehebung\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Passwort wiederholen\",\n    \"current_creds\": \"Aktuelle Zugangsdaten\",\n    \"new_creds\": \"Neue Zugangsdaten\",\n    \"new_username_desc\": \"Wenn nicht angegeben, wird der Benutzername nicht geändert\",\n    \"password_change\": \"Passwortänderung\",\n    \"success_msg\": \"Passwort wurde erfolgreich geändert! Diese Seite wird bald neu geladen, Ihr Browser wird Sie nach den neuen Zugangsdaten fragen.\"\n  },\n  \"pin\": {\n    \"actions\": \"Aktionen\",\n    \"cancel_editing\": \"Bearbeitung abbrechen\",\n    \"client_name\": \"Name\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Löschen bestätigen\",\n    \"delete_client\": \"Client löschen\",\n    \"delete_confirm_message\": \"Sind Sie sicher, dass Sie <strong>{name}</strong> löschen möchten?\",\n    \"delete_warning\": \"Diese Aktion kann nicht rückgängig gemacht werden.\",\n    \"device_name\": \"Gerätename\",\n    \"device_size\": \"Gerätegröße\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Groß - TV\",\n    \"device_size_medium\": \"Mittel - Tablet\",\n    \"device_size_small\": \"Klein - Telefon\",\n    \"edit_client_settings\": \"Client-Einstellungen bearbeiten\",\n    \"hdr_profile\": \"HDR-Profil\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Laden...\",\n    \"loading_clients\": \"Clients werden geladen...\",\n    \"modify_in_gui\": \"Bitte in der GUI ändern\",\n    \"none\": \"-- Keine --\",\n    \"or_manual_pin\": \"oder PIN manuell eingeben\",\n    \"pair_failure\": \"Paarung fehlgeschlagen: Prüfen Sie, ob die PIN korrekt eingegeben wurde\",\n    \"pair_success\": \"Erfolgreich! Bitte überprüfe Mondlicht um fortzufahren\",\n    \"pin_pairing\": \"PIN Pairing\",\n    \"qr_expires_in\": \"Läuft ab in\",\n    \"qr_generate\": \"QR-Code generieren\",\n    \"qr_paired_success\": \"Erfolgreich gekoppelt!\",\n    \"qr_pairing\": \"QR-Code-Kopplung\",\n    \"qr_pairing_desc\": \"Generieren Sie einen QR-Code zur schnellen Kopplung. Scannen Sie ihn mit dem Moonlight-Client zur automatischen Kopplung.\",\n    \"qr_pairing_warning\": \"Experimentelle Funktion. Falls die Kopplung fehlschlägt, verwenden Sie bitte die manuelle PIN-Kopplung unten. Hinweis: Diese Funktion funktioniert nur im LAN.\",\n    \"qr_refresh\": \"QR-Code aktualisieren\",\n    \"remove_paired_devices_desc\": \"Entfernen Sie Ihre gepaarten Geräte.\",\n    \"save_changes\": \"Änderungen speichern\",\n    \"save_failed\": \"Fehler beim Speichern der Client-Einstellungen. Bitte versuchen Sie es erneut.\",\n    \"save_or_cancel_first\": \"Bitte speichern oder die Bearbeitung zuerst abbrechen\",\n    \"send\": \"Senden\",\n    \"unknown_client\": \"Unbekannter Client\",\n    \"unpair_all_confirm\": \"Sind Sie sicher, dass Sie alle Clients trennen möchten? Diese Aktion kann nicht rückgängig gemacht werden.\",\n    \"unsaved_changes\": \"Nicht gespeicherte Änderungen\",\n    \"warning_msg\": \"Stellen Sie sicher, dass Sie Zugriff auf den Client haben, mit dem Sie sich verbinden. Diese Software kann Ihrem Computer die totale Kontrolle geben, also seien Sie vorsichtig!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android empfohlen\",\n    \"client_downloads\": \"Client-Downloads\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"GitHub Discussions\",\n    \"gpl_license_text_1\": \"Diese Software ist unter GPL-3.0 lizenziert. Sie können sie frei verwenden, modifizieren und verteilen.\",\n    \"gpl_license_text_2\": \"Um das Open-Source-Ökosystem zu schützen, vermeiden Sie bitte die Verwendung von Software, die die GPL-3.0-Lizenz verletzt.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Community beitreten\",\n    \"join_group_desc\": \"Hilfe erhalten und Erfahrungen austauschen\",\n    \"legal\": \"Rechtlich\",\n    \"legal_desc\": \"Durch die Weiterverwendung dieser Software erklären Sie sich mit den Nutzungsbedingungen in den folgenden Dokumenten einverstanden.\",\n    \"license\": \"Lizenz\",\n    \"lizardbyte_website\": \"LizardByte Webseite\",\n    \"official_website\": \"Offizielle Website\",\n    \"official_website_title\": \"AlkaidLab - Offizielle Website\",\n    \"open_source\": \"Open Source\",\n    \"open_source_desc\": \"Star & Fork um das Projekt zu unterstützen\",\n    \"quick_start\": \"Schnellstart\",\n    \"resources\": \"Ressourcen\",\n    \"resources_desc\": \"Ressourcen für Sunshine!\",\n    \"third_party_desc\": \"Drittanbieter-Komponentenhinweise\",\n    \"third_party_moonlight\": \"Freundliche Links\",\n    \"third_party_notice\": \"Drittanbieter-Mitteilung\",\n    \"tutorial\": \"Anleitung\",\n    \"tutorial_desc\": \"Detaillierte Konfigurations- und Nutzungsanleitung\",\n    \"view_license\": \"Vollständige Lizenz anzeigen\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Konfigurationszusammenfassung\",\n    \"android_client\": \"Android-Client\",\n    \"base_display_title\": \"Virtuelles Display\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Konfiguration wurde erfolgreich gespeichert.\",\n    \"description\": \"Lassen Sie uns mit einer schnellen Einrichtung beginnen\",\n    \"device_id\": \"Geräte-ID\",\n    \"device_state\": \"Zustand\",\n    \"download_clients\": \"Clients herunterladen\",\n    \"finish\": \"Einrichtung abschließen\",\n    \"go_to_apps\": \"Anwendungen konfigurieren\",\n    \"harmony_goto_repo\": \"Zum Repository\",\n    \"harmony_modal_desc\": \"Für HarmonyOS NEXT Moonlight suchen Sie bitte Moonlight V+ im HarmonyOS App Store\",\n    \"harmony_modal_link_notice\": \"Dieser Link leitet zum Projekt-Repository weiter\",\n    \"ios_client\": \"iOS-Client\",\n    \"load_error\": \"Konfiguration konnte nicht geladen werden\",\n    \"next\": \"Weiter\",\n    \"physical_display\": \"Physische Anzeige/EDID-Emulator\",\n    \"physical_display_desc\": \"Streamen Sie Ihre tatsächlichen physischen Monitore\",\n    \"previous\": \"Zurück\",\n    \"restart_countdown_unit\": \"Sekunden\",\n    \"restart_desc\": \"Konfiguration gespeichert. Sunshine wird neu gestartet, um die Anzeigeeinstellungen anzuwenden.\",\n    \"restart_go_now\": \"Jetzt gehen\",\n    \"restart_title\": \"Sunshine wird neu gestartet\",\n    \"save_error\": \"Konfiguration konnte nicht gespeichert werden\",\n    \"select_adapter\": \"Grafikadapter\",\n    \"selected_adapter\": \"Ausgewählter Adapter\",\n    \"selected_display\": \"Ausgewählte Anzeige\",\n    \"setup_complete\": \"Einrichtung abgeschlossen!\",\n    \"setup_complete_desc\": \"Die Grundeinstellungen sind jetzt aktiv. Sie können sofort mit einem Moonlight-Client streamen!\",\n    \"skip\": \"Einrichtungsassistent überspringen\",\n    \"skip_confirm\": \"Sind Sie sicher, dass Sie den Einrichtungsassistenten überspringen möchten? Sie können diese Optionen später auf der Einstellungsseite konfigurieren.\",\n    \"skip_confirm_title\": \"Einrichtungsassistent überspringen\",\n    \"skip_error\": \"Überspringen fehlgeschlagen\",\n    \"state_active\": \"Aktiv\",\n    \"state_inactive\": \"Inaktiv\",\n    \"state_primary\": \"Primär\",\n    \"state_unknown\": \"Unbekannt\",\n    \"step0_description\": \"Wählen Sie Ihre Benutzeroberflächensprache\",\n    \"step0_title\": \"Sprache\",\n    \"step1_description\": \"Wählen Sie die zu streamende Anzeige\",\n    \"step1_title\": \"Anzeigeauswahl\",\n    \"step1_vdd_intro\": \"Das Basis-Display (VDD) ist das integrierte intelligente virtuelle Display von Sunshine Foundation. Es unterstützt beliebige Auflösungen, Bildraten und HDR-Optimierung und ist die bevorzugte Wahl für Streaming bei ausgeschaltetem Bildschirm und erweitertes Display-Streaming.\",\n    \"step2_description\": \"Wählen Sie Ihren Grafikadapter\",\n    \"step2_title\": \"Adapter auswählen\",\n    \"step3_description\": \"Wählen Sie die Strategie zur Vorbereitung des Anzeigegeräts\",\n    \"step3_ensure_active\": \"Aktivierung sicherstellen\",\n    \"step3_ensure_active_desc\": \"Aktiviert die Anzeige, wenn sie nicht bereits aktiv ist\",\n    \"step3_ensure_only_display\": \"Einzige Anzeige sicherstellen\",\n    \"step3_ensure_only_display_desc\": \"Deaktiviert alle anderen Anzeigen und aktiviert nur die angegebene Anzeige (empfohlen)\",\n    \"step3_ensure_primary\": \"Hauptanzeige sicherstellen\",\n    \"step3_ensure_primary_desc\": \"Aktiviert die Anzeige und legt sie als Hauptanzeige fest\",\n    \"step3_ensure_secondary\": \"Sekundäres Streaming\",\n    \"step3_ensure_secondary_desc\": \"Verwendet nur die virtuelle Anzeige für sekundäres erweitertes Streaming\",\n    \"step3_no_operation\": \"Keine Aktion\",\n    \"step3_no_operation_desc\": \"Keine Änderungen am Anzeigestatus; der Benutzer muss sicherstellen, dass die Anzeige bereit ist\",\n    \"step3_title\": \"Anzeigestrategie\",\n    \"step4_title\": \"Abgeschlossen\",\n    \"stream_mode\": \"Stream-Modus\",\n    \"unknown_display\": \"Unbekannte Anzeige\",\n    \"virtual_display\": \"Virtuelle Anzeige (ZakoHDR)\",\n    \"virtual_display_desc\": \"Streamen mit einem virtuellen Anzeigegerät (erfordert ZakoVDD-Treiberinstallation)\",\n    \"welcome\": \"Willkommen bei Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Erweitert\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoder\",\n    \"files\": \"Konfigurationsdateien\",\n    \"general\": \"Allgemein\",\n    \"input\": \"Eingabe\",\n    \"network\": \"Netzwerk\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software-Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analysiert...\",\n    \"ai_analyzing_logs\": \"Protokolle werden analysiert, bitte warten...\",\n    \"ai_config\": \"KI-Konfiguration\",\n    \"ai_copy_result\": \"Kopieren\",\n    \"ai_diagnosis\": \"KI-Diagnose\",\n    \"ai_diagnosis_title\": \"KI-Protokolldiagnose\",\n    \"ai_error\": \"Analyse fehlgeschlagen\",\n    \"ai_key_local\": \"API-Schlüssel wird nur lokal gespeichert und niemals hochgeladen\",\n    \"ai_model\": \"Modell\",\n    \"ai_provider\": \"Anbieter\",\n    \"ai_reanalyze\": \"Erneut analysieren\",\n    \"ai_result\": \"Diagnoseergebnis\",\n    \"ai_retry\": \"Wiederholen\",\n    \"ai_start_diagnosis\": \"Diagnose starten\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Wenn Sie Sunshine sofort herunterfahren müssen, können Sie diese Funktion verwenden. Beachten Sie, dass Sie es nach dem Herunterfahren manuell neu starten müssen.\",\n    \"boom_sunshine_success\": \"Sunshine wurde heruntergefahren\",\n    \"confirm_boom\": \"Wirklich beenden?\",\n    \"confirm_boom_desc\": \"Sie wollen also wirklich beenden? Nun, ich kann Sie nicht aufhalten, klicken Sie einfach nochmal\",\n    \"confirm_logout\": \"Abmeldung bestätigen?\",\n    \"confirm_logout_desc\": \"Sie müssen Ihr Passwort erneut eingeben, um auf die Weboberfläche zuzugreifen.\",\n    \"copy_config\": \"Konfiguration kopieren\",\n    \"copy_config_error\": \"Konfiguration konnte nicht kopiert werden\",\n    \"copy_config_success\": \"Konfiguration in die Zwischenablage kopiert!\",\n    \"copy_logs\": \"Logs kopieren\",\n    \"download_logs\": \"Logs herunterladen\",\n    \"force_close\": \"Schließen erzwingen\",\n    \"force_close_desc\": \"Wenn sich Moonlight über eine aktuell laufende App beschwert, sollte das Schließen der App das Problem beheben.\",\n    \"force_close_error\": \"Fehler beim Schließen der Anwendung\",\n    \"force_close_success\": \"Anwendung erfolgreich geschlossen!\",\n    \"ignore_case\": \"Groß-/Kleinschreibung ignorieren\",\n    \"logout\": \"Abmelden\",\n    \"logout_desc\": \"Abmelden. Möglicherweise müssen Sie sich erneut anmelden.\",\n    \"logout_localhost_tip\": \"Aktuelle Umgebung ohne Anmeldung; Abmeldung löst keine Passwortabfrage aus.\",\n    \"logs\": \"Logs\",\n    \"logs_desc\": \"Siehe die Logs hochgeladen von Sunshine\",\n    \"logs_find\": \"Suchen...\",\n    \"match_contains\": \"Enthält\",\n    \"match_exact\": \"Exakt\",\n    \"match_regex\": \"Regulärer Ausdruck\",\n    \"reopen_setup_wizard\": \"Setup-Assistent erneut öffnen\",\n    \"reopen_setup_wizard_desc\": \"Öffnen Sie die Setup-Assistenten-Seite erneut, um die anfänglichen Einstellungen neu zu konfigurieren.\",\n    \"reopen_setup_wizard_error\": \"Fehler beim erneuten Öffnen des Setup-Assistenten\",\n    \"reset_display_device_desc_windows\": \"Wenn Sunshine beim Versuch, die geänderten Anzeigegeräteeinstellungen wiederherzustellen, hängen bleibt, können Sie die Einstellungen zurücksetzen und den Anzeigezustand manuell wiederherstellen.\\nDies kann aus verschiedenen Gründen geschehen: Das Gerät ist nicht mehr verfügbar, wurde an einen anderen Port angeschlossen usw.\",\n    \"reset_display_device_error_windows\": \"Fehler beim Zurücksetzen der Persistenz!\",\n    \"reset_display_device_success_windows\": \"Persistenz erfolgreich zurückgesetzt!\",\n    \"reset_display_device_windows\": \"Anzeigespeicher zurücksetzen\",\n    \"restart_sunshine\": \"Sunshine neu starten\",\n    \"restart_sunshine_desc\": \"Wenn Sunshine nicht richtig funktioniert, können Sie versuchen, es neu zu starten. Dies wird alle laufenden Sitzungen beenden.\",\n    \"restart_sunshine_success\": \"Sunshine wird neu gestartet\",\n    \"troubleshooting\": \"Fehlerbehebung\",\n    \"unpair_all\": \"Alle trennen\",\n    \"unpair_all_error\": \"Fehler beim Entkoppeln\",\n    \"unpair_all_success\": \"Erfolgreich getrennt!\",\n    \"unpair_desc\": \"Entferne deine gekoppelten Geräte. Einzelne nicht gekoppelte Geräte mit einer aktiven Sitzung bleiben verbunden, können aber keine Sitzung starten oder fortsetzen.\",\n    \"unpair_single_no_devices\": \"Es gibt keine gekoppelten Geräte.\",\n    \"unpair_single_success\": \"Die Geräte(n) können sich jedoch immer noch in einer aktiven Sitzung befinden. Benutzen Sie die Schaltfläche \\\"Schließen erzwingen\\\", um alle geöffneten Sitzungen zu beenden.\",\n    \"unpair_single_unknown\": \"Unbekannter Client\",\n    \"unpair_title\": \"Geräte trennen\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Passwort bestätigen\",\n    \"create_creds\": \"Bevor Sie loslegen, müssen Sie einen neuen Benutzernamen und ein neues Passwort für den Zugriff auf die Web-Oberfläche erstellen.\",\n    \"create_creds_alert\": \"Die unten angegebenen Anmeldedaten werden benötigt, um auf das Webinterface von Sunshine zuzugreifen. Halten Sie sie sicher, da Sie sie nie wieder sehen werden!\",\n    \"creds_local_only\": \"Ihre Anmeldedaten werden nur lokal und offline gespeichert und niemals auf einen Server hochgeladen.\",\n    \"error\": \"Fehler!\",\n    \"greeting\": \"Willkommen bei Sunshine Foundation!\",\n    \"hide_password\": \"Passwort verbergen\",\n    \"login\": \"Anmelden\",\n    \"network_error\": \"Netzwerkfehler, bitte überprüfen Sie Ihre Verbindung\",\n    \"password\": \"Passwort\",\n    \"password_match\": \"Passwörter stimmen überein\",\n    \"password_mismatch\": \"Passwörter stimmen nicht überein\",\n    \"server_error\": \"Serverfehler\",\n    \"show_password\": \"Passwort anzeigen\",\n    \"success\": \"Erfolgreich!\",\n    \"username\": \"Benutzername\",\n    \"welcome_success\": \"Diese Seite wird bald neu geladen, Ihr Browser wird Sie nach den neuen Anmeldeinformationen fragen\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/en.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Apply\",\n    \"auto\": \"Automatic\",\n    \"autodetect\": \"Autodetect (recommended)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"Cancel\",\n    \"close\": \"Close\",\n    \"copied\": \"Copied to clipboard\",\n    \"copy\": \"Copy\",\n    \"delete\": \"Delete\",\n    \"description\": \"Description\",\n    \"disabled\": \"Disabled\",\n    \"disabled_def\": \"Disabled (default)\",\n    \"dismiss\": \"Dismiss\",\n    \"do_cmd\": \"Do Command\",\n    \"download\": \"Download\",\n    \"edit\": \"Edit\",\n    \"elevated\": \"Elevated\",\n    \"enabled\": \"Enabled\",\n    \"enabled_def\": \"Enabled (default)\",\n    \"error\": \"Error!\",\n    \"no_changes\": \"No changes\",\n    \"note\": \"Note:\",\n    \"password\": \"Password\",\n    \"remove\": \"Remove\",\n    \"run_as\": \"Run as Admin\",\n    \"save\": \"Save\",\n    \"see_more\": \"See More\",\n    \"success\": \"Success!\",\n    \"undo_cmd\": \"Undo Command\",\n    \"username\": \"Username\",\n    \"warning\": \"Warning!\"\n  },\n  \"apps\": {\n    \"actions\": \"Actions\",\n    \"add_cmds\": \"Add Commands\",\n    \"add_new\": \"Add New\",\n    \"advanced_options\": \"Advanced Options\",\n    \"app_name\": \"Application Name\",\n    \"app_name_desc\": \"Application Name, as shown on Moonlight\",\n    \"applications_desc\": \"Applications are refreshed only when Client is restarted\",\n    \"applications_title\": \"Applications\",\n    \"auto_detach\": \"Continue streaming if the application exits quickly\",\n    \"auto_detach_desc\": \"This will attempt to automatically detect launcher-type apps that close quickly after launching another program or instance of themselves. When a launcher-type app is detected, it is treated as a detached app.\",\n    \"basic_info\": \"Basic Information\",\n    \"cmd\": \"Command\",\n    \"cmd_desc\": \"The main application to start. If blank, no application will be started.\",\n    \"cmd_examples_title\": \"Common examples:\",\n    \"cmd_note\": \"If the path to the command executable contains spaces, you must enclose it in quotes.\",\n    \"cmd_prep_desc\": \"A list of commands to be run before/after this application. If any of the prep-commands fail, starting the application is aborted.\",\n    \"cmd_prep_name\": \"Command Preparations\",\n    \"command_settings\": \"Command Settings\",\n    \"covers_found\": \"Covers Found\",\n    \"delete\": \"Delete\",\n    \"delete_confirm\": \"Are you sure you want to delete \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Detached Commands\",\n    \"detached_cmds_add\": \"Add Detached Command\",\n    \"detached_cmds_desc\": \"A list of commands to be run in the background.\",\n    \"detached_cmds_note\": \"If the path to the command executable contains spaces, you must enclose it in quotes.\",\n    \"detached_cmds_remove\": \"Remove Detached Command\",\n    \"edit\": \"Edit\",\n    \"env_app_id\": \"App ID\",\n    \"env_app_name\": \"App Name\",\n    \"env_client_audio_config\": \"The Audio Configuration requested by the client (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"The client has requested the option to optimize the game for optimal streaming (true/false)\",\n    \"env_client_fps\": \"The FPS requested by the client (int)\",\n    \"env_client_gcmap\": \"The requested gamepad mask, in a bitset/bitfield format (int)\",\n    \"env_client_hdr\": \"HDR is enabled by the client (true/false)\",\n    \"env_client_height\": \"The Height requested by the client (int)\",\n    \"env_client_host_audio\": \"The client has requested host audio (true/false)\",\n    \"env_client_name\": \"Client friendly name (string)\",\n    \"env_client_width\": \"The Width requested by the client (int)\",\n    \"env_displayplacer_example\": \"Example - displayplacer for Resolution Automation:\",\n    \"env_qres_example\": \"Example - QRes for Resolution Automation:\",\n    \"env_qres_path\": \"qres path\",\n    \"env_var_name\": \"Var Name\",\n    \"env_vars_about\": \"About Environment Variables\",\n    \"env_vars_desc\": \"All commands get these environment variables by default:\",\n    \"env_xrandr_example\": \"Example - Xrandr for Resolution Automation:\",\n    \"exit_timeout\": \"Exit Timeout\",\n    \"exit_timeout_desc\": \"Number of seconds to wait for all app processes to gracefully exit when requested to quit. If unset, the default is to wait up to 5 seconds. If set to zero or a negative value, the app will be immediately terminated.\",\n    \"mouse_mode\": \"Mouse Mode\",\n    \"mouse_mode_desc\": \"Select the mouse input method for this application. Auto uses the global setting, Virtual Mouse uses the HID driver, SendInput uses the Windows API.\",\n    \"mouse_mode_auto\": \"Auto (Global Setting)\",\n    \"mouse_mode_vmouse\": \"Virtual Mouse\",\n    \"mouse_mode_sendinput\": \"SendInput\",\n    \"file_selector_not_initialized\": \"File selector not initialized\",\n    \"find_cover\": \"Find Cover\",\n    \"form_invalid\": \"Please check required fields\",\n    \"form_valid\": \"Valid application\",\n    \"global_prep_desc\": \"Enable/Disable the execution of Global Prep Commands for this application.\",\n    \"global_prep_name\": \"Global Prep Commands\",\n    \"image\": \"Image\",\n    \"image_desc\": \"Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Sunshine will send default box image.\",\n    \"image_settings\": \"Image Settings\",\n    \"loading\": \"Loading...\",\n    \"menu_cmd_actions\": \"Actions\",\n    \"menu_cmd_add\": \"Add Menu Command\",\n    \"menu_cmd_command\": \"Command\",\n    \"menu_cmd_desc\": \"After configuration, these commands will be visible in the client's return menu, allowing quick execution of specific operations without interrupting the stream, such as launching helper programs.\\nExample: Display Name - Close your computer; Command - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Display Name\",\n    \"menu_cmd_drag_sort\": \"Drag to sort\",\n    \"menu_cmd_name\": \"Menu Commands\",\n    \"menu_cmd_placeholder_command\": \"Command\",\n    \"menu_cmd_placeholder_display_name\": \"Display name\",\n    \"menu_cmd_placeholder_execute\": \"Execute command\",\n    \"menu_cmd_placeholder_undo\": \"Undo command\",\n    \"menu_cmd_remove_menu\": \"Remove menu command\",\n    \"menu_cmd_remove_prep\": \"Remove prep command\",\n    \"name\": \"Name\",\n    \"output_desc\": \"The file where the output of the command is stored, if it is not specified, the output is ignored\",\n    \"output_name\": \"Output\",\n    \"run_as_desc\": \"This can be necessary for some applications that require administrator permissions to run properly.\",\n    \"scan_result_add_all\": \"Add All\",\n    \"scan_result_edit_title\": \"Add and edit\",\n    \"scan_result_filter_all\": \"All\",\n    \"scan_result_filter_executable\": \"Executable\",\n    \"scan_result_filter_executable_title\": \"Executable file\",\n    \"scan_result_filter_script\": \"Script\",\n    \"scan_result_filter_script_title\": \"Batch/Command script\",\n    \"scan_result_filter_shortcut\": \"Shortcut\",\n    \"scan_result_filter_shortcut_title\": \"Shortcut\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_filter_steam_title\": \"Steam games\",\n    \"scan_result_filter_epic_title\": \"Epic Games games\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxy games\",\n    \"scan_result_game\": \"Game\",\n    \"scan_result_games_only\": \"Games Only\",\n    \"scan_result_matched\": \"Matched: {count}\",\n    \"scan_result_no_apps\": \"No applications found to add\",\n    \"scan_result_no_matches\": \"No matching applications found\",\n    \"scan_result_quick_add_title\": \"Quick add\",\n    \"scan_result_remove_title\": \"Remove from list\",\n    \"scan_result_search_placeholder\": \"Search application name, command or path...\",\n    \"scan_result_show_all\": \"Show All\",\n    \"scan_result_title\": \"Scan Results\",\n    \"scan_result_try_different_keywords\": \"Try using different search keywords\",\n    \"scan_result_type_batch\": \"Batch\",\n    \"scan_result_type_command\": \"Command script\",\n    \"scan_result_type_executable\": \"Executable file\",\n    \"scan_result_type_shortcut\": \"Shortcut\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Search applications...\",\n    \"select\": \"Select\",\n    \"test_menu_cmd\": \"Test Command\",\n    \"test_menu_cmd_empty\": \"Command cannot be empty\",\n    \"test_menu_cmd_executing\": \"Executing command...\",\n    \"test_menu_cmd_failed\": \"Command execution failed\",\n    \"test_menu_cmd_success\": \"Command executed successfully!\",\n    \"use_desktop_image\": \"Use current desktop wallpaper\",\n    \"wait_all\": \"Continue streaming until all app processes exit\",\n    \"wait_all_desc\": \"This will continue streaming until all processes started by the app have terminated. When unchecked, streaming will stop when the initial app process exits, even if other app processes are still running.\",\n    \"working_dir\": \"Working Directory\",\n    \"working_dir_desc\": \"The working directory that should be passed to the process. For example, some applications use the working directory to search for configuration files. If not set, Sunshine will default to the parent directory of the command\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Adapter Name\",\n    \"adapter_name_desc_linux_1\": \"Manually specify a GPU to use for capture.\",\n    \"adapter_name_desc_linux_2\": \"to find all devices capable of VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Replace ``renderD129`` with the device from above to lists the name and capabilities of the device. To be supported by Sunshine, it needs to have at the very minimum:\",\n    \"adapter_name_desc_windows\": \"Manually specify a GPU to use for capture. If unset, the GPU is chosen automatically. Note: This GPU must have a display connected and powered on. If your laptop cannot enable direct GPU output, please set this to automatic.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"If the latest version of the virtual display is installed, it can automatically associate with the GPU binding\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Add\",\n    \"address_family\": \"Address Family\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Set the address family used by Sunshine\",\n    \"address_family_ipv4\": \"IPv4 only\",\n    \"always_send_scancodes\": \"Always Send Scancodes\",\n    \"always_send_scancodes_desc\": \"Sending scancodes enhances compatibility with games and apps but may result in incorrect keyboard input from certain clients that aren't using a US English keyboard layout. Enable if keyboard input is not working at all in certain applications. Disable if keys on the client are generating the wrong input on the host.\",\n    \"amd_coder\": \"AMF Coder (H264)\",\n    \"amd_coder_desc\": \"Allows you to select the entropy encoding to prioritize quality or encoding speed. H.264 only.\",\n    \"amd_slices_per_frame\": \"AMF Slices Per Frame\",\n    \"amd_slices_per_frame_auto\": \"Auto (client decides)\",\n    \"amd_slices_per_frame_desc\": \"Splits each frame into multiple slices/tiles for parallel encoding and decoding, which can reduce latency. For H.264/HEVC this sets slices per frame; for AV1 this sets tiles per frame. Set to 0 to let the client decide.\",\n    \"amd_enforce_hrd\": \"AMF Hypothetical Reference Decoder (HRD) Enforcement\",\n    \"amd_enforce_hrd_desc\": \"Increases the constraints on rate control to meet HRD model requirements. This greatly reduces bitrate overflows, but may cause encoding artifacts or reduced quality on certain cards.\",\n    \"amd_preanalysis\": \"AMF Preanalysis\",\n    \"amd_preanalysis_desc\": \"This enables rate-control preanalysis, which may increase quality at the expense of increased encoding latency.\",\n    \"amd_quality\": \"AMF Quality\",\n    \"amd_quality_balanced\": \"balanced -- balanced (default)\",\n    \"amd_quality_desc\": \"This controls the balance between encoding speed and quality.\",\n    \"amd_quality_group\": \"AMF Quality Settings\",\n    \"amd_quality_quality\": \"quality -- prefer quality\",\n    \"amd_quality_speed\": \"speed -- prefer speed\",\n    \"amd_qvbr_quality\": \"AMF QVBR Quality Level\",\n    \"amd_qvbr_quality_desc\": \"Quality level for QVBR rate control mode. Range: 1-51 (lower = better quality). Default: 23. Only applies when rate control is set to 'qvbr'.\",\n    \"amd_rc\": \"AMF Rate Control\",\n    \"amd_rc_cbr\": \"cbr -- constant bitrate (recommended if HRD is enabled)\",\n    \"amd_rc_cqp\": \"cqp -- constant qp mode\",\n    \"amd_rc_desc\": \"This controls the rate control method to ensure we are not exceeding the client bitrate target. 'cqp' is not suitable for bitrate targeting, and other options besides 'vbr_latency' depend on HRD Enforcement to help constrain bitrate overflows.\",\n    \"amd_rc_group\": \"AMF Rate Control Settings\",\n    \"amd_rc_hqcbr\": \"hqcbr -- high quality constant bitrate\",\n    \"amd_rc_hqvbr\": \"hqvbr -- high quality variable bitrate\",\n    \"amd_rc_qvbr\": \"qvbr -- quality variable bitrate (uses QVBR quality level)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- latency constrained variable bitrate (recommended if HRD is disabled; default)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- peak constrained variable bitrate\",\n    \"amd_usage\": \"AMF Usage\",\n    \"amd_usage_desc\": \"This sets the base encoding profile. All options presented below will override a subset of the usage profile, but there are additional hidden settings applied that cannot be configured elsewhere.\",\n    \"amd_usage_lowlatency\": \"lowlatency - low latency (fastest)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - low latency, high quality (fast)\",\n    \"amd_usage_transcoding\": \"transcoding -- transcoding (slowest)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency - ultra low latency (fastest; default)\",\n    \"amd_usage_webcam\": \"webcam -- webcam (slow)\",\n    \"amd_vbaq\": \"AMF Variance Based Adaptive Quantization (VBAQ)\",\n    \"amd_vbaq_desc\": \"The human visual system is typically less sensitive to artifacts in highly textured areas. In VBAQ mode, pixel variance is used to indicate the complexity of spatial textures, allowing the encoder to allocate more bits to smoother areas. Enabling this feature leads to improvements in subjective visual quality with some content.\",\n    \"amf_draw_mouse_cursor\": \"Draw a simple cursor when using AMF capture method\",\n    \"amf_draw_mouse_cursor_desc\": \"In some cases, using AMF capture will not display the mouse pointer. Enabling this option will draw a simple mouse pointer on the screen. Note: The position of the mouse pointer will only be updated when there is an update to the content screen, so in non-game scenarios such as on the desktop, you may observe sluggish mouse pointer movement.\",\n    \"apply_note\": \"Click 'Apply' to restart Sunshine and apply changes. This will terminate any running sessions.\",\n    \"audio_sink\": \"Audio Sink\",\n    \"audio_sink_desc_linux\": \"The name of the audio sink used for Audio Loopback. If you do not specify this variable, pulseaudio will select the default monitor device. You can find the name of the audio sink using either command:\",\n    \"audio_sink_desc_macos\": \"The name of the audio sink used for Audio Loopback. Sunshine can only access microphones on macOS due to system limitations. To stream system audio using Soundflower or BlackHole.\",\n    \"audio_sink_desc_windows\": \"Manually specify a specific audio device to capture. If unset, the device is chosen automatically. We strongly recommend leaving this field blank to use automatic device selection! If you have multiple audio devices with identical names, you can get the Device ID using the following command:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Speakers (High Definition Audio Device)\",\n    \"av1_mode\": \"AV1 Support\",\n    \"av1_mode_0\": \"Sunshine will advertise support for AV1 based on encoder capabilities (recommended)\",\n    \"av1_mode_1\": \"Sunshine will not advertise support for AV1\",\n    \"av1_mode_2\": \"Sunshine will advertise support for AV1 Main 8-bit profile\",\n    \"av1_mode_3\": \"Sunshine will advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles\",\n    \"av1_mode_desc\": \"Allows the client to request AV1 Main 8-bit or 10-bit video streams. AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.\",\n    \"back_button_timeout\": \"Home/Guide Button Emulation Timeout\",\n    \"back_button_timeout_desc\": \"If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated. If set to a value < 0 (default), holding the Back/Select button will not emulate the Home/Guide button.\",\n    \"bind_address\": \"Bind address (test feature)\",\n    \"bind_address_desc\": \"Set the specific IP address Sunshine will bind to. If left blank, Sunshine will bind to all available addresses.\",\n    \"capture\": \"Force a Specific Capture Method\",\n    \"capture_desc\": \"On automatic mode Sunshine will use the first one that works. NvFBC requires patched nvidia drivers.\",\n    \"amd_capture_no_virtual_display\": \"AMD Display Capture does not support virtual display drivers (e.g. IddCx/VDD). If you are using a virtual display, please use WGC or Desktop Duplication API instead.\",\n    \"capture_target\": \"Capture Target\",\n    \"capture_target_desc\": \"Select the type of target to capture. When selecting 'Window', you can capture a specific application window (such as AI frame interpolation software) instead of the entire display.\",\n    \"capture_target_display\": \"Display\",\n    \"capture_target_window\": \"Window\",\n    \"cert\": \"Certificate\",\n    \"cert_desc\": \"The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key.\",\n    \"channels\": \"Maximum Connected Clients\",\n    \"channels_desc_1\": \"Sunshine can allow a single streaming session to be shared with multiple clients simultaneously.\",\n    \"channels_desc_2\": \"Some hardware encoders may have limitations that reduce performance with multiple streams.\",\n    \"close_verify_safe\": \"Safe Verify Compatible with Old Clients\",\n    \"close_verify_safe_desc\": \"Old clients may not be able to connect to Sunshine, please disable this option or update the client\",\n    \"coder_cabac\": \"cabac -- context adaptive binary arithmetic coding - higher quality\",\n    \"coder_cavlc\": \"cavlc -- context adaptive variable-length coding - faster decode\",\n    \"configuration\": \"Configuration\",\n    \"controller\": \"Enable Gamepad Input\",\n    \"controller_desc\": \"Allows guests to control the host system with a gamepad / controller\",\n    \"credentials_file\": \"Credentials File\",\n    \"credentials_file_desc\": \"Store Username/Password separately from Sunshine's state file.\",\n    \"display_device_options_note_desc_windows\": \"Windows saves various display settings for each combination of currently active displays.\\nSunshine then applies changes to a display(-s) belonging to such a display combination.\\nIf you disconnect a device which was active when Sunshine applied the settings, the changes can not be\\nreverted back unless the combination can be activated again by the time Sunshine tries to revert changes!\",\n    \"display_device_options_note_windows\": \"Note about how settings are applied\",\n    \"display_device_options_windows\": \"Display device options\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Activates the display if it is not already active\",\n    \"display_device_prep_ensure_active_windows\": \"Auto-Activate Display for Streaming\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Disables all other displays, only enables the specified display\",\n    \"display_device_prep_ensure_only_display_windows\": \"Exclusive Display Streaming (Disable Other Displays)\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Activates the display and sets it as the primary display\",\n    \"display_device_prep_ensure_primary_windows\": \"Primary Display Streaming (Auto-Activate & Set as Primary)\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Uses only the virtual display for secondary extended streaming\",\n    \"display_device_prep_ensure_secondary_windows\": \"Secondary Display Streaming (Virtual Display Only)\",\n    \"display_device_prep_no_operation_desc_windows\": \"No changes to display state; user must ensure display is ready\",\n    \"display_device_prep_no_operation_windows\": \"Disabled\",\n    \"display_device_prep_windows\": \"Display Combination Settings for Streaming\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"At least one \\\"received\\\" and one \\\"final\\\" value must be specified.\\nEmpty field in \\\"received\\\" section means \\\"match any\\\". Empty field in \\\"final\\\" section means \\\"keep received value\\\".\\nYou can match specific FPS value to specific resolution if you wish so...\\n\\nNote: if \\\"Optimize game settings\\\" option is not enabled on the Moonlight client, the rows containing resolution value(-s) are ignored.\",\n    \"display_mode_remapping_desc_windows\": \"Specify how a specific resolution and/or refresh rate should be remapped to other values.\\nYou can stream at lower resolution, while rendering at higher resolution on host for a supersampling effect.\\nOr you can stream at higher FPS while limiting the host to the lower refresh rate.\\nMatching is performed top to bottom. Once the entry is matched, others are no longer checked, but still validated.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Final refresh rate\",\n    \"display_mode_remapping_final_resolution_windows\": \"Final resolution\",\n    \"display_mode_remapping_optional\": \"optional\",\n    \"display_mode_remapping_received_fps_windows\": \"Received FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"Received resolution\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Note: if \\\"Optimize game settings\\\" option is not enabled on the Moonlight client, the remapping is disabled.\",\n    \"display_mode_remapping_windows\": \"Remap display modes\",\n    \"display_modes\": \"Display Modes\",\n    \"ds4_back_as_touchpad_click\": \"Map Back/Select to Touchpad Click\",\n    \"ds4_back_as_touchpad_click_desc\": \"When forcing DS4 emulation, map Back/Select to Touchpad Click\",\n    \"dsu_server_port\": \"DSU Server Port\",\n    \"dsu_server_port_desc\": \"DSU server listening port (default 26760). Sunshine will act as a DSU server to receive client connections and send motion data. Enable DSU server in your client(Yuzu,Ryujinx etc.) and set DSU server address(127.0.0.1) and port(26760)\",\n    \"enable_dsu_server\": \"Enable DSU Server\",\n    \"enable_dsu_server_desc\": \"Enable DSU server to receive client connections and send motion data\",\n    \"encoder\": \"Force a Specific Encoder\",\n    \"encoder_desc\": \"Force a specific encoder, otherwise Sunshine will select the best available option. Note: If you specify a hardware encoder on Windows, it must match the GPU where the display is connected.\",\n    \"encoder_software\": \"Software\",\n    \"experimental\": \"Experimental\",\n    \"experimental_features\": \"Experimental Features\",\n    \"external_ip\": \"External IP\",\n    \"external_ip_desc\": \"If no external IP address is given, Sunshine will automatically detect external IP\",\n    \"fec_percentage\": \"FEC Percentage\",\n    \"fec_percentage_desc\": \"Percentage of error correcting packets per data packet in each video frame. Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage.\",\n    \"ffmpeg_auto\": \"auto -- let ffmpeg decide (default)\",\n    \"file_apps\": \"Apps File\",\n    \"file_apps_desc\": \"The file where current apps of Sunshine are stored.\",\n    \"file_state\": \"State File\",\n    \"file_state_desc\": \"The file where current state of Sunshine is stored\",\n    \"fps\": \"Advertised FPS\",\n    \"gamepad\": \"Emulated Gamepad Type\",\n    \"gamepad_auto\": \"Automatic selection options\",\n    \"gamepad_desc\": \"Choose which type of gamepad to emulate on the host\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 Manual Options\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Manual DS4 options\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Command Preparations\",\n    \"global_prep_cmd_desc\": \"Configure a list of commands to be executed before or after running any application. If any of the specified preparation commands fail, the application launch process will be aborted.\",\n    \"hdr_luminance_analysis\": \"HDR Dynamic Metadata (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Enables per-frame GPU luminance analysis and injects HDR10+ (ST 2094-40) and HDR Vivid (CUVA) dynamic metadata into the encoded bitstream. This provides per-frame tone-mapping hints to supported displays (e.g. Huawei HDR Vivid terminals). Adds minor GPU overhead (~0.5-1.5ms/frame at high resolutions). Disable if you experience frame rate drops with HDR enabled; streaming will then use static HDR metadata only.\",\n    \"hdr_prep_automatic_windows\": \"Switch on/off the HDR mode as requested by the client\",\n    \"hdr_prep_no_operation_windows\": \"Disabled\",\n    \"hdr_prep_windows\": \"HDR state change\",\n    \"hevc_mode\": \"HEVC Support\",\n    \"hevc_mode_0\": \"Sunshine will advertise support for HEVC based on encoder capabilities (recommended)\",\n    \"hevc_mode_1\": \"Sunshine will not advertise support for HEVC\",\n    \"hevc_mode_2\": \"Sunshine will advertise support for HEVC Main profile\",\n    \"hevc_mode_3\": \"Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles\",\n    \"hevc_mode_desc\": \"Allows the client to request HEVC Main or HEVC Main10 video streams. HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.\",\n    \"high_resolution_scrolling\": \"High Resolution Scrolling Support\",\n    \"high_resolution_scrolling_desc\": \"When enabled, Sunshine will pass through high resolution scroll events from Moonlight clients. This can be useful to disable for older applications that scroll too fast with high resolution scroll events.\",\n    \"install_steam_audio_drivers\": \"Install Steam Audio Drivers\",\n    \"install_steam_audio_drivers_desc\": \"If Steam is installed, this will automatically install the Steam Streaming Speakers driver to support 5.1/7.1 surround sound and muting host audio.\",\n    \"key_repeat_delay\": \"Key Repeat Delay\",\n    \"key_repeat_delay_desc\": \"Control how fast keys will repeat themselves. The initial delay in milliseconds before repeating keys.\",\n    \"key_repeat_frequency\": \"Key Repeat Frequency\",\n    \"key_repeat_frequency_desc\": \"How often keys repeat every second. This configurable option supports decimals.\",\n    \"key_rightalt_to_key_win\": \"Map Right Alt key to Windows key\",\n    \"key_rightalt_to_key_win_desc\": \"It may be possible that you cannot send the Windows Key from Moonlight directly. In those cases it may be useful to make Sunshine think the Right Alt key is the Windows key\",\n    \"key_rightalt_to_key_windows\": \"Map Right Alt key to Windows key\",\n    \"keyboard\": \"Enable Keyboard Input\",\n    \"keyboard_desc\": \"Allows guests to control the host system with the keyboard\",\n    \"lan_encryption_mode\": \"LAN Encryption Mode\",\n    \"lan_encryption_mode_1\": \"Enabled for supported clients\",\n    \"lan_encryption_mode_2\": \"Required for all clients\",\n    \"lan_encryption_mode_desc\": \"This determines when encryption will be used when streaming over your local network. Encryption can reduce streaming performance, particularly on less powerful hosts and clients.\",\n    \"locale\": \"Locale\",\n    \"locale_desc\": \"The locale used for Sunshine's user interface.\",\n    \"log_level\": \"Log Level\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"Info\",\n    \"log_level_3\": \"Warning\",\n    \"log_level_4\": \"Error\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"None\",\n    \"log_level_desc\": \"The minimum log level printed to standard out\",\n    \"log_path\": \"Logfile Path\",\n    \"log_path_desc\": \"The file where the current logs of Sunshine are stored.\",\n    \"max_bitrate\": \"Maximum Bitrate\",\n    \"max_bitrate_desc\": \"The maximum bitrate (in Kbps) that Sunshine will encode the stream at. If set to 0, it will always use the bitrate requested by Moonlight.\",\n    \"max_fps_reached\": \"Maximum FPS values reached\",\n    \"max_resolutions_reached\": \"Maximum resolutions reached\",\n    \"mdns_broadcast\": \"Find this computer in the local network\",\n    \"mdns_broadcast_desc\": \"If this option is enabled, Sunshine will allow other devices to find this computer automatically. Moonlight must also be configured to find this computer automatically in the local network.\",\n    \"min_threads\": \"Minimum CPU Thread Count\",\n    \"min_threads_desc\": \"Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest value that can reliably encode at your desired streaming settings on your hardware.\",\n    \"minimum_fps_target\": \"Minimum FPS Target\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Miscellaneous options\",\n    \"motion_as_ds4\": \"Emulate a DS4 gamepad if the client gamepad reports motion sensors are present\",\n    \"motion_as_ds4_desc\": \"If disabled, motion sensors will not be taken into account during gamepad type selection.\",\n    \"mouse\": \"Enable Mouse Input\",\n    \"mouse_desc\": \"Allows guests to control the host system with the mouse\",\n    \"native_pen_touch\": \"Native Pen/Touch Support\",\n    \"native_pen_touch_desc\": \"When enabled, Sunshine will pass through native pen/touch events from Moonlight clients. This can be useful to disable for older applications without native pen/touch support.\",\n    \"no_fps\": \"No FPS values added\",\n    \"no_resolutions\": \"No resolutions added\",\n    \"notify_pre_releases\": \"PreRelease Notifications\",\n    \"notify_pre_releases_desc\": \"Whether to be notified of new pre-release versions of Sunshine\",\n    \"nvenc_h264_cavlc\": \"Prefer CAVLC over CABAC in H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Simpler form of entropy coding. CAVLC needs around 10% more bitrate for same quality. Only relevant for really old decoding devices.\",\n    \"nvenc_latency_over_power\": \"Prefer lower encoding latency over power savings\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine requests maximum GPU clock speed while streaming to reduce encoding latency. Disabling it is not recommended since this can lead to significantly increased encoding latency.\",\n    \"nvenc_lookahead_depth\": \"Lookahead depth\",\n    \"nvenc_lookahead_depth_desc\": \"Number of frames to look ahead during encoding (0-32). Lookahead improves encoding quality, especially in complex scenes, by providing better motion estimation and bitrate distribution. Higher values improve quality but increase encoding latency. Set to 0 to disable lookahead. Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_lookahead_level\": \"Lookahead level\",\n    \"nvenc_lookahead_level_0\": \"Level 0 (lowest quality, fastest)\",\n    \"nvenc_lookahead_level_1\": \"Level 1\",\n    \"nvenc_lookahead_level_2\": \"Level 2\",\n    \"nvenc_lookahead_level_3\": \"Level 3 (highest quality, slowest)\",\n    \"nvenc_lookahead_level_autoselect\": \"Auto-select (let driver choose optimal level)\",\n    \"nvenc_lookahead_level_desc\": \"Lookahead quality level. Higher levels improve quality at the expense of performance. This option only takes effect when lookahead_depth is greater than 0. Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_lookahead_level_disabled\": \"Disabled (same as level 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Present OpenGL/Vulkan on top of DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine can't capture fullscreen OpenGL and Vulkan programs at full frame rate unless they present on top of DXGI. This is system-wide setting that is reverted on sunshine program exit.\",\n    \"nvenc_preset\": \"Performance preset\",\n    \"nvenc_preset_1\": \"(fastest, default)\",\n    \"nvenc_preset_7\": \"(slowest)\",\n    \"nvenc_preset_desc\": \"Higher numbers improve compression (quality at given bitrate) at the cost of increased encoding latency. Recommended to change only when limited by network or decoder, otherwise similar effect can be accomplished by increasing bitrate.\",\n    \"nvenc_rate_control\": \"Rate control mode\",\n    \"nvenc_rate_control_cbr\": \"CBR (Constant Bitrate) - Low latency\",\n    \"nvenc_rate_control_desc\": \"Select rate control mode. CBR (Constant Bitrate) provides fixed bitrate for low latency streaming. VBR (Variable Bitrate) allows bitrate to vary based on scene complexity, providing better quality for complex scenes at the cost of variable bitrate.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Variable Bitrate) - Better quality\",\n    \"nvenc_realtime_hags\": \"Use realtime priority in hardware accelerated gpu scheduling\",\n    \"nvenc_realtime_hags_desc\": \"Currently NVIDIA drivers may freeze in encoder when HAGS is enabled, realtime priority is used and VRAM utilization is close to maximum. Disabling this option lowers the priority to high, sidestepping the freeze at the cost of reduced capture performance when the GPU is heavily loaded.\",\n    \"nvenc_spatial_aq\": \"Spatial AQ\",\n    \"nvenc_spatial_aq_desc\": \"Assign higher QP values to flat regions of the video. Recommended to enable when streaming at lower bitrates.\",\n    \"nvenc_spatial_aq_disabled\": \"Disabled (faster, default)\",\n    \"nvenc_spatial_aq_enabled\": \"Enabled (slower)\",\n    \"nvenc_split_encode\": \"Split frame encoding\",\n    \"nvenc_split_encode_desc\": \"Split the encoding of each video frame over multiple NVENC hardware units. Significantly reduces encoding latency with a marginal compression efficiency penalty. This option is ignored if your GPU has a singular NVENC unit.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Driver decides (default)\",\n    \"nvenc_split_encode_four_strips\": \"Force 4-strip split (requires 4+ NVENC engines)\",\n    \"nvenc_split_encode_three_strips\": \"Force 3-strip split (requires 3+ NVENC engines)\",\n    \"nvenc_split_encode_two_strips\": \"Force 2-strip split (requires 2+ NVENC engines)\",\n    \"nvenc_target_quality\": \"Target quality (VBR mode)\",\n    \"nvenc_target_quality_desc\": \"Target quality level for VBR mode (0-51 for H.264/HEVC, 0-63 for AV1). Lower values = higher quality. Set to 0 for automatic quality selection. Only used when rate control mode is VBR.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Two-pass mode\",\n    \"nvenc_twopass_desc\": \"Adds preliminary encoding pass. This allows to detect more motion vectors, better distribute bitrate across the frame and more strictly adhere to bitrate limits. Disabling it is not recommended since this can lead to occasional bitrate overshoot and subsequent packet loss.\",\n    \"nvenc_twopass_disabled\": \"Disabled (fastest, not recommended)\",\n    \"nvenc_twopass_full_res\": \"Full resolution (slower)\",\n    \"nvenc_twopass_quarter_res\": \"Quarter resolution (faster, default)\",\n    \"nvenc_vbv_increase\": \"Single-frame VBV/HRD percentage increase\",\n    \"nvenc_vbv_increase_desc\": \"By default sunshine uses single-frame VBV/HRD, which means any encoded video frame size is not expected to exceed requested bitrate divided by requested frame rate. Relaxing this restriction can be beneficial and act as low-latency variable bitrate, but may also lead to packet loss if the network doesn't have buffer headroom to handle bitrate spikes. Maximum accepted value is 400, which corresponds to 5x increased encoded video frame upper size limit.\",\n    \"origin_web_ui_allowed\": \"Origin Web UI Allowed\",\n    \"origin_web_ui_allowed_desc\": \"The origin of the remote endpoint address that is not denied access to Web UI\",\n    \"origin_web_ui_allowed_lan\": \"Only those in LAN may access Web UI\",\n    \"origin_web_ui_allowed_pc\": \"Only localhost may access Web UI\",\n    \"origin_web_ui_allowed_wan\": \"Anyone may access Web UI\",\n    \"output_name_desc_unix\": \"During Sunshine startup, you should see the list of detected displays. Note: You need to use the id value inside the parenthesis.\",\n    \"output_name_desc_windows\": \"Manually specify a display device id to use for capture. If unset, the primary display is captured.\\nNote: If you specified a GPU above, this display must be connected to that GPU.\\n\\nDuring Sunshine startup, you should see the list of detected display devices and their ids, e.g.:\",\n    \"output_name_unix\": \"Display number\",\n    \"output_name_windows\": \"Display Device Specify\",\n    \"ping_timeout\": \"Ping Timeout\",\n    \"ping_timeout_desc\": \"How long to wait in milliseconds for data from moonlight before shutting down the stream\",\n    \"pkey\": \"Private Key\",\n    \"pkey_desc\": \"The private key used for the web UI and Moonlight client pairing. For best compatibility, this should be an RSA-2048 private key.\",\n    \"port\": \"Port\",\n    \"port_alert_1\": \"Sunshine cannot use ports below 1024!\",\n    \"port_alert_2\": \"Ports above 65535 are not available!\",\n    \"port_desc\": \"Set the family of ports used by Sunshine\",\n    \"port_http_port_note\": \"Use this port to connect with Moonlight.\",\n    \"port_note\": \"Note\",\n    \"port_port\": \"Port\",\n    \"port_protocol\": \"Protocol\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Exposing the Web UI to the internet is a security risk! Proceed at your own risk!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Quantization Parameter\",\n    \"qp_desc\": \"Some devices may not support Constant Bit Rate. For those devices, QP is used instead. Higher value means more compression, but less quality.\",\n    \"qsv_coder\": \"QuickSync Coder (H264)\",\n    \"qsv_preset\": \"QuickSync Preset\",\n    \"qsv_preset_fast\": \"fast (low quality)\",\n    \"qsv_preset_faster\": \"faster (lower quality)\",\n    \"qsv_preset_medium\": \"medium (default)\",\n    \"qsv_preset_slow\": \"slow (good quality)\",\n    \"qsv_preset_slower\": \"slower (better quality)\",\n    \"qsv_preset_slowest\": \"slowest (best quality)\",\n    \"qsv_preset_veryfast\": \"fastest (lowest quality)\",\n    \"qsv_slow_hevc\": \"Allow Slow HEVC Encoding\",\n    \"qsv_slow_hevc_desc\": \"This can enable HEVC encoding on older Intel GPUs, at the cost of higher GPU usage and worse performance.\",\n    \"refresh_rate_change_automatic_windows\": \"Use FPS value provided by the client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Enter the refresh rate to be used\",\n    \"refresh_rate_change_manual_windows\": \"Use manually entered refresh rate\",\n    \"refresh_rate_change_no_operation_windows\": \"Disabled\",\n    \"refresh_rate_change_windows\": \"FPS change\",\n    \"res_fps_desc\": \"The display modes advertised by Sunshine. Some versions of Moonlight, such as Moonlight-nx (Switch), rely on these lists to ensure that the requested resolutions and fps are supported. This setting does not change how the screen stream is sent to Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Use resolution provided by the client\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_manual_windows\": \"Use manually entered resolution\",\n    \"resolution_change_no_operation_windows\": \"Disabled\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_windows\": \"Resolution change\",\n    \"resolutions\": \"Advertised Resolutions\",\n    \"restart_note\": \"Sunshine is restarting to apply changes.\",\n    \"sleep_mode\": \"Sleep Mode\",\n    \"sleep_mode_away\": \"Away Mode (Display Off, Instant Wake)\",\n    \"sleep_mode_desc\": \"Controls what happens when the client sends a sleep command. Suspend (S3): traditional sleep, low power but requires WOL to wake. Hibernate (S4): saves to disk, very low power. Away Mode: display turns off but system stays running for instant wake - ideal for game streaming servers.\",\n    \"sleep_mode_hibernate\": \"Hibernate (S4)\",\n    \"sleep_mode_suspend\": \"Suspend (S3 Sleep)\",\n    \"stream_audio\": \"Enable audio streaming\",\n    \"stream_audio_desc\": \"Disable this option to stop audio streaming.\",\n    \"stream_mic\": \"Enable Microphone Streaming\",\n    \"stream_mic_desc\": \"Disable this option to stop microphone streaming.\",\n    \"stream_mic_download_btn\": \"Download Virtual Microphone\",\n    \"stream_mic_download_confirm\": \"You are about to be redirected to the virtual microphone download page. Continue?\",\n    \"stream_mic_note\": \"This feature requires installing a virtual microphone\",\n    \"sunshine_name\": \"Sunshine Name\",\n    \"sunshine_name_desc\": \"The name displayed by Moonlight. If not specified, the PC's hostname is used\",\n    \"sw_preset\": \"SW Presets\",\n    \"sw_preset_desc\": \"Optimize the trade-off between encoding speed (encoded frames per second) and compression efficiency (quality per bit in the bitstream). Defaults to superfast.\",\n    \"sw_preset_fast\": \"fast\",\n    \"sw_preset_faster\": \"faster\",\n    \"sw_preset_medium\": \"medium\",\n    \"sw_preset_slow\": \"slow\",\n    \"sw_preset_slower\": \"slower\",\n    \"sw_preset_superfast\": \"superfast (default)\",\n    \"sw_preset_ultrafast\": \"ultrafast\",\n    \"sw_preset_veryfast\": \"veryfast\",\n    \"sw_preset_veryslow\": \"veryslow\",\n    \"sw_tune\": \"SW Tune\",\n    \"sw_tune_animation\": \"animation -- good for cartoons; uses higher deblocking and more reference frames\",\n    \"sw_tune_desc\": \"Tuning options, which are applied after the preset. Defaults to zerolatency.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- allows faster decoding by disabling certain filters\",\n    \"sw_tune_film\": \"film -- use for high quality movie content; lowers deblocking\",\n    \"sw_tune_grain\": \"grain -- preserves the grain structure in old, grainy film material\",\n    \"sw_tune_stillimage\": \"stillimage -- good for slideshow-like content\",\n    \"sw_tune_zerolatency\": \"zerolatency -- good for fast encoding and low-latency streaming (default)\",\n    \"system_tray\": \"Enable System Tray\",\n    \"system_tray_desc\": \"Whether to enable system tray. If enabled, Sunshine will display an icon in the system tray and can be controlled from the system tray.\",\n    \"touchpad_as_ds4\": \"Emulate a DS4 gamepad if the client gamepad reports a touchpad is present\",\n    \"touchpad_as_ds4_desc\": \"If disabled, touchpad presence will not be taken into account during gamepad type selection.\",\n    \"unsaved_changes_tooltip\": \"You have unsaved changes. Click to save.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Automatically configure port forwarding for streaming over the Internet\",\n    \"variable_refresh_rate\": \"Variable Refresh Rate (VRR)\",\n    \"variable_refresh_rate_desc\": \"Allow video stream framerate to match render framerate for VRR support. When enabled, encoding only occurs when new frames are available, allowing the stream to follow the actual render framerate.\",\n    \"vdd_reuse_desc_windows\": \"When enabled, all clients will share the same VDD (Virtual Display Device). When disabled (default), each client gets its own unique VDD. Enable this for faster client switching, but note that all clients will share the same display settings.\",\n    \"vdd_reuse_windows\": \"Reuse Same VDD for All Clients\",\n    \"virtual_display\": \"Virtual Display\",\n    \"virtual_mouse\": \"Virtual Mouse Driver\",\n    \"virtual_mouse_desc\": \"When enabled, Sunshine will use the Zako Virtual Mouse driver (if installed) to simulate mouse input at the HID level. This allows games using Raw Input to receive mouse events. When disabled or driver not installed, falls back to SendInput.\",\n    \"virtual_sink\": \"Virtual Sink\",\n    \"virtual_sink_desc\": \"Manually specify a virtual audio device to use. If unset, the device is chosen automatically. We strongly recommend leaving this field blank to use automatic device selection!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Install the virtual mouse driver?\",\n    \"vmouse_confirm_uninstall\": \"Uninstall the virtual mouse driver?\",\n    \"vmouse_install\": \"Install Driver\",\n    \"vmouse_installing\": \"Installing...\",\n    \"vmouse_note\": \"The virtual mouse driver requires separate installation. Please use the Sunshine Control Panel to install or manage the driver.\",\n    \"vmouse_refresh\": \"Refresh Status\",\n    \"vmouse_status_installed\": \"Installed (not active)\",\n    \"vmouse_status_not_installed\": \"Not Installed\",\n    \"vmouse_status_running\": \"Running\",\n    \"vmouse_uninstall\": \"Uninstall Driver\",\n    \"vmouse_uninstalling\": \"Uninstalling...\",\n    \"vt_coder\": \"VideoToolbox Coder\",\n    \"vt_realtime\": \"VideoToolbox Realtime Encoding\",\n    \"vt_software\": \"VideoToolbox Software Encoding\",\n    \"vt_software_allowed\": \"Allowed\",\n    \"vt_software_forced\": \"Forced\",\n    \"wan_encryption_mode\": \"WAN Encryption Mode\",\n    \"wan_encryption_mode_1\": \"Enabled for supported clients (default)\",\n    \"wan_encryption_mode_2\": \"Required for all clients\",\n    \"wan_encryption_mode_desc\": \"This determines when encryption will be used when streaming over the Internet. Encryption can reduce streaming performance, particularly on less powerful hosts and clients.\",\n    \"webhook_curl_command\": \"Command\",\n    \"webhook_curl_command_desc\": \"Copy the following command to your terminal to test if the webhook is working properly:\",\n    \"webhook_curl_copy_failed\": \"Copy failed, please manually select and copy\",\n    \"webhook_enabled\": \"Webhook Notifications\",\n    \"webhook_enabled_desc\": \"When enabled, Sunshine will send event notifications to the specified Webhook URL\",\n    \"webhook_group\": \"Webhook Notification Settings\",\n    \"webhook_skip_ssl_verify\": \"Skip SSL Certificate Verification\",\n    \"webhook_skip_ssl_verify_desc\": \"Skip SSL certificate verification for HTTPS connections, only for testing or self-signed certificates\",\n    \"webhook_test\": \"Test\",\n    \"webhook_test_failed\": \"Webhook test failed\",\n    \"webhook_test_failed_note\": \"Note: Please check if the URL is correct, or check the browser console for more information.\",\n    \"webhook_test_success\": \"Webhook test successful!\",\n    \"webhook_test_success_cors_note\": \"Note: Due to CORS restrictions, the server response status cannot be confirmed.\\nThe request has been sent. If the webhook is configured correctly, the message should have been delivered.\\n\\nSuggestion: Check the Network tab in your browser's developer tools for request details.\",\n    \"webhook_test_url_required\": \"Please enter Webhook URL first\",\n    \"webhook_timeout\": \"Request Timeout\",\n    \"webhook_timeout_desc\": \"Timeout for webhook requests in milliseconds, range 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"The URL to receive event notifications, supports HTTP/HTTPS protocols\",\n    \"wgc_checking_mode\": \"Checking...\",\n    \"wgc_checking_running_mode\": \"Checking running mode...\",\n    \"wgc_control_panel_only\": \"This feature is only available in Sunshine Control Panel\",\n    \"wgc_mode_switch_failed\": \"Failed to switch mode\",\n    \"wgc_mode_switch_started\": \"Mode switch initiated. If a UAC prompt appears, please click 'Yes' to confirm.\",\n    \"wgc_service_mode_warning\": \"WGC capture requires running in user mode. If currently running in service mode, please click the button above to switch to user mode.\",\n    \"wgc_switch_to_service_mode\": \"Switch to Service Mode\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Currently running in user mode. Click to switch to service mode.\",\n    \"wgc_switch_to_user_mode\": \"Switch to User Mode\",\n    \"wgc_switch_to_user_mode_tooltip\": \"WGC capture requires running in user mode. Click this button to switch to user mode.\",\n    \"wgc_user_mode_available\": \"Currently running in user mode. WGC capture is available.\",\n    \"window_title\": \"Window Title\",\n    \"window_title_desc\": \"The title of the window to capture (partial match, case-insensitive). If left empty, the current running application name will be used automatically.\",\n    \"wgc_disable_secure_desktop\": \"Auto-Elevate UAC (WGC Mode)\",\n    \"wgc_disable_secure_desktop_desc\": \"When using WGC capture, temporarily disable UAC prompts by auto-elevating admin requests without user confirmation. This allows seamless remote operation without UAC dialog interruptions. The original UAC settings are restored when streaming ends. WARNING: This reduces system security — any program requesting admin privileges will be silently elevated during streaming.\",\n    \"window_title_placeholder\": \"e.g., Application Name\"\n  },\n  \"index\": {\n    \"description\": \"Foundation Sunshine - The elegant game streaming solution for the community\",\n    \"download\": \"Download\",\n    \"installed_version_not_stable\": \"You are running a pre-release version of Sunshine. You may experience bugs or other issues. Please report any issues you encounter. Thank you for helping to make Sunshine a better software!\",\n    \"loading_latest\": \"Loading latest release...\",\n    \"new_pre_release\": \"A new Pre-Release Version is Available!\",\n    \"new_stable\": \"A new Stable Version is Available!\",\n    \"startup_errors\": \"<b>Attention!</b> Sunshine detected these errors during startup. We <b>STRONGLY RECOMMEND</b> fixing them before streaming.\",\n    \"update_download_confirm\": \"You are about to open the update download page in your browser. Continue?\",\n    \"version_dirty\": \"Thank you for helping to make Sunshine a better software!\",\n    \"version_latest\": \"You are running the latest version of Sunshine\",\n    \"view_logs\": \"View Logs\",\n    \"welcome\": \"Hello, Foundation Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Applications\",\n    \"configuration\": \"Configuration\",\n    \"home\": \"Home\",\n    \"password\": \"Change Password\",\n    \"pin\": \"PIN\",\n    \"theme_auto\": \"Auto\",\n    \"theme_dark\": \"Dark\",\n    \"theme_light\": \"Light\",\n    \"toggle_theme\": \"Theme\",\n    \"troubleshoot\": \"Troubleshooting\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Confirm Password\",\n    \"current_creds\": \"Current Credentials\",\n    \"new_creds\": \"New Credentials\",\n    \"new_username_desc\": \"If not specified, the username will not change\",\n    \"password_change\": \"Password Change\",\n    \"success_msg\": \"Password has been changed successfully! This page will reload soon, your browser will ask you for the new credentials.\"\n  },\n  \"pin\": {\n    \"actions\": \"Actions\",\n    \"cancel_editing\": \"Cancel editing\",\n    \"client_name\": \"Name\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Confirm Delete\",\n    \"delete_client\": \"Delete client\",\n    \"delete_confirm_message\": \"Are you sure you want to delete <strong>{name}</strong>?\",\n    \"delete_warning\": \"This action cannot be undone.\",\n    \"device_name\": \"Device Name\",\n    \"device_size\": \"Device Size\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Large - TV\",\n    \"device_size_medium\": \"Medium - Tablet\",\n    \"device_size_small\": \"Small - Phone\",\n    \"edit_client_settings\": \"Edit client settings\",\n    \"hdr_profile\": \"HDR Profile\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Loading...\",\n    \"loading_clients\": \"Loading clients...\",\n    \"modify_in_gui\": \"Please modify in GUI\",\n    \"none\": \"-- None --\",\n    \"or_manual_pin\": \"or enter PIN manually\",\n    \"pair_failure\": \"Pairing Failed: Check if the PIN is typed correctly\",\n    \"pair_success\": \"Success! Please check Moonlight to continue\",\n    \"pin_pairing\": \"PIN Pairing\",\n    \"qr_expires_in\": \"Expires in {seconds}s\",\n    \"qr_generate\": \"Generate QR Code\",\n    \"qr_paired_success\": \"Pairing successful!\",\n    \"qr_pairing\": \"QR Code Pairing\",\n    \"qr_pairing_desc\": \"Generate a QR code for quick pairing. Scan it with the Moonlight client to pair automatically.\",\n    \"qr_pairing_warning\": \"Experimental feature. If pairing fails, please use the manual PIN pairing below. Note: This feature only works on LAN.\",\n    \"qr_refresh\": \"Refresh\",\n    \"remove_paired_devices_desc\": \"Remove your paired devices.\",\n    \"save_changes\": \"Save changes\",\n    \"save_failed\": \"Failed to save client settings. Please try again.\",\n    \"save_or_cancel_first\": \"Please save or cancel editing first\",\n    \"send\": \"Send\",\n    \"unknown_client\": \"Unknown Client\",\n    \"unpair_all_confirm\": \"Are you sure you want to unpair all clients? This action cannot be undone.\",\n    \"unsaved_changes\": \"Unsaved changes\",\n    \"warning_msg\": \"Make sure you have access to the client you are pairing with. This software can give total control to your computer, so be careful!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android Recommended\",\n    \"client_downloads\": \"Client Downloads\",\n    \"crown_edition\": \"moonlight-android (Crown)\",\n    \"crown_edition_desc\": \"by WACrown · Android\",\n    \"moonlight_macos_enhanced\": \"moonlight-macos-enhanced\",\n    \"moonlight_macos_enhanced_desc\": \"by skyhua0224 · macOS native enhanced client\",\n    \"moonlight_ohos\": \"moonlight-ohos\",\n    \"moonlight_ohos_desc\": \"by smdsbz · OpenHarmony pioneer\",\n    \"github_discussions\": \"GitHub Discussions\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Join Community\",\n    \"join_group_desc\": \"Get help and share experience\",\n    \"legal\": \"Legal\",\n    \"legal_desc\": \"By continuing to use this software you agree to the terms and conditions in the following documents.\",\n    \"license\": \"License\",\n    \"lizardbyte_website\": \"LizardByte Website\",\n    \"official_website\": \"Official Website\",\n    \"official_website_title\": \"AlkaidLab - Official Website\",\n    \"open_source\": \"Open Source\",\n    \"open_source_desc\": \"Star & Fork to support the project\",\n    \"quick_start\": \"Quick Start\",\n    \"resources\": \"Resources\",\n    \"resources_desc\": \"Resources for Sunshine!\",\n    \"third_party_desc\": \"Third-party component notices\",\n    \"third_party_moonlight\": \"Friendly Links\",\n    \"third_party_notice\": \"Third Party Notice\",\n    \"tutorial\": \"Tutorial\",\n    \"tutorial_desc\": \"Detailed configuration and usage guide\",\n    \"view_license\": \"View full license\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Virtual Display\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"restart_title\": \"Restarting Sunshine\",\n    \"restart_desc\": \"Configuration saved. Sunshine is restarting to apply display settings.\",\n    \"restart_countdown_unit\": \"seconds\",\n    \"restart_go_now\": \"Go now\",\n    \"harmony_goto_repo\": \"Go to Repository\",\n    \"harmony_modal_desc\": \"For HarmonyOS NEXT Moonlight, please search for Moonlight V+ in the HarmonyOS App Store\",\n    \"harmony_modal_link_notice\": \"This link will redirect to the project repository\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"Basic settings are now active. You can start streaming with a Moonlight client right away!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"The Base Display (VDD) is Sunshine Foundation's built-in smart virtual display, supporting any resolution, frame rate, and HDR optimization. It is the preferred choice for screen-off streaming and extended display streaming.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Ensure Active\",\n    \"step3_ensure_active_desc\": \"Activates the display if it is not already active\",\n    \"step3_ensure_only_display\": \"Ensure Only Display\",\n    \"step3_ensure_only_display_desc\": \"Disables all other displays, only enables the specified display (recommended)\",\n    \"step3_ensure_primary\": \"Ensure Primary\",\n    \"step3_ensure_primary_desc\": \"Activates the display and sets it as the primary display\",\n    \"step3_ensure_secondary\": \"Secondary Streaming\",\n    \"step3_ensure_secondary_desc\": \"Uses only the virtual display for secondary extended streaming\",\n    \"step3_no_operation\": \"No Operation\",\n    \"step3_no_operation_desc\": \"No changes to display state; user must ensure display is ready\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Advanced\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Config Files\",\n    \"general\": \"General\",\n    \"input\": \"Input\",\n    \"network\": \"Network\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"If you need to immediately shut down Sunshine, you can use this function. Note that you will need to manually start it again after shutdown.\",\n    \"boom_sunshine_success\": \"Sunshine has been shut down\",\n    \"confirm_boom\": \"Really want to exit?\",\n    \"confirm_boom_desc\": \"So you really want to exit? Well, I can't stop you, go ahead and click again\",\n    \"confirm_logout\": \"Confirm logout?\",\n    \"confirm_logout_desc\": \"You will need to enter your password again to access the web UI.\",\n    \"copy_config\": \"Copy Config\",\n    \"copy_config_error\": \"Failed to copy config\",\n    \"copy_config_success\": \"Config copied to clipboard!\",\n    \"copy_logs\": \"Copy logs\",\n    \"download_logs\": \"Download logs\",\n    \"ai_diagnosis\": \"AI Diagnosis\",\n    \"ai_diagnosis_title\": \"AI Log Diagnosis\",\n    \"ai_config\": \"AI Configuration\",\n    \"ai_provider\": \"Provider\",\n    \"ai_model\": \"Model\",\n    \"ai_key_local\": \"API key is stored locally only and never uploaded\",\n    \"ai_start_diagnosis\": \"Start Diagnosis\",\n    \"ai_analyzing\": \"Analyzing...\",\n    \"ai_analyzing_logs\": \"Analyzing logs, please wait...\",\n    \"ai_error\": \"Analysis failed\",\n    \"ai_retry\": \"Retry\",\n    \"ai_result\": \"Diagnosis Result\",\n    \"ai_copy_result\": \"Copy\",\n    \"ai_reanalyze\": \"Re-analyze\",\n    \"force_close\": \"Force Close\",\n    \"force_close_desc\": \"If Moonlight complains about an app currently running, force closing the app should fix the issue.\",\n    \"force_close_error\": \"Error while closing Application\",\n    \"force_close_success\": \"Application Closed Successfully!\",\n    \"ignore_case\": \"Ignore case\",\n    \"logout\": \"Logout\",\n    \"logout_desc\": \"Logout. You may need to log in again.\",\n    \"logout_localhost_tip\": \"Current environment does not require login; logout will not trigger a credential prompt.\",\n    \"logs\": \"Logs\",\n    \"logs_desc\": \"See the logs uploaded by Sunshine\",\n    \"logs_find\": \"Find...\",\n    \"match_contains\": \"Contains\",\n    \"match_exact\": \"Exact\",\n    \"match_regex\": \"Regex\",\n    \"reopen_setup_wizard\": \"Reopen Setup Wizard\",\n    \"reopen_setup_wizard_desc\": \"Reopen the setup wizard page to reconfigure initial settings.\",\n    \"reopen_setup_wizard_error\": \"Failed to reopen setup wizard\",\n    \"reset_display_device_desc_windows\": \"If Sunshine is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.\\nThis could happen for various reasons: device is no longer available, has been plugged to a different port and so on.\",\n    \"reset_display_device_error_windows\": \"Error while resetting persistence!\",\n    \"reset_display_device_success_windows\": \"Success resetting persistence!\",\n    \"reset_display_device_windows\": \"Reset Display Memory\",\n    \"restart_sunshine\": \"Restart Sunshine\",\n    \"restart_sunshine_desc\": \"If Sunshine isn't working properly, you can try restarting it. This will terminate any running sessions.\",\n    \"restart_sunshine_success\": \"Sunshine is restarting\",\n    \"troubleshooting\": \"Troubleshooting\",\n    \"unpair_all\": \"Unpair All\",\n    \"unpair_all_error\": \"Error while unpairing\",\n    \"unpair_all_success\": \"All devices unpaired.\",\n    \"unpair_desc\": \"Remove your paired devices. Individually unpaired devices with an active session will remain connected, but cannot start or resume a session.\",\n    \"unpair_single_no_devices\": \"There are no paired devices.\",\n    \"unpair_single_success\": \"However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.\",\n    \"unpair_single_unknown\": \"Unknown Client\",\n    \"unpair_title\": \"Unpair Devices\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Confirm password\",\n    \"create_creds\": \"Before Getting Started, we need you to make a new username and password for accessing the Web UI.\",\n    \"create_creds_alert\": \"The credentials below are needed to access Sunshine's Web UI. Keep them safe, since you will never see them again!\",\n    \"creds_local_only\": \"Your credentials are stored locally offline and will never be uploaded to any server.\",\n    \"error\": \"Error!\",\n    \"greeting\": \"Welcome to Sunshine Foundation!\",\n    \"hide_password\": \"Hide password\",\n    \"login\": \"Login\",\n    \"network_error\": \"Network error, please check your connection\",\n    \"password\": \"Password\",\n    \"password_match\": \"Passwords match\",\n    \"password_mismatch\": \"Passwords do not match\",\n    \"server_error\": \"Server error\",\n    \"show_password\": \"Show password\",\n    \"success\": \"Success!\",\n    \"username\": \"Username\",\n    \"welcome_success\": \"This page will reload soon, your browser will ask you for the new credentials\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/en_GB.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Apply\",\n    \"auto\": \"Automatic\",\n    \"autodetect\": \"Autodetect (recommended)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"Cancel\",\n    \"close\": \"Close\",\n    \"copied\": \"Copied to clipboard\",\n    \"copy\": \"Copy\",\n    \"delete\": \"Delete\",\n    \"description\": \"Description\",\n    \"disabled\": \"Disabled\",\n    \"disabled_def\": \"Disabled (default)\",\n    \"dismiss\": \"Dismiss\",\n    \"do_cmd\": \"Do Command\",\n    \"download\": \"Download\",\n    \"edit\": \"Edit\",\n    \"elevated\": \"Elevated\",\n    \"enabled\": \"Enabled\",\n    \"enabled_def\": \"Enabled (default)\",\n    \"error\": \"Error!\",\n    \"no_changes\": \"No changes\",\n    \"note\": \"Note:\",\n    \"password\": \"Password\",\n    \"remove\": \"Remove\",\n    \"run_as\": \"Run as Admin\",\n    \"save\": \"Save\",\n    \"see_more\": \"See More\",\n    \"success\": \"Success!\",\n    \"undo_cmd\": \"Undo Command\",\n    \"username\": \"Username\",\n    \"warning\": \"Warning!\"\n  },\n  \"apps\": {\n    \"actions\": \"Actions\",\n    \"add_cmds\": \"Add Commands\",\n    \"add_new\": \"Add New\",\n    \"advanced_options\": \"Advanced Options\",\n    \"app_name\": \"Application Name\",\n    \"app_name_desc\": \"Application Name, as shown on Moonlight\",\n    \"applications_desc\": \"Applications are refreshed only when Client is restarted\",\n    \"applications_title\": \"Applications\",\n    \"auto_detach\": \"Continue streaming if the application exits quickly\",\n    \"auto_detach_desc\": \"This will attempt to automatically detect launcher-type apps that close quickly after launching another program or instance of themselves. When a launcher-type app is detected, it is treated as a detached app.\",\n    \"basic_info\": \"Basic Information\",\n    \"cmd\": \"Command\",\n    \"cmd_desc\": \"The main application to start. If blank, no application will be started.\",\n    \"cmd_examples_title\": \"Common examples:\",\n    \"cmd_note\": \"If the path to the command executable contains spaces, you must enclose it in quotes.\",\n    \"cmd_prep_desc\": \"A list of commands to be run before/after this application. If any of the prep-commands fail, starting the application is aborted.\",\n    \"cmd_prep_name\": \"Command Preparations\",\n    \"command_settings\": \"Command Settings\",\n    \"covers_found\": \"Covers Found\",\n    \"delete\": \"Delete\",\n    \"delete_confirm\": \"Are you sure you want to delete \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Detached Commands\",\n    \"detached_cmds_add\": \"Add Detached Command\",\n    \"detached_cmds_desc\": \"A list of commands to be run in the background.\",\n    \"detached_cmds_note\": \"If the path to the command executable contains spaces, you must enclose it in quotes.\",\n    \"detached_cmds_remove\": \"Remove Detached Command\",\n    \"edit\": \"Edit\",\n    \"env_app_id\": \"App ID\",\n    \"env_app_name\": \"App Name\",\n    \"env_client_audio_config\": \"The Audio Configuration requested by the client (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"The client has requested the option to optimize the game for optimal streaming (true/false)\",\n    \"env_client_fps\": \"The FPS requested by the client (int)\",\n    \"env_client_gcmap\": \"The requested gamepad mask, in a bitset/bitfield format (int)\",\n    \"env_client_hdr\": \"HDR is enabled by the client (true/false)\",\n    \"env_client_height\": \"The Height requested by the client (int)\",\n    \"env_client_host_audio\": \"The client has requested host audio (true/false)\",\n    \"env_client_name\": \"Client friendly name (string)\",\n    \"env_client_width\": \"The Width requested by the client (int)\",\n    \"env_displayplacer_example\": \"Example - displayplacer for Resolution Automation:\",\n    \"env_qres_example\": \"Example - QRes for Resolution Automation:\",\n    \"env_qres_path\": \"qres path\",\n    \"env_var_name\": \"Var Name\",\n    \"env_vars_about\": \"About Environment Variables\",\n    \"env_vars_desc\": \"All commands get these environment variables by default:\",\n    \"env_xrandr_example\": \"Example - Xrandr for Resolution Automation:\",\n    \"exit_timeout\": \"Exit Timeout\",\n    \"exit_timeout_desc\": \"Number of seconds to wait for all app processes to gracefully exit when requested to quit. If unset, the default is to wait up to 5 seconds. If set to zero or a negative value, the app will be immediately terminated.\",\n    \"file_selector_not_initialized\": \"File selector not initialized\",\n    \"find_cover\": \"Find Cover\",\n    \"form_invalid\": \"Please check required fields\",\n    \"form_valid\": \"Valid application\",\n    \"global_prep_desc\": \"Enable/Disable the execution of Global Prep Commands for this application.\",\n    \"global_prep_name\": \"Global Prep Commands\",\n    \"image\": \"Image\",\n    \"image_desc\": \"Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Sunshine will send default box image.\",\n    \"image_settings\": \"Image Settings\",\n    \"loading\": \"Loading...\",\n    \"menu_cmd_actions\": \"Actions\",\n    \"menu_cmd_add\": \"Add Menu Command\",\n    \"menu_cmd_command\": \"Command\",\n    \"menu_cmd_desc\": \"After configuration, these commands will be visible in the client's return menu, allowing quick execution of specific operations without interrupting the stream, such as launching helper programs.\\nExample: Display Name - Close your computer; Command - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Display Name\",\n    \"menu_cmd_drag_sort\": \"Drag to sort\",\n    \"menu_cmd_name\": \"Menu Commands\",\n    \"menu_cmd_placeholder_command\": \"Command\",\n    \"menu_cmd_placeholder_display_name\": \"Display name\",\n    \"menu_cmd_placeholder_execute\": \"Execute command\",\n    \"menu_cmd_placeholder_undo\": \"Undo command\",\n    \"menu_cmd_remove_menu\": \"Remove menu command\",\n    \"menu_cmd_remove_prep\": \"Remove prep command\",\n    \"mouse_mode\": \"Mouse Mode\",\n    \"mouse_mode_auto\": \"Auto (Global Setting)\",\n    \"mouse_mode_desc\": \"Select the mouse input method for this application. Auto uses the global setting, Virtual Mouse uses the HID driver, SendInput uses the Windows API.\",\n    \"mouse_mode_sendinput\": \"SendInput\",\n    \"mouse_mode_vmouse\": \"Virtual Mouse\",\n    \"name\": \"Name\",\n    \"output_desc\": \"The file where the output of the command is stored, if it is not specified, the output is ignored\",\n    \"output_name\": \"Output\",\n    \"run_as_desc\": \"This can be necessary for some applications that require administrator permissions to run properly.\",\n    \"scan_result_add_all\": \"Add All\",\n    \"scan_result_edit_title\": \"Add and edit\",\n    \"scan_result_filter_all\": \"All\",\n    \"scan_result_filter_epic_title\": \"Epic Games games\",\n    \"scan_result_filter_executable\": \"Executable\",\n    \"scan_result_filter_executable_title\": \"Executable file\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxy games\",\n    \"scan_result_filter_script\": \"Script\",\n    \"scan_result_filter_script_title\": \"Batch/Command script\",\n    \"scan_result_filter_shortcut\": \"Shortcut\",\n    \"scan_result_filter_shortcut_title\": \"Shortcut\",\n    \"scan_result_filter_steam_title\": \"Steam games\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Game\",\n    \"scan_result_games_only\": \"Games Only\",\n    \"scan_result_matched\": \"Matched: {count}\",\n    \"scan_result_no_apps\": \"No applications found to add\",\n    \"scan_result_no_matches\": \"No matching applications found\",\n    \"scan_result_quick_add_title\": \"Quick add\",\n    \"scan_result_remove_title\": \"Remove from list\",\n    \"scan_result_search_placeholder\": \"Search application name, command or path...\",\n    \"scan_result_show_all\": \"Show All\",\n    \"scan_result_title\": \"Scan Results\",\n    \"scan_result_try_different_keywords\": \"Try using different search keywords\",\n    \"scan_result_type_batch\": \"Batch\",\n    \"scan_result_type_command\": \"Command script\",\n    \"scan_result_type_executable\": \"Executable file\",\n    \"scan_result_type_shortcut\": \"Shortcut\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Search applications...\",\n    \"select\": \"Select\",\n    \"test_menu_cmd\": \"Test Command\",\n    \"test_menu_cmd_empty\": \"Command cannot be empty\",\n    \"test_menu_cmd_executing\": \"Executing command...\",\n    \"test_menu_cmd_failed\": \"Command execution failed\",\n    \"test_menu_cmd_success\": \"Command executed successfully!\",\n    \"use_desktop_image\": \"Use current desktop wallpaper\",\n    \"wait_all\": \"Continue streaming until all app processes exit\",\n    \"wait_all_desc\": \"This will continue streaming until all processes started by the app have terminated. When unchecked, streaming will stop when the initial app process exits, even if other app processes are still running.\",\n    \"working_dir\": \"Working Directory\",\n    \"working_dir_desc\": \"The working directory that should be passed to the process. For example, some applications use the working directory to search for configuration files. If not set, Sunshine will default to the parent directory of the command\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Adapter Name\",\n    \"adapter_name_desc_linux_1\": \"Manually specify a GPU to use for capture.\",\n    \"adapter_name_desc_linux_2\": \"to find all devices capable of VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Replace ``renderD129`` with the device from above to lists the name and capabilities of the device. To be supported by Sunshine, it needs to have at the very minimum:\",\n    \"adapter_name_desc_windows\": \"Manually specify a GPU to use for capture. If unset, the GPU is chosen automatically. Note: This GPU must have a display connected and powered on. If your laptop cannot enable direct GPU output, please set this to automatic.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"If the latest version of the virtual display is installed, it can automatically associate with the GPU binding\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Add\",\n    \"address_family\": \"Address Family\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Set the address family used by Sunshine\",\n    \"address_family_ipv4\": \"IPv4 only\",\n    \"always_send_scancodes\": \"Always Send Scancodes\",\n    \"always_send_scancodes_desc\": \"Sending scancodes enhances compatibility with games and apps but may result in incorrect keyboard input from certain clients that aren't using a US English keyboard layout. Enable if keyboard input is not working at all in certain applications. Disable if keys on the client are generating the wrong input on the host.\",\n    \"amd_coder\": \"AMF Coder (H264)\",\n    \"amd_coder_desc\": \"Allows you to select the entropy encoding to prioritize quality or encoding speed. H.264 only.\",\n    \"amd_enforce_hrd\": \"AMF Hypothetical Reference Decoder (HRD) Enforcement\",\n    \"amd_enforce_hrd_desc\": \"Increases the constraints on rate control to meet HRD model requirements. This greatly reduces bitrate overflows, but may cause encoding artifacts or reduced quality on certain cards.\",\n    \"amd_preanalysis\": \"AMF Preanalysis\",\n    \"amd_preanalysis_desc\": \"This enables rate-control preanalysis, which may increase quality at the expense of increased encoding latency.\",\n    \"amd_quality\": \"AMF Quality\",\n    \"amd_quality_balanced\": \"balanced -- balanced (default)\",\n    \"amd_quality_desc\": \"This controls the balance between encoding speed and quality.\",\n    \"amd_quality_group\": \"AMF Quality Settings\",\n    \"amd_quality_quality\": \"quality -- prefer quality\",\n    \"amd_quality_speed\": \"speed -- prefer speed\",\n    \"amd_qvbr_quality\": \"AMF QVBR Quality Level\",\n    \"amd_qvbr_quality_desc\": \"Quality level for QVBR rate control mode. Range: 1-51 (lower = better quality). Default: 23. Only applies when rate control is set to 'qvbr'.\",\n    \"amd_rc\": \"AMF Rate Control\",\n    \"amd_rc_cbr\": \"cbr -- constant bitrate\",\n    \"amd_rc_cqp\": \"cqp -- constant qp mode\",\n    \"amd_rc_desc\": \"This controls the rate control method to ensure we are not exceeding the client bitrate target. 'cqp' is not suitable for bitrate targeting, and other options besides 'vbr_latency' depend on HRD Enforcement to help constrain bitrate overflows.\",\n    \"amd_rc_group\": \"AMF Rate Control Settings\",\n    \"amd_rc_hqcbr\": \"hqcbr -- high quality constant bitrate\",\n    \"amd_rc_hqvbr\": \"hqvbr -- high quality variable bitrate\",\n    \"amd_rc_qvbr\": \"qvbr -- quality variable bitrate (uses QVBR quality level)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- latency constrained variable bitrate (default)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- peak constrained variable bitrate\",\n    \"amd_usage\": \"AMF Usage\",\n    \"amd_usage_desc\": \"This sets the base encoding profile. All options presented below will override a subset of the usage profile, but there are additional hidden settings applied that cannot be configured elsewhere.\",\n    \"amd_usage_lowlatency\": \"lowlatency - low latency (fast)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - low latency, high quality (fast)\",\n    \"amd_usage_transcoding\": \"transcoding -- transcoding (slowest)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency - ultra low latency (fastest)\",\n    \"amd_usage_webcam\": \"webcam -- webcam (slow)\",\n    \"amd_vbaq\": \"AMF Variance Based Adaptive Quantization (VBAQ)\",\n    \"amd_vbaq_desc\": \"The human visual system is typically less sensitive to artifacts in highly textured areas. In VBAQ mode, pixel variance is used to indicate the complexity of spatial textures, allowing the encoder to allocate more bits to smoother areas. Enabling this feature leads to improvements in subjective visual quality with some content.\",\n    \"amf_draw_mouse_cursor\": \"Draw a simple cursor when using AMF capture method\",\n    \"amf_draw_mouse_cursor_desc\": \"In some cases, using AMF capture will not display the mouse pointer. Enabling this option will draw a simple mouse pointer on the screen. Note: The position of the mouse pointer will only be updated when there is an update to the content screen, so in non-game scenarios such as on the desktop, you may observe sluggish mouse pointer movement.\",\n    \"apply_note\": \"Click 'Apply' to restart Sunshine and apply changes. This will terminate any running sessions.\",\n    \"audio_sink\": \"Audio Sink\",\n    \"audio_sink_desc_linux\": \"The name of the audio sink used for Audio Loopback. If you do not specify this variable, pulseaudio will select the default monitor device. You can find the name of the audio sink using either command:\",\n    \"audio_sink_desc_macos\": \"The name of the audio sink used for Audio Loopback. Sunshine can only access microphones on macOS due to system limitations. To stream system audio using Soundflower or BlackHole.\",\n    \"audio_sink_desc_windows\": \"Manually specify a specific audio device to capture. If unset, the device is chosen automatically. We strongly recommend leaving this field blank to use automatic device selection! If you have multiple audio devices with identical names, you can get the Device ID using the following command:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Speakers (High Definition Audio Device)\",\n    \"av1_mode\": \"AV1 Support\",\n    \"av1_mode_0\": \"Sunshine will advertise support for AV1 based on encoder capabilities (recommended)\",\n    \"av1_mode_1\": \"Sunshine will not advertise support for AV1\",\n    \"av1_mode_2\": \"Sunshine will advertise support for AV1 Main 8-bit profile\",\n    \"av1_mode_3\": \"Sunshine will advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles\",\n    \"av1_mode_desc\": \"Allows the client to request AV1 Main 8-bit or 10-bit video streams. AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.\",\n    \"back_button_timeout\": \"Home/Guide Button Emulation Timeout\",\n    \"back_button_timeout_desc\": \"If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated. If set to a value < 0 (default), holding the Back/Select button will not emulate the Home/Guide button.\",\n    \"bind_address\": \"Bind address (test feature)\",\n    \"bind_address_desc\": \"Set the specific IP address Sunshine will bind to. If left blank, Sunshine will bind to all available addresses.\",\n    \"capture\": \"Force a Specific Capture Method\",\n    \"capture_desc\": \"On automatic mode Sunshine will use the first one that works. NvFBC requires patched nvidia drivers.\",\n    \"capture_target\": \"Capture Target\",\n    \"capture_target_desc\": \"Select the type of target to capture. When selecting 'Window', you can capture a specific application window (such as AI frame interpolation software) instead of the entire display.\",\n    \"capture_target_display\": \"Display\",\n    \"capture_target_window\": \"Window\",\n    \"cert\": \"Certificate\",\n    \"cert_desc\": \"The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key.\",\n    \"channels\": \"Maximum Connected Clients\",\n    \"channels_desc_1\": \"Sunshine can allow a single streaming session to be shared with multiple clients simultaneously.\",\n    \"channels_desc_2\": \"Some hardware encoders may have limitations that reduce performance with multiple streams.\",\n    \"close_verify_safe\": \"Safe Verify Compatible with Old Clients\",\n    \"close_verify_safe_desc\": \"Old clients may not be able to connect to Sunshine, please disable this option or update the client\",\n    \"coder_cabac\": \"cabac -- context adaptive binary arithmetic coding - higher quality\",\n    \"coder_cavlc\": \"cavlc -- context adaptive variable-length coding - faster decode\",\n    \"configuration\": \"Configuration\",\n    \"controller\": \"Enable Gamepad Input\",\n    \"controller_desc\": \"Allows guests to control the host system with a gamepad / controller\",\n    \"credentials_file\": \"Credentials File\",\n    \"credentials_file_desc\": \"Store Username/Password separately from Sunshine's state file.\",\n    \"display_device_options_note_desc_windows\": \"Windows saves various display settings for each combination of currently active displays.\\nSunshine then applies changes to a display(-s) belonging to such a display combination.\\nIf you disconnect a device which was active when Sunshine applied the settings, the changes can not be\\nreverted back unless the combination can be activated again by the time Sunshine tries to revert changes!\",\n    \"display_device_options_note_windows\": \"Note about how settings are applied\",\n    \"display_device_options_windows\": \"Display device options\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Activates the display if it is not already active\",\n    \"display_device_prep_ensure_active_windows\": \"Activate the display automatically\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Disables all other displays, only enables the specified display\",\n    \"display_device_prep_ensure_only_display_windows\": \"Deactivate other displays and activate only the specified display\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Activates the display and sets it as the primary display\",\n    \"display_device_prep_ensure_primary_windows\": \"Activate the display automatically and make it a primary display\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Uses only the virtual display for secondary extended streaming\",\n    \"display_device_prep_ensure_secondary_windows\": \"Secondary Display Streaming (Virtual Display Only)\",\n    \"display_device_prep_no_operation_desc_windows\": \"No changes to display state; user must ensure display is ready\",\n    \"display_device_prep_no_operation_windows\": \"Disabled\",\n    \"display_device_prep_windows\": \"Display preparation\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"At least one \\\"received\\\" and one \\\"final\\\" value must be specified.\\nEmpty field in \\\"received\\\" section means \\\"match any\\\". Empty field in \\\"final\\\" section means \\\"keep received value\\\".\\nYou can match specific FPS value to specific resolution if you wish so...\\n\\nNote: if \\\"Optimize game settings\\\" option is not enabled on the Moonlight client, the rows containing resolution value(-s) are ignored.\",\n    \"display_mode_remapping_desc_windows\": \"Specify how a specific resolution and/or refresh rate should be remapped to other values.\\nYou can stream at lower resolution, while rendering at higher resolution on host for a supersampling effect.\\nOr you can stream at higher FPS while limiting the host to the lower refresh rate.\\nMatching is performed top to bottom. Once the entry is matched, others are no longer checked, but still validated.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Final refresh rate\",\n    \"display_mode_remapping_final_resolution_windows\": \"Final resolution\",\n    \"display_mode_remapping_optional\": \"optional\",\n    \"display_mode_remapping_received_fps_windows\": \"Received FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"Received resolution\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Note: if \\\"Optimize game settings\\\" option is not enabled on the Moonlight client, the remapping is disabled.\",\n    \"display_mode_remapping_windows\": \"Remap display modes\",\n    \"display_modes\": \"Display Modes\",\n    \"ds4_back_as_touchpad_click\": \"Map Back/Select to Touchpad Click\",\n    \"ds4_back_as_touchpad_click_desc\": \"When forcing DS4 emulation, map Back/Select to Touchpad Click\",\n    \"dsu_server_port\": \"DSU Server Port\",\n    \"dsu_server_port_desc\": \"DSU server listening port (default 26760). Sunshine will act as a DSU server to receive client connections and send motion data. Enable DSU server in your client(Yuzu,Ryujinx etc.) and set DSU server address(127.0.0.1) and port(26760)\",\n    \"enable_dsu_server\": \"Enable DSU Server\",\n    \"enable_dsu_server_desc\": \"Enable DSU server to receive client connections and send motion data\",\n    \"encoder\": \"Force a Specific Encoder\",\n    \"encoder_desc\": \"Force a specific encoder, otherwise Sunshine will select the best available option. Note: If you specify a hardware encoder on Windows, it must match the GPU where the display is connected.\",\n    \"encoder_software\": \"Software\",\n    \"experimental\": \"Experimental\",\n    \"experimental_features\": \"Experimental Features\",\n    \"external_ip\": \"External IP\",\n    \"external_ip_desc\": \"If no external IP address is given, Sunshine will automatically detect external IP\",\n    \"fec_percentage\": \"FEC Percentage\",\n    \"fec_percentage_desc\": \"Percentage of error correcting packets per data packet in each video frame. Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage.\",\n    \"ffmpeg_auto\": \"auto -- let ffmpeg decide (default)\",\n    \"file_apps\": \"Apps File\",\n    \"file_apps_desc\": \"The file where current apps of Sunshine are stored.\",\n    \"file_state\": \"State File\",\n    \"file_state_desc\": \"The file where current state of Sunshine is stored\",\n    \"fps\": \"Advertised FPS\",\n    \"gamepad\": \"Emulated Gamepad Type\",\n    \"gamepad_auto\": \"Automatic selection options\",\n    \"gamepad_desc\": \"Choose which type of gamepad to emulate on the host\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 Manual Options\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Manual DS4 options\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Command Preparations\",\n    \"global_prep_cmd_desc\": \"Configure a list of commands to be executed before or after running any application. If any of the specified preparation commands fail, the application launch process will be aborted.\",\n    \"hdr_luminance_analysis\": \"HDR Dynamic Metadata (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Enables per-frame GPU luminance analysis and injects HDR10+ (ST 2094-40) and HDR Vivid (CUVA) dynamic metadata into the encoded bitstream. This provides per-frame tone-mapping hints to supported displays (e.g. Huawei HDR Vivid terminals). Adds minor GPU overhead (~0.5-1.5ms/frame at high resolutions). Disable if you experience frame rate drops with HDR enabled; streaming will then use static HDR metadata only.\",\n    \"hdr_prep_automatic_windows\": \"Switch on/off the HDR mode as requested by the client\",\n    \"hdr_prep_no_operation_windows\": \"Disabled\",\n    \"hdr_prep_windows\": \"HDR state change\",\n    \"hevc_mode\": \"HEVC Support\",\n    \"hevc_mode_0\": \"Sunshine will advertise support for HEVC based on encoder capabilities (recommended)\",\n    \"hevc_mode_1\": \"Sunshine will not advertise support for HEVC\",\n    \"hevc_mode_2\": \"Sunshine will advertise support for HEVC Main profile\",\n    \"hevc_mode_3\": \"Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles\",\n    \"hevc_mode_desc\": \"Allows the client to request HEVC Main or HEVC Main10 video streams. HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.\",\n    \"high_resolution_scrolling\": \"High Resolution Scrolling Support\",\n    \"high_resolution_scrolling_desc\": \"When enabled, Sunshine will pass through high resolution scroll events from Moonlight clients. This can be useful to disable for older applications that scroll too fast with high resolution scroll events.\",\n    \"install_steam_audio_drivers\": \"Install Steam Audio Drivers\",\n    \"install_steam_audio_drivers_desc\": \"If Steam is installed, this will automatically install the Steam Streaming Speakers driver to support 5.1/7.1 surround sound and muting host audio.\",\n    \"key_repeat_delay\": \"Key Repeat Delay\",\n    \"key_repeat_delay_desc\": \"Control how fast keys will repeat themselves. The initial delay in milliseconds before repeating keys.\",\n    \"key_repeat_frequency\": \"Key Repeat Frequency\",\n    \"key_repeat_frequency_desc\": \"How often keys repeat every second. This configurable option supports decimals.\",\n    \"key_rightalt_to_key_win\": \"Map Right Alt key to Windows key\",\n    \"key_rightalt_to_key_win_desc\": \"It may be possible that you cannot send the Windows Key from Moonlight directly. In those cases it may be useful to make Sunshine think the Right Alt key is the Windows key\",\n    \"key_rightalt_to_key_windows\": \"Map Right Alt key to Windows key\",\n    \"keyboard\": \"Enable Keyboard Input\",\n    \"keyboard_desc\": \"Allows guests to control the host system with the keyboard\",\n    \"lan_encryption_mode\": \"LAN Encryption Mode\",\n    \"lan_encryption_mode_1\": \"Enabled for supported clients\",\n    \"lan_encryption_mode_2\": \"Required for all clients\",\n    \"lan_encryption_mode_desc\": \"This determines when encryption will be used when streaming over your local network. Encryption can reduce streaming performance, particularly on less powerful hosts and clients.\",\n    \"locale\": \"Locale\",\n    \"locale_desc\": \"The locale used for Sunshine's user interface.\",\n    \"log_level\": \"Log Level\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"Info\",\n    \"log_level_3\": \"Warning\",\n    \"log_level_4\": \"Error\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"None\",\n    \"log_level_desc\": \"The minimum log level printed to standard out\",\n    \"log_path\": \"Logfile Path\",\n    \"log_path_desc\": \"The file where the current logs of Sunshine are stored.\",\n    \"max_bitrate\": \"Maximum Bitrate\",\n    \"max_bitrate_desc\": \"The maximum bitrate (in Kbps) that Sunshine will encode the stream at. If set to 0, it will always use the bitrate requested by Moonlight.\",\n    \"max_fps_reached\": \"Maximum FPS values reached\",\n    \"max_resolutions_reached\": \"Maximum resolutions reached\",\n    \"mdns_broadcast\": \"Find this computer in the local network\",\n    \"mdns_broadcast_desc\": \"If this option is enabled, Sunshine will allow other devices to find this computer automatically. Moonlight must also be configured to find this computer automatically in the local network.\",\n    \"min_threads\": \"Minimum CPU Thread Count\",\n    \"min_threads_desc\": \"Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest value that can reliably encode at your desired streaming settings on your hardware.\",\n    \"minimum_fps_target\": \"Minimum FPS Target\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Miscellaneous options\",\n    \"motion_as_ds4\": \"Emulate a DS4 gamepad if the client gamepad reports motion sensors are present\",\n    \"motion_as_ds4_desc\": \"If disabled, motion sensors will not be taken into account during gamepad type selection.\",\n    \"mouse\": \"Enable Mouse Input\",\n    \"mouse_desc\": \"Allows guests to control the host system with the mouse\",\n    \"native_pen_touch\": \"Native Pen/Touch Support\",\n    \"native_pen_touch_desc\": \"When enabled, Sunshine will pass through native pen/touch events from Moonlight clients. This can be useful to disable for older applications without native pen/touch support.\",\n    \"no_fps\": \"No FPS values added\",\n    \"no_resolutions\": \"No resolutions added\",\n    \"notify_pre_releases\": \"PreRelease Notifications\",\n    \"notify_pre_releases_desc\": \"Whether to be notified of new pre-release versions of Sunshine\",\n    \"nvenc_h264_cavlc\": \"Prefer CAVLC over CABAC in H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Simpler form of entropy coding. CAVLC needs around 10% more bitrate for same quality. Only relevant for really old decoding devices.\",\n    \"nvenc_latency_over_power\": \"Prefer lower encoding latency over power savings\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine requests maximum GPU clock speed while streaming to reduce encoding latency. Disabling it is not recommended since this can lead to significantly increased encoding latency.\",\n    \"nvenc_lookahead_depth\": \"Lookahead depth\",\n    \"nvenc_lookahead_depth_desc\": \"Number of frames to look ahead during encoding (0-32). Lookahead improves encoding quality, especially in complex scenes, by providing better motion estimation and bitrate distribution. Higher values improve quality but increase encoding latency. Set to 0 to disable lookahead. Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_lookahead_level\": \"Lookahead level\",\n    \"nvenc_lookahead_level_0\": \"Level 0 (lowest quality, fastest)\",\n    \"nvenc_lookahead_level_1\": \"Level 1\",\n    \"nvenc_lookahead_level_2\": \"Level 2\",\n    \"nvenc_lookahead_level_3\": \"Level 3 (highest quality, slowest)\",\n    \"nvenc_lookahead_level_autoselect\": \"Auto-select (let driver choose optimal level)\",\n    \"nvenc_lookahead_level_desc\": \"Lookahead quality level. Higher levels improve quality at the expense of performance. This option only takes effect when lookahead_depth is greater than 0. Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_lookahead_level_disabled\": \"Disabled (same as level 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Present OpenGL/Vulkan on top of DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine can't capture fullscreen OpenGL and Vulkan programs at full frame rate unless they present on top of DXGI. This is system-wide setting that is reverted on sunshine program exit.\",\n    \"nvenc_preset\": \"Performance preset\",\n    \"nvenc_preset_1\": \"(fastest, default)\",\n    \"nvenc_preset_7\": \"(slowest)\",\n    \"nvenc_preset_desc\": \"Higher numbers improve compression (quality at given bitrate) at the cost of increased encoding latency. Recommended to change only when limited by network or decoder, otherwise similar effect can be accomplished by increasing bitrate.\",\n    \"nvenc_rate_control\": \"Rate control mode\",\n    \"nvenc_rate_control_cbr\": \"CBR (Constant Bitrate) - Low latency\",\n    \"nvenc_rate_control_desc\": \"Select rate control mode. CBR (Constant Bitrate) provides fixed bitrate for low latency streaming. VBR (Variable Bitrate) allows bitrate to vary based on scene complexity, providing better quality for complex scenes at the cost of variable bitrate.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Variable Bitrate) - Better quality\",\n    \"nvenc_realtime_hags\": \"Use realtime priority in hardware accelerated gpu scheduling\",\n    \"nvenc_realtime_hags_desc\": \"Currently NVIDIA drivers may freeze in encoder when HAGS is enabled, realtime priority is used and VRAM utilization is close to maximum. Disabling this option lowers the priority to high, sidestepping the freeze at the cost of reduced capture performance when the GPU is heavily loaded.\",\n    \"nvenc_spatial_aq\": \"Spatial AQ\",\n    \"nvenc_spatial_aq_desc\": \"Assign higher QP values to flat regions of the video. Recommended to enable when streaming at lower bitrates.\",\n    \"nvenc_spatial_aq_disabled\": \"Disabled (faster, default)\",\n    \"nvenc_spatial_aq_enabled\": \"Enabled (slower)\",\n    \"nvenc_split_encode\": \"Split frame encoding\",\n    \"nvenc_split_encode_desc\": \"Split the encoding of each video frame over multiple NVENC hardware units. Significantly reduces encoding latency with a marginal compression efficiency penalty. This option is ignored if your GPU has a singular NVENC unit.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Driver decides (default)\",\n    \"nvenc_split_encode_four_strips\": \"Force 4-strip split (requires 4+ NVENC engines)\",\n    \"nvenc_split_encode_three_strips\": \"Force 3-strip split (requires 3+ NVENC engines)\",\n    \"nvenc_split_encode_two_strips\": \"Force 2-strip split (requires 2+ NVENC engines)\",\n    \"nvenc_target_quality\": \"Target quality (VBR mode)\",\n    \"nvenc_target_quality_desc\": \"Target quality level for VBR mode (0-51 for H.264/HEVC, 0-63 for AV1). Lower values = higher quality. Set to 0 for automatic quality selection. Only used when rate control mode is VBR.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Two-pass mode\",\n    \"nvenc_twopass_desc\": \"Adds preliminary encoding pass. This allows to detect more motion vectors, better distribute bitrate across the frame and more strictly adhere to bitrate limits. Disabling it is not recommended since this can lead to occasional bitrate overshoot and subsequent packet loss.\",\n    \"nvenc_twopass_disabled\": \"Disabled (fastest, not recommended)\",\n    \"nvenc_twopass_full_res\": \"Full resolution (slower)\",\n    \"nvenc_twopass_quarter_res\": \"Quarter resolution (faster, default)\",\n    \"nvenc_vbv_increase\": \"Single-frame VBV/HRD percentage increase\",\n    \"nvenc_vbv_increase_desc\": \"By default sunshine uses single-frame VBV/HRD, which means any encoded video frame size is not expected to exceed requested bitrate divided by requested frame rate. Relaxing this restriction can be beneficial and act as low-latency variable bitrate, but may also lead to packet loss if the network doesn't have buffer headroom to handle bitrate spikes. Maximum accepted value is 400, which corresponds to 5x increased encoded video frame upper size limit.\",\n    \"origin_web_ui_allowed\": \"Origin Web UI Allowed\",\n    \"origin_web_ui_allowed_desc\": \"The origin of the remote endpoint address that is not denied access to Web UI\",\n    \"origin_web_ui_allowed_lan\": \"Only those in LAN may access Web UI\",\n    \"origin_web_ui_allowed_pc\": \"Only localhost may access Web UI\",\n    \"origin_web_ui_allowed_wan\": \"Anyone may access Web UI\",\n    \"output_name_desc_unix\": \"During Sunshine startup, you should see the list of detected displays. Note: You need to use the id value inside the parenthesis.\",\n    \"output_name_desc_windows\": \"Manually specify a display to use for capture. If unset, the primary display is captured. Note: If you specified a GPU above, this display must be connected to that GPU. The appropriate values can be found using the following command:\",\n    \"output_name_unix\": \"Display number\",\n    \"output_name_windows\": \"Display device specify\",\n    \"ping_timeout\": \"Ping Timeout\",\n    \"ping_timeout_desc\": \"How long to wait in milliseconds for data from moonlight before shutting down the stream\",\n    \"pkey\": \"Private Key\",\n    \"pkey_desc\": \"The private key used for the web UI and Moonlight client pairing. For best compatibility, this should be an RSA-2048 private key.\",\n    \"port\": \"Port\",\n    \"port_alert_1\": \"Sunshine cannot use ports below 1024!\",\n    \"port_alert_2\": \"Ports above 65535 are not available!\",\n    \"port_desc\": \"Set the family of ports used by Sunshine\",\n    \"port_http_port_note\": \"Use this port to connect with Moonlight.\",\n    \"port_note\": \"Note\",\n    \"port_port\": \"Port\",\n    \"port_protocol\": \"Protocol\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Exposing the Web UI to the internet is a security risk! Proceed at your own risk!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Quantization Parameter\",\n    \"qp_desc\": \"Some devices may not support Constant Bit Rate. For those devices, QP is used instead. Higher value means more compression, but less quality.\",\n    \"qsv_coder\": \"QuickSync Coder (H264)\",\n    \"qsv_preset\": \"QuickSync Preset\",\n    \"qsv_preset_fast\": \"faster (lower quality)\",\n    \"qsv_preset_faster\": \"fastest (lowest quality)\",\n    \"qsv_preset_medium\": \"medium (default)\",\n    \"qsv_preset_slow\": \"slow (good quality)\",\n    \"qsv_preset_slower\": \"slower (better quality)\",\n    \"qsv_preset_slowest\": \"slowest (best quality)\",\n    \"qsv_preset_veryfast\": \"fastest (lowest quality)\",\n    \"qsv_slow_hevc\": \"Allow Slow HEVC Encoding\",\n    \"qsv_slow_hevc_desc\": \"This can enable HEVC encoding on older Intel GPUs, at the cost of higher GPU usage and worse performance.\",\n    \"refresh_rate_change_automatic_windows\": \"Use FPS value provided by the client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Enter the refresh rate to be used\",\n    \"refresh_rate_change_manual_windows\": \"Use manually entered refresh rate\",\n    \"refresh_rate_change_no_operation_windows\": \"Disabled\",\n    \"refresh_rate_change_windows\": \"FPS change\",\n    \"res_fps_desc\": \"The display modes advertised by Sunshine. Some versions of Moonlight, such as Moonlight-nx (Switch), rely on these lists to ensure that the requested resolutions and fps are supported. This setting does not change how the screen stream is sent to Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Use resolution provided by the client\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_manual_windows\": \"Use manually entered resolution\",\n    \"resolution_change_no_operation_windows\": \"Disabled\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_windows\": \"Resolution change\",\n    \"resolutions\": \"Advertised Resolutions\",\n    \"restart_note\": \"Sunshine is restarting to apply changes.\",\n    \"sleep_mode\": \"Sleep Mode\",\n    \"sleep_mode_away\": \"Away Mode (Display Off, Instant Wake)\",\n    \"sleep_mode_desc\": \"Controls what happens when the client sends a sleep command. Suspend (S3): traditional sleep, low power but requires WOL to wake. Hibernate (S4): saves to disk, very low power. Away Mode: display turns off but system stays running for instant wake - ideal for game streaming servers.\",\n    \"sleep_mode_hibernate\": \"Hibernate (S4)\",\n    \"sleep_mode_suspend\": \"Suspend (S3 Sleep)\",\n    \"stream_audio\": \"Enable audio streaming\",\n    \"stream_audio_desc\": \"Disable this option to stop audio streaming.\",\n    \"stream_mic\": \"Enable Microphone Streaming\",\n    \"stream_mic_desc\": \"Disable this option to stop microphone streaming.\",\n    \"stream_mic_download_btn\": \"Download Virtual Microphone\",\n    \"stream_mic_download_confirm\": \"You are about to be redirected to the virtual microphone download page. Continue?\",\n    \"stream_mic_note\": \"This feature requires installing a virtual microphone\",\n    \"sunshine_name\": \"Sunshine Name\",\n    \"sunshine_name_desc\": \"The name displayed by Moonlight. If not specified, the PC's hostname is used\",\n    \"sw_preset\": \"SW Presets\",\n    \"sw_preset_desc\": \"Optimize the trade-off between encoding speed (encoded frames per second) and compression efficiency (quality per bit in the bitstream). Defaults to superfast.\",\n    \"sw_preset_fast\": \"fast\",\n    \"sw_preset_faster\": \"faster\",\n    \"sw_preset_medium\": \"medium\",\n    \"sw_preset_slow\": \"slow\",\n    \"sw_preset_slower\": \"slower\",\n    \"sw_preset_superfast\": \"superfast (default)\",\n    \"sw_preset_ultrafast\": \"ultrafast\",\n    \"sw_preset_veryfast\": \"veryfast\",\n    \"sw_preset_veryslow\": \"veryslow\",\n    \"sw_tune\": \"SW Tune\",\n    \"sw_tune_animation\": \"animation -- good for cartoons; uses higher deblocking and more reference frames\",\n    \"sw_tune_desc\": \"Tuning options, which are applied after the preset. Defaults to zerolatency.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- allows faster decoding by disabling certain filters\",\n    \"sw_tune_film\": \"film -- use for high quality movie content; lowers deblocking\",\n    \"sw_tune_grain\": \"grain -- preserves the grain structure in old, grainy film material\",\n    \"sw_tune_stillimage\": \"stillimage -- good for slideshow-like content\",\n    \"sw_tune_zerolatency\": \"zerolatency -- good for fast encoding and low-latency streaming (default)\",\n    \"system_tray\": \"Enable System Tray\",\n    \"system_tray_desc\": \"Whether to enable system tray. If enabled, Sunshine will display an icon in the system tray and can be controlled from the system tray.\",\n    \"touchpad_as_ds4\": \"Emulate a DS4 gamepad if the client gamepad reports a touchpad is present\",\n    \"touchpad_as_ds4_desc\": \"If disabled, touchpad presence will not be taken into account during gamepad type selection.\",\n    \"unsaved_changes_tooltip\": \"You have unsaved changes. Click to save.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Automatically configure port forwarding for streaming over the Internet\",\n    \"variable_refresh_rate\": \"Variable Refresh Rate (VRR)\",\n    \"variable_refresh_rate_desc\": \"Allow video stream framerate to match render framerate for VRR support. When enabled, encoding only occurs when new frames are available, allowing the stream to follow the actual render framerate.\",\n    \"vdd_reuse_desc_windows\": \"When enabled, all clients will share the same VDD (Virtual Display Device). When disabled (default), each client gets its own unique VDD. Enable this for faster client switching, but note that all clients will share the same display settings.\",\n    \"vdd_reuse_windows\": \"Reuse Same VDD for All Clients\",\n    \"virtual_display\": \"Virtual Display\",\n    \"virtual_mouse\": \"Virtual Mouse Driver\",\n    \"virtual_mouse_desc\": \"When enabled, Sunshine will use the Zako Virtual Mouse driver (if installed) to simulate mouse input at the HID level. This allows games using Raw Input to receive mouse events. When disabled or driver not installed, falls back to SendInput.\",\n    \"virtual_sink\": \"Virtual Sink\",\n    \"virtual_sink_desc\": \"Manually specify a virtual audio device to use. If unset, the device is chosen automatically. We strongly recommend leaving this field blank to use automatic device selection!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Install the virtual mouse driver?\",\n    \"vmouse_confirm_uninstall\": \"Uninstall the virtual mouse driver?\",\n    \"vmouse_install\": \"Install Driver\",\n    \"vmouse_installing\": \"Installing...\",\n    \"vmouse_note\": \"The virtual mouse driver requires separate installation. Please use the Sunshine Control Panel to install or manage the driver.\",\n    \"vmouse_refresh\": \"Refresh Status\",\n    \"vmouse_status_installed\": \"Installed (not active)\",\n    \"vmouse_status_not_installed\": \"Not Installed\",\n    \"vmouse_status_running\": \"Running\",\n    \"vmouse_uninstall\": \"Uninstall Driver\",\n    \"vmouse_uninstalling\": \"Uninstalling...\",\n    \"vt_coder\": \"VideoToolbox Coder\",\n    \"vt_realtime\": \"VideoToolbox Realtime Encoding\",\n    \"vt_software\": \"VideoToolbox Software Encoding\",\n    \"vt_software_allowed\": \"Allowed\",\n    \"vt_software_forced\": \"Forced\",\n    \"wan_encryption_mode\": \"WAN Encryption Mode\",\n    \"wan_encryption_mode_1\": \"Enabled for supported clients (default)\",\n    \"wan_encryption_mode_2\": \"Required for all clients\",\n    \"wan_encryption_mode_desc\": \"This determines when encryption will be used when streaming over the Internet. Encryption can reduce streaming performance, particularly on less powerful hosts and clients.\",\n    \"webhook_curl_command\": \"Command\",\n    \"webhook_curl_command_desc\": \"Copy the following command to your terminal to test if the webhook is working properly:\",\n    \"webhook_curl_copy_failed\": \"Copy failed, please manually select and copy\",\n    \"webhook_enabled\": \"Webhook Notifications\",\n    \"webhook_enabled_desc\": \"When enabled, Sunshine will send event notifications to the specified Webhook URL\",\n    \"webhook_group\": \"Webhook Notification Settings\",\n    \"webhook_skip_ssl_verify\": \"Skip SSL Certificate Verification\",\n    \"webhook_skip_ssl_verify_desc\": \"Skip SSL certificate verification for HTTPS connections, only for testing or self-signed certificates\",\n    \"webhook_test\": \"Test\",\n    \"webhook_test_failed\": \"Webhook test failed\",\n    \"webhook_test_failed_note\": \"Note: Please check if the URL is correct, or check the browser console for more information.\",\n    \"webhook_test_success\": \"Webhook test successful!\",\n    \"webhook_test_success_cors_note\": \"Note: Due to CORS restrictions, the server response status cannot be confirmed.\\nThe request has been sent. If the webhook is configured correctly, the message should have been delivered.\\n\\nSuggestion: Check the Network tab in your browser's developer tools for request details.\",\n    \"webhook_test_url_required\": \"Please enter Webhook URL first\",\n    \"webhook_timeout\": \"Request Timeout\",\n    \"webhook_timeout_desc\": \"Timeout for webhook requests in milliseconds, range 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"The URL to receive event notifications, supports HTTP/HTTPS protocols\",\n    \"wgc_checking_mode\": \"Checking...\",\n    \"wgc_checking_running_mode\": \"Checking running mode...\",\n    \"wgc_control_panel_only\": \"This feature is only available in Sunshine Control Panel\",\n    \"wgc_mode_switch_failed\": \"Failed to switch mode\",\n    \"wgc_mode_switch_started\": \"Mode switch initiated. If a UAC prompt appears, please click 'Yes' to confirm.\",\n    \"wgc_service_mode_warning\": \"WGC capture requires running in user mode. If currently running in service mode, please click the button above to switch to user mode.\",\n    \"wgc_switch_to_service_mode\": \"Switch to Service Mode\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Currently running in user mode. Click to switch to service mode.\",\n    \"wgc_switch_to_user_mode\": \"Switch to User Mode\",\n    \"wgc_switch_to_user_mode_tooltip\": \"WGC capture requires running in user mode. Click this button to switch to user mode.\",\n    \"wgc_user_mode_available\": \"Currently running in user mode. WGC capture is available.\",\n    \"window_title\": \"Window Title\",\n    \"window_title_desc\": \"The title of the window to capture (partial match, case-insensitive). If left empty, the current running application name will be used automatically.\",\n    \"window_title_placeholder\": \"e.g., Application Name\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine is a self-hosted game stream host for Moonlight.\",\n    \"download\": \"Download\",\n    \"installed_version_not_stable\": \"You are running a pre-release version of Sunshine. You may experience bugs or other issues. Please report any issues you encounter. Thank you for helping to make Sunshine a better software!\",\n    \"loading_latest\": \"Loading latest release...\",\n    \"new_pre_release\": \"A new Pre-Release Version is Available!\",\n    \"new_stable\": \"A new Stable Version is Available!\",\n    \"startup_errors\": \"<b>Attention!</b> Sunshine detected these errors during startup. We <b>STRONGLY RECOMMEND</b> fixing them before streaming.\",\n    \"update_download_confirm\": \"You are about to open the update download page in your browser. Continue?\",\n    \"version_dirty\": \"Thank you for helping to make Sunshine a better software!\",\n    \"version_latest\": \"You are running the latest version of Sunshine\",\n    \"view_logs\": \"View Logs\",\n    \"welcome\": \"Hello, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Applications\",\n    \"configuration\": \"Configuration\",\n    \"home\": \"Home\",\n    \"password\": \"Change Password\",\n    \"pin\": \"PIN\",\n    \"theme_auto\": \"Auto\",\n    \"theme_dark\": \"Dark\",\n    \"theme_light\": \"Light\",\n    \"toggle_theme\": \"Theme\",\n    \"troubleshoot\": \"Troubleshooting\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Confirm Password\",\n    \"current_creds\": \"Current Credentials\",\n    \"new_creds\": \"New Credentials\",\n    \"new_username_desc\": \"If not specified, the username will not change\",\n    \"password_change\": \"Password Change\",\n    \"success_msg\": \"Password has been changed successfully! This page will reload soon, your browser will ask you for the new credentials.\"\n  },\n  \"pin\": {\n    \"actions\": \"Actions\",\n    \"cancel_editing\": \"Cancel editing\",\n    \"client_name\": \"Name\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Confirm Delete\",\n    \"delete_client\": \"Delete client\",\n    \"delete_confirm_message\": \"Are you sure you want to delete <strong>{name}</strong>?\",\n    \"delete_warning\": \"This action cannot be undone.\",\n    \"device_name\": \"Device Name\",\n    \"device_size\": \"Device Size\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Large - TV\",\n    \"device_size_medium\": \"Medium - Tablet\",\n    \"device_size_small\": \"Small - Phone\",\n    \"edit_client_settings\": \"Edit client settings\",\n    \"hdr_profile\": \"HDR Profile\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Loading...\",\n    \"loading_clients\": \"Loading clients...\",\n    \"modify_in_gui\": \"Please modify in GUI\",\n    \"none\": \"-- None --\",\n    \"or_manual_pin\": \"or enter PIN manually\",\n    \"pair_failure\": \"Pairing Failed: Check if the PIN is typed correctly\",\n    \"pair_success\": \"Success! Please check Moonlight to continue\",\n    \"pin_pairing\": \"PIN Pairing\",\n    \"qr_expires_in\": \"Expires in\",\n    \"qr_generate\": \"Generate QR Code\",\n    \"qr_paired_success\": \"Paired successfully!\",\n    \"qr_pairing\": \"QR Code Pairing\",\n    \"qr_pairing_desc\": \"Generate a QR code for quick pairing. Scan it with the Moonlight client to pair automatically.\",\n    \"qr_pairing_warning\": \"Experimental feature. If pairing fails, please use the manual PIN pairing below. Note: This feature only works on LAN.\",\n    \"qr_refresh\": \"Refresh QR Code\",\n    \"remove_paired_devices_desc\": \"Remove your paired devices.\",\n    \"save_changes\": \"Save changes\",\n    \"save_failed\": \"Failed to save client settings. Please try again.\",\n    \"save_or_cancel_first\": \"Please save or cancel editing first\",\n    \"send\": \"Send\",\n    \"unknown_client\": \"Unknown Client\",\n    \"unpair_all_confirm\": \"Are you sure you want to unpair all clients? This action cannot be undone.\",\n    \"unsaved_changes\": \"Unsaved changes\",\n    \"warning_msg\": \"Make sure you have access to the client you are pairing with. This software can give total control to your computer, so be careful!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android Recommended\",\n    \"client_downloads\": \"Client Downloads\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"GitHub Discussions\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Join Community\",\n    \"join_group_desc\": \"Get help and share experience\",\n    \"legal\": \"Legal\",\n    \"legal_desc\": \"By continuing to use this software you agree to the terms and conditions in the following documents.\",\n    \"license\": \"License\",\n    \"lizardbyte_website\": \"LizardByte Website\",\n    \"official_website\": \"Official Website\",\n    \"official_website_title\": \"AlkaidLab - Official Website\",\n    \"open_source\": \"Open Source\",\n    \"open_source_desc\": \"Star & Fork to support the project\",\n    \"quick_start\": \"Quick Start\",\n    \"resources\": \"Resources\",\n    \"resources_desc\": \"Resources for Sunshine!\",\n    \"third_party_desc\": \"Third-party component notices\",\n    \"third_party_moonlight\": \"Friendly Links\",\n    \"third_party_notice\": \"Third Party Notice\",\n    \"tutorial\": \"Tutorial\",\n    \"tutorial_desc\": \"Detailed configuration and usage guide\",\n    \"view_license\": \"View full licence\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Virtual Display\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"Go to Repository\",\n    \"harmony_modal_desc\": \"For HarmonyOS NEXT Moonlight, please search for Moonlight V+ in the HarmonyOS App Store\",\n    \"harmony_modal_link_notice\": \"This link will redirect to the project repository\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"seconds\",\n    \"restart_desc\": \"Configuration saved. Sunshine is restarting to apply display settings.\",\n    \"restart_go_now\": \"Go now\",\n    \"restart_title\": \"Restarting Sunshine\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"Basic settings are now active. You can start streaming with a Moonlight client right away!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"The Base Display (VDD) is Sunshine Foundation's built-in smart virtual display, supporting any resolution, frame rate, and HDR optimization. It is the preferred choice for screen-off streaming and extended display streaming.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Ensure Active\",\n    \"step3_ensure_active_desc\": \"Activates the display if it is not already active\",\n    \"step3_ensure_only_display\": \"Ensure Only Display\",\n    \"step3_ensure_only_display_desc\": \"Disables all other displays, only enables the specified display (recommended)\",\n    \"step3_ensure_primary\": \"Ensure Primary\",\n    \"step3_ensure_primary_desc\": \"Activates the display and sets it as the primary display\",\n    \"step3_ensure_secondary\": \"Secondary Streaming\",\n    \"step3_ensure_secondary_desc\": \"Uses only the virtual display for secondary extended streaming\",\n    \"step3_no_operation\": \"No Operation\",\n    \"step3_no_operation_desc\": \"No changes to display state; user must ensure display is ready\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Advanced\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Config Files\",\n    \"general\": \"General\",\n    \"input\": \"Input\",\n    \"network\": \"Network\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analyzing...\",\n    \"ai_analyzing_logs\": \"Analyzing logs, please wait...\",\n    \"ai_config\": \"AI Configuration\",\n    \"ai_copy_result\": \"Copy\",\n    \"ai_diagnosis\": \"AI Diagnosis\",\n    \"ai_diagnosis_title\": \"AI Log Diagnosis\",\n    \"ai_error\": \"Analysis failed\",\n    \"ai_key_local\": \"API key is stored locally only and never uploaded\",\n    \"ai_model\": \"Model\",\n    \"ai_provider\": \"Provider\",\n    \"ai_reanalyze\": \"Re-analyze\",\n    \"ai_result\": \"Diagnosis Result\",\n    \"ai_retry\": \"Retry\",\n    \"ai_start_diagnosis\": \"Start Diagnosis\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"If you need to immediately shut down Sunshine, you can use this function. Note that you will need to manually start it again after shutdown.\",\n    \"boom_sunshine_success\": \"Sunshine has been shut down\",\n    \"confirm_boom\": \"Really want to exit?\",\n    \"confirm_boom_desc\": \"So you really want to exit? Well, I can't stop you, go ahead and click again\",\n    \"confirm_logout\": \"Confirm logout?\",\n    \"confirm_logout_desc\": \"You will need to enter your password again to access the web UI.\",\n    \"copy_config\": \"Copy Config\",\n    \"copy_config_error\": \"Failed to copy config\",\n    \"copy_config_success\": \"Config copied to clipboard!\",\n    \"copy_logs\": \"Copy logs\",\n    \"download_logs\": \"Download logs\",\n    \"force_close\": \"Force Close\",\n    \"force_close_desc\": \"If Moonlight complains about an app currently running, force closing the app should fix the issue.\",\n    \"force_close_error\": \"Error while closing Application\",\n    \"force_close_success\": \"Application Closed Successfully!\",\n    \"ignore_case\": \"Ignore case\",\n    \"logout\": \"Logout\",\n    \"logout_desc\": \"Logout. You may need to log in again.\",\n    \"logout_localhost_tip\": \"Current environment does not require login; logout will not trigger a credential prompt.\",\n    \"logs\": \"Logs\",\n    \"logs_desc\": \"See the logs uploaded by Sunshine\",\n    \"logs_find\": \"Find...\",\n    \"match_contains\": \"Contains\",\n    \"match_exact\": \"Exact\",\n    \"match_regex\": \"Regex\",\n    \"reopen_setup_wizard\": \"Reopen Setup Wizard\",\n    \"reopen_setup_wizard_desc\": \"Reopen the setup wizard page to reconfigure the initial settings.\",\n    \"reopen_setup_wizard_error\": \"Failed to reopen setup wizard\",\n    \"reset_display_device_desc_windows\": \"If Sunshine is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.\\nThis could happen for various reasons: device is no longer available, has been plugged to a different port and so on.\",\n    \"reset_display_device_error_windows\": \"Error while resetting persistence!\",\n    \"reset_display_device_success_windows\": \"Success resetting persistence!\",\n    \"reset_display_device_windows\": \"Reset Display Memory\",\n    \"restart_sunshine\": \"Restart Sunshine\",\n    \"restart_sunshine_desc\": \"If Sunshine isn't working properly, you can try restarting it. This will terminate any running sessions.\",\n    \"restart_sunshine_success\": \"Sunshine is restarting\",\n    \"troubleshooting\": \"Troubleshooting\",\n    \"unpair_all\": \"Unpair All\",\n    \"unpair_all_error\": \"Error while unpairing\",\n    \"unpair_all_success\": \"All devices unpaired.\",\n    \"unpair_desc\": \"Remove your paired devices. Individually unpaired devices with an active session will remain connected, but cannot start or resume a session.\",\n    \"unpair_single_no_devices\": \"There are no paired devices.\",\n    \"unpair_single_success\": \"However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.\",\n    \"unpair_single_unknown\": \"Unknown Client\",\n    \"unpair_title\": \"Unpair Devices\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Confirm password\",\n    \"create_creds\": \"Before Getting Started, we need you to make a new username and password for accessing the Web UI.\",\n    \"create_creds_alert\": \"The credentials below are needed to access Sunshine's Web UI. Keep them safe, since you will never see them again!\",\n    \"creds_local_only\": \"Your credentials are stored locally offline and will never be uploaded to any server.\",\n    \"error\": \"Error!\",\n    \"greeting\": \"Welcome to Sunshine Foundation!\",\n    \"hide_password\": \"Hide password\",\n    \"login\": \"Login\",\n    \"network_error\": \"Network error, please check your connection\",\n    \"password\": \"Password\",\n    \"password_match\": \"Passwords match\",\n    \"password_mismatch\": \"Passwords do not match\",\n    \"server_error\": \"Server error\",\n    \"show_password\": \"Show password\",\n    \"success\": \"Success!\",\n    \"username\": \"Username\",\n    \"welcome_success\": \"This page will reload soon, your browser will ask you for the new credentials\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/en_US.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Apply\",\n    \"auto\": \"Automatic\",\n    \"autodetect\": \"Autodetect (recommended)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"Cancel\",\n    \"close\": \"Close\",\n    \"copied\": \"Copied to clipboard\",\n    \"copy\": \"Copy\",\n    \"delete\": \"Delete\",\n    \"description\": \"Description\",\n    \"disabled\": \"Disabled\",\n    \"disabled_def\": \"Disabled (default)\",\n    \"dismiss\": \"Dismiss\",\n    \"do_cmd\": \"Do Command\",\n    \"download\": \"Download\",\n    \"edit\": \"Edit\",\n    \"elevated\": \"Elevated\",\n    \"enabled\": \"Enabled\",\n    \"enabled_def\": \"Enabled (default)\",\n    \"error\": \"Error!\",\n    \"no_changes\": \"No changes\",\n    \"note\": \"Note:\",\n    \"password\": \"Password\",\n    \"remove\": \"Remove\",\n    \"run_as\": \"Run as Admin\",\n    \"save\": \"Save\",\n    \"see_more\": \"See More\",\n    \"success\": \"Success!\",\n    \"undo_cmd\": \"Undo Command\",\n    \"username\": \"Username\",\n    \"warning\": \"Warning!\"\n  },\n  \"apps\": {\n    \"actions\": \"Actions\",\n    \"add_cmds\": \"Add Commands\",\n    \"add_new\": \"Add New\",\n    \"advanced_options\": \"Advanced Options\",\n    \"app_name\": \"Application Name\",\n    \"app_name_desc\": \"Application Name, as shown on Moonlight\",\n    \"applications_desc\": \"Applications are refreshed only when Client is restarted\",\n    \"applications_title\": \"Applications\",\n    \"auto_detach\": \"Continue streaming if the application exits quickly\",\n    \"auto_detach_desc\": \"This will attempt to automatically detect launcher-type apps that close quickly after launching another program or instance of themselves. When a launcher-type app is detected, it is treated as a detached app.\",\n    \"basic_info\": \"Basic Information\",\n    \"cmd\": \"Command\",\n    \"cmd_desc\": \"The main application to start. If blank, no application will be started.\",\n    \"cmd_examples_title\": \"Common examples:\",\n    \"cmd_note\": \"If the path to the command executable contains spaces, you must enclose it in quotes.\",\n    \"cmd_prep_desc\": \"A list of commands to be run before/after this application. If any of the prep-commands fail, starting the application is aborted.\",\n    \"cmd_prep_name\": \"Command Preparations\",\n    \"command_settings\": \"Command Settings\",\n    \"covers_found\": \"Covers Found\",\n    \"delete\": \"Delete\",\n    \"delete_confirm\": \"Are you sure you want to delete \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Detached Commands\",\n    \"detached_cmds_add\": \"Add Detached Command\",\n    \"detached_cmds_desc\": \"A list of commands to be run in the background.\",\n    \"detached_cmds_note\": \"If the path to the command executable contains spaces, you must enclose it in quotes.\",\n    \"detached_cmds_remove\": \"Remove Detached Command\",\n    \"edit\": \"Edit\",\n    \"env_app_id\": \"App ID\",\n    \"env_app_name\": \"App Name\",\n    \"env_client_audio_config\": \"The Audio Configuration requested by the client (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"The client has requested the option to optimize the game for optimal streaming (true/false)\",\n    \"env_client_fps\": \"The FPS requested by the client (int)\",\n    \"env_client_gcmap\": \"The requested gamepad mask, in a bitset/bitfield format (int)\",\n    \"env_client_hdr\": \"HDR is enabled by the client (true/false)\",\n    \"env_client_height\": \"The Height requested by the client (int)\",\n    \"env_client_host_audio\": \"The client has requested host audio (true/false)\",\n    \"env_client_name\": \"Client friendly name (string)\",\n    \"env_client_width\": \"The Width requested by the client (int)\",\n    \"env_displayplacer_example\": \"Example - displayplacer for Resolution Automation:\",\n    \"env_qres_example\": \"Example - QRes for Resolution Automation:\",\n    \"env_qres_path\": \"qres path\",\n    \"env_var_name\": \"Var Name\",\n    \"env_vars_about\": \"About Environment Variables\",\n    \"env_vars_desc\": \"All commands get these environment variables by default:\",\n    \"env_xrandr_example\": \"Example - Xrandr for Resolution Automation:\",\n    \"exit_timeout\": \"Exit Timeout\",\n    \"exit_timeout_desc\": \"Number of seconds to wait for all app processes to gracefully exit when requested to quit. If unset, the default is to wait up to 5 seconds. If set to 0, the app will be immediately terminated.\",\n    \"file_selector_not_initialized\": \"File selector not initialized\",\n    \"find_cover\": \"Find Cover\",\n    \"form_invalid\": \"Please check required fields\",\n    \"form_valid\": \"Valid application\",\n    \"global_prep_desc\": \"Enable/Disable the execution of Global Prep Commands for this application.\",\n    \"global_prep_name\": \"Global Prep Commands\",\n    \"image\": \"Image\",\n    \"image_desc\": \"Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Sunshine will send default box image.\",\n    \"image_settings\": \"Image Settings\",\n    \"loading\": \"Loading...\",\n    \"menu_cmd_actions\": \"Actions\",\n    \"menu_cmd_add\": \"Add Menu Command\",\n    \"menu_cmd_command\": \"Command\",\n    \"menu_cmd_desc\": \"After configuration, these commands will be visible in the client's return menu, allowing quick execution of specific operations without interrupting the stream, such as launching helper programs.\\nExample: Display Name - Close your computer; Command - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Display Name\",\n    \"menu_cmd_drag_sort\": \"Drag to sort\",\n    \"menu_cmd_name\": \"Menu Commands\",\n    \"menu_cmd_placeholder_command\": \"Command\",\n    \"menu_cmd_placeholder_display_name\": \"Display name\",\n    \"menu_cmd_placeholder_execute\": \"Execute command\",\n    \"menu_cmd_placeholder_undo\": \"Undo command\",\n    \"menu_cmd_remove_menu\": \"Remove menu command\",\n    \"menu_cmd_remove_prep\": \"Remove prep command\",\n    \"mouse_mode\": \"Mouse Mode\",\n    \"mouse_mode_auto\": \"Auto (Global Setting)\",\n    \"mouse_mode_desc\": \"Select the mouse input method for this application. Auto uses the global setting, Virtual Mouse uses the HID driver, SendInput uses the Windows API.\",\n    \"mouse_mode_sendinput\": \"SendInput\",\n    \"mouse_mode_vmouse\": \"Virtual Mouse\",\n    \"name\": \"Name\",\n    \"output_desc\": \"The file where the output of the command is stored, if it is not specified, the output is ignored\",\n    \"output_name\": \"Output\",\n    \"run_as_desc\": \"This can be necessary for some applications that require administrator permissions to run properly.\",\n    \"scan_result_add_all\": \"Add All\",\n    \"scan_result_edit_title\": \"Add and edit\",\n    \"scan_result_filter_all\": \"All\",\n    \"scan_result_filter_epic_title\": \"Epic Games games\",\n    \"scan_result_filter_executable\": \"Executable\",\n    \"scan_result_filter_executable_title\": \"Executable file\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxy games\",\n    \"scan_result_filter_script\": \"Script\",\n    \"scan_result_filter_script_title\": \"Batch/Command script\",\n    \"scan_result_filter_shortcut\": \"Shortcut\",\n    \"scan_result_filter_shortcut_title\": \"Shortcut\",\n    \"scan_result_filter_steam_title\": \"Steam games\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Game\",\n    \"scan_result_games_only\": \"Games Only\",\n    \"scan_result_matched\": \"Matched: {count}\",\n    \"scan_result_no_apps\": \"No applications found to add\",\n    \"scan_result_no_matches\": \"No matching applications found\",\n    \"scan_result_quick_add_title\": \"Quick add\",\n    \"scan_result_remove_title\": \"Remove from list\",\n    \"scan_result_search_placeholder\": \"Search application name, command or path...\",\n    \"scan_result_show_all\": \"Show All\",\n    \"scan_result_title\": \"Scan Results\",\n    \"scan_result_try_different_keywords\": \"Try using different search keywords\",\n    \"scan_result_type_batch\": \"Batch\",\n    \"scan_result_type_command\": \"Command script\",\n    \"scan_result_type_executable\": \"Executable file\",\n    \"scan_result_type_shortcut\": \"Shortcut\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Search applications...\",\n    \"select\": \"Select\",\n    \"test_menu_cmd\": \"Test Command\",\n    \"test_menu_cmd_empty\": \"Command cannot be empty\",\n    \"test_menu_cmd_executing\": \"Executing command...\",\n    \"test_menu_cmd_failed\": \"Command execution failed\",\n    \"test_menu_cmd_success\": \"Command executed successfully!\",\n    \"use_desktop_image\": \"Use current desktop wallpaper\",\n    \"wait_all\": \"Continue streaming until all app processes exit\",\n    \"wait_all_desc\": \"This will continue streaming until all processes started by the app have terminated. When unchecked, streaming will stop when the initial app process exits, even if other app processes are still running.\",\n    \"working_dir\": \"Working Directory\",\n    \"working_dir_desc\": \"The working directory that should be passed to the process. For example, some applications use the working directory to search for configuration files. If not set, Sunshine will default to the parent directory of the command\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Adapter Name\",\n    \"adapter_name_desc_linux_1\": \"Manually specify a GPU to use for capture.\",\n    \"adapter_name_desc_linux_2\": \"to find all devices capable of VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Replace ``renderD129`` with the device from above to lists the name and capabilities of the device. To be supported by Sunshine, it needs to have at the very minimum:\",\n    \"adapter_name_desc_windows\": \"Manually specify a GPU to use for capture. If unset, the GPU is chosen automatically. Note: This GPU must have a display connected and powered on. If your laptop cannot enable direct GPU output, please set this to automatic.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"If the latest version of the virtual display is installed, it can automatically associate with the GPU binding\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Add\",\n    \"address_family\": \"Address Family\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Set the address family used by Sunshine\",\n    \"address_family_ipv4\": \"IPv4 only\",\n    \"always_send_scancodes\": \"Always Send Scancodes\",\n    \"always_send_scancodes_desc\": \"Sending scancodes enhances compatibility with games and apps but may result in incorrect keyboard input from certain clients that aren't using a US English keyboard layout. Enable if keyboard input is not working at all in certain applications. Disable if keys on the client are generating the wrong input on the host.\",\n    \"amd_coder\": \"AMF Coder (H264)\",\n    \"amd_coder_desc\": \"Allows you to select the entropy encoding to prioritize quality or encoding speed. H.264 only.\",\n    \"amd_enforce_hrd\": \"AMF Hypothetical Reference Decoder (HRD) Enforcement\",\n    \"amd_enforce_hrd_desc\": \"Increases the constraints on rate control to meet HRD model requirements. This greatly reduces bitrate overflows, but may cause encoding artifacts or reduced quality on certain cards.\",\n    \"amd_preanalysis\": \"AMF Preanalysis\",\n    \"amd_preanalysis_desc\": \"This enables rate-control preanalysis, which may increase quality at the expense of increased encoding latency.\",\n    \"amd_quality\": \"AMF Quality\",\n    \"amd_quality_balanced\": \"balanced -- balanced (default)\",\n    \"amd_quality_desc\": \"This controls the balance between encoding speed and quality.\",\n    \"amd_quality_group\": \"AMF Quality Settings\",\n    \"amd_quality_quality\": \"quality -- prefer quality\",\n    \"amd_quality_speed\": \"speed -- prefer speed\",\n    \"amd_qvbr_quality\": \"AMF QVBR Quality Level\",\n    \"amd_qvbr_quality_desc\": \"Quality level for QVBR rate control mode. Range: 1-51 (lower = better quality). Default: 23. Only applies when rate control is set to 'qvbr'.\",\n    \"amd_rc\": \"AMF Rate Control\",\n    \"amd_rc_cbr\": \"cbr -- constant bitrate (recommended if HRD is enabled)\",\n    \"amd_rc_cqp\": \"cqp -- constant qp mode\",\n    \"amd_rc_desc\": \"This controls the rate control method to ensure we are not exceeding the client bitrate target. 'cqp' is not suitable for bitrate targeting, and other options besides 'vbr_latency' depend on HRD Enforcement to help constrain bitrate overflows.\",\n    \"amd_rc_group\": \"AMF Rate Control Settings\",\n    \"amd_rc_hqcbr\": \"hqcbr -- high quality constant bitrate\",\n    \"amd_rc_hqvbr\": \"hqvbr -- high quality variable bitrate\",\n    \"amd_rc_qvbr\": \"qvbr -- quality variable bitrate (uses QVBR quality level)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- latency constrained variable bitrate (recommended if HRD is disabled; default)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- peak constrained variable bitrate\",\n    \"amd_usage\": \"AMF Usage\",\n    \"amd_usage_desc\": \"This sets the base encoding profile. All options presented below will override a subset of the usage profile, but there are additional hidden settings applied that cannot be configured elsewhere.\",\n    \"amd_usage_lowlatency\": \"lowlatency - low latency (fastest)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - low latency, high quality (fast)\",\n    \"amd_usage_transcoding\": \"transcoding -- transcoding (slowest)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency - ultra low latency (fastest; default)\",\n    \"amd_usage_webcam\": \"webcam -- webcam (slow)\",\n    \"amd_vbaq\": \"AMF Variance Based Adaptive Quantization (VBAQ)\",\n    \"amd_vbaq_desc\": \"The human visual system is typically less sensitive to artifacts in highly textured areas. In VBAQ mode, pixel variance is used to indicate the complexity of spatial textures, allowing the encoder to allocate more bits to smoother areas. Enabling this feature leads to improvements in subjective visual quality with some content.\",\n    \"amf_draw_mouse_cursor\": \"Draw a simple cursor when using AMF capture method\",\n    \"amf_draw_mouse_cursor_desc\": \"In some cases, using AMF capture will not display the mouse pointer. Enabling this option will draw a simple mouse pointer on the screen. Note: The position of the mouse pointer will only be updated when there is an update to the content screen, so in non-game scenarios such as on the desktop, you may observe sluggish mouse pointer movement.\",\n    \"apply_note\": \"Click 'Apply' to restart Sunshine and apply changes. This will terminate any running sessions.\",\n    \"audio_sink\": \"Audio Sink\",\n    \"audio_sink_desc_linux\": \"The name of the audio sink used for Audio Loopback. If you do not specify this variable, pulseaudio will select the default monitor device. You can find the name of the audio sink using either command:\",\n    \"audio_sink_desc_macos\": \"The name of the audio sink used for Audio Loopback. Sunshine can only access microphones on macOS due to system limitations. To stream system audio using Soundflower or BlackHole.\",\n    \"audio_sink_desc_windows\": \"Manually specify a specific audio device to capture. If unset, the device is chosen automatically. We strongly recommend leaving this field blank to use automatic device selection! If you have multiple audio devices with identical names, you can get the Device ID using the following command:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Speakers (High Definition Audio Device)\",\n    \"av1_mode\": \"AV1 Support\",\n    \"av1_mode_0\": \"Sunshine will advertise support for AV1 based on encoder capabilities (recommended)\",\n    \"av1_mode_1\": \"Sunshine will not advertise support for AV1\",\n    \"av1_mode_2\": \"Sunshine will advertise support for AV1 Main 8-bit profile\",\n    \"av1_mode_3\": \"Sunshine will advertise support for AV1 Main 8-bit and 10-bit (HDR) profiles\",\n    \"av1_mode_desc\": \"Allows the client to request AV1 Main 8-bit or 10-bit video streams. AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.\",\n    \"back_button_timeout\": \"Home/Guide Button Emulation Timeout\",\n    \"back_button_timeout_desc\": \"If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated. If set to a value < 0 (default), holding the Back/Select button will not emulate the Home/Guide button.\",\n    \"bind_address\": \"Bind address (test feature)\",\n    \"bind_address_desc\": \"Set the specific IP address Sunshine will bind to. If left blank, Sunshine will bind to all available addresses.\",\n    \"capture\": \"Force a Specific Capture Method\",\n    \"capture_desc\": \"On automatic mode Sunshine will use the first one that works. NvFBC requires patched nvidia drivers.\",\n    \"capture_target\": \"Capture Target\",\n    \"capture_target_desc\": \"Select the type of target to capture. When selecting 'Window', you can capture a specific application window (such as AI frame interpolation software) instead of the entire display.\",\n    \"capture_target_display\": \"Display\",\n    \"capture_target_window\": \"Window\",\n    \"cert\": \"Certificate\",\n    \"cert_desc\": \"The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key.\",\n    \"channels\": \"Maximum Connected Clients\",\n    \"channels_desc_1\": \"Sunshine can allow a single streaming session to be shared with multiple clients simultaneously.\",\n    \"channels_desc_2\": \"Some hardware encoders may have limitations that reduce performance with multiple streams.\",\n    \"close_verify_safe\": \"Safe Verify Compatible with Old Clients\",\n    \"close_verify_safe_desc\": \"Old clients may not be able to connect to Sunshine, please disable this option or update the client\",\n    \"coder_cabac\": \"cabac -- context adaptive binary arithmetic coding - higher quality\",\n    \"coder_cavlc\": \"cavlc -- context adaptive variable-length coding - faster decode\",\n    \"configuration\": \"Configuration\",\n    \"controller\": \"Enable Gamepad Input\",\n    \"controller_desc\": \"Allows guests to control the host system with a gamepad / controller\",\n    \"credentials_file\": \"Credentials File\",\n    \"credentials_file_desc\": \"Store Username/Password separately from Sunshine's state file.\",\n    \"display_device_options_note_desc_windows\": \"Windows saves various display settings for each combination of currently active displays.\\nSunshine then applies changes to a display(-s) belonging to such a display combination.\\nIf you disconnect a device which was active when Sunshine applied the settings, the changes can not be\\nreverted back unless the combination can be activated again by the time Sunshine tries to revert changes!\",\n    \"display_device_options_note_windows\": \"Note about how settings are applied\",\n    \"display_device_options_windows\": \"Display device options\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Activates the display if it is not already active\",\n    \"display_device_prep_ensure_active_windows\": \"Activate the display automatically\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Disables all other displays, only enables the specified display\",\n    \"display_device_prep_ensure_only_display_windows\": \"Deactivate other displays and activate only the specified display\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Activates the display and sets it as the primary display\",\n    \"display_device_prep_ensure_primary_windows\": \"Activate the display automatically and make it a primary display\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Uses only the virtual display for secondary extended streaming\",\n    \"display_device_prep_ensure_secondary_windows\": \"Secondary Display Streaming (Virtual Display Only)\",\n    \"display_device_prep_no_operation_desc_windows\": \"No changes to display state; user must ensure display is ready\",\n    \"display_device_prep_no_operation_windows\": \"Disabled\",\n    \"display_device_prep_windows\": \"Display preparation\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"At least one \\\"received\\\" and one \\\"final\\\" value must be specified.\\nEmpty field in \\\"received\\\" section means \\\"match any\\\". Empty field in \\\"final\\\" section means \\\"keep received value\\\".\\nYou can match specific FPS value to specific resolution if you wish so...\\n\\nNote: if \\\"Optimize game settings\\\" option is not enabled on the Moonlight client, the rows containing resolution value(-s) are ignored.\",\n    \"display_mode_remapping_desc_windows\": \"Specify how a specific resolution and/or refresh rate should be remapped to other values.\\nYou can stream at lower resolution, while rendering at higher resolution on host for a supersampling effect.\\nOr you can stream at higher FPS while limiting the host to the lower refresh rate.\\nMatching is performed top to bottom. Once the entry is matched, others are no longer checked, but still validated.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Final refresh rate\",\n    \"display_mode_remapping_final_resolution_windows\": \"Final resolution\",\n    \"display_mode_remapping_optional\": \"optional\",\n    \"display_mode_remapping_received_fps_windows\": \"Received FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"Received resolution\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Note: if \\\"Optimize game settings\\\" option is not enabled on the Moonlight client, the remapping is disabled.\",\n    \"display_mode_remapping_windows\": \"Remap display modes\",\n    \"display_modes\": \"Display Modes\",\n    \"ds4_back_as_touchpad_click\": \"Map Back/Select to Touchpad Click\",\n    \"ds4_back_as_touchpad_click_desc\": \"When forcing DS4 emulation, map Back/Select to Touchpad Click\",\n    \"dsu_server_port\": \"DSU Server Port\",\n    \"dsu_server_port_desc\": \"DSU server listening port (default 26760). Sunshine will act as a DSU server to receive client connections and send motion data. Enable DSU server in your client(Yuzu,Ryujinx etc.) and set DSU server address(127.0.0.1) and port(26760)\",\n    \"enable_dsu_server\": \"Enable DSU Server\",\n    \"enable_dsu_server_desc\": \"Enable DSU server to receive client connections and send motion data\",\n    \"encoder\": \"Force a Specific Encoder\",\n    \"encoder_desc\": \"Force a specific encoder, otherwise Sunshine will select the best available option. Note: If you specify a hardware encoder on Windows, it must match the GPU where the display is connected.\",\n    \"encoder_software\": \"Software\",\n    \"experimental\": \"Experimental\",\n    \"experimental_features\": \"Experimental Features\",\n    \"external_ip\": \"External IP\",\n    \"external_ip_desc\": \"If no external IP address is given, Sunshine will automatically detect external IP\",\n    \"fec_percentage\": \"FEC Percentage\",\n    \"fec_percentage_desc\": \"Percentage of error correcting packets per data packet in each video frame. Higher values can correct for more network packet loss, but at the cost of increasing bandwidth usage.\",\n    \"ffmpeg_auto\": \"auto -- let ffmpeg decide (default)\",\n    \"file_apps\": \"Apps File\",\n    \"file_apps_desc\": \"The file where current apps of Sunshine are stored.\",\n    \"file_state\": \"State File\",\n    \"file_state_desc\": \"The file where current state of Sunshine is stored\",\n    \"fps\": \"Advertised FPS\",\n    \"gamepad\": \"Emulated Gamepad Type\",\n    \"gamepad_auto\": \"Automatic selection options\",\n    \"gamepad_desc\": \"Choose which type of gamepad to emulate on the host\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 Manual Options\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Manual DS4 options\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Command Preparations\",\n    \"global_prep_cmd_desc\": \"Configure a list of commands to be executed before or after running any application. If any of the specified preparation commands fail, the application launch process will be aborted.\",\n    \"hdr_luminance_analysis\": \"HDR Dynamic Metadata (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Enables per-frame GPU luminance analysis and injects HDR10+ (ST 2094-40) and HDR Vivid (CUVA) dynamic metadata into the encoded bitstream. This provides per-frame tone-mapping hints to supported displays (e.g. Huawei HDR Vivid terminals). Adds minor GPU overhead (~0.5-1.5ms/frame at high resolutions). Disable if you experience frame rate drops with HDR enabled; streaming will then use static HDR metadata only.\",\n    \"hdr_prep_automatic_windows\": \"Switch on/off the HDR mode as requested by the client\",\n    \"hdr_prep_no_operation_windows\": \"Disabled\",\n    \"hdr_prep_windows\": \"HDR state change\",\n    \"hevc_mode\": \"HEVC Support\",\n    \"hevc_mode_0\": \"Sunshine will advertise support for HEVC based on encoder capabilities (recommended)\",\n    \"hevc_mode_1\": \"Sunshine will not advertise support for HEVC\",\n    \"hevc_mode_2\": \"Sunshine will advertise support for HEVC Main profile\",\n    \"hevc_mode_3\": \"Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles\",\n    \"hevc_mode_desc\": \"Allows the client to request HEVC Main or HEVC Main10 video streams. HEVC is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.\",\n    \"high_resolution_scrolling\": \"High Resolution Scrolling Support\",\n    \"high_resolution_scrolling_desc\": \"When enabled, Sunshine will pass through high resolution scroll events from Moonlight clients. This can be useful to disable for older applications that scroll too fast with high resolution scroll events.\",\n    \"install_steam_audio_drivers\": \"Install Steam Audio Drivers\",\n    \"install_steam_audio_drivers_desc\": \"If Steam is installed, this will automatically install the Steam Streaming Speakers driver to support 5.1/7.1 surround sound and muting host audio.\",\n    \"key_repeat_delay\": \"Key Repeat Delay\",\n    \"key_repeat_delay_desc\": \"Control how fast keys will repeat themselves. The initial delay in milliseconds before repeating keys.\",\n    \"key_repeat_frequency\": \"Key Repeat Frequency\",\n    \"key_repeat_frequency_desc\": \"How often keys repeat every second. This configurable option supports decimals.\",\n    \"key_rightalt_to_key_win\": \"Map Right Alt key to Windows key\",\n    \"key_rightalt_to_key_win_desc\": \"It may be possible that you cannot send the Windows Key from Moonlight directly. In those cases it may be useful to make Sunshine think the Right Alt key is the Windows key\",\n    \"key_rightalt_to_key_windows\": \"Map Right Alt key to Windows key\",\n    \"keyboard\": \"Enable Keyboard Input\",\n    \"keyboard_desc\": \"Allows guests to control the host system with the keyboard\",\n    \"lan_encryption_mode\": \"LAN Encryption Mode\",\n    \"lan_encryption_mode_1\": \"Enabled for supported clients\",\n    \"lan_encryption_mode_2\": \"Required for all clients\",\n    \"lan_encryption_mode_desc\": \"This determines when encryption will be used when streaming over your local network. Encryption can reduce streaming performance, particularly on less powerful hosts and clients.\",\n    \"locale\": \"Locale\",\n    \"locale_desc\": \"The locale used for Sunshine's user interface.\",\n    \"log_level\": \"Log Level\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"Info\",\n    \"log_level_3\": \"Warning\",\n    \"log_level_4\": \"Error\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"None\",\n    \"log_level_desc\": \"The minimum log level printed to standard out\",\n    \"log_path\": \"Logfile Path\",\n    \"log_path_desc\": \"The file where the current logs of Sunshine are stored.\",\n    \"max_bitrate\": \"Maximum Bitrate\",\n    \"max_bitrate_desc\": \"The maximum bitrate (in Kbps) that Sunshine will encode the stream at. If set to 0, it will always use the bitrate requested by Moonlight.\",\n    \"max_fps_reached\": \"Maximum FPS values reached\",\n    \"max_resolutions_reached\": \"Maximum resolutions reached\",\n    \"mdns_broadcast\": \"Find this computer in the local network\",\n    \"mdns_broadcast_desc\": \"If this option is enabled, Sunshine will allow other devices to find this computer automatically. Moonlight must also be configured to find this computer automatically in the local network.\",\n    \"min_threads\": \"Minimum CPU Thread Count\",\n    \"min_threads_desc\": \"Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest value that can reliably encode at your desired streaming settings on your hardware.\",\n    \"minimum_fps_target\": \"Minimum FPS Target\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Miscellaneous options\",\n    \"motion_as_ds4\": \"Emulate a DS4 gamepad if the client gamepad reports motion sensors are present\",\n    \"motion_as_ds4_desc\": \"If disabled, motion sensors will not be taken into account during gamepad type selection.\",\n    \"mouse\": \"Enable Mouse Input\",\n    \"mouse_desc\": \"Allows guests to control the host system with the mouse\",\n    \"native_pen_touch\": \"Native Pen/Touch Support\",\n    \"native_pen_touch_desc\": \"When enabled, Sunshine will pass through native pen/touch events from Moonlight clients. This can be useful to disable for older applications without native pen/touch support.\",\n    \"no_fps\": \"No FPS values added\",\n    \"no_resolutions\": \"No resolutions added\",\n    \"notify_pre_releases\": \"PreRelease Notifications\",\n    \"notify_pre_releases_desc\": \"Whether to be notified of new pre-release versions of Sunshine\",\n    \"nvenc_h264_cavlc\": \"Prefer CAVLC over CABAC in H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Simpler form of entropy coding. CAVLC needs around 10% more bitrate for same quality. Only relevant for really old decoding devices.\",\n    \"nvenc_latency_over_power\": \"Prefer lower encoding latency over power savings\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine requests maximum GPU clock speed while streaming to reduce encoding latency. Disabling it is not recommended since this can lead to significantly increased encoding latency.\",\n    \"nvenc_lookahead_depth\": \"Lookahead depth\",\n    \"nvenc_lookahead_depth_desc\": \"Number of frames to look ahead during encoding (0-32). Lookahead improves encoding quality, especially in complex scenes, by providing better motion estimation and bitrate distribution. Higher values improve quality but increase encoding latency. Set to 0 to disable lookahead. Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_lookahead_level\": \"Lookahead level\",\n    \"nvenc_lookahead_level_0\": \"Level 0 (lowest quality, fastest)\",\n    \"nvenc_lookahead_level_1\": \"Level 1\",\n    \"nvenc_lookahead_level_2\": \"Level 2\",\n    \"nvenc_lookahead_level_3\": \"Level 3 (highest quality, slowest)\",\n    \"nvenc_lookahead_level_autoselect\": \"Auto-select (let driver choose optimal level)\",\n    \"nvenc_lookahead_level_desc\": \"Lookahead quality level. Higher levels improve quality at the expense of performance. This option only takes effect when lookahead_depth is greater than 0. Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_lookahead_level_disabled\": \"Disabled (same as level 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Present OpenGL/Vulkan on top of DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine can't capture fullscreen OpenGL and Vulkan programs at full frame rate unless they present on top of DXGI. This is system-wide setting that is reverted on sunshine program exit.\",\n    \"nvenc_preset\": \"Performance preset\",\n    \"nvenc_preset_1\": \"(fastest, default)\",\n    \"nvenc_preset_7\": \"(slowest)\",\n    \"nvenc_preset_desc\": \"Higher numbers improve compression (quality at given bitrate) at the cost of increased encoding latency. Recommended to change only when limited by network or decoder, otherwise similar effect can be accomplished by increasing bitrate.\",\n    \"nvenc_rate_control\": \"Rate control mode\",\n    \"nvenc_rate_control_cbr\": \"CBR (Constant Bitrate) - Low latency\",\n    \"nvenc_rate_control_desc\": \"Select rate control mode. CBR (Constant Bitrate) provides fixed bitrate for low latency streaming. VBR (Variable Bitrate) allows bitrate to vary based on scene complexity, providing better quality for complex scenes at the cost of variable bitrate.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Variable Bitrate) - Better quality\",\n    \"nvenc_realtime_hags\": \"Use realtime priority in hardware accelerated gpu scheduling\",\n    \"nvenc_realtime_hags_desc\": \"Currently NVIDIA drivers may freeze in encoder when HAGS is enabled, realtime priority is used and VRAM utilization is close to maximum. Disabling this option lowers the priority to high, sidestepping the freeze at the cost of reduced capture performance when the GPU is heavily loaded.\",\n    \"nvenc_spatial_aq\": \"Spatial AQ\",\n    \"nvenc_spatial_aq_desc\": \"Assign higher QP values to flat regions of the video. Recommended to enable when streaming at lower bitrates.\",\n    \"nvenc_spatial_aq_disabled\": \"Disabled (faster, default)\",\n    \"nvenc_spatial_aq_enabled\": \"Enabled (slower)\",\n    \"nvenc_split_encode\": \"Split frame encoding\",\n    \"nvenc_split_encode_desc\": \"Split the encoding of each video frame over multiple NVENC hardware units. Significantly reduces encoding latency with a marginal compression efficiency penalty. This option is ignored if your GPU has a singular NVENC unit.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Driver decides (default)\",\n    \"nvenc_split_encode_four_strips\": \"Force 4-strip split (requires 4+ NVENC engines)\",\n    \"nvenc_split_encode_three_strips\": \"Force 3-strip split (requires 3+ NVENC engines)\",\n    \"nvenc_split_encode_two_strips\": \"Force 2-strip split (requires 2+ NVENC engines)\",\n    \"nvenc_target_quality\": \"Target quality (VBR mode)\",\n    \"nvenc_target_quality_desc\": \"Target quality level for VBR mode (0-51 for H.264/HEVC, 0-63 for AV1). Lower values = higher quality. Set to 0 for automatic quality selection. Only used when rate control mode is VBR.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Two-pass mode\",\n    \"nvenc_twopass_desc\": \"Adds preliminary encoding pass. This allows to detect more motion vectors, better distribute bitrate across the frame and more strictly adhere to bitrate limits. Disabling it is not recommended since this can lead to occasional bitrate overshoot and subsequent packet loss.\",\n    \"nvenc_twopass_disabled\": \"Disabled (fastest, not recommended)\",\n    \"nvenc_twopass_full_res\": \"Full resolution (slower)\",\n    \"nvenc_twopass_quarter_res\": \"Quarter resolution (faster, default)\",\n    \"nvenc_vbv_increase\": \"Single-frame VBV/HRD percentage increase\",\n    \"nvenc_vbv_increase_desc\": \"By default sunshine uses single-frame VBV/HRD, which means any encoded video frame size is not expected to exceed requested bitrate divided by requested frame rate. Relaxing this restriction can be beneficial and act as low-latency variable bitrate, but may also lead to packet loss if the network doesn't have buffer headroom to handle bitrate spikes. Maximum accepted value is 400, which corresponds to 5x increased encoded video frame upper size limit.\",\n    \"origin_web_ui_allowed\": \"Origin Web UI Allowed\",\n    \"origin_web_ui_allowed_desc\": \"The origin of the remote endpoint address that is not denied access to Web UI\",\n    \"origin_web_ui_allowed_lan\": \"Only those in LAN may access Web UI\",\n    \"origin_web_ui_allowed_pc\": \"Only localhost may access Web UI\",\n    \"origin_web_ui_allowed_wan\": \"Anyone may access Web UI\",\n    \"output_name_desc_unix\": \"During Sunshine startup, you should see the list of detected displays. Note: You need to use the id value inside the parenthesis. Below is an example; the actual output can be found in the Troubleshooting tab.\",\n    \"output_name_desc_windows\": \"Manually specify a display to use for capture. If unset, the primary display is captured. Note: If you specified a GPU above, this display must be connected to that GPU. The appropriate values can be found using the following command:\",\n    \"output_name_unix\": \"Display number\",\n    \"output_name_windows\": \"Display device specify\",\n    \"ping_timeout\": \"Ping Timeout\",\n    \"ping_timeout_desc\": \"How long to wait in milliseconds for data from moonlight before shutting down the stream\",\n    \"pkey\": \"Private Key\",\n    \"pkey_desc\": \"The private key used for the web UI and Moonlight client pairing. For best compatibility, this should be an RSA-2048 private key.\",\n    \"port\": \"Port\",\n    \"port_alert_1\": \"Sunshine cannot use ports below 1024!\",\n    \"port_alert_2\": \"Ports above 65535 are not available!\",\n    \"port_desc\": \"Set the family of ports used by Sunshine\",\n    \"port_http_port_note\": \"Use this port to connect with Moonlight.\",\n    \"port_note\": \"Note\",\n    \"port_port\": \"Port\",\n    \"port_protocol\": \"Protocol\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Exposing the Web UI to the internet is a security risk! Proceed at your own risk!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Quantization Parameter\",\n    \"qp_desc\": \"Some devices may not support Constant Bit Rate. For those devices, QP is used instead. Higher value means more compression, but less quality.\",\n    \"qsv_coder\": \"QuickSync Coder (H264)\",\n    \"qsv_preset\": \"QuickSync Preset\",\n    \"qsv_preset_fast\": \"fast (low quality)\",\n    \"qsv_preset_faster\": \"faster (lower quality)\",\n    \"qsv_preset_medium\": \"medium (default)\",\n    \"qsv_preset_slow\": \"slow (good quality)\",\n    \"qsv_preset_slower\": \"slower (better quality)\",\n    \"qsv_preset_slowest\": \"slowest (best quality)\",\n    \"qsv_preset_veryfast\": \"fastest (lowest quality)\",\n    \"qsv_slow_hevc\": \"Allow Slow HEVC Encoding\",\n    \"qsv_slow_hevc_desc\": \"This can enable HEVC encoding on older Intel GPUs, at the cost of higher GPU usage and worse performance.\",\n    \"refresh_rate_change_automatic_windows\": \"Use FPS value provided by the client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Enter the refresh rate to be used\",\n    \"refresh_rate_change_manual_windows\": \"Use manually entered refresh rate\",\n    \"refresh_rate_change_no_operation_windows\": \"Disabled\",\n    \"refresh_rate_change_windows\": \"FPS change\",\n    \"res_fps_desc\": \"The display modes advertised by Sunshine. Some versions of Moonlight, such as Moonlight-nx (Switch), rely on these lists to ensure that the requested resolutions and fps are supported. This setting does not change how the screen stream is sent to Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Use resolution provided by the client\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_manual_windows\": \"Use manually entered resolution\",\n    \"resolution_change_no_operation_windows\": \"Disabled\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_windows\": \"Resolution change\",\n    \"resolutions\": \"Advertised Resolutions\",\n    \"restart_note\": \"Sunshine is restarting to apply changes.\",\n    \"sleep_mode\": \"Sleep Mode\",\n    \"sleep_mode_away\": \"Away Mode (Display Off, Instant Wake)\",\n    \"sleep_mode_desc\": \"Controls what happens when the client sends a sleep command. Suspend (S3): traditional sleep, low power but requires WOL to wake. Hibernate (S4): saves to disk, very low power. Away Mode: display turns off but system stays running for instant wake - ideal for game streaming servers.\",\n    \"sleep_mode_hibernate\": \"Hibernate (S4)\",\n    \"sleep_mode_suspend\": \"Suspend (S3 Sleep)\",\n    \"stream_audio\": \"Enable audio streaming\",\n    \"stream_audio_desc\": \"Disable this option to stop audio streaming.\",\n    \"stream_mic\": \"Enable Microphone Streaming\",\n    \"stream_mic_desc\": \"Disable this option to stop microphone streaming.\",\n    \"stream_mic_download_btn\": \"Download Virtual Microphone\",\n    \"stream_mic_download_confirm\": \"You are about to be redirected to the virtual microphone download page. Continue?\",\n    \"stream_mic_note\": \"This feature requires installing a virtual microphone\",\n    \"sunshine_name\": \"Sunshine Name\",\n    \"sunshine_name_desc\": \"The name displayed by Moonlight. If not specified, the PC's hostname is used\",\n    \"sw_preset\": \"SW Presets\",\n    \"sw_preset_desc\": \"Optimize the trade-off between encoding speed (encoded frames per second) and compression efficiency (quality per bit in the bitstream). Defaults to superfast.\",\n    \"sw_preset_fast\": \"fast\",\n    \"sw_preset_faster\": \"faster\",\n    \"sw_preset_medium\": \"medium\",\n    \"sw_preset_slow\": \"slow\",\n    \"sw_preset_slower\": \"slower\",\n    \"sw_preset_superfast\": \"superfast (default)\",\n    \"sw_preset_ultrafast\": \"ultrafast\",\n    \"sw_preset_veryfast\": \"veryfast\",\n    \"sw_preset_veryslow\": \"veryslow\",\n    \"sw_tune\": \"SW Tune\",\n    \"sw_tune_animation\": \"animation -- good for cartoons; uses higher deblocking and more reference frames\",\n    \"sw_tune_desc\": \"Tuning options, which are applied after the preset. Defaults to zerolatency.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- allows faster decoding by disabling certain filters\",\n    \"sw_tune_film\": \"film -- use for high quality movie content; lowers deblocking\",\n    \"sw_tune_grain\": \"grain -- preserves the grain structure in old, grainy film material\",\n    \"sw_tune_stillimage\": \"stillimage -- good for slideshow-like content\",\n    \"sw_tune_zerolatency\": \"zerolatency -- good for fast encoding and low-latency streaming (default)\",\n    \"system_tray\": \"Enable System Tray\",\n    \"system_tray_desc\": \"Whether to enable system tray. If enabled, Sunshine will display an icon in the system tray and can be controlled from the system tray.\",\n    \"touchpad_as_ds4\": \"Emulate a DS4 gamepad if the client gamepad reports a touchpad is present\",\n    \"touchpad_as_ds4_desc\": \"If disabled, touchpad presence will not be taken into account during gamepad type selection.\",\n    \"unsaved_changes_tooltip\": \"You have unsaved changes. Click to save.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Automatically configure port forwarding for streaming over the Internet\",\n    \"variable_refresh_rate\": \"Variable Refresh Rate (VRR)\",\n    \"variable_refresh_rate_desc\": \"Allow video stream framerate to match render framerate for VRR support. When enabled, encoding only occurs when new frames are available, allowing the stream to follow the actual render framerate.\",\n    \"vdd_reuse_desc_windows\": \"When enabled, all clients will share the same VDD (Virtual Display Device). When disabled (default), each client gets its own unique VDD. Enable this for faster client switching, but note that all clients will share the same display settings.\",\n    \"vdd_reuse_windows\": \"Reuse Same VDD for All Clients\",\n    \"virtual_display\": \"Virtual Display\",\n    \"virtual_mouse\": \"Virtual Mouse Driver\",\n    \"virtual_mouse_desc\": \"When enabled, Sunshine will use the Zako Virtual Mouse driver (if installed) to simulate mouse input at the HID level. This allows games using Raw Input to receive mouse events. When disabled or driver not installed, falls back to SendInput.\",\n    \"virtual_sink\": \"Virtual Sink\",\n    \"virtual_sink_desc\": \"Manually specify a virtual audio device to use. If unset, the device is chosen automatically. We strongly recommend leaving this field blank to use automatic device selection!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Install the virtual mouse driver?\",\n    \"vmouse_confirm_uninstall\": \"Uninstall the virtual mouse driver?\",\n    \"vmouse_install\": \"Install Driver\",\n    \"vmouse_installing\": \"Installing...\",\n    \"vmouse_note\": \"The virtual mouse driver requires separate installation. Please use the Sunshine Control Panel to install or manage the driver.\",\n    \"vmouse_refresh\": \"Refresh Status\",\n    \"vmouse_status_installed\": \"Installed (not active)\",\n    \"vmouse_status_not_installed\": \"Not Installed\",\n    \"vmouse_status_running\": \"Running\",\n    \"vmouse_uninstall\": \"Uninstall Driver\",\n    \"vmouse_uninstalling\": \"Uninstalling...\",\n    \"vt_coder\": \"VideoToolbox Coder\",\n    \"vt_realtime\": \"VideoToolbox Realtime Encoding\",\n    \"vt_software\": \"VideoToolbox Software Encoding\",\n    \"vt_software_allowed\": \"Allowed\",\n    \"vt_software_forced\": \"Forced\",\n    \"wan_encryption_mode\": \"WAN Encryption Mode\",\n    \"wan_encryption_mode_1\": \"Enabled for supported clients (default)\",\n    \"wan_encryption_mode_2\": \"Required for all clients\",\n    \"wan_encryption_mode_desc\": \"This determines when encryption will be used when streaming over the Internet. Encryption can reduce streaming performance, particularly on less powerful hosts and clients.\",\n    \"webhook_curl_command\": \"Command\",\n    \"webhook_curl_command_desc\": \"Copy the following command to your terminal to test if the webhook is working properly:\",\n    \"webhook_curl_copy_failed\": \"Copy failed, please manually select and copy\",\n    \"webhook_enabled\": \"Webhook Notifications\",\n    \"webhook_enabled_desc\": \"When enabled, Sunshine will send event notifications to the specified Webhook URL\",\n    \"webhook_group\": \"Webhook Notification Settings\",\n    \"webhook_skip_ssl_verify\": \"Skip SSL Certificate Verification\",\n    \"webhook_skip_ssl_verify_desc\": \"Skip SSL certificate verification for HTTPS connections, only for testing or self-signed certificates\",\n    \"webhook_test\": \"Test\",\n    \"webhook_test_failed\": \"Webhook test failed\",\n    \"webhook_test_failed_note\": \"Note: Please check if the URL is correct, or check the browser console for more information.\",\n    \"webhook_test_success\": \"Webhook test successful!\",\n    \"webhook_test_success_cors_note\": \"Note: Due to CORS restrictions, the server response status cannot be confirmed.\\nThe request has been sent. If the webhook is configured correctly, the message should have been delivered.\\n\\nSuggestion: Check the Network tab in your browser's developer tools for request details.\",\n    \"webhook_test_url_required\": \"Please enter Webhook URL first\",\n    \"webhook_timeout\": \"Request Timeout\",\n    \"webhook_timeout_desc\": \"Timeout for webhook requests in milliseconds, range 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"The URL to receive event notifications, supports HTTP/HTTPS protocols\",\n    \"wgc_checking_mode\": \"Checking...\",\n    \"wgc_checking_running_mode\": \"Checking running mode...\",\n    \"wgc_control_panel_only\": \"This feature is only available in Sunshine Control Panel\",\n    \"wgc_mode_switch_failed\": \"Failed to switch mode\",\n    \"wgc_mode_switch_started\": \"Mode switch initiated. If a UAC prompt appears, please click 'Yes' to confirm.\",\n    \"wgc_service_mode_warning\": \"WGC capture requires running in user mode. If currently running in service mode, please click the button above to switch to user mode.\",\n    \"wgc_switch_to_service_mode\": \"Switch to Service Mode\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Currently running in user mode. Click to switch to service mode.\",\n    \"wgc_switch_to_user_mode\": \"Switch to User Mode\",\n    \"wgc_switch_to_user_mode_tooltip\": \"WGC capture requires running in user mode. Click this button to switch to user mode.\",\n    \"wgc_user_mode_available\": \"Currently running in user mode. WGC capture is available.\",\n    \"window_title\": \"Window Title\",\n    \"window_title_desc\": \"The title of the window to capture (partial match, case-insensitive). If left empty, the current running application name will be used automatically.\",\n    \"window_title_placeholder\": \"e.g., Application Name\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine is a self-hosted game stream host for Moonlight.\",\n    \"download\": \"Download\",\n    \"installed_version_not_stable\": \"You are running a pre-release version of Sunshine. You may experience bugs or other issues. Please report any issues you encounter. Thank you for helping to make Sunshine a better software!\",\n    \"loading_latest\": \"Loading latest release...\",\n    \"new_pre_release\": \"A new Pre-Release Version is Available!\",\n    \"new_stable\": \"A new Stable Version is Available!\",\n    \"startup_errors\": \"<b>Attention!</b> Sunshine detected these errors during startup. We <b>STRONGLY RECOMMEND</b> fixing them before streaming.\",\n    \"update_download_confirm\": \"You are about to open the update download page in your browser. Continue?\",\n    \"version_dirty\": \"Thank you for helping to make Sunshine a better software!\",\n    \"version_latest\": \"You are running the latest version of Sunshine\",\n    \"view_logs\": \"View Logs\",\n    \"welcome\": \"Hello, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Applications\",\n    \"configuration\": \"Configuration\",\n    \"home\": \"Home\",\n    \"password\": \"Change Password\",\n    \"pin\": \"PIN\",\n    \"theme_auto\": \"Auto\",\n    \"theme_dark\": \"Dark\",\n    \"theme_light\": \"Light\",\n    \"toggle_theme\": \"Theme\",\n    \"troubleshoot\": \"Troubleshooting\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Confirm Password\",\n    \"current_creds\": \"Current Credentials\",\n    \"new_creds\": \"New Credentials\",\n    \"new_username_desc\": \"If not specified, the username will not change\",\n    \"password_change\": \"Password Change\",\n    \"success_msg\": \"Password has been changed successfully! This page will reload soon, your browser will ask you for the new credentials.\"\n  },\n  \"pin\": {\n    \"actions\": \"Actions\",\n    \"cancel_editing\": \"Cancel editing\",\n    \"client_name\": \"Name\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Confirm Delete\",\n    \"delete_client\": \"Delete client\",\n    \"delete_confirm_message\": \"Are you sure you want to delete <strong>{name}</strong>?\",\n    \"delete_warning\": \"This action cannot be undone.\",\n    \"device_name\": \"Device Name\",\n    \"device_size\": \"Device Size\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Large - TV\",\n    \"device_size_medium\": \"Medium - Tablet\",\n    \"device_size_small\": \"Small - Phone\",\n    \"edit_client_settings\": \"Edit client settings\",\n    \"hdr_profile\": \"HDR Profile\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Loading...\",\n    \"loading_clients\": \"Loading clients...\",\n    \"modify_in_gui\": \"Please modify in GUI\",\n    \"none\": \"-- None --\",\n    \"or_manual_pin\": \"or enter PIN manually\",\n    \"pair_failure\": \"Pairing Failed: Check if the PIN is typed correctly\",\n    \"pair_success\": \"Success! Please check Moonlight to continue\",\n    \"pin_pairing\": \"PIN Pairing\",\n    \"qr_expires_in\": \"Expires in\",\n    \"qr_generate\": \"Generate QR Code\",\n    \"qr_paired_success\": \"Paired successfully!\",\n    \"qr_pairing\": \"QR Code Pairing\",\n    \"qr_pairing_desc\": \"Generate a QR code for quick pairing. Scan it with the Moonlight client to pair automatically.\",\n    \"qr_pairing_warning\": \"Experimental feature. If pairing fails, please use the manual PIN pairing below. Note: This feature only works on LAN.\",\n    \"qr_refresh\": \"Refresh QR Code\",\n    \"remove_paired_devices_desc\": \"Remove your paired devices.\",\n    \"save_changes\": \"Save changes\",\n    \"save_failed\": \"Failed to save client settings. Please try again.\",\n    \"save_or_cancel_first\": \"Please save or cancel editing first\",\n    \"send\": \"Send\",\n    \"unknown_client\": \"Unknown Client\",\n    \"unpair_all_confirm\": \"Are you sure you want to unpair all clients? This action cannot be undone.\",\n    \"unsaved_changes\": \"Unsaved changes\",\n    \"warning_msg\": \"Make sure you have access to the client you are pairing with. This software can give total control to your computer, so be careful!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android Recommended\",\n    \"client_downloads\": \"Client Downloads\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"GitHub Discussions\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Join Community\",\n    \"join_group_desc\": \"Get help and share experience\",\n    \"legal\": \"Legal\",\n    \"legal_desc\": \"By continuing to use this software you agree to the terms and conditions in the following documents.\",\n    \"license\": \"License\",\n    \"lizardbyte_website\": \"LizardByte Website\",\n    \"official_website\": \"Official Website\",\n    \"official_website_title\": \"AlkaidLab - Official Website\",\n    \"open_source\": \"Open Source\",\n    \"open_source_desc\": \"Star & Fork to support the project\",\n    \"quick_start\": \"Quick Start\",\n    \"resources\": \"Resources\",\n    \"resources_desc\": \"Resources for Sunshine!\",\n    \"third_party_desc\": \"Third-party component notices\",\n    \"third_party_moonlight\": \"Friendly Links\",\n    \"third_party_notice\": \"Third Party Notice\",\n    \"tutorial\": \"Tutorial\",\n    \"tutorial_desc\": \"Detailed configuration and usage guide\",\n    \"view_license\": \"View full license\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Virtual Display\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"Go to Repository\",\n    \"harmony_modal_desc\": \"For HarmonyOS NEXT Moonlight, please search for Moonlight V+ in the HarmonyOS App Store\",\n    \"harmony_modal_link_notice\": \"This link will redirect to the project repository\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"seconds\",\n    \"restart_desc\": \"Configuration saved. Sunshine is restarting to apply display settings.\",\n    \"restart_go_now\": \"Go now\",\n    \"restart_title\": \"Restarting Sunshine\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"Basic settings are now active. You can start streaming with a Moonlight client right away!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"The Base Display (VDD) is Sunshine Foundation's built-in smart virtual display, supporting any resolution, frame rate, and HDR optimization. It is the preferred choice for screen-off streaming and extended display streaming.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Ensure Active\",\n    \"step3_ensure_active_desc\": \"Activates the display if it is not already active\",\n    \"step3_ensure_only_display\": \"Ensure Only Display\",\n    \"step3_ensure_only_display_desc\": \"Disables all other displays, only enables the specified display (recommended)\",\n    \"step3_ensure_primary\": \"Ensure Primary\",\n    \"step3_ensure_primary_desc\": \"Activates the display and sets it as the primary display\",\n    \"step3_ensure_secondary\": \"Secondary Streaming\",\n    \"step3_ensure_secondary_desc\": \"Uses only the virtual display for secondary extended streaming\",\n    \"step3_no_operation\": \"No Operation\",\n    \"step3_no_operation_desc\": \"No changes to display state; user must ensure display is ready\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Advanced\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Config Files\",\n    \"general\": \"General\",\n    \"input\": \"Input\",\n    \"network\": \"Network\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analyzing...\",\n    \"ai_analyzing_logs\": \"Analyzing logs, please wait...\",\n    \"ai_config\": \"AI Configuration\",\n    \"ai_copy_result\": \"Copy\",\n    \"ai_diagnosis\": \"AI Diagnosis\",\n    \"ai_diagnosis_title\": \"AI Log Diagnosis\",\n    \"ai_error\": \"Analysis failed\",\n    \"ai_key_local\": \"API key is stored locally only and never uploaded\",\n    \"ai_model\": \"Model\",\n    \"ai_provider\": \"Provider\",\n    \"ai_reanalyze\": \"Re-analyze\",\n    \"ai_result\": \"Diagnosis Result\",\n    \"ai_retry\": \"Retry\",\n    \"ai_start_diagnosis\": \"Start Diagnosis\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"If you need to immediately shut down Sunshine, you can use this function. Note that you will need to manually start it again after shutdown.\",\n    \"boom_sunshine_success\": \"Sunshine has been shut down\",\n    \"confirm_boom\": \"Really want to exit?\",\n    \"confirm_boom_desc\": \"So you really want to exit? Well, I can't stop you, go ahead and click again\",\n    \"confirm_logout\": \"Confirm logout?\",\n    \"confirm_logout_desc\": \"You will need to enter your password again to access the web UI.\",\n    \"copy_config\": \"Copy Config\",\n    \"copy_config_error\": \"Failed to copy config\",\n    \"copy_config_success\": \"Config copied to clipboard!\",\n    \"copy_logs\": \"Copy logs\",\n    \"download_logs\": \"Download logs\",\n    \"force_close\": \"Force Close\",\n    \"force_close_desc\": \"If Moonlight complains about an app currently running, force closing the app should fix the issue.\",\n    \"force_close_error\": \"Error while closing Application\",\n    \"force_close_success\": \"Application Closed Successfully!\",\n    \"ignore_case\": \"Ignore case\",\n    \"logout\": \"Logout\",\n    \"logout_desc\": \"Logout. You may need to log in again.\",\n    \"logout_localhost_tip\": \"Current environment does not require login; logout will not trigger a credential prompt.\",\n    \"logs\": \"Logs\",\n    \"logs_desc\": \"See the logs uploaded by Sunshine\",\n    \"logs_find\": \"Find...\",\n    \"match_contains\": \"Contains\",\n    \"match_exact\": \"Exact\",\n    \"match_regex\": \"Regex\",\n    \"reopen_setup_wizard\": \"Reopen Setup Wizard\",\n    \"reopen_setup_wizard_desc\": \"Reopen the setup wizard page to reconfigure the initial settings.\",\n    \"reopen_setup_wizard_error\": \"Failed to reopen setup wizard\",\n    \"reset_display_device_desc_windows\": \"If Sunshine is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.\\nThis could happen for various reasons: device is no longer available, has been plugged to a different port and so on.\",\n    \"reset_display_device_error_windows\": \"Error while resetting persistence!\",\n    \"reset_display_device_success_windows\": \"Success resetting persistence!\",\n    \"reset_display_device_windows\": \"Reset Display Memory\",\n    \"restart_sunshine\": \"Restart Sunshine\",\n    \"restart_sunshine_desc\": \"If Sunshine isn't working properly, you can try restarting it. This will terminate any running sessions.\",\n    \"restart_sunshine_success\": \"Sunshine is restarting\",\n    \"troubleshooting\": \"Troubleshooting\",\n    \"unpair_all\": \"Unpair All\",\n    \"unpair_all_error\": \"Error while unpairing\",\n    \"unpair_all_success\": \"All devices unpaired.\",\n    \"unpair_desc\": \"Remove your paired devices. Individually unpaired devices with an active session will remain connected, but cannot start or resume a session.\",\n    \"unpair_single_no_devices\": \"There are no paired devices.\",\n    \"unpair_single_success\": \"However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.\",\n    \"unpair_single_unknown\": \"Unknown Client\",\n    \"unpair_title\": \"Unpair Devices\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Confirm password\",\n    \"create_creds\": \"Before Getting Started, we need you to make a new username and password for accessing the Web UI.\",\n    \"create_creds_alert\": \"The credentials below are needed to access Sunshine's Web UI. Keep them safe, since you will never see them again!\",\n    \"creds_local_only\": \"Your credentials are stored locally offline and will never be uploaded to any server.\",\n    \"error\": \"Error!\",\n    \"greeting\": \"Welcome to Sunshine Foundation!\",\n    \"hide_password\": \"Hide password\",\n    \"login\": \"Login\",\n    \"network_error\": \"Network error, please check your connection\",\n    \"password\": \"Password\",\n    \"password_match\": \"Passwords match\",\n    \"password_mismatch\": \"Passwords do not match\",\n    \"server_error\": \"Server error\",\n    \"show_password\": \"Show password\",\n    \"success\": \"Success!\",\n    \"username\": \"Username\",\n    \"welcome_success\": \"This page will reload soon, your browser will ask you for the new credentials\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/es.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Aplicar\",\n    \"auto\": \"Automático\",\n    \"autodetect\": \"Autodetectar (recomendado)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"Cancelar\",\n    \"close\": \"Cerrar\",\n    \"copied\": \"Copiado al portapapeles\",\n    \"copy\": \"Copiar\",\n    \"delete\": \"Eliminar\",\n    \"description\": \"Descripción\",\n    \"disabled\": \"Deshabilitado\",\n    \"disabled_def\": \"Desactivado (por defecto)\",\n    \"dismiss\": \"Descartar\",\n    \"do_cmd\": \"Hacer comando\",\n    \"download\": \"Descargar\",\n    \"edit\": \"Editar\",\n    \"elevated\": \"Elevado\",\n    \"enabled\": \"Habilitado\",\n    \"enabled_def\": \"Habilitado (por defecto)\",\n    \"error\": \"¡Error!\",\n    \"no_changes\": \"No hay cambios\",\n    \"note\": \"Nota:\",\n    \"password\": \"Contraseña\",\n    \"remove\": \"Eliminar\",\n    \"run_as\": \"Ejecutar como administrador\",\n    \"save\": \"Guardar\",\n    \"see_more\": \"Ver más\",\n    \"success\": \"¡Éxito!\",\n    \"undo_cmd\": \"Deshacer comando\",\n    \"username\": \"Nombre de usuario\",\n    \"warning\": \"¡Advertencia!\"\n  },\n  \"apps\": {\n    \"actions\": \"Acciones\",\n    \"add_cmds\": \"Añadir comandos\",\n    \"add_new\": \"Añadir nuevo\",\n    \"advanced_options\": \"Opciones avanzadas\",\n    \"app_name\": \"Nombre de la aplicación\",\n    \"app_name_desc\": \"Nombre de la aplicación, como se muestra en Moonlight\",\n    \"applications_desc\": \"Las aplicaciones se actualizan sólo cuando se reinicia Client\",\n    \"applications_title\": \"Aplicaciones\",\n    \"auto_detach\": \"Continuar la transmisión si la aplicación cierra rápidamente\",\n    \"auto_detach_desc\": \"Esto intentará detectar automáticamente aplicaciones de tipo launcher que se cierran rápidamente después de iniciar otro programa o instancia de sí mismos. Cuando se detecta una aplicación tipo launcher, se trata como una aplicación separada.\",\n    \"basic_info\": \"Información básica\",\n    \"cmd\": \"Comando\",\n    \"cmd_desc\": \"La aplicación principal a iniciar. Si está en blanco, no se iniciará ninguna aplicación.\",\n    \"cmd_examples_title\": \"Ejemplos comunes:\",\n    \"cmd_note\": \"Si la ruta al comando ejecutable contiene espacios, debe encerrarla entre comillas.\",\n    \"cmd_prep_desc\": \"Una lista de comandos que se ejecutarán antes/después de esta aplicación. Si alguno de los comandos prep fallan, el inicio de la aplicación es abortado.\",\n    \"cmd_prep_name\": \"Preparaciones de Comando\",\n    \"command_settings\": \"Configuración de comandos\",\n    \"covers_found\": \"Cubiertas encontradas\",\n    \"delete\": \"Eliminar\",\n    \"delete_confirm\": \"¿Está seguro de que desea eliminar \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Comandos separados\",\n    \"detached_cmds_add\": \"Añadir comando separado\",\n    \"detached_cmds_desc\": \"Una lista de comandos a ejecutar en segundo plano.\",\n    \"detached_cmds_note\": \"Si la ruta al comando ejecutable contiene espacios, debe encerrarla entre comillas.\",\n    \"detached_cmds_remove\": \"Eliminar comando separado\",\n    \"edit\": \"Editar\",\n    \"env_app_id\": \"ID de la aplicación\",\n    \"env_app_name\": \"Nombre de la aplicación\",\n    \"env_client_audio_config\": \"La configuración de audio solicitada por el cliente (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"El cliente ha solicitado la opción de optimizar el juego para una transmisión óptima (verdadero/falso)\",\n    \"env_client_fps\": \"El FPS solicitado por el cliente (int)\",\n    \"env_client_gcmap\": \"La máscara de gamepad solicitada, en formato bitset/bitfield (int)\",\n    \"env_client_hdr\": \"HDR está activado por el cliente (verdadero/falso)\",\n    \"env_client_height\": \"La altura solicitada por el cliente (int)\",\n    \"env_client_host_audio\": \"El cliente ha solicitado audio del host (verdadero/falso)\",\n    \"env_client_name\": \"Nombre descriptivo del cliente (cadena)\",\n    \"env_client_width\": \"Anchura solicitada por el cliente (int)\",\n    \"env_displayplacer_example\": \"Ejemplo - displayplacer para Automatización de Resoluciones:\",\n    \"env_qres_example\": \"Ejemplo - QRes para Automatización de Resoluciones:\",\n    \"env_qres_path\": \"ruta de qres\",\n    \"env_var_name\": \"Nombre Var\",\n    \"env_vars_about\": \"Acerca de variables de entorno\",\n    \"env_vars_desc\": \"Todos los comandos obtienen estas variables de entorno de forma predeterminada:\",\n    \"env_xrandr_example\": \"Ejemplo - Xrandr para Automatización de Resolución:\",\n    \"exit_timeout\": \"Tiempo de espera de salida\",\n    \"exit_timeout_desc\": \"Segundos a esperar para que todos los procesos de la aplicación se cierren de manera ordenada cuando se solicite cerrar. Si no se establece, el valor predeterminado es esperar hasta 5 segundos. Si se establece en 0, la aplicación se cerrará inmediatamente.\",\n    \"file_selector_not_initialized\": \"Selector de archivos no inicializado\",\n    \"find_cover\": \"Encontrar portada\",\n    \"form_invalid\": \"Por favor, verifique los campos requeridos\",\n    \"form_valid\": \"Aplicación válida\",\n    \"global_prep_desc\": \"Activar/Desactivar la ejecución de Comandos de Preparación Global para esta aplicación.\",\n    \"global_prep_name\": \"Comandos de preparación global\",\n    \"image\": \"Imagen\",\n    \"image_desc\": \"Ruta de la aplicación/dibujo/imagen que se enviará al cliente. La imagen debe ser un archivo PNG. Si no se establece, Sunshine enviará la imagen predeterminada de la caja.\",\n    \"image_settings\": \"Configuración de imagen\",\n    \"loading\": \"Cargando...\",\n    \"menu_cmd_actions\": \"Acciones\",\n    \"menu_cmd_add\": \"Añadir comando de menú\",\n    \"menu_cmd_command\": \"Comando\",\n    \"menu_cmd_desc\": \"Después de la configuración, estos comandos serán visibles en el menú de retorno del cliente, permitiendo la ejecución rápida de operaciones específicas sin interrumpir la transmisión, como iniciar programas auxiliares.\\nEjemplo: Nombre para mostrar - Cerrar tu computadora; Comando - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Nombre para mostrar\",\n    \"menu_cmd_drag_sort\": \"Arrastrar para ordenar\",\n    \"menu_cmd_name\": \"Comandos de menú\",\n    \"menu_cmd_placeholder_command\": \"Comando\",\n    \"menu_cmd_placeholder_display_name\": \"Nombre para mostrar\",\n    \"menu_cmd_placeholder_execute\": \"Ejecutar comando\",\n    \"menu_cmd_placeholder_undo\": \"Deshacer comando\",\n    \"menu_cmd_remove_menu\": \"Eliminar comando de menú\",\n    \"menu_cmd_remove_prep\": \"Eliminar comando de preparación\",\n    \"mouse_mode\": \"Modo de ratón\",\n    \"mouse_mode_auto\": \"Auto (Configuración global)\",\n    \"mouse_mode_desc\": \"Seleccione el método de entrada del ratón para esta aplicación. Auto usa la configuración global, Ratón virtual usa el controlador HID, SendInput usa la API de Windows.\",\n    \"mouse_mode_sendinput\": \"SendInput (API de Windows)\",\n    \"mouse_mode_vmouse\": \"Ratón virtual\",\n    \"name\": \"Nombre\",\n    \"output_desc\": \"El archivo donde se almacena la salida del comando, si no se especifica, se ignora la salida\",\n    \"output_name\": \"Salida\",\n    \"run_as_desc\": \"Esto puede ser necesario para que algunas aplicaciones que requieren permisos de administrador, funcionen correctamente.\",\n    \"scan_result_add_all\": \"Añadir todo\",\n    \"scan_result_edit_title\": \"Añadir y editar\",\n    \"scan_result_filter_all\": \"Todo\",\n    \"scan_result_filter_epic_title\": \"Juegos de Epic Games\",\n    \"scan_result_filter_executable\": \"Ejecutable\",\n    \"scan_result_filter_executable_title\": \"Archivo ejecutable\",\n    \"scan_result_filter_gog_title\": \"Juegos de GOG Galaxy\",\n    \"scan_result_filter_script\": \"Script\",\n    \"scan_result_filter_script_title\": \"Script de lote/comando\",\n    \"scan_result_filter_shortcut\": \"Acceso directo\",\n    \"scan_result_filter_shortcut_title\": \"Acceso directo\",\n    \"scan_result_filter_steam_title\": \"Juegos de Steam\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Juego\",\n    \"scan_result_games_only\": \"Solo juegos\",\n    \"scan_result_matched\": \"Coincidencias: {count}\",\n    \"scan_result_no_apps\": \"No se encontraron aplicaciones para añadir\",\n    \"scan_result_no_matches\": \"No se encontraron aplicaciones coincidentes\",\n    \"scan_result_quick_add_title\": \"Añadir rápidamente\",\n    \"scan_result_remove_title\": \"Eliminar de la lista\",\n    \"scan_result_search_placeholder\": \"Buscar nombre de aplicación, comando o ruta...\",\n    \"scan_result_show_all\": \"Mostrar todo\",\n    \"scan_result_title\": \"Resultados del escaneo\",\n    \"scan_result_try_different_keywords\": \"Intente usar diferentes palabras clave de búsqueda\",\n    \"scan_result_type_batch\": \"Lote\",\n    \"scan_result_type_command\": \"Script de comando\",\n    \"scan_result_type_executable\": \"Archivo ejecutable\",\n    \"scan_result_type_shortcut\": \"Acceso directo\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Buscar aplicaciones...\",\n    \"select\": \"Seleccionar\",\n    \"test_menu_cmd\": \"Probar comando\",\n    \"test_menu_cmd_empty\": \"El comando no puede estar vacío\",\n    \"test_menu_cmd_executing\": \"Ejecutando comando...\",\n    \"test_menu_cmd_failed\": \"Error al ejecutar el comando\",\n    \"test_menu_cmd_success\": \"¡Comando ejecutado con éxito!\",\n    \"use_desktop_image\": \"Usar el fondo de escritorio actual\",\n    \"wait_all\": \"Continuar la transmisión hasta que todos los procesos de la aplicación salgan\",\n    \"wait_all_desc\": \"Esto continuará transmitiendo hasta que todos los procesos iniciados por la aplicación hayan terminado. Cuando no está marcado, la transmisión se detendrá cuando el proceso inicial de la aplicación se cierre, incluso si otros procesos de aplicación siguen ejecutándose.\",\n    \"working_dir\": \"Directorio de trabajo\",\n    \"working_dir_desc\": \"El directorio de trabajo que debe pasarse al proceso. Por ejemplo, algunas aplicaciones usan el directorio de trabajo para buscar archivos de configuración. Si no se establece, Sunshine se establecerá por defecto en el directorio padre del comando\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Nombre del adaptador\",\n    \"adapter_name_desc_linux_1\": \"Especifique manualmente un GPU a usar para capturar.\",\n    \"adapter_name_desc_linux_2\": \"para encontrar todos los dispositivos capaces de VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Reemplace ``renderD129`` con el dispositivo de arriba para listar el nombre y las capacidades del dispositivo. Para tener el soporte de Sunshine, necesita tener como mínimo:\",\n    \"adapter_name_desc_windows\": \"Especifique manualmente una GPU a usar para capturar. Si no está activado, la GPU se elige automáticamente. ¡Recomendamos encarecidamente dejar este campo en blanco para utilizar la selección automática de GPU! Nota: Esta GPU debe tener una pantalla conectada y encendida. Los valores apropiados se pueden encontrar usando el siguiente comando:\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Si está instalada la última versión de la pantalla virtual, puede asociarse automáticamente con la vinculación de la GPU\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Añadir\",\n    \"address_family\": \"Familia de dirección\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Establecer la familia de direcciones usada por Sunshine\",\n    \"address_family_ipv4\": \"Sólo IPv4\",\n    \"always_send_scancodes\": \"Enviar siempre códigos de escaneo\",\n    \"always_send_scancodes_desc\": \"Enviar códigos de escaneo mejora la compatibilidad con juegos y aplicaciones, pero puede resultar en una entrada de teclado incorrecta de ciertos clientes que no están usando una disposición de teclado en inglés de los Estados Unidos. Activar si la entrada de teclado no funciona en absoluto en ciertas aplicaciones. Desactivar si las claves del cliente están generando una entrada incorrecta en el host.\",\n    \"amd_coder\": \"Codificador AMF (H264)\",\n    \"amd_coder_desc\": \"Le permite seleccionar la codificación entropía para priorizar la calidad o la velocidad de codificación. H.264 solamente.\",\n    \"amd_enforce_hrd\": \"Aplicación del decodificador de referencia hipotético (HRD) AMF\",\n    \"amd_enforce_hrd_desc\": \"Aumenta las restricciones en el control de velocidad para cumplir con los requisitos del modelo de HRD. Esto reduce en gran medida los desbordamientos de la velocidad de bits pero puede causar artefactos de codificación o menor calidad en ciertas tarjetas.\",\n    \"amd_preanalysis\": \"Análisis previo de AMF\",\n    \"amd_preanalysis_desc\": \"Esto permite un pre-análisis de control de velocidad que puede aumentar la calidad a expensas de una mayor latencia de codificación.\",\n    \"amd_quality\": \"Calidad AMF\",\n    \"amd_quality_balanced\": \"balanceada -- balanceado (por defecto)\",\n    \"amd_quality_desc\": \"Controla el equilibrio entre la velocidad de codificación y la calidad.\",\n    \"amd_quality_group\": \"Ajustes de calidad AMF\",\n    \"amd_quality_quality\": \"calidad -- Preferir calidad\",\n    \"amd_quality_speed\": \"velocidad -- preferir velocidad\",\n    \"amd_qvbr_quality\": \"Nivel de calidad AMF QVBR\",\n    \"amd_qvbr_quality_desc\": \"Nivel de calidad para el modo de control de tasa QVBR. Rango: 1-51 (menor = mejor calidad). Predeterminado: 23. Solo aplica cuando el control de tasa está en 'qvbr'.\",\n    \"amd_rc\": \"Control de tasa AMF\",\n    \"amd_rc_cbr\": \"cbr -- velocidad de bits constante (recomendada si HRD está habilitado)\",\n    \"amd_rc_cqp\": \"cqp -- modo qp constante\",\n    \"amd_rc_desc\": \"Esto controla el método de control de velocidad para asegurarse de que no estamos excediendo el objetivo de velocidad del bits del cliente. 'cqp' no es apto para targeting, y otras opciones además de 'vbr_latency' dependen de HRD para ayudar a restringir los desbordamientos de velocidad de bits.\",\n    \"amd_rc_group\": \"Ajustes de control de tasa AMF\",\n    \"amd_rc_hqcbr\": \"hqcbr -- tasa de bits constante alta calidad\",\n    \"amd_rc_hqvbr\": \"hqvbr -- tasa de bits variable alta calidad\",\n    \"amd_rc_qvbr\": \"qvbr -- tasa de bits variable de calidad (usa nivel de calidad QVBR)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- tasa de bits variable restringida por latencia (por defecto)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- tasa de bits variable restringida máxima\",\n    \"amd_usage\": \"Uso de AMF\",\n    \"amd_usage_desc\": \"Establece el perfil de codificación base. Todas las opciones presentadas a continuación anularán un subconjunto del perfil de uso, pero hay opciones ocultas adicionales que no se pueden configurar en otros lugares.\",\n    \"amd_usage_lowlatency\": \"baja latencia - baja latencia (la más rápida)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - baja latencia, alta calidad (rápido)\",\n    \"amd_usage_transcoding\": \"transcodificación -- transcodificación (más lenta)\",\n    \"amd_usage_ultralowlatency\": \"latencia ultra baja - latencia ultra baja (más rápida)\",\n    \"amd_usage_webcam\": \"cámara web -- cámara web (lento)\",\n    \"amd_vbaq\": \"Cuantización adaptativa basada en la varianza AMF (VBAQ)\",\n    \"amd_vbaq_desc\": \"El sistema visual humano normalmente es menos sensible a los artefactos en áreas altamente texturizadas. En modo VBAQ, la variación de píxeles se utiliza para indicar la complejidad de las texturas espaciales, permitiendo al codificador asignar más bits a áreas más suaves. Activar esta función conduce a mejoras en la calidad visual objetiva con algunos contenidos.\",\n    \"amf_draw_mouse_cursor\": \"Dibujar un cursor simple al usar el método de captura AMF\",\n    \"amf_draw_mouse_cursor_desc\": \"En algunos casos, usar la captura AMF no mostrará el puntero del mouse. Habilitar esta opción dibujará un puntero del mouse simple en la pantalla. Nota: La posición del puntero del mouse solo se actualizará cuando haya una actualización en la pantalla de contenido, por lo que en escenarios que no son juegos, como en el escritorio, puede observar un movimiento lento del puntero del mouse.\",\n    \"apply_note\": \"Haga clic en 'Aplicar' para reiniciar Sunshine y aplicar los cambios. Esto terminará cualquier sesión en ejecución.\",\n    \"audio_sink\": \"Salida de audio\",\n    \"audio_sink_desc_linux\": \"El nombre de la salida de audio usado para Audio Loopback. Si no especifica esta variable, pulseaudio seleccionará el dispositivo de monitor predeterminado. Puede encontrar el nombre de la salida de audio usando cualquiera de los comandos:\",\n    \"audio_sink_desc_macos\": \"El nombre de la salida de audio usado para Audio Loopback. Sunshine sólo puede acceder a micrófonos en macOS debido a limitaciones del sistema. Para transmitir audio del sistema usando Soundflower o BlackHole.\",\n    \"audio_sink_desc_windows\": \"Especifique manualmente un dispositivo de audio específico para capturar. Si no está activado, el dispositivo se elige automáticamente. ¡Recomendamos encarecidamente dejar este campo en blanco para usar la selección automática de dispositivos! Si tiene varios dispositivos de audio con nombres idénticos, puede obtener el ID del dispositivo usando el siguiente comando:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Altavoces (Dispositivo de audio de alta definición)\",\n    \"av1_mode\": \"Soporte AV1\",\n    \"av1_mode_0\": \"Sunshine anunciará soporte para AV1 basado en las capacidades del codificador (recomendado)\",\n    \"av1_mode_1\": \"Sunshine no anunciará soporte para AV1\",\n    \"av1_mode_2\": \"Sunshine anunciará soporte para el perfil principal de 8 bits AV1\",\n    \"av1_mode_3\": \"Sunshine anunciará soporte para perfiles AV1 de 8-bit y 10-bit (HDR)\",\n    \"av1_mode_desc\": \"Permite al cliente solicitar flujos de vídeo AV1 Main de 8-bit o de 10-bits. AV1 es más intensivo en CPU para codificar, por lo que permitir esto puede reducir el rendimiento al usar la codificación de software.\",\n    \"back_button_timeout\": \"Tiempo de Emulación de Botón de Inicio/Guía\",\n    \"back_button_timeout_desc\": \"Si se mantiene presionado el botón Atrás/Seleccionar para el número de milisegundos especificado, se emula un botón Inicio/Guía. Si se establece un valor < 0 (por defecto), mantener presionado el botón Volver/Seleccionar no se activará el botón Inicio/Guía.\",\n    \"bind_address\": \"Dirección de enlace (función de prueba)\",\n    \"bind_address_desc\": \"Establezca la dirección IP específica a la que se vinculará Sunshine. Si se deja en blanco, Sunshine se vinculará a todas las direcciones disponibles.\",\n    \"capture\": \"Forzar un método de captura específico\",\n    \"capture_desc\": \"En modo automático Sunshine usará el primero que funcione.\",\n    \"capture_target\": \"Objetivo de captura\",\n    \"capture_target_desc\": \"Seleccione el tipo de objetivo a capturar. Al seleccionar 'Ventana', puede capturar una ventana de aplicación específica (como el software de interpolación de cuadros por IA) en lugar de toda la pantalla.\",\n    \"capture_target_display\": \"Pantalla\",\n    \"capture_target_window\": \"Ventana\",\n    \"cert\": \"Certificado\",\n    \"cert_desc\": \"El certificado utilizado para la conexión del cliente de UI web y Moonlight. Para la mejor compatibilidad, debe tener una clave pública RSA-2048.\",\n    \"channels\": \"Máximo de clientes conectados\",\n    \"channels_desc_1\": \"Sunshine puede permitir que una sola sesión de transmisión sea compartida con varios clientes simultáneamente.\",\n    \"channels_desc_2\": \"Algunos codificadores de hardware pueden tener limitaciones que reducen el rendimiento con múltiples secuencias.\",\n    \"close_verify_safe\": \"Verificación segura compatible con clientes antiguos\",\n    \"close_verify_safe_desc\": \"Los clientes antiguos pueden no poder conectarse a Sunshine, por favor, desactive esta opción o actualice el cliente\",\n    \"coder_cabac\": \"cabac -- codificación aritmética binaria adaptativa contextual - mayor calidad\",\n    \"coder_cavlc\": \"cavlc -- codificación de longitud variable adaptativa de contexto - decodificación más rápida\",\n    \"configuration\": \"Configuración\",\n    \"controller\": \"Activar entrada de Gamepad\",\n    \"controller_desc\": \"Permite a los huéspedes controlar el sistema de host con un gamepad / controlador\",\n    \"credentials_file\": \"Archivo de credenciales\",\n    \"credentials_file_desc\": \"Guardar nombre de usuario/contraseña por separado del archivo de estado de Sunshine.\",\n    \"display_device_options_note_desc_windows\": \"Windows guarda varias configuraciones de pantalla para cada combinación de pantallas actualmente activas.\\nSunshine luego aplica cambios a una pantalla(-s) que pertenece a tal combinación de pantallas.\\nSi desconecta un dispositivo que estaba activo cuando Sunshine aplicó la configuración, los cambios no se pueden\\nrevertir a menos que la combinación pueda activarse nuevamente cuando Sunshine intente revertir los cambios!\",\n    \"display_device_options_note_windows\": \"Nota sobre cómo se aplican las configuraciones\",\n    \"display_device_options_windows\": \"Opciones de dispositivo de pantalla\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Activa la pantalla si no está activa\",\n    \"display_device_prep_ensure_active_windows\": \"Activar la pantalla automáticamente\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Desactiva todas las demás pantallas y solo activa la pantalla especificada\",\n    \"display_device_prep_ensure_only_display_windows\": \"Desactivar otras pantallas y activar solo la pantalla especificada\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Activa la pantalla y la establece como pantalla principal\",\n    \"display_device_prep_ensure_primary_windows\": \"Activar la pantalla automáticamente y convertirla en pantalla principal\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Usa solo la pantalla virtual para streaming secundario extendido\",\n    \"display_device_prep_ensure_secondary_windows\": \"Streaming de pantalla secundaria (solo pantalla virtual)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Sin cambios en el estado de la pantalla; el usuario debe asegurarse de que esté lista\",\n    \"display_device_prep_no_operation_windows\": \"Deshabilitado\",\n    \"display_device_prep_windows\": \"Preparación de pantalla\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Se debe especificar al menos un valor \\\"recibido\\\" y un valor \\\"final\\\".\\nCampo vacío en la sección \\\"recibido\\\" significa \\\"coincidir con cualquiera\\\". Campo vacío en la sección \\\"final\\\" significa \\\"mantener el valor recibido\\\".\\nPuede hacer coincidir un valor FPS específico con una resolución específica si lo desea...\\n\\nNota: si la opción \\\"Optimizar configuración de juegos\\\" no está habilitada en el cliente Moonlight, las filas que contienen valores de resolución se ignoran.\",\n    \"display_mode_remapping_desc_windows\": \"Especifique cómo una resolución y/o frecuencia de actualización específica debe reasignarse a otros valores.\\nPuede transmitir a menor resolución, mientras renderiza a mayor resolución en el host para un efecto de supermuestreo.\\nO puede transmitir a mayor FPS mientras limita el host a la frecuencia de actualización más baja.\\nLa coincidencia se realiza de arriba hacia abajo. Una vez que se encuentra una coincidencia, las demás ya no se verifican, pero aún se validan.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Frecuencia de actualización final\",\n    \"display_mode_remapping_final_resolution_windows\": \"Resolución final\",\n    \"display_mode_remapping_optional\": \"opcional\",\n    \"display_mode_remapping_received_fps_windows\": \"FPS recibido\",\n    \"display_mode_remapping_received_resolution_windows\": \"Resolución recibida\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Nota: si la opción \\\"Optimizar configuración de juegos\\\" no está habilitada en el cliente Moonlight, la reasignación está deshabilitada.\",\n    \"display_mode_remapping_windows\": \"Reasignar modos de pantalla\",\n    \"display_modes\": \"Modos de pantalla\",\n    \"ds4_back_as_touchpad_click\": \"Mapa Atrás/Seleccionar a Touchpad Clic\",\n    \"ds4_back_as_touchpad_click_desc\": \"Al forzar la emulación DS4, mapar Atrás/Seleccionar a Touchpad Clic\",\n    \"dsu_server_port\": \"Puerto del servidor DSU\",\n    \"dsu_server_port_desc\": \"Puerto de escucha del servidor DSU (predeterminado 26760). Sunshine actuará como un servidor DSU para recibir conexiones de clientes y enviar datos de movimiento. Habilite el servidor DSU en su cliente (Yuzu, Ryujinx, etc.) y configure la dirección del servidor DSU (127.0.0.1) y el puerto (26760)\",\n    \"enable_dsu_server\": \"Habilitar servidor DSU\",\n    \"enable_dsu_server_desc\": \"Habilitar servidor DSU para recibir conexiones de clientes y enviar datos de movimiento\",\n    \"encoder\": \"Forzar un codificador específico\",\n    \"encoder_desc\": \"Forzar un codificador específico, de lo contrario Sunshine seleccionará la mejor opción disponible. Nota: Si especifica un codificador de hardware en Windows, debe coincidir con el GPU donde la pantalla está conectada.\",\n    \"encoder_software\": \"Software\",\n    \"experimental\": \"Experimental\",\n    \"experimental_features\": \"Funciones experimentales\",\n    \"external_ip\": \"IP externa\",\n    \"external_ip_desc\": \"Si no se da ninguna dirección IP externa, Sunshine detectará automáticamente IP externa\",\n    \"fec_percentage\": \"Porcentaje FEC\",\n    \"fec_percentage_desc\": \"Porcentaje de errores corrigiendo paquetes por paquete de datos en cada fotograma de vídeo. Valores más altos pueden corregir para más pérdida de paquetes de red, pero a costa de aumentar el uso del ancho de banda.\",\n    \"ffmpeg_auto\": \"auto -- dejar que ffmpeg decida (por defecto)\",\n    \"file_apps\": \"Archivo de aplicaciones\",\n    \"file_apps_desc\": \"El archivo donde se almacenan las aplicaciones actuales de Sunshine.\",\n    \"file_state\": \"Archivo de estado\",\n    \"file_state_desc\": \"El archivo donde se almacena el estado actual de Sunshine\",\n    \"fps\": \"FPS anunciados\",\n    \"gamepad\": \"Tipo de Gamepad emulado\",\n    \"gamepad_auto\": \"Opciones de selección automática\",\n    \"gamepad_desc\": \"Elegir qué tipo de gamepad emular en el host\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 Manual Options\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Opciones Manual de DS4\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Preparaciones de Comando\",\n    \"global_prep_cmd_desc\": \"Configurar una lista de comandos a ejecutar antes o después de ejecutar cualquier aplicación. Si alguno de los comandos de preparación especificados falla, el proceso de inicio de la aplicación será abortado.\",\n    \"hdr_luminance_analysis\": \"Metadatos dinámicos HDR (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Habilita el análisis de luminancia GPU por cuadro e inyecta metadatos dinámicos HDR10+ (ST 2094-40) y HDR Vivid (CUVA) en el flujo codificado. Proporciona sugerencias de mapeo de tonos por cuadro para pantallas compatibles. Agrega una pequeña sobrecarga GPU (~0,5-1,5ms/cuadro en altas resoluciones). Deshabilite si experimenta caídas de FPS con HDR activado.\",\n    \"hdr_prep_automatic_windows\": \"Activar/desactivar el modo HDR según lo solicitado por el cliente\",\n    \"hdr_prep_no_operation_windows\": \"Deshabilitado\",\n    \"hdr_prep_windows\": \"Cambio de estado HDR\",\n    \"hevc_mode\": \"Soporte de HEVC\",\n    \"hevc_mode_0\": \"Sunshine anunciará el soporte para HEVC basado en las capacidades del codificador (recomendado)\",\n    \"hevc_mode_1\": \"Sunshine no anunciará soporte para HEVC\",\n    \"hevc_mode_2\": \"Sunshine anunciará soporte para el perfil principal de HEVC\",\n    \"hevc_mode_3\": \"Sunshine anunciará soporte para perfiles HEVC Main y Main10 (HDR)\",\n    \"hevc_mode_desc\": \"Permite al cliente solicitar videos HEVC Main o HEVC Main10. HEVC es más intensivo en CPU para codificar, por lo que habilitar esto puede reducir el rendimiento al usar la codificación de software.\",\n    \"high_resolution_scrolling\": \"Soporte de desplazamiento de alta resolución\",\n    \"high_resolution_scrolling_desc\": \"Cuando está activado, Sunshine pasará a través de eventos de desplazamiento de alta resolución desde clientes de Moonlight. Esto puede ser útil para aplicaciones antiguas que se desplazan demasiado rápido con eventos de desplazamiento de alta resolución.\",\n    \"install_steam_audio_drivers\": \"Instalar los controladores de audio de Steam\",\n    \"install_steam_audio_drivers_desc\": \"Si Steam está instalado, automáticamente instalará el controlador de altavoces de Steam Streaming para soportar sonido envolvente 5.1/7.1 y silenciar audio del host.\",\n    \"key_repeat_delay\": \"Retardo de repetición de Clave\",\n    \"key_repeat_delay_desc\": \"Controla cómo se repetirán las teclas rápidas. El retardo inicial en milisegundos antes de repetir las teclas.\",\n    \"key_repeat_frequency\": \"Frecuencia de repetición de clave\",\n    \"key_repeat_frequency_desc\": \"Con qué frecuencia las claves se repiten cada segundo. Esta opción configurable soporta decimales.\",\n    \"key_rightalt_to_key_win\": \"Mapear la tecla Alt derecha a la tecla Windows\",\n    \"key_rightalt_to_key_win_desc\": \"Es posible que no pueda enviar directamente la tecla de Windows desde Moonlight. En esos casos puede ser útil hacer que Sunshine piense que la tecla Alt correcta es la clave de Windows\",\n    \"key_rightalt_to_key_windows\": \"Asignar la tecla Alt derecha a la tecla Windows\",\n    \"keyboard\": \"Activar entrada de teclado\",\n    \"keyboard_desc\": \"Permite a los invitados controlar el sistema de host con el teclado\",\n    \"lan_encryption_mode\": \"Modo de cifrado LAN\",\n    \"lan_encryption_mode_1\": \"Habilitado para clientes compatibles\",\n    \"lan_encryption_mode_2\": \"Requerido para todos los clientes\",\n    \"lan_encryption_mode_desc\": \"Esto determina cuándo se utilizará el cifrado al transmitir a través de su red local. El cifrado puede reducir el rendimiento de la transmisión, especialmente en hosts y clientes menos poderosos.\",\n    \"locale\": \"Local\",\n    \"locale_desc\": \"La configuración regional utilizada para la interfaz de usuario de Sunshine.\",\n    \"log_level\": \"Nivel de registro\",\n    \"log_level_0\": \"Detallado\",\n    \"log_level_1\": \"Depurar\",\n    \"log_level_2\": \"Información\",\n    \"log_level_3\": \"Advertencia\",\n    \"log_level_4\": \"Error\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"Ninguna\",\n    \"log_level_desc\": \"El nivel mínimo de registro impreso a nivel estándar\",\n    \"log_path\": \"Ruta del archivo de registro\",\n    \"log_path_desc\": \"El archivo donde se almacenan los registros actuales de Sunshine.\",\n    \"max_bitrate\": \"Tasa de bits máxima\",\n    \"max_bitrate_desc\": \"La tasa de bits máxima (en Kbps) que Sunshine codificará la secuencia. Si se establece en 0, siempre utilizará la tasa de bits solicitada por la luz lunar.\",\n    \"max_fps_reached\": \"Se alcanzaron los valores máximos de FPS\",\n    \"max_resolutions_reached\": \"Se alcanzó el número máximo de resoluciones\",\n    \"mdns_broadcast\": \"Encontrar este ordenador en la red local\",\n    \"mdns_broadcast_desc\": \"Si esta opción está activada, Sunshine permitirá a otros dispositivos encontrar este ordenador automáticamente. Moonlight debe estar configurado para encontrar este ordenador automáticamente en la red local.\",\n    \"min_threads\": \"Recuento mínimo de hilos de CPU\",\n    \"min_threads_desc\": \"Incrementar el valor reduce ligeramente la eficiencia de la codificación, pero la compensación suele valer la pena para obtener el uso de más núcleos de CPU para la codificación. El valor ideal es el valor más bajo que puede codificar de forma fiable en los ajustes de streaming deseados en su hardware.\",\n    \"minimum_fps_target\": \"Objetivo de FPS mínimo\",\n    \"minimum_fps_target_desc\": \"FPS mínimo a mantener al codificar (0 = automático, aproximadamente la mitad del FPS de la transmisión; 1-1000 = FPS mínimo a mantener). Cuando la frecuencia de actualización variable está habilitada, esta configuración se ignora si está establecida en 0.\",\n    \"misc\": \"Opciones varias\",\n    \"motion_as_ds4\": \"Emular un gamepad DS4 si el gamepad del cliente informa que los sensores de movimiento están presentes\",\n    \"motion_as_ds4_desc\": \"Si está desactivado, los sensores de movimiento no se tendrán en cuenta durante la selección del tipo gamepad.\",\n    \"mouse\": \"Activar entrada del ratón\",\n    \"mouse_desc\": \"Permite a los huéspedes controlar el sistema de host con el ratón\",\n    \"native_pen_touch\": \"Soporte de lápiz/táctil nativo\",\n    \"native_pen_touch_desc\": \"Cuando está activado, Sunshine pasará a través de eventos nativos de pluma/táctil de clientes de Sunshine. Esto puede ser útil para deshabilitar para aplicaciones antiguas sin soporte nativo de pluma/táctil.\",\n    \"no_fps\": \"No se agregaron valores de FPS\",\n    \"no_resolutions\": \"No se agregaron resoluciones\",\n    \"notify_pre_releases\": \"Notificaciones de pre-lanzamiento\",\n    \"notify_pre_releases_desc\": \"Si desea ser notificado de las nuevas versiones de Sunshine\",\n    \"nvenc_h264_cavlc\": \"Preferir CAVLC sobre CABAC en H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Forma más simple de codificación entropía. CAVLC necesita alrededor del 10% más de la tasa de bits para la misma calidad. Sólo relevante para dispositivos de decodificación realmente antiguos.\",\n    \"nvenc_latency_over_power\": \"Preferir menor latencia de codificación sobre ahorro de energía\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine solicita la máxima velocidad de reloj GPU mientras se transmite para reducir la latencia de codificación. Deshabilitar no es recomendable ya que esto puede llevar a un aumento significativo de la latencia de la codificación.\",\n    \"nvenc_lookahead_depth\": \"Profundidad de predicción (Lookahead)\",\n    \"nvenc_lookahead_depth_desc\": \"Número de fotogramas para mirar hacia adelante durante la codificación (0-32). Lookahead mejora la calidad de codificación, especialmente en escenas complejas, proporcionando una mejor estimación de movimiento y distribución de bitrate. Los valores más altos mejoran la calidad pero aumentan la latencia de codificación. Establezca en 0 para desactivar lookahead. Requiere NVENC SDK 13.0 (1202) o posterior.\",\n    \"nvenc_lookahead_level\": \"Nivel de predicción (Lookahead)\",\n    \"nvenc_lookahead_level_0\": \"Nivel 0 (calidad más baja, más rápido)\",\n    \"nvenc_lookahead_level_1\": \"Nivel 1\",\n    \"nvenc_lookahead_level_2\": \"Nivel 2\",\n    \"nvenc_lookahead_level_3\": \"Nivel 3 (calidad más alta, más lento)\",\n    \"nvenc_lookahead_level_autoselect\": \"Selección automática (dejar que el controlador elija el nivel óptimo)\",\n    \"nvenc_lookahead_level_desc\": \"Nivel de calidad de Lookahead. Los niveles más altos mejoran la calidad a expensas del rendimiento. Esta opción solo tiene efecto cuando lookahead_depth es mayor que 0. Requiere NVENC SDK 13.0 (1202) o posterior.\",\n    \"nvenc_lookahead_level_disabled\": \"Desactivado (igual que el nivel 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Presentar OpenGL/Vulkan por encima de DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine no puede capturar programas OpenGL y Vulkan de pantalla completa a menos que se presenten sobre DXGI. Este es un ajuste para todo el sistema que se revierte al salir del programa Sunshine.\",\n    \"nvenc_preset\": \"Rendimiento predefinido\",\n    \"nvenc_preset_1\": \"(más rápido, por defecto)\",\n    \"nvenc_preset_7\": \"(más lento)\",\n    \"nvenc_preset_desc\": \"Los números más altos mejoran la compresión (calidad a una tasa de bits dada) a costa de una mayor latencia de codificación. Se recomienda cambiar sólo cuando esté limitado por la red o el decodificador; de lo contrario, se puede conseguir un efecto similar aumentando la tasa de bits.\",\n    \"nvenc_rate_control\": \"Modo de control de tasa\",\n    \"nvenc_rate_control_cbr\": \"CBR (Tasa de bits constante) - Baja latencia\",\n    \"nvenc_rate_control_desc\": \"Seleccione el modo de control de tasa. CBR (Tasa de bits constante) proporciona una tasa de bits fija para transmisión de baja latencia. VBR (Tasa de bits variable) permite que la tasa de bits varíe según la complejidad de la escena, proporcionando una mejor calidad para escenas complejas a costa de una tasa de bits variable.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Tasa de bits variable) - Mejor calidad\",\n    \"nvenc_realtime_hags\": \"Usar prioridad en tiempo real en la programación de gpu acelerada por hardware\",\n    \"nvenc_realtime_hags_desc\": \"Actualmente los controladores NVIDIA pueden congelarse en el codificador cuando HAGS está habilitado, se utiliza prioridad en tiempo real y la utilización de VRAM está cerca del máximo. Deshabilitar esta opción reduce la prioridad a alto, evitando la congelación a costa de un menor rendimiento de captura cuando la GPU está muy cargada.\",\n    \"nvenc_spatial_aq\": \"AQ espacial\",\n    \"nvenc_spatial_aq_desc\": \"Asigne valores más altos de QP a regiones planas del vídeo. Recomendado para habilitar al transmitir a tasas de bits más bajas.\",\n    \"nvenc_spatial_aq_disabled\": \"Deshabilitado (más rápido, predeterminado)\",\n    \"nvenc_spatial_aq_enabled\": \"Habilitado (más lento)\",\n    \"nvenc_split_encode\": \"Codificación de fotogramas dividida\",\n    \"nvenc_split_encode_desc\": \"Dividir la codificación de cada fotograma de vídeo en múltiples unidades de hardware NVENC. Reduce significativamente la latencia de codificación con una penalización marginal de eficiencia de compresión. Esta opción se ignora si su GPU tiene una única unidad NVENC.\",\n    \"nvenc_split_encode_driver_decides_def\": \"El controlador decide (predeterminado)\",\n    \"nvenc_split_encode_four_strips\": \"Force 4-strip split (requires 4+ NVENC engines)\",\n    \"nvenc_split_encode_three_strips\": \"Force 3-strip split (requires 3+ NVENC engines)\",\n    \"nvenc_split_encode_two_strips\": \"Force 2-strip split (requires 2+ NVENC engines)\",\n    \"nvenc_target_quality\": \"Calidad objetivo (modo VBR)\",\n    \"nvenc_target_quality_desc\": \"Nivel de calidad objetivo para el modo VBR (0-51 para H.264/HEVC, 0-63 para AV1). Valores más bajos = mayor calidad. Establecer en 0 para la selección automática de calidad. Solo se usa cuando el modo de control de tasa es VBR.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Modo dos pases\",\n    \"nvenc_twopass_desc\": \"Añade una tarjeta de codificación preliminar. Esto permite detectar más vectores de movimiento, distribuir mejor la tasa de bits a lo largo del fotograma y adherirse más estrictamente a los límites de la tasa de bits. Deshabilitar no es recomendable ya que esto puede llevar a un rebasamiento ocasional de la tasa de bits y a la pérdida posterior de paquetes.\",\n    \"nvenc_twopass_disabled\": \"Desactivado (lo más rápido, no recomendado)\",\n    \"nvenc_twopass_full_res\": \"Resolución completa (más lento)\",\n    \"nvenc_twopass_quarter_res\": \"Resolución de un cuarto (más rápido, por defecto)\",\n    \"nvenc_vbv_increase\": \"Incremento porcentual de VBV/HRD de un solo fotograma\",\n    \"nvenc_vbv_increase_desc\": \"Por defecto, Sunshine utiliza VBV/HRD de fotograma único, lo que significa que no se espera que el tamaño de ningún fotograma de vídeo codificado supere la tasa de bits solicitada dividida por la tasa de fotogramas solicitada. Flexibilizar esta restricción puede ser beneficioso y actuar como una tasa de bit variable de baja latencia, pero también puede conducir a la pérdida de paquetes si la red no tiene espacio en el búfer para manejar los picos de la tasa de bits. El valor máximo aceptado es 400, que corresponde a un límite de tamaño superior de fotograma de vídeo codificado 5 veces mayor.\",\n    \"origin_web_ui_allowed\": \"Origin Web UI Permitido\",\n    \"origin_web_ui_allowed_desc\": \"El origen de la dirección del punto final remoto al que no se le niega el acceso a la UI web\",\n    \"origin_web_ui_allowed_lan\": \"Sólo aquellos en LAN pueden acceder a la Web UI\",\n    \"origin_web_ui_allowed_pc\": \"Sólo localhost puede acceder a la Web UI\",\n    \"origin_web_ui_allowed_wan\": \"Cualquiera puede acceder a Web UI\",\n    \"output_name_desc_unix\": \"Durante el arranque de Sunshine, debería ver la lista de pantallas detectadas. Nota: Necesita usar el valor id dentro del paréntesis.\",\n    \"output_name_desc_windows\": \"Especifique manualmente una pantalla a usar para capturar. Si no está activada, se captura la pantalla principal. Nota: Si ha especificado un GPU arriba, esta pantalla debe estar conectada a esa GPU. Los valores apropiados se pueden encontrar usando el siguiente comando:\",\n    \"output_name_unix\": \"Mostrar número\",\n    \"output_name_windows\": \"Nombre de salida\",\n    \"ping_timeout\": \"Tiempo de espera\",\n    \"ping_timeout_desc\": \"Cuánto tiempo esperar en milisegundos los datos de Moonlight antes de apagar la corriente\",\n    \"pkey\": \"Clave Privada\",\n    \"pkey_desc\": \"La clave privada usada para la conexión del cliente de interfaz web y luz lunar. Para la mejor compatibilidad, debe ser una clave privada RSA-2048.\",\n    \"port\": \"Puerto\",\n    \"port_alert_1\": \"¡Sunshine no puede usar puertos por debajo de 1024!\",\n    \"port_alert_2\": \"¡Los puertos superiores a 65535 no están disponibles!\",\n    \"port_desc\": \"Establecer la familia de puertos utilizados por Sunshine\",\n    \"port_http_port_note\": \"Use este puerto para conectar con Moonlgiht\",\n    \"port_note\": \"Nota\",\n    \"port_port\": \"Puerto\",\n    \"port_protocol\": \"Protocolo\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"¡Exponer la UI web a Internet es un riesgo para la seguridad! ¡Proceda bajo su propia responsabilidad!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Parámetro de Cuantización\",\n    \"qp_desc\": \"Algunos dispositivos pueden no soportar tasa de bits constante. Para esos dispositivos, se utiliza QP en su lugar. Un valor más alto significa más compresión, pero menos calidad.\",\n    \"qsv_coder\": \"Codificador QuickSync (H264)\",\n    \"qsv_preset\": \"Preajuste QuickSync\",\n    \"qsv_preset_fast\": \"rápido (baja calidad)\",\n    \"qsv_preset_faster\": \"más rápido (menor calidad)\",\n    \"qsv_preset_medium\": \"medio (por defecto)\",\n    \"qsv_preset_slow\": \"lento (buena calidad)\",\n    \"qsv_preset_slower\": \"lento (mejor calidad)\",\n    \"qsv_preset_slowest\": \"Lo más lento (la mejor calidad)\",\n    \"qsv_preset_veryfast\": \"más rápido (menor calidad)\",\n    \"qsv_slow_hevc\": \"Permitir codificación HEVC lenta\",\n    \"qsv_slow_hevc_desc\": \"Esto puede habilitar la codificación HEVC en GPU de Intel, a costa de un mayor uso de GPU y un peor rendimiento.\",\n    \"refresh_rate_change_automatic_windows\": \"Usar valor de FPS proporcionado por el cliente\",\n    \"refresh_rate_change_manual_desc_windows\": \"Ingrese la frecuencia de actualización a usar\",\n    \"refresh_rate_change_manual_windows\": \"Usar frecuencia de actualización ingresada manualmente\",\n    \"refresh_rate_change_no_operation_windows\": \"Deshabilitado\",\n    \"refresh_rate_change_windows\": \"Cambio de FPS\",\n    \"res_fps_desc\": \"Los modos de pantalla anunciados por Sunshine. Algunas versiones de Moonlight, como Moonlight-nx (Switch), dependen de estas listas para asegurar que las resoluciones y fps solicitados sean compatibles. Esta configuración no cambia cómo se envía el flujo de pantalla a Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Usar resolución proporcionada por el cliente\",\n    \"resolution_change_manual_desc_windows\": \"La opción \\\"Optimizar configuración de juegos\\\" debe estar habilitada en el cliente Moonlight para que esto funcione.\",\n    \"resolution_change_manual_windows\": \"Usar resolución ingresada manualmente\",\n    \"resolution_change_no_operation_windows\": \"Deshabilitado\",\n    \"resolution_change_ogs_desc_windows\": \"La opción \\\"Optimizar configuración de juegos\\\" debe estar habilitada en el cliente Moonlight para que esto funcione.\",\n    \"resolution_change_windows\": \"Cambio de resolución\",\n    \"resolutions\": \"Resoluciones anunciadas\",\n    \"restart_note\": \"Sunshine se está reiniciando para aplicar cambios.\",\n    \"sleep_mode\": \"Modo de suspensión\",\n    \"sleep_mode_away\": \"Modo ausente (Pantalla apagada, despertar instantáneo)\",\n    \"sleep_mode_desc\": \"Controla lo que sucede cuando el cliente envía un comando de suspensión. Suspensión (S3): suspensión tradicional, bajo consumo pero requiere WOL para despertar. Hibernación (S4): guarda en disco, consumo muy bajo. Modo ausente: la pantalla se apaga pero el sistema sigue funcionando para despertar instantáneo - ideal para servidores de streaming de juegos.\",\n    \"sleep_mode_hibernate\": \"Hibernación (S4)\",\n    \"sleep_mode_suspend\": \"Suspensión (S3)\",\n    \"stream_audio\": \"Habilitar transmisión de audio\",\n    \"stream_audio_desc\": \"Desactive esta opción para detener la transmisión de audio.\",\n    \"stream_mic\": \"Habilitar transmisión de micrófono\",\n    \"stream_mic_desc\": \"Desactive esta opción para detener la transmisión del micrófono.\",\n    \"stream_mic_download_btn\": \"Descargar micrófono virtual\",\n    \"stream_mic_download_confirm\": \"Está a punto de ser redirigido a la página de descarga del micrófono virtual. ¿Continuar?\",\n    \"stream_mic_note\": \"Esta función requiere instalar un micrófono virtual\",\n    \"sunshine_name\": \"Nombre de Sunshine\",\n    \"sunshine_name_desc\": \"El nombre mostrado por Moonlight. Si no se especifica, se utiliza el nombre de host del PC\",\n    \"sw_preset\": \"Preajustes SW\",\n    \"sw_preset_desc\": \"Optimice la compensación entre la velocidad de codificación (fotogramas codificados por segundo) y la eficiencia de la compresión (calidad por bit en el flujo de bits). Por defecto es superrápido.\",\n    \"sw_preset_fast\": \"rápido\",\n    \"sw_preset_faster\": \"más rápido\",\n    \"sw_preset_medium\": \"medio\",\n    \"sw_preset_slow\": \"lento\",\n    \"sw_preset_slower\": \"más lento\",\n    \"sw_preset_superfast\": \"superrápido (por defecto)\",\n    \"sw_preset_ultrafast\": \"Super Veloz\",\n    \"sw_preset_veryfast\": \"muy rápido\",\n    \"sw_preset_veryslow\": \"muy lento\",\n    \"sw_tune\": \"Sintonía SW\",\n    \"sw_tune_animation\": \"animación -- buena para dibujos animados; utiliza un mayor desbloqueo y más fotogramas de referencia\",\n    \"sw_tune_desc\": \"Opciones de ajuste, que se aplican después de la predeterminada. Por defecto es cero.\",\n    \"sw_tune_fastdecode\": \"decodificación rápida -- permite una decodificación más rápida deshabilitando ciertos filtros\",\n    \"sw_tune_film\": \"Película: se utiliza para películas de alta calidad; reduce el desbloqueo.\",\n    \"sw_tune_grain\": \"grano -- conserva la estructura del grano en el viejo material de película de grano\",\n    \"sw_tune_stillimage\": \"imagen fija -- bueno para contenido de diapositivas\",\n    \"sw_tune_zerolatency\": \"latencia cero -- bueno para codificación rápida y transmisión de baja latencia (por defecto)\",\n    \"system_tray\": \"Habilitar bandeja del sistema\",\n    \"system_tray_desc\": \"Si se debe habilitar la bandeja del sistema. Si está habilitada, Sunshine mostrará un icono en la bandeja del sistema y se puede controlar desde la bandeja del sistema.\",\n    \"touchpad_as_ds4\": \"Emular un gamepad DS4 si el gamepad del cliente reporta que un touchpad está presente\",\n    \"touchpad_as_ds4_desc\": \"Si está desactivado, la presencia del touchpad no se tendrá en cuenta durante la selección del tipo gamepad.\",\n    \"unsaved_changes_tooltip\": \"Tienes cambios sin guardar. Haz clic para guardar.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Configurar automáticamente el reenvío de puertos para transmitir a través de Internet\",\n    \"variable_refresh_rate\": \"Variable Refresh Rate (VRR)\",\n    \"variable_refresh_rate_desc\": \"Permitir que la frecuencia de fotogramas del flujo de vídeo coincida con la frecuencia de fotogramas de renderizado para soporte VRR. Cuando está habilitado, la codificación solo ocurre cuando hay nuevos fotogramas disponibles, permitiendo que el flujo siga la frecuencia de fotogramas de renderizado real.\",\n    \"vdd_reuse_desc_windows\": \"Cuando está habilitado, todos los clientes compartirán el mismo VDD (Virtual Display Device). Cuando está deshabilitado (predeterminado), cada cliente obtiene su propio VDD. Habilite esto para un cambio de cliente más rápido, pero tenga en cuenta que todos los clientes compartirán la misma configuración de pantalla.\",\n    \"vdd_reuse_windows\": \"Reutilizar el mismo VDD para todos los clientes\",\n    \"virtual_display\": \"Pantalla virtual\",\n    \"virtual_mouse\": \"Controlador de ratón virtual\",\n    \"virtual_mouse_desc\": \"Cuando está habilitado, Sunshine usará el controlador Zako Virtual Mouse (si está instalado) para simular la entrada del ratón a nivel HID. Esto permite que los juegos que usan Raw Input reciban eventos del ratón. Cuando está deshabilitado o el controlador no está instalado, recurre a SendInput.\",\n    \"virtual_sink\": \"Enlace virtual\",\n    \"virtual_sink_desc\": \"Especifique manualmente un dispositivo de audio virtual a utilizar. Si no está activado, el dispositivo se elige automáticamente. ¡Recomendamos encarecidamente dejar este campo en blanco para usar la selección automática de dispositivos!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"¿Instalar el controlador de ratón virtual?\",\n    \"vmouse_confirm_uninstall\": \"¿Desinstalar el controlador de ratón virtual?\",\n    \"vmouse_install\": \"Instalar controlador\",\n    \"vmouse_installing\": \"Instalando...\",\n    \"vmouse_note\": \"El controlador de ratón virtual requiere instalación por separado. Use el panel de control de Sunshine para instalar o administrar el controlador.\",\n    \"vmouse_refresh\": \"Actualizar estado\",\n    \"vmouse_status_installed\": \"Instalado (no activo)\",\n    \"vmouse_status_not_installed\": \"No instalado\",\n    \"vmouse_status_running\": \"En ejecución\",\n    \"vmouse_uninstall\": \"Desinstalar controlador\",\n    \"vmouse_uninstalling\": \"Desinstalando...\",\n    \"vt_coder\": \"Codificador de VideoToolbox\",\n    \"vt_realtime\": \"Codificación de tiempo real de VideoToolbox\",\n    \"vt_software\": \"Codificación de software VideoToolbox\",\n    \"vt_software_allowed\": \"Permitido\",\n    \"vt_software_forced\": \"Forzado\",\n    \"wan_encryption_mode\": \"Modo de cifrado WAN\",\n    \"wan_encryption_mode_1\": \"Activado para clientes compatibles (por defecto)\",\n    \"wan_encryption_mode_2\": \"Requerido para todos los clientes\",\n    \"wan_encryption_mode_desc\": \"Esto determina cuándo se utilizará el cifrado al transmitir a través de Internet. El cifrado puede reducir el rendimiento de la transmisión, especialmente en hosts y clientes menos poderosos.\",\n    \"webhook_curl_command\": \"Comando\",\n    \"webhook_curl_command_desc\": \"Copie el siguiente comando en su terminal para probar si el webhook funciona correctamente:\",\n    \"webhook_curl_copy_failed\": \"Error al copiar, por favor seleccione y copie manualmente\",\n    \"webhook_enabled\": \"Notificaciones Webhook\",\n    \"webhook_enabled_desc\": \"Cuando esté habilitado, Sunshine enviará notificaciones de eventos a la URL Webhook especificada\",\n    \"webhook_group\": \"Configuración de notificaciones Webhook\",\n    \"webhook_skip_ssl_verify\": \"Omitir verificación de certificado SSL\",\n    \"webhook_skip_ssl_verify_desc\": \"Omitir verificación de certificado SSL para conexiones HTTPS, solo para pruebas o certificados autofirmados\",\n    \"webhook_test\": \"Probar\",\n    \"webhook_test_failed\": \"Prueba de Webhook falló\",\n    \"webhook_test_failed_note\": \"Nota: Por favor, verifique si la URL es correcta, o consulte la consola del navegador para más información.\",\n    \"webhook_test_success\": \"¡Prueba de Webhook exitosa!\",\n    \"webhook_test_success_cors_note\": \"Nota: Debido a las restricciones CORS, no se puede confirmar el estado de respuesta del servidor.\\nLa solicitud ha sido enviada. Si el webhook está configurado correctamente, el mensaje debería haber sido entregado.\\n\\nSugerencia: Verifique la pestaña Red en las herramientas de desarrollo de su navegador para ver los detalles de la solicitud.\",\n    \"webhook_test_url_required\": \"Por favor, ingrese primero la URL Webhook\",\n    \"webhook_timeout\": \"Tiempo de espera de solicitud\",\n    \"webhook_timeout_desc\": \"Tiempo de espera para solicitudes Webhook en milisegundos, rango 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"La URL para recibir notificaciones de eventos, admite protocolos HTTP/HTTPS\",\n    \"wgc_checking_mode\": \"Comprobando modo...\",\n    \"wgc_checking_running_mode\": \"Comprobando modo de ejecución...\",\n    \"wgc_control_panel_only\": \"Esta característica solo está disponible en el Panel de Control de Sunshine\",\n    \"wgc_mode_switch_failed\": \"Error al cambiar de modo\",\n    \"wgc_mode_switch_started\": \"Cambio de modo iniciado. Si aparece un aviso de UAC, haga clic en 'Sí' para confirmar.\",\n    \"wgc_service_mode_warning\": \"La captura WGC requiere ejecutarse en modo usuario. Si actualmente se ejecuta en modo servicio, haga clic en el botón de arriba para cambiar al modo usuario.\",\n    \"wgc_switch_to_service_mode\": \"Cambiar a Modo Servicio\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Actualmente ejecutándose en modo usuario. Haga clic para cambiar al modo servicio.\",\n    \"wgc_switch_to_user_mode\": \"Cambiar a Modo Usuario\",\n    \"wgc_switch_to_user_mode_tooltip\": \"La captura WGC requiere ejecutarse en modo usuario. Haga clic en este botón para cambiar al modo usuario.\",\n    \"wgc_user_mode_available\": \"Actualmente ejecutándose en modo usuario. La captura WGC está disponible.\",\n    \"window_title\": \"Título de la ventana\",\n    \"window_title_desc\": \"El título de la ventana a capturar (coincidencia parcial, no distingue entre mayúsculas y minúsculas). Si se deja vacío, el nombre de la aplicación en ejecución actual se usará automáticamente.\",\n    \"window_title_placeholder\": \"ej., Nombre de la aplicación\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine es un servidor de transmisión de juego autoalojado para Moonlight.\",\n    \"download\": \"Descargar\",\n    \"installed_version_not_stable\": \"Está ejecutando una versión pre-lanzamiento de Sunshine. Puede que experimente fallos u otros problemas. Por favor, informe de cualquier problema que encuentre. ¡Gracias por ayudar a hacer de Sunshine un mejor software!\",\n    \"loading_latest\": \"Cargando la última versión...\",\n    \"new_pre_release\": \"¡Una nueva versión de pre-lanzamiento está disponible!\",\n    \"new_stable\": \"¡Una nueva versión estable está disponible!\",\n    \"startup_errors\": \"<b>Atención.</b> Sunshine ha detectado estos errores durante el arranque. <b>RECOMENDAMOS ENCARECIDAMENTE</b> solucionarlos antes de transmitir.\",\n    \"update_download_confirm\": \"Está a punto de abrir la página de descarga de actualizaciones en su navegador. ¿Continuar?\",\n    \"version_dirty\": \"¡Gracias por ayudar a hacer de Sunshine un mejor software!\",\n    \"version_latest\": \"Está ejecutando la última versión de Sunshine\",\n    \"view_logs\": \"Ver registros\",\n    \"welcome\": \"¡Hola, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Aplicaciones\",\n    \"configuration\": \"Configuración\",\n    \"home\": \"Inicio\",\n    \"password\": \"Cambiar contraseña\",\n    \"pin\": \"Pin\",\n    \"theme_auto\": \"Auto\",\n    \"theme_dark\": \"Oscuro\",\n    \"theme_light\": \"Claro\",\n    \"toggle_theme\": \"Tema\",\n    \"troubleshoot\": \"Resolución de problemas\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Confirmar contraseña\",\n    \"current_creds\": \"Credenciales actuales\",\n    \"new_creds\": \"Nuevas credenciales\",\n    \"new_username_desc\": \"Si no se especifica, el nombre de usuario no cambiará\",\n    \"password_change\": \"Cambio de contraseña\",\n    \"success_msg\": \"¡La contraseña se ha cambiado con éxito! Esta página se recargará pronto, su navegador le pedirá las nuevas credenciales.\"\n  },\n  \"pin\": {\n    \"actions\": \"Acciones\",\n    \"cancel_editing\": \"Cancelar edición\",\n    \"client_name\": \"Nombre\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Confirmar eliminación\",\n    \"delete_client\": \"Eliminar cliente\",\n    \"delete_confirm_message\": \"¿Está seguro de que desea eliminar <strong>{name}</strong>?\",\n    \"delete_warning\": \"Esta acción no se puede deshacer.\",\n    \"device_name\": \"Nombre del dispositivo\",\n    \"device_size\": \"Tamaño del dispositivo\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Grande - TV\",\n    \"device_size_medium\": \"Mediano - Tableta\",\n    \"device_size_small\": \"Pequeño - Teléfono\",\n    \"edit_client_settings\": \"Editar configuración del cliente\",\n    \"hdr_profile\": \"Perfil HDR\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Cargando...\",\n    \"loading_clients\": \"Cargando clientes...\",\n    \"modify_in_gui\": \"Por favor, modifique en la interfaz gráfica\",\n    \"none\": \"-- Ninguno --\",\n    \"or_manual_pin\": \"o introducir PIN manualmente\",\n    \"pair_failure\": \"Falló el emparejamiento: Compruebe si el PIN está escrito correctamente\",\n    \"pair_success\": \"¡Éxito! Por favor revise Moonlight para continuar\",\n    \"pin_pairing\": \"Emparejamiento de PIN\",\n    \"qr_expires_in\": \"Expira en\",\n    \"qr_generate\": \"Generar código QR\",\n    \"qr_paired_success\": \"¡Emparejado correctamente!\",\n    \"qr_pairing\": \"Emparejamiento por QR\",\n    \"qr_pairing_desc\": \"Genera un código QR para emparejar rápidamente. Escanéalo con el cliente Moonlight para emparejar automáticamente.\",\n    \"qr_pairing_warning\": \"Función experimental. Si el emparejamiento falla, utilice el emparejamiento manual con PIN a continuación. Nota: Esta función solo funciona en LAN.\",\n    \"qr_refresh\": \"Actualizar código QR\",\n    \"remove_paired_devices_desc\": \"Elimine sus dispositivos emparejados.\",\n    \"save_changes\": \"Guardar cambios\",\n    \"save_failed\": \"Error al guardar la configuración del cliente. Por favor, inténtelo de nuevo.\",\n    \"save_or_cancel_first\": \"Por favor, guarde o cancele la edición primero\",\n    \"send\": \"Enviar\",\n    \"unknown_client\": \"Cliente desconocido\",\n    \"unpair_all_confirm\": \"¿Está seguro de que desea desemparejar todos los clientes? Esta acción no se puede deshacer.\",\n    \"unsaved_changes\": \"Cambios no guardados\",\n    \"warning_msg\": \"Asegúrate de tener acceso al cliente con el que estás emparejando. Este software puede dar control total a tu computadora, ¡así que ten cuidado!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android recomendado\",\n    \"client_downloads\": \"Descargas de clientes\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"Discusiones GitHub\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Unirse a la comunidad\",\n    \"join_group_desc\": \"Obtener ayuda y compartir experiencias\",\n    \"legal\": \"Legal\",\n    \"legal_desc\": \"Al continuar utilizando este software usted acepta los términos y condiciones de los siguientes documentos.\",\n    \"license\": \"Licencia\",\n    \"lizardbyte_website\": \"Sitio web de LizardByte\",\n    \"official_website\": \"Sitio web oficial\",\n    \"official_website_title\": \"AlkaidLab - Sitio web oficial\",\n    \"open_source\": \"Código abierto\",\n    \"open_source_desc\": \"Star & Fork para apoyar el proyecto\",\n    \"quick_start\": \"Inicio rápido\",\n    \"resources\": \"Recursos\",\n    \"resources_desc\": \"¡Recursos para Sunshine!\",\n    \"third_party_desc\": \"Avisos de componentes de terceros\",\n    \"third_party_moonlight\": \"Enlaces amigos\",\n    \"third_party_notice\": \"Aviso de Terceros\",\n    \"tutorial\": \"Tutorial\",\n    \"tutorial_desc\": \"Guía detallada de configuración y uso\",\n    \"view_license\": \"Ver licencia completa\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Pantalla virtual\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"Ir al repositorio\",\n    \"harmony_modal_desc\": \"Para HarmonyOS NEXT Moonlight, busque Moonlight V+ en la tienda de HarmonyOS\",\n    \"harmony_modal_link_notice\": \"Este enlace redirigirá al repositorio del proyecto\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"segundos\",\n    \"restart_desc\": \"Configuración guardada. Sunshine se está reiniciando para aplicar la configuración de pantalla.\",\n    \"restart_go_now\": \"Ir ahora\",\n    \"restart_title\": \"Reiniciando Sunshine\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"La configuración básica ya está activa. ¡Puedes comenzar a transmitir con un cliente Moonlight de inmediato!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"La pantalla base (VDD) es la pantalla virtual inteligente integrada de Sunshine Foundation, compatible con cualquier resolución, frecuencia de cuadros y optimización HDR. Es la opción preferida para streaming con pantalla apagada y streaming de pantalla extendida.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Asegurar activación\",\n    \"step3_ensure_active_desc\": \"Activa la pantalla si no está activa\",\n    \"step3_ensure_only_display\": \"Asegurar pantalla única\",\n    \"step3_ensure_only_display_desc\": \"Desactiva todas las demás pantallas y solo activa la especificada (recomendado)\",\n    \"step3_ensure_primary\": \"Asegurar pantalla principal\",\n    \"step3_ensure_primary_desc\": \"Activa la pantalla y la establece como pantalla principal\",\n    \"step3_ensure_secondary\": \"Streaming secundario\",\n    \"step3_ensure_secondary_desc\": \"Usa solo la pantalla virtual para streaming secundario extendido\",\n    \"step3_no_operation\": \"Sin operación\",\n    \"step3_no_operation_desc\": \"Sin cambios en el estado de la pantalla; el usuario debe asegurarse de que esté lista\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Pantalla virtual (ZakoHDR)\",\n    \"virtual_display_desc\": \"Transmitir usando un dispositivo de pantalla virtual (requiere instalación del controlador ZakoVDD)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Avanzado\",\n    \"amd\": \"Codificador AMD AMF\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Codificadores\",\n    \"files\": \"Archivos de configuración\",\n    \"general\": \"General\",\n    \"input\": \"Entrada\",\n    \"network\": \"Red\",\n    \"nv\": \"Codificador NVIDIA NVENC\",\n    \"qsv\": \"Codificador Intel QuickSync\",\n    \"sw\": \"Codificador de software\",\n    \"vaapi\": \"Codificador VAAPI\",\n    \"vt\": \"Codificador VideoToolbox\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analizando...\",\n    \"ai_analyzing_logs\": \"Analizando registros, por favor espere...\",\n    \"ai_config\": \"Configuración IA\",\n    \"ai_copy_result\": \"Copiar\",\n    \"ai_diagnosis\": \"Diagnóstico IA\",\n    \"ai_diagnosis_title\": \"Diagnóstico IA de registros\",\n    \"ai_error\": \"Análisis fallido\",\n    \"ai_key_local\": \"La clave API se almacena solo localmente y nunca se sube\",\n    \"ai_model\": \"Modelo\",\n    \"ai_provider\": \"Proveedor\",\n    \"ai_reanalyze\": \"Reanalizar\",\n    \"ai_result\": \"Resultado del diagnóstico\",\n    \"ai_retry\": \"Reintentar\",\n    \"ai_start_diagnosis\": \"Iniciar diagnóstico\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Si necesita apagar Sunshine inmediatamente, puede usar esta función. Tenga en cuenta que deberá iniciarlo manualmente después del apagado.\",\n    \"boom_sunshine_success\": \"Sunshine se ha apagado\",\n    \"confirm_boom\": \"¿Realmente quieres salir?\",\n    \"confirm_boom_desc\": \"¿Así que realmente quieres salir? Bueno, no puedo detenerte, adelante y haz clic de nuevo\",\n    \"confirm_logout\": \"¿Confirmar cierre de sesión?\",\n    \"confirm_logout_desc\": \"Tendrás que volver a introducir tu contraseña para acceder a la interfaz web.\",\n    \"copy_config\": \"Copiar configuración\",\n    \"copy_config_error\": \"Error al copiar la configuración\",\n    \"copy_config_success\": \"¡Configuración copiada al portapapeles!\",\n    \"copy_logs\": \"Copiar logs\",\n    \"download_logs\": \"Descargar logs\",\n    \"force_close\": \"Forzar cierre\",\n    \"force_close_desc\": \"Si Moonlight se queja de una aplicación actualmente en funcionamiento, forzar el cierre de la aplicación debería solucionar el problema.\",\n    \"force_close_error\": \"Error al cerrar la aplicación\",\n    \"force_close_success\": \"¡Aplicación cerrada con éxito!\",\n    \"ignore_case\": \"Ignorar mayúsculas y minúsculas\",\n    \"logout\": \"Cerrar sesión\",\n    \"logout_desc\": \"Cerrar sesión. Puede que necesite iniciar sesión de nuevo.\",\n    \"logout_localhost_tip\": \"El entorno actual no requiere inicio de sesión; cerrar sesión no mostrará la solicitud de contraseña.\",\n    \"logs\": \"Registros\",\n    \"logs_desc\": \"Ver los registros cargados por Sunshine\",\n    \"logs_find\": \"Encontrar...\",\n    \"match_contains\": \"Contiene\",\n    \"match_exact\": \"Exacto\",\n    \"match_regex\": \"Expresión regular\",\n    \"reopen_setup_wizard\": \"Reabrir asistente de configuración\",\n    \"reopen_setup_wizard_desc\": \"Reabrir la página del asistente de configuración para reconfigurar la configuración inicial.\",\n    \"reopen_setup_wizard_error\": \"Error al reabrir el asistente de configuración\",\n    \"reset_display_device_desc_windows\": \"Si Sunshine está atascado intentando restaurar la configuración del dispositivo de visualización modificada, puede restablecer la configuración y proceder a restaurar el estado de la pantalla manualmente.\\nEsto puede ocurrir por varias razones: el dispositivo ya no está disponible, se ha conectado a un puerto diferente, etc.\",\n    \"reset_display_device_error_windows\": \"Error al restablecer la persistencia!\",\n    \"reset_display_device_success_windows\": \"Restablecimiento de persistencia exitoso!\",\n    \"reset_display_device_windows\": \"Restablecer memoria de pantalla\",\n    \"restart_sunshine\": \"Reiniciar Sunshine\",\n    \"restart_sunshine_desc\": \"Si Sunshine no funciona correctamente, puede intentar reiniciarlo. Esto terminará cualquier sesión en ejecución.\",\n    \"restart_sunshine_success\": \"Sunshine se está reiniciando\",\n    \"troubleshooting\": \"Resolución de problemas\",\n    \"unpair_all\": \"Desemparejar todo\",\n    \"unpair_all_error\": \"Error al desvincular\",\n    \"unpair_all_success\": \"Todos los dispositivos están desvínculados.\",\n    \"unpair_desc\": \"Retire sus dispositivos vínculados. Los dispositivos no vinculados con una sesión activa permanecerán conectados, pero no podrán iniciar ni reanudar una sesión.\",\n    \"unpair_single_no_devices\": \"No hay dispositivos vinculados.\",\n    \"unpair_single_success\": \"Sin embargo, los dispositivo(s) todavía pueden estar en una sesión activa. Utilice el botón \\\"Forzar cierre\\\" de arriba para terminar cualquier sesión abierta.\",\n    \"unpair_single_unknown\": \"Cliente desconocido\",\n    \"unpair_title\": \"Desvincular dispositivos\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Confirmar contraseña\",\n    \"create_creds\": \"Antes de empezar, necesitamos que crees un nuevo nombre de usuario y contraseña para acceder a la Web UI.\",\n    \"create_creds_alert\": \"Las credenciales a continuación son necesarias para acceder a la interfaz web de Sunshine. Manténgalas seguras, ¡ya que nunca volverá a verlas!\",\n    \"creds_local_only\": \"Sus credenciales se almacenan localmente sin conexión y nunca se cargarán en ningún servidor.\",\n    \"error\": \"¡Error!\",\n    \"greeting\": \"¡Bienvenido a Sunshine Foundation!\",\n    \"hide_password\": \"Ocultar contraseña\",\n    \"login\": \"Iniciar sesión\",\n    \"network_error\": \"Error de red, por favor verifique su conexión\",\n    \"password\": \"Contraseña\",\n    \"password_match\": \"Las contraseñas coinciden\",\n    \"password_mismatch\": \"Las contraseñas no coinciden\",\n    \"server_error\": \"Error del servidor\",\n    \"show_password\": \"Mostrar contraseña\",\n    \"success\": \"¡Éxito!\",\n    \"username\": \"Nombre de usuario\",\n    \"welcome_success\": \"Esta página se recargará pronto, su navegador le pedirá las nuevas credenciales\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/fr.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Appliquer\",\n    \"auto\": \"Automatique\",\n    \"autodetect\": \"Détection automatique (recommandé)\",\n    \"beta\": \"(bêta)\",\n    \"cancel\": \"Annuler\",\n    \"close\": \"Fermer\",\n    \"copied\": \"Copié dans le presse-papiers\",\n    \"copy\": \"Copier\",\n    \"delete\": \"Supprimer\",\n    \"description\": \"Description\",\n    \"disabled\": \"Désactivé\",\n    \"disabled_def\": \"Désactivé (par défaut)\",\n    \"dismiss\": \"Ignorer\",\n    \"do_cmd\": \"Commande de début\",\n    \"download\": \"Télécharger\",\n    \"edit\": \"Modifier\",\n    \"elevated\": \"Élevée\",\n    \"enabled\": \"Activé\",\n    \"enabled_def\": \"Activé (par défaut)\",\n    \"error\": \"Erreur !\",\n    \"no_changes\": \"Aucune modification\",\n    \"note\": \"Note :\",\n    \"password\": \"Mot de passe\",\n    \"remove\": \"Supprimer\",\n    \"run_as\": \"Exécuter en tant qu'Administrateur\",\n    \"save\": \"Sauvegarder\",\n    \"see_more\": \"Voir plus\",\n    \"success\": \"Succès !\",\n    \"undo_cmd\": \"Commande de fin\",\n    \"username\": \"Nom d’utilisateur\",\n    \"warning\": \"Attention !\"\n  },\n  \"apps\": {\n    \"actions\": \"Actions\",\n    \"add_cmds\": \"Ajouter des commandes\",\n    \"add_new\": \"Ajouter une application\",\n    \"advanced_options\": \"Options avancées\",\n    \"app_name\": \"Nom de l'application\",\n    \"app_name_desc\": \"Nom de l'application, affiché dans Moonlight\",\n    \"applications_desc\": \"Les applications ne sont actualisées qu'au redémarrage du client\",\n    \"applications_title\": \"Applications\",\n    \"auto_detach\": \"Continuer le streaming si l'application quitte rapidement\",\n    \"auto_detach_desc\": \"Cela va tenter de détecter automatiquement les applications de type launcher qui se ferment rapidement après le lancement d'un autre programme ou d'une instance d'eux-mêmes. Lorsqu'une application de type lanceur est détectée, elle est traitée comme une application détachée.\",\n    \"basic_info\": \"Informations de base\",\n    \"cmd\": \"Commande\",\n    \"cmd_desc\": \"L'application principale à démarrer. Si vide, aucune application ne sera démarrée.\",\n    \"cmd_examples_title\": \"Exemples courants :\",\n    \"cmd_note\": \"Si le chemin vers l'exécutable de la commande contient des espaces, vous devez l'entourer de guillemets.\",\n    \"cmd_prep_desc\": \"Une liste de commandes à exécuter avant/après cette application. Si l'une des commandes préalables échoue, le démarrage de l'application est interrompu.\",\n    \"cmd_prep_name\": \"Commandes de préparation\",\n    \"command_settings\": \"Paramètres de commande\",\n    \"covers_found\": \"Jaquettes trouvées\",\n    \"delete\": \"Supprimer\",\n    \"delete_confirm\": \"Êtes-vous sûr de vouloir supprimer \\\"{name}\\\" ?\",\n    \"detached_cmds\": \"Commandes détachées\",\n    \"detached_cmds_add\": \"Ajouter une commande détachée\",\n    \"detached_cmds_desc\": \"Une liste de commandes à exécuter en arrière-plan.\",\n    \"detached_cmds_note\": \"Si le chemin vers l'exécutable de la commande contient des espaces, vous devez l'entourer de guillemets.\",\n    \"detached_cmds_remove\": \"Supprimer la commande détachée\",\n    \"edit\": \"Modifier\",\n    \"env_app_id\": \"ID de l'application\",\n    \"env_app_name\": \"Nom de l'application\",\n    \"env_client_audio_config\": \"La configuration audio demandée par le client (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"Le client a activé l'option pour optimiser le jeu pour une diffusion optimale (true/false)\",\n    \"env_client_fps\": \"FPS demandé par le client (int)\",\n    \"env_client_gcmap\": \"Le masque de manette demandé, au format bitset/bitfield (int)\",\n    \"env_client_hdr\": \"Le HDR est activé par le client (true/false)\",\n    \"env_client_height\": \"La hauteur demandée par le client (int)\",\n    \"env_client_host_audio\": \"Le client a activé l'audio côté audio (true/false)\",\n    \"env_client_name\": \"Nom convivial du client (chaîne)\",\n    \"env_client_width\": \"La largeur demandée par le client (int)\",\n    \"env_displayplacer_example\": \"Exemple - displayplacer pour l'automatisation de la résolution :\",\n    \"env_qres_example\": \"Exemple - QRes pour l'automatisation de la résolution :\",\n    \"env_qres_path\": \"chemin de qres\",\n    \"env_var_name\": \"Nom de la variable\",\n    \"env_vars_about\": \"À propos des variables d'environnement\",\n    \"env_vars_desc\": \"Toutes les commandes récupèrent ces variables d'environnement par défaut :\",\n    \"env_xrandr_example\": \"Exemple - Xrandr pour l'automatisation de la résolution :\",\n    \"exit_timeout\": \"Délai de fermeture\",\n    \"exit_timeout_desc\": \"Nombre de secondes d'attente pour que tous les processus de l'application se ferment gracieusement lorsque demandé à quitter. Si non défini, la valeur par défaut est d'attendre jusqu'à 5 secondes. Si elle est définie à zéro ou à une valeur négative, l'application sera immédiatement fermée.\",\n    \"file_selector_not_initialized\": \"Sélecteur de fichiers non initialisé\",\n    \"find_cover\": \"Trouver une jaquette\",\n    \"form_invalid\": \"Veuillez vérifier les champs requis\",\n    \"form_valid\": \"Application valide\",\n    \"global_prep_desc\": \"Activer/désactiver l'exécution des commandes globales de préparation pour cette application.\",\n    \"global_prep_name\": \"Commandes globales de préparation\",\n    \"image\": \"Image\",\n    \"image_desc\": \"Chemin d'accès à l'icône/image de l'application qui sera envoyée au client. L'image doit être un fichier PNG. Si ce n'est pas le cas, Sunshine enverra une jaquette par défaut.\",\n    \"image_settings\": \"Paramètres d'image\",\n    \"loading\": \"Chargement...\",\n    \"menu_cmd_actions\": \"Actions\",\n    \"menu_cmd_add\": \"Ajouter une commande de menu\",\n    \"menu_cmd_command\": \"Commande\",\n    \"menu_cmd_desc\": \"Après configuration, ces commandes seront visibles dans le menu de retour du client, permettant l'exécution rapide d'opérations spécifiques sans interrompre le flux, comme le lancement de programmes d'aide.\\nExemple : Nom d'affichage - Fermer votre ordinateur ; Commande - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Nom d'affichage\",\n    \"menu_cmd_drag_sort\": \"Glisser pour trier\",\n    \"menu_cmd_name\": \"Commandes de menu\",\n    \"menu_cmd_placeholder_command\": \"Commande\",\n    \"menu_cmd_placeholder_display_name\": \"Nom d'affichage\",\n    \"menu_cmd_placeholder_execute\": \"Exécuter la commande\",\n    \"menu_cmd_placeholder_undo\": \"Annuler la commande\",\n    \"menu_cmd_remove_menu\": \"Supprimer la commande de menu\",\n    \"menu_cmd_remove_prep\": \"Supprimer la commande de préparation\",\n    \"mouse_mode\": \"Mode souris\",\n    \"mouse_mode_auto\": \"Auto (Paramètre global)\",\n    \"mouse_mode_desc\": \"Sélectionnez la méthode d'entrée souris pour cette application. Auto utilise le paramètre global, Souris virtuelle utilise le pilote HID, SendInput utilise l'API Windows.\",\n    \"mouse_mode_sendinput\": \"SendInput (API Windows)\",\n    \"mouse_mode_vmouse\": \"Souris virtuelle\",\n    \"name\": \"Nom\",\n    \"output_desc\": \"Le fichier dans lequel la sortie de la commande est stockée, s'il n'est pas spécifié, la sortie est ignorée\",\n    \"output_name\": \"Sortie\",\n    \"run_as_desc\": \"Cela peut être nécessaire pour certaines applications qui nécessitent des autorisations d'administrateur pour fonctionner correctement.\",\n    \"scan_result_add_all\": \"Tout ajouter\",\n    \"scan_result_edit_title\": \"Ajouter et modifier\",\n    \"scan_result_filter_all\": \"Tout\",\n    \"scan_result_filter_epic_title\": \"Jeux Epic Games\",\n    \"scan_result_filter_executable\": \"Exécutable\",\n    \"scan_result_filter_executable_title\": \"Fichier exécutable\",\n    \"scan_result_filter_gog_title\": \"Jeux GOG Galaxy\",\n    \"scan_result_filter_script\": \"Script\",\n    \"scan_result_filter_script_title\": \"Script batch/commande\",\n    \"scan_result_filter_shortcut\": \"Raccourci\",\n    \"scan_result_filter_shortcut_title\": \"Raccourci\",\n    \"scan_result_filter_steam_title\": \"Jeux Steam\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Jeu\",\n    \"scan_result_games_only\": \"Jeux uniquement\",\n    \"scan_result_matched\": \"Correspondance : {count}\",\n    \"scan_result_no_apps\": \"Aucune application trouvée à ajouter\",\n    \"scan_result_no_matches\": \"Aucune application correspondante trouvée\",\n    \"scan_result_quick_add_title\": \"Ajout rapide\",\n    \"scan_result_remove_title\": \"Retirer de la liste\",\n    \"scan_result_search_placeholder\": \"Rechercher le nom de l'application, la commande ou le chemin...\",\n    \"scan_result_show_all\": \"Afficher tout\",\n    \"scan_result_title\": \"Résultats de l'analyse\",\n    \"scan_result_try_different_keywords\": \"Essayez d'utiliser différents mots-clés de recherche\",\n    \"scan_result_type_batch\": \"Batch\",\n    \"scan_result_type_command\": \"Script de commande\",\n    \"scan_result_type_executable\": \"Fichier exécutable\",\n    \"scan_result_type_shortcut\": \"Raccourci\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Rechercher des applications...\",\n    \"select\": \"Sélectionner\",\n    \"test_menu_cmd\": \"Tester la commande\",\n    \"test_menu_cmd_empty\": \"La commande ne peut pas être vide\",\n    \"test_menu_cmd_executing\": \"Exécution de la commande...\",\n    \"test_menu_cmd_failed\": \"Échec de l'exécution de la commande\",\n    \"test_menu_cmd_success\": \"Commande exécutée avec succès !\",\n    \"use_desktop_image\": \"Utiliser le fond d'écran actuel du bureau\",\n    \"wait_all\": \"Continuer le streaming jusqu'à ce que tous les processus de l'application quittent\",\n    \"wait_all_desc\": \"Cela continuera le streaming jusqu'à ce que tous les processus démarrés par l'application soient terminés. Si non coché, le streaming s'arrêtera lorsque le processus initial de l'application se terminera, même si d'autres processus sont toujours en cours d'exécution.\",\n    \"working_dir\": \"Démarrer dans\",\n    \"working_dir_desc\": \"Le répertoire de démarrage qui doit être passé au processus. Par exemple, certaines applications utilisent le répertoire de démarrage \\n pour rechercher des fichiers de configuration. Si non défini, Sunshine utilisera par défaut le répertoire parent de la commande\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Nom de l'adaptateur\",\n    \"adapter_name_desc_linux_1\": \"Spécifiez manuellement un GPU à utiliser pour la capture.\",\n    \"adapter_name_desc_linux_2\": \"pour trouver tous les appareils capables d'utiliser l'interface VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Remplacez ``renderD129`` par le dispositif ci-dessus pour énumérer le nom et les capacités du dispositif. Pour être pris en charge par Sunshine, il doit avoir au minimum les caractéristiques suivantes :\",\n    \"adapter_name_desc_windows\": \"Spécifiez manuellement un GPU à utiliser pour la capture. Si non défini, le GPU est choisi automatiquement. Remarque : Ce GPU doit avoir un écran connecté et allumé. Si votre ordinateur portable ne peut pas activer la sortie GPU directe, veuillez le définir sur automatique.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Si la dernière version de l'écran virtuel est installée, elle peut s'associer automatiquement à la liaison GPU\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Ajouter\",\n    \"address_family\": \"Famille d'adresses\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Définir la famille d'adresses utilisée par Sunshine\",\n    \"address_family_ipv4\": \"IPv4 uniquement\",\n    \"always_send_scancodes\": \"Toujours envoyer les codes de balayage\",\n    \"always_send_scancodes_desc\": \"L'envoi de codes de numérisation améliore la compatibilité avec les jeux et les applications, mais peut entraîner une saisie incorrecte du clavier de certains clients qui n'utilisent pas de disposition de clavier anglais américain. Activer si l'entrée du clavier ne fonctionne pas du tout dans certaines applications. Désactiver si les clés du client génèrent la mauvaise entrée sur l'hôte.\",\n    \"amd_coder\": \"Codeur AMF (H264)\",\n    \"amd_coder_desc\": \"Permet de sélectionner l'encodage de l'entropie pour prioriser la qualité ou la vitesse d'encodage. H.264 seulement.\",\n    \"amd_enforce_hrd\": \"Enforcement du Décodeur Hypothetical Reference Decoder (HRD) de l'AMF\",\n    \"amd_enforce_hrd_desc\": \"Augmente les contraintes sur le contrôle du débit pour répondre aux exigences du modèle HRD. Cela réduit considérablement les débordements de débit, mais peut causer des artefacts d'encodage ou une qualité réduite sur certaines cartes.\",\n    \"amd_preanalysis\": \"Pré-analyse AMF\",\n    \"amd_preanalysis_desc\": \"Cela permet une préanalyse de contrôle de débit, ce qui peut augmenter la qualité au détriment d'une latence d'encodage accrue.\",\n    \"amd_quality\": \"Qualité AMF\",\n    \"amd_quality_balanced\": \"balanced -- équilibré (par défaut)\",\n    \"amd_quality_desc\": \"Ceci contrôle l'équilibre entre la vitesse d'encodage et la qualité.\",\n    \"amd_quality_group\": \"Paramètres de qualité AMF\",\n    \"amd_quality_quality\": \"qualité -- préférer la qualité\",\n    \"amd_quality_speed\": \"Vitesse -- Préférez la vitesse\",\n    \"amd_qvbr_quality\": \"Niveau de qualité AMF QVBR\",\n    \"amd_qvbr_quality_desc\": \"Niveau de qualité pour le mode de contrôle de débit QVBR. Plage : 1-51 (plus bas = meilleure qualité). Par défaut : 23. S'applique uniquement quand le contrôle de débit est réglé sur 'qvbr'.\",\n    \"amd_rc\": \"Contrôle du débit AMF\",\n    \"amd_rc_cbr\": \"cbr -- débit constant\",\n    \"amd_rc_cqp\": \"cqp -- mode constant qp\",\n    \"amd_rc_desc\": \"Ceci contrôle la méthode de contrôle du débit pour s'assurer que nous ne dépassons pas la cible du bitrate client. 'cqp' n'est pas adapté pour le ciblage de débit, et d'autres options en plus de 'vbr_latency' dépendent de HRD Enforcement pour aider à limiter les débordements de débit.\",\n    \"amd_rc_group\": \"Réglages de contrôle du débit AMF\",\n    \"amd_rc_hqcbr\": \"hqcbr -- débit constant haute qualité\",\n    \"amd_rc_hqvbr\": \"hqvbr -- débit variable haute qualité\",\n    \"amd_rc_qvbr\": \"qvbr -- débit variable de qualité (utilise le niveau de qualité QVBR)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- Débit variable limité de latence (par défaut)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- Débit variable contraint par le pic\",\n    \"amd_usage\": \"Utilisation de l'AMF\",\n    \"amd_usage_desc\": \"Définit le profil d'encodage de base. Toutes les options présentées ci-dessous remplaceront un sous-ensemble du profil d'utilisation, mais il y a d'autres paramètres cachés qui ne peuvent pas être configurés ailleurs.\",\n    \"amd_usage_lowlatency\": \"lowlatency - faible latence (rapide)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - faible latence, haute qualité (rapide)\",\n    \"amd_usage_transcoding\": \"transcoding -- transcodage (plus lent)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency - latence ultra basse (plus rapide)\",\n    \"amd_usage_webcam\": \"webcam -- webcam (lent)\",\n    \"amd_vbaq\": \"Quantification adaptative basée sur la variance AMF (VBAQ)\",\n    \"amd_vbaq_desc\": \"Le système visuel humain est généralement moins sensible aux artefacts dans les zones hautement texturées. En mode VBAQ, la variance de pixels est utilisée pour indiquer la complexité des textures spatiales, ce qui permet à l'encodeur d'allouer plus de bits à des zones plus lisses. L'activation de cette fonctionnalité conduit à des améliorations de la qualité visuelle subjective avec un certain contenu.\",\n    \"amf_draw_mouse_cursor\": \"Dessiner un curseur simple lors de l'utilisation de la méthode de capture AMF\",\n    \"amf_draw_mouse_cursor_desc\": \"Dans certains cas, l'utilisation de la capture AMF n'affichera pas le pointeur de la souris. L'activation de cette option dessinera un pointeur de souris simple à l'écran. Note : La position du pointeur de la souris ne sera mise à jour que lorsqu'il y a une mise à jour de l'écran de contenu, donc dans des scénarios non-jeu comme sur le bureau, vous pouvez observer un mouvement lent du pointeur de la souris.\",\n    \"apply_note\": \"Cliquez sur \\\"Appliquer\\\" pour redémarrer Sunshine et appliquer les modifications. Cela mettra fin à toutes les sessions en cours.\",\n    \"audio_sink\": \"Sink audio\",\n    \"audio_sink_desc_linux\": \"Le nom du dissipateur audio utilisé pour la boucle audio. Si vous ne spécifiez pas cette variable, pulseaudio sélectionnera le périphérique de moniteur par défaut. Vous pouvez trouver le nom du dissipateur audio en utilisant l'une des commandes:\",\n    \"audio_sink_desc_macos\": \"Le nom du dissipateur audio utilisé pour la boucle audio. Sunshine ne peut accéder aux micros que sur macOS en raison de limitations du système. Diffusez de l'audio système en utilisant Soundflower ou BlackHole.\",\n    \"audio_sink_desc_windows\": \"Spécifiez manuellement un périphérique audio spécifique à capturer. Si non défini, le périphérique est choisi automatiquement. Nous vous recommandons fortement de laisser ce champ vide pour utiliser la sélection automatique de l'appareil ! Si vous avez plusieurs périphériques audio avec des noms identiques, vous pouvez obtenir l'ID du périphérique en utilisant la commande suivante :\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Haut-parleurs (High Definition Audio Device)\",\n    \"av1_mode\": \"Support de l'AV1\",\n    \"av1_mode_0\": \"Sunshine annoncera la prise en charge de l'AV1 en fonction des capacités de l'encodeur (recommandé)\",\n    \"av1_mode_1\": \"Sunshine n'annoncera pas la prise en charge de l'AV1\",\n    \"av1_mode_2\": \"Sunshine annoncera la prise en charge de l'AV1 Main 8-bit profile\",\n    \"av1_mode_3\": \"Sunshine annoncera la prise en charge de l'AV1 Main 8-bit et 10-bit (HDR) profiles\",\n    \"av1_mode_desc\": \"Permet au client de demander des flux vidéo AV1 8-bit ou 10-bit. AV1 est plus gourmand en CPU pour l'encodage, ce qui peut réduire les performances lors de l'utilisation de l'encodage logiciel.\",\n    \"back_button_timeout\": \"Délai d'émulation du bouton Home/Guide\",\n    \"back_button_timeout_desc\": \"Si le bouton Précédent/Sélection est enfoncé pour le nombre spécifié de millisecondes, un bouton Accueil/Guide est émulé. Si défini à une valeur < 0 (par défaut), maintenir le bouton Retour/Sélectionner n'émulera pas le bouton Accueil/Guide.\",\n    \"bind_address\": \"Adresse de liaison (fonctionnalité de test)\",\n    \"bind_address_desc\": \"Définir l'adresse IP spécifique à laquelle Sunshine se liera. Si laissé vide, Sunshine se liera à toutes les adresses disponibles.\",\n    \"capture\": \"Forcer une méthode de capture spécifique\",\n    \"capture_desc\": \"En mode automatique, Sunshine utilisera le premier qui fonctionne. NvFBC nécessite des pilotes nvidia corrigés.\",\n    \"capture_target\": \"Cible de capture\",\n    \"capture_target_desc\": \"Sélectionnez le type de cible à capturer. En sélectionnant 'Fenêtre', vous pouvez capturer une fenêtre d'application spécifique (comme un logiciel d'interpolation d'images AI) au lieu de l'affichage entier.\",\n    \"capture_target_display\": \"Écran\",\n    \"capture_target_window\": \"Fenêtre\",\n    \"cert\": \"Certificat\",\n    \"cert_desc\": \"Le certificat utilisé pour l'appairage de l'interface web et du client Moonlight. Pour une meilleure compatibilité, cela devrait avoir une clé publique RSA-2048.\",\n    \"channels\": \"Nombre maximum de clients connectés\",\n    \"channels_desc_1\": \"Sunshine peut permettre à une seule session de streaming d'être partagée simultanément avec plusieurs clients.\",\n    \"channels_desc_2\": \"Certains encodeurs matériels peuvent avoir des limitations qui réduisent les performances avec plusieurs flux.\",\n    \"close_verify_safe\": \"Vérification sécurisée compatible avec les anciens clients\",\n    \"close_verify_safe_desc\": \"Les anciens clients peuvent ne pas pouvoir se connecter à Sunshine, veuillez désactiver cette option ou mettre à jour le client\",\n    \"coder_cabac\": \"cabac -- Contexte de codage arithmétique binaire adaptatif - qualité supérieure\",\n    \"coder_cavlc\": \"cavlc -- codage de la durée adaptative du contexte - décodage plus rapide\",\n    \"configuration\": \"Configuration\",\n    \"controller\": \"Activer l'entrée manette\",\n    \"controller_desc\": \"Permet aux invités de contrôler le système hôte avec une manette\",\n    \"credentials_file\": \"Fichier des identifiants\",\n    \"credentials_file_desc\": \"Stocker le nom d'utilisateur/mot de passe séparément du fichier de données de Sunshine.\",\n    \"display_device_options_note_desc_windows\": \"Windows enregistre divers paramètres d'affichage pour chaque combinaison d'écrans actuellement actifs.\\nSunshine applique ensuite les modifications à un ou plusieurs écrans appartenant à une telle combinaison d'écrans.\\nSi vous déconnectez un périphérique qui était actif lorsque Sunshine a appliqué les paramètres, les modifications ne peuvent pas être\\nrestaurées sauf si la combinaison peut être réactivée au moment où Sunshine essaie de restaurer les modifications !\",\n    \"display_device_options_note_windows\": \"Note sur la façon dont les paramètres sont appliqués\",\n    \"display_device_options_windows\": \"Options du périphérique d'affichage\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Active l'écran s'il n'est pas déjà actif\",\n    \"display_device_prep_ensure_active_windows\": \"Activer l'écran automatiquement\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Désactive tous les autres écrans et n'active que l'écran spécifié\",\n    \"display_device_prep_ensure_only_display_windows\": \"Désactiver les autres écrans et activer uniquement l'écran spécifié\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Active l'écran et le définit comme écran principal\",\n    \"display_device_prep_ensure_primary_windows\": \"Activer l'écran automatiquement et en faire l'écran principal\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Utilise uniquement l'écran virtuel pour le streaming secondaire étendu\",\n    \"display_device_prep_ensure_secondary_windows\": \"Streaming d'écran secondaire (écran virtuel uniquement)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Aucune modification de l'état de l'écran ; l'utilisateur doit s'assurer que l'écran est prêt\",\n    \"display_device_prep_no_operation_windows\": \"Désactivé\",\n    \"display_device_prep_windows\": \"Préparation de l'affichage\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Au moins une valeur \\\"reçue\\\" et une valeur \\\"finale\\\" doivent être spécifiées.\\nUn champ vide dans la section \\\"reçue\\\" signifie \\\"correspondre à n'importe quelle valeur\\\". Un champ vide dans la section \\\"finale\\\" signifie \\\"garder la valeur reçue\\\".\\nVous pouvez faire correspondre une valeur FPS spécifique à une résolution spécifique si vous le souhaitez...\\n\\nNote : si l'option \\\"Optimiser les paramètres du jeu\\\" n'est pas activée sur le client Moonlight, les lignes contenant la valeur de résolution sont ignorées.\",\n    \"display_mode_remapping_desc_windows\": \"Spécifiez comment une résolution et/ou un taux de rafraîchissement spécifiques doivent être remappés vers d'autres valeurs.\\nVous pouvez diffuser à une résolution plus faible, tout en rendant à une résolution plus élevée sur l'hôte pour un effet de suréchantillonnage.\\nOu vous pouvez diffuser à un FPS plus élevé tout en limitant l'hôte au taux de rafraîchissement plus faible.\\nLa correspondance est effectuée de haut en bas. Une fois qu'une entrée correspond, les autres ne sont plus vérifiées, mais sont toujours validées.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Taux de rafraîchissement final\",\n    \"display_mode_remapping_final_resolution_windows\": \"Résolution finale\",\n    \"display_mode_remapping_optional\": \"optionnel\",\n    \"display_mode_remapping_received_fps_windows\": \"FPS reçu\",\n    \"display_mode_remapping_received_resolution_windows\": \"Résolution reçue\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Note : si l'option \\\"Optimiser les paramètres du jeu\\\" n'est pas activée sur le client Moonlight, le remappage est désactivé.\",\n    \"display_mode_remapping_windows\": \"Remapper les modes d'affichage\",\n    \"display_modes\": \"Modes d'affichage\",\n    \"ds4_back_as_touchpad_click\": \"Mapper Retour/Sélection au clic du pavé tactile\",\n    \"ds4_back_as_touchpad_click_desc\": \"Lorsque vous forcez l'émulation DS4, mapper retour/sélectionner sur Touchpad Clic\",\n    \"dsu_server_port\": \"Port du serveur DSU\",\n    \"dsu_server_port_desc\": \"Port d'écoute du serveur DSU (par défaut 26760). Sunshine agira comme un serveur DSU pour recevoir les connexions client et envoyer les données de mouvement. Activez le serveur DSU dans votre client (Yuzu, Ryujinx, etc.) et définissez l'adresse du serveur DSU (127.0.0.1) et le port (26760)\",\n    \"enable_dsu_server\": \"Activer le serveur DSU\",\n    \"enable_dsu_server_desc\": \"Activer le serveur DSU pour recevoir les connexions client et envoyer les données de mouvement\",\n    \"encoder\": \"Forcer un encodeur spécifique\",\n    \"encoder_desc\": \"Forcer un encodeur spécifique, sinon Sunshine sélectionnera la meilleure option disponible. Note : Si vous spécifiez un encodeur matériel sous Windows, il doit correspondre au GPU où l'affichage est connecté.\",\n    \"encoder_software\": \"Logiciel\",\n    \"experimental\": \"Expérimental\",\n    \"experimental_features\": \"Fonctionnalités expérimentales\",\n    \"external_ip\": \"Adresse IP externe\",\n    \"external_ip_desc\": \"Si aucune adresse IP externe n'est fournie, Sunshine la détectera automatiquement\",\n    \"fec_percentage\": \"Pourcentage de FEC\",\n    \"fec_percentage_desc\": \"Pourcentage de paquets corrigeant les erreurs par paquet de données dans chaque image vidéo. Des valeurs plus élevées permettent de corriger davantage de pertes de paquets sur le réseau, mais au prix d'une augmentation de l'utilisation de la bande passante.\",\n    \"ffmpeg_auto\": \"auto -- laisser ffmpeg décider (par défaut)\",\n    \"file_apps\": \"Fichier des applications\",\n    \"file_apps_desc\": \"Le fichier où sont stockées les applications de Sunshine.\",\n    \"file_state\": \"Fichier des données\",\n    \"file_state_desc\": \"Le fichier où l'état actuel de Sunshine est stocké\",\n    \"fps\": \"FPS annoncés\",\n    \"gamepad\": \"Type de manette émulée\",\n    \"gamepad_auto\": \"Options de la sélection automatique\",\n    \"gamepad_desc\": \"Choisissez le type de manette à émuler sur l'hôte\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"Options manuelles DS4\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Options manuelles pour DS4\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Commandes de préparation\",\n    \"global_prep_cmd_desc\": \"Configurer une liste de commandes à exécuter avant ou après l'exécution d'une application. Si l'une des commandes de préparation spécifiées échoue, le processus de lancement de l'application sera interrompu.\",\n    \"hdr_luminance_analysis\": \"Métadonnées dynamiques HDR (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Active l'analyse de luminance GPU par image et injecte les métadonnées dynamiques HDR10+ (ST 2094-40) et HDR Vivid (CUVA) dans le flux encodé. Fournit des indications de tone-mapping par image pour les écrans compatibles. Ajoute un léger surcoût GPU (~0,5-1,5ms/image en haute résolution). Désactivez si vous constatez des baisses de framerate avec le HDR activé.\",\n    \"hdr_prep_automatic_windows\": \"Activer/désactiver le mode HDR selon la demande du client\",\n    \"hdr_prep_no_operation_windows\": \"Désactivé\",\n    \"hdr_prep_windows\": \"Changement d'état HDR\",\n    \"hevc_mode\": \"Support du HEVC\",\n    \"hevc_mode_0\": \"Sunshine annoncera la prise en charge de HEVC en fonction des capacités de l'encodeur (recommandé)\",\n    \"hevc_mode_1\": \"Sunshine n'annoncera pas la prise en charge du HEVC\",\n    \"hevc_mode_2\": \"Sunshine annoncera la prise en charge du HEVC Main profile\",\n    \"hevc_mode_3\": \"Sunshine annoncera la prise en charge du HEVC Main et Main10 (HDR)\",\n    \"hevc_mode_desc\": \"Permet au client de demander des flux vidéo HEVC Main ou HEVC Main10. HEVC est plus gourmand en CPU pour l'encodage, ce qui peut réduire les performances lors de l'utilisation de l'encodage logiciel.\",\n    \"high_resolution_scrolling\": \"Prise en charge du défilement haute résolution\",\n    \"high_resolution_scrolling_desc\": \"Lorsque cette option est activée, Sunshine passera par les événements de défilement haute résolution des clients de Lune. Cela peut être utile pour désactiver pour les anciennes applications qui font défiler trop vite avec des événements de défilement haute résolution.\",\n    \"install_steam_audio_drivers\": \"Installer les pilotes audio Steam\",\n    \"install_steam_audio_drivers_desc\": \"Si Steam est installé, cela installera automatiquement le pilote Steam Streaming Speakers pour prendre en charge le son surround 5.1/7.1 et rendre muet l'audio de l'hôte.\",\n    \"key_repeat_delay\": \"Délai de répétition de la clé\",\n    \"key_repeat_delay_desc\": \"Contrôle la vitesse à laquelle les clés se répètent. Le délai initial en millisecondes avant de répéter les clés.\",\n    \"key_repeat_frequency\": \"Fréquence de répétition des touches\",\n    \"key_repeat_frequency_desc\": \"Fréquence de répétition des touches chaque seconde. Cette option configurable prend en charge les décimaux.\",\n    \"key_rightalt_to_key_win\": \"Mapper la touche Alt droite à la touche Windows\",\n    \"key_rightalt_to_key_win_desc\": \"Il est possible que vous ne puissiez pas envoyer directement la touche Windows à partir de Moonlight. Dans ce cas, il peut être utile de faire croire à Sunshine que la touche Alt droite est la touche Windows\",\n    \"key_rightalt_to_key_windows\": \"Mapper la touche Alt droite à la touche Windows\",\n    \"keyboard\": \"Activer l'entrée clavier\",\n    \"keyboard_desc\": \"Permet aux invités de contrôler le système hôte avec le clavier\",\n    \"lan_encryption_mode\": \"Mode de chiffrement LAN\",\n    \"lan_encryption_mode_1\": \"Activé pour les clients pris en charge\",\n    \"lan_encryption_mode_2\": \"Obligatoire pour tous les clients\",\n    \"lan_encryption_mode_desc\": \"Ceci détermine quand le chiffrement sera utilisé lors du streaming sur votre réseau local. Le chiffrement peut réduire les performances de streaming, en particulier sur les hôtes et clients moins puissants.\",\n    \"locale\": \"Langue\",\n    \"locale_desc\": \"La langue utilisée pour l'interface utilisateur de Sunshine.\",\n    \"log_level\": \"Niveau de journalisation\",\n    \"log_level_0\": \"Verbeux\",\n    \"log_level_1\": \"Débug\",\n    \"log_level_2\": \"Info\",\n    \"log_level_3\": \"Avertissements\",\n    \"log_level_4\": \"Erreurs\",\n    \"log_level_5\": \"Erreurs fatales\",\n    \"log_level_6\": \"Aucun\",\n    \"log_level_desc\": \"Le niveau minimum de journal imprimé à la sortie standard\",\n    \"log_path\": \"Chemin du fichier journal\",\n    \"log_path_desc\": \"Le fichier où sont stockés les logs actuels de Sunshine.\",\n    \"max_bitrate\": \"Débit maximum\",\n    \"max_bitrate_desc\": \"Le débit maximum (en Kbps) auquel Sunshine encode le flux. Si réglé sur 0, il utilisera toujours le débit demandé par Lune.\",\n    \"max_fps_reached\": \"Valeurs FPS maximales atteintes\",\n    \"max_resolutions_reached\": \"Nombre maximum de résolutions atteint\",\n    \"mdns_broadcast\": \"Trouver cet ordinateur dans le réseau local\",\n    \"mdns_broadcast_desc\": \"Si cette option est activée, Sunshine permettra à d'autres appareils de trouver automatiquement cet ordinateur. Moonlight doit également être configuré pour trouver automatiquement cet ordinateur dans le réseau local.\",\n    \"min_threads\": \"Nombre minimum de threads CPU\",\n    \"min_threads_desc\": \"Augmenter la valeur réduit légèrement l'efficacité de l'encodage, mais le compromis vaut généralement la peine de gagner l'utilisation de plus de cœurs CPU pour l'encodage. La valeur idéale est la valeur la plus basse qui peut de manière fiable encoder les paramètres de streaming désirés sur votre matériel.\",\n    \"minimum_fps_target\": \"FPS minimum cible\",\n    \"minimum_fps_target_desc\": \"FPS minimum à maintenir lors de l'encodage (0 = automatique, environ la moitié du FPS du flux ; 1-1000 = FPS minimum à maintenir). Lorsque le taux de rafraîchissement variable est activé, ce paramètre est ignoré s'il est défini sur 0.\",\n    \"misc\": \"Options diverses\",\n    \"motion_as_ds4\": \"Émuler une manette DS4 si la manette client signale qu'elle dispose de capteurs de mouvement\",\n    \"motion_as_ds4_desc\": \"Si désactivé, la présence de capteurs de mouvement ne sera pas pris en compte lors de la sélection du type de manette.\",\n    \"mouse\": \"Activer l'entrée de la souris\",\n    \"mouse_desc\": \"Permet aux invités de contrôler le système hôte avec la souris\",\n    \"native_pen_touch\": \"Prise en charge stylo/écran tactile native\",\n    \"native_pen_touch_desc\": \"Lorsque cette option est activée, Sunshine transmet les événements stylo/touche natifs des clients Moonlight. Il peut être utile de désactiver cette fonction pour les applications plus anciennes qui ne prennent pas en charge le stylet et le tactile.\",\n    \"no_fps\": \"Aucune valeur FPS ajoutée\",\n    \"no_resolutions\": \"Aucune résolution ajoutée\",\n    \"notify_pre_releases\": \"Notifications de pré-publication\",\n    \"notify_pre_releases_desc\": \"Si vous voulez être informé des nouvelles versions de la pré-version de Sunshine\",\n    \"nvenc_h264_cavlc\": \"Préférer CAVLC sur CABAC en H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Forme plus simple de codage entropique. CAVLC a besoin d'environ 10% de débit en plus pour la même qualité. Uniquement pour les périphériques de décodage vraiment anciens.\",\n    \"nvenc_latency_over_power\": \"Préférer une latence d'encodage plus faible aux économies d'énergie\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine demande une vitesse maximale d'horloge GPU pendant le streaming pour réduire la latence d'encodage. La désactivation n'est pas recommandée car cela peut entraîner une augmentation significative de la latence d'encodage.\",\n    \"nvenc_lookahead_depth\": \"Lookahead depth\",\n    \"nvenc_lookahead_depth_desc\": \"Number of frames to look ahead during encoding (0-32). Lookahead improves encoding quality, especially in complex scenes, by providing better motion estimation and bitrate distribution. Higher values improve quality but increase encoding latency. Set to 0 to disable lookahead. Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_lookahead_level\": \"Lookahead level\",\n    \"nvenc_lookahead_level_0\": \"Level 0 (lowest quality, fastest)\",\n    \"nvenc_lookahead_level_1\": \"Level 1\",\n    \"nvenc_lookahead_level_2\": \"Level 2\",\n    \"nvenc_lookahead_level_3\": \"Level 3 (highest quality, slowest)\",\n    \"nvenc_lookahead_level_autoselect\": \"Auto-select (let driver choose optimal level)\",\n    \"nvenc_lookahead_level_desc\": \"Lookahead quality level. Higher levels improve quality at the expense of performance. This option only takes effect when lookahead_depth is greater than 0. Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_lookahead_level_disabled\": \"Disabled (same as level 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Présenter OpenGL/Vulkan sur DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine ne peut pas capturer les programmes plein écran OpenGL et Vulkan à un rythme plein d'images à moins qu'ils ne soient présents sur DXGI. Il s'agit d'un réglage de l'ensemble du système qui est rétabli à la sortie du programme de soleil.\",\n    \"nvenc_preset\": \"Préréglage des performances\",\n    \"nvenc_preset_1\": \"(plus rapide, par défaut)\",\n    \"nvenc_preset_7\": \"(plus lent)\",\n    \"nvenc_preset_desc\": \"Des nombres plus élevés améliorent la compression (qualité à un débit donné) au prix d'une latence d'encodage accrue. Il est recommandé de modifier uniquement lorsque limité par le réseau ou le décodage, sinon l'effet similaire peut être atteint en augmentant le débit.\",\n    \"nvenc_rate_control\": \"Mode de contrôle de débit\",\n    \"nvenc_rate_control_cbr\": \"CBR (Débit constant) - Faible latence\",\n    \"nvenc_rate_control_desc\": \"Sélectionnez le mode de contrôle du débit. CBR (Débit constant) fournit un débit fixe pour une diffusion à faible latence. VBR (Débit variable) permet au débit de varier en fonction de la complexité de la scène, offrant une meilleure qualité pour des scènes complexes au prix d'un débit variable.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Débit variable) - Meilleure qualité\",\n    \"nvenc_realtime_hags\": \"Utiliser la priorité en temps réel dans la planification matérielle du gpu accéléré\",\n    \"nvenc_realtime_hags_desc\": \"Actuellement, les pilotes NVIDIA peuvent geler dans l'encodeur lorsque HAGS est activé, la priorité en temps réel est utilisée et l'utilisation de VRAM est proche du maximum. Désactiver cette option réduit la priorité à la hauteur, en évitant le gel au prix de performances de capture réduites lorsque le GPU est lourdement chargé.\",\n    \"nvenc_spatial_aq\": \"AQ spatiale\",\n    \"nvenc_spatial_aq_desc\": \"Assignez des valeurs QP plus élevées aux zones uniformes de la vidéo. Il est conseillé de l'activer lors de la diffusion à des débits binaires réduits.\",\n    \"nvenc_spatial_aq_disabled\": \"Désactivé (plus rapide, par défaut)\",\n    \"nvenc_spatial_aq_enabled\": \"Activé (plus lent)\",\n    \"nvenc_split_encode\": \"Encodage de trame divisé\",\n    \"nvenc_split_encode_desc\": \"Diviser l'encodage de chaque trame vidéo sur plusieurs unités matérielles NVENC. Réduit considérablement la latence d'encodage avec une pénalité d'efficacité de compression marginale. Cette option est ignorée si votre GPU a une unité NVENC unique.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Le pilote décide (par défaut)\",\n    \"nvenc_split_encode_four_strips\": \"Forcer la division en 4 bandes (nécessite 4+ moteurs NVENC)\",\n    \"nvenc_split_encode_three_strips\": \"Forcer la division en 3 bandes (nécessite 3+ moteurs NVENC)\",\n    \"nvenc_split_encode_two_strips\": \"Forcer la division en 2 bandes (nécessite 2+ moteurs NVENC)\",\n    \"nvenc_target_quality\": \"Qualité cible (mode VBR)\",\n    \"nvenc_target_quality_desc\": \"Niveau de qualité cible pour le mode VBR (0-51 pour H.264/HEVC, 0-63 pour AV1). Valeurs inférieures = meilleure qualité. Définir à 0 pour la sélection automatique de la qualité. Utilisé uniquement lorsque le mode de contrôle de débit est VBR.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Mode bi-passe\",\n    \"nvenc_twopass_desc\": \"Ajoute le passage d'encodage préliminaire. Cela permet de détecter plus de vecteurs de mouvement, de mieux répartir le débit sur la trame et de respecter plus strictement les limites de débit. La désactivation n'est pas recommandée car cela peut entraîner des dépassements occasionnels de débit et des pertes de paquets subséquentes.\",\n    \"nvenc_twopass_disabled\": \"Désactivé (plus rapide, non recommandé)\",\n    \"nvenc_twopass_full_res\": \"Résolution complète (plus lente)\",\n    \"nvenc_twopass_quarter_res\": \"Résolution trimestrielle (plus rapide, par défaut)\",\n    \"nvenc_vbv_increase\": \"Augmentation du pourcentage de VBV/HRD à une seule image\",\n    \"nvenc_vbv_increase_desc\": \"Par défaut, le soleil utilise un VBV/HRD mono-image, ce qui signifie que toute taille d'image vidéo encodée ne devrait pas dépasser le débit demandé divisé par le débit d'images demandé. Relaxer cette restriction peut être bénéfique et agir en tant que débit variable de faible latence, mais peut aussi conduire à une perte de paquets si le réseau ne dispose pas de mémoire tampon pour gérer les pics de débit. La valeur maximale acceptée est de 400, ce qui correspond à la limite de taille supérieure de l'image vidéo encodée de 5x.\",\n    \"origin_web_ui_allowed\": \"Origines autorisées à accéder à l'interface web\",\n    \"origin_web_ui_allowed_desc\": \"Origine de l'adresse du point de terminaison distant à laquelle l'accès à l'interface utilisateur Web n'est pas refusé\",\n    \"origin_web_ui_allowed_lan\": \"Seuls ceux qui sont en LAN peuvent accéder à l'interface Web\",\n    \"origin_web_ui_allowed_pc\": \"Seul localhost peut accéder à l'interface Web\",\n    \"origin_web_ui_allowed_wan\": \"N'importe qui peut accéder à l'interface Web\",\n    \"output_name_desc_unix\": \"Lors du démarrage de Sunshine, vous devriez voir la liste des affichages détectés. Note : Vous devez utiliser la valeur de l'id entre parenthèses.\",\n    \"output_name_desc_windows\": \"Spécifier manuellement un affichage à utiliser pour la capture. Si non défini, l'affichage principal est capturé. Note: Si vous avez spécifié un GPU ci-dessus, cet affichage doit être connecté à ce GPU. Les valeurs appropriées peuvent être trouvées en utilisant la commande suivante :\",\n    \"output_name_unix\": \"Afficher le numéro\",\n    \"output_name_windows\": \"Nom de la sortie\",\n    \"ping_timeout\": \"Timeout du ping\",\n    \"ping_timeout_desc\": \"Combien de temps attendre en millisecondes pour des données de Moonlight avant de couper le stream\",\n    \"pkey\": \"Clé privée\",\n    \"pkey_desc\": \"La clé privée utilisée pour l'interface web et pour l'appairage des clients Moonlight. Pour une meilleure compatibilité, il devrait s'agir d'une clé privée RSA-2048.\",\n    \"port\": \"Port\",\n    \"port_alert_1\": \"Sunshine ne peut pas utiliser les ports inférieurs à 1024 !\",\n    \"port_alert_2\": \"Les ports supérieurs à 65535 ne sont pas disponibles !\",\n    \"port_desc\": \"Définir la famille de ports utilisés par Sunshine\",\n    \"port_http_port_note\": \"Utilisez ce port pour vous connecter avec Moonlight.\",\n    \"port_note\": \"Note \",\n    \"port_port\": \"Port\",\n    \"port_protocol\": \"Protocole\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Exposer l'interface Web à Internet est un risque de sécurité ! Procédez à vos propres risques !\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Paramètre de quantification\",\n    \"qp_desc\": \"Certains appareils peuvent ne pas prendre en charge un taux de bits constant. Pour ceux-ci, QP est utilisé à la place. Une valeur plus élevée signifie plus de compression, mais moins de qualité.\",\n    \"qsv_coder\": \"Codeur QuickSync (H264)\",\n    \"qsv_preset\": \"Préréglage QuickSync\",\n    \"qsv_preset_fast\": \"plus rapide (qualité inférieure)\",\n    \"qsv_preset_faster\": \"le plus rapide (qualité la plus basse)\",\n    \"qsv_preset_medium\": \"medium (par défaut)\",\n    \"qsv_preset_slow\": \"lent (bonne qualité)\",\n    \"qsv_preset_slower\": \"plus lent (meilleure qualité)\",\n    \"qsv_preset_slowest\": \"plus lent (meilleure qualité)\",\n    \"qsv_preset_veryfast\": \"le plus rapide (qualité la plus basse)\",\n    \"qsv_slow_hevc\": \"Autoriser l'encodage Slow HEVC\",\n    \"qsv_slow_hevc_desc\": \"Cela peut permettre l'encodage HEVC sur les anciens GPU Intel, au prix d'une utilisation plus élevée du GPU et de performances moins élevées.\",\n    \"refresh_rate_change_automatic_windows\": \"Utiliser la valeur FPS fournie par le client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Entrez le taux de rafraîchissement à utiliser\",\n    \"refresh_rate_change_manual_windows\": \"Utiliser le taux de rafraîchissement saisi manuellement\",\n    \"refresh_rate_change_no_operation_windows\": \"Désactivé\",\n    \"refresh_rate_change_windows\": \"Changement de FPS\",\n    \"res_fps_desc\": \"Les modes d'affichage annoncés par Sunshine. Certaines versions de Moonlight, telles que Moonlight-nx (Switch), s'appuient sur ces listes pour s'assurer que les résolutions et fps demandés sont pris en charge. Ce paramètre ne change pas la façon dont le flux d'écran est envoyé à Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Utiliser la résolution fournie par le client\",\n    \"resolution_change_manual_desc_windows\": \"L'option \\\"Optimiser les paramètres du jeu\\\" doit être activée sur le client Moonlight pour que cela fonctionne.\",\n    \"resolution_change_manual_windows\": \"Utiliser la résolution saisie manuellement\",\n    \"resolution_change_no_operation_windows\": \"Désactivé\",\n    \"resolution_change_ogs_desc_windows\": \"L'option \\\"Optimiser les paramètres du jeu\\\" doit être activée sur le client Moonlight pour que cela fonctionne.\",\n    \"resolution_change_windows\": \"Changement de résolution\",\n    \"resolutions\": \"Résolutions annoncées\",\n    \"restart_note\": \"Sunshine redémarre pour appliquer les changements.\",\n    \"sleep_mode\": \"Mode veille\",\n    \"sleep_mode_away\": \"Mode absence (Écran éteint, réveil instantané)\",\n    \"sleep_mode_desc\": \"Contrôle ce qui se passe lorsque le client envoie une commande de mise en veille. Suspension (S3) : veille traditionnelle, faible consommation mais nécessite WOL pour le réveil. Hibernation (S4) : sauvegarde sur disque, très faible consommation. Mode absence : l'écran s'éteint mais le système reste actif pour un réveil instantané - idéal pour les serveurs de streaming de jeux.\",\n    \"sleep_mode_hibernate\": \"Hibernation (S4)\",\n    \"sleep_mode_suspend\": \"Suspension (S3 Veille)\",\n    \"stream_audio\": \"Activer la diffusion audio\",\n    \"stream_audio_desc\": \"Désactivez cette option pour arrêter la diffusion audio.\",\n    \"stream_mic\": \"Activer la diffusion du microphone\",\n    \"stream_mic_desc\": \"Désactivez cette option pour arrêter la diffusion du microphone.\",\n    \"stream_mic_download_btn\": \"Télécharger le microphone virtuel\",\n    \"stream_mic_download_confirm\": \"Vous allez être redirigé vers la page de téléchargement du microphone virtuel. Continuer?\",\n    \"stream_mic_note\": \"Cette fonctionnalité nécessite l'installation d'un microphone virtuel\",\n    \"sunshine_name\": \"Nom de Sunshine\",\n    \"sunshine_name_desc\": \"Le nom affiché par Moonlight. S'il n'est pas spécifié, le nom d'hôte du PC est utilisé.\",\n    \"sw_preset\": \"Préréglages SW\",\n    \"sw_preset_desc\": \"Optimiser le compromis entre la vitesse d'encodage (images encodées par seconde) et l'efficacité de la compression (qualité par bit dans le flux bit). La valeur par défaut est superfast.\",\n    \"sw_preset_fast\": \"Rapide\",\n    \"sw_preset_faster\": \"plus rapide\",\n    \"sw_preset_medium\": \"Moyen\",\n    \"sw_preset_slow\": \"lent\",\n    \"sw_preset_slower\": \"plus lent\",\n    \"sw_preset_superfast\": \"superfast (par défaut)\",\n    \"sw_preset_ultrafast\": \"ultrafast\",\n    \"sw_preset_veryfast\": \"veryfast\",\n    \"sw_preset_veryslow\": \"veryslow\",\n    \"sw_tune\": \"Réglage SW\",\n    \"sw_tune_animation\": \"animation -- bonne pour les dessins animés ; utilise un déblocage plus élevé et plus d'images de référence\",\n    \"sw_tune_desc\": \"Les options de réglage, qui sont appliquées après le préréglage. Par défaut, la valeur est zéro.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- permet un décodage plus rapide en désactivant certains filtres\",\n    \"sw_tune_film\": \"film -- utilisé pour le contenu de film de haute qualité; baisse le déblocage\",\n    \"sw_tune_grain\": \"grain -- conserve la structure du grain dans le vieux matériel de film graineux\",\n    \"sw_tune_stillimage\": \"stillimage -- bon pour le contenu du diaporama\",\n    \"sw_tune_zerolatency\": \"zerolatency -- bon pour un encodage rapide et un streaming à faible latence (par défaut)\",\n    \"system_tray\": \"Activer la barre d'état système\",\n    \"system_tray_desc\": \"S'il faut activer la barre d'état système. Si activé, Sunshine affichera une icône dans la barre d'état système et pourra être contrôlé depuis la barre d'état système.\",\n    \"touchpad_as_ds4\": \"Émuler une manette DS4 si la manette client signale qu'elle dispose d'un pavé tactile\",\n    \"touchpad_as_ds4_desc\": \"Si désactivé, la présence du pavé tactile ne sera pas prise en compte lors de la sélection du type du pavé tactile.\",\n    \"unsaved_changes_tooltip\": \"Vous avez des modifications non enregistrées. Cliquez pour enregistrer.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Configurer automatiquement la redirection de port pour le streaming sur Internet\",\n    \"variable_refresh_rate\": \"Taux de rafraîchissement variable (VRR)\",\n    \"variable_refresh_rate_desc\": \"Permettre au taux d'images du flux vidéo de correspondre au taux d'images de rendu pour la prise en charge VRR. Lorsqu'il est activé, l'encodage ne se produit que lorsque de nouvelles images sont disponibles, permettant au flux de suivre le taux d'images de rendu réel.\",\n    \"vdd_reuse_desc_windows\": \"Lorsqu'activé, tous les clients partageront le même VDD (Virtual Display Device). Lorsque désactivé (par défaut), chaque client obtient son propre VDD. Activez cette option pour un changement de client plus rapide, mais notez que tous les clients partageront les mêmes paramètres d'affichage.\",\n    \"vdd_reuse_windows\": \"Réutiliser le même VDD pour tous les clients\",\n    \"virtual_display\": \"Écran virtuel\",\n    \"virtual_mouse\": \"Pilote de souris virtuelle\",\n    \"virtual_mouse_desc\": \"Lorsqu'activé, Sunshine utilisera le pilote Zako Virtual Mouse (s'il est installé) pour simuler l'entrée souris au niveau HID. Cela permet aux jeux utilisant Raw Input de recevoir les événements souris. Lorsque désactivé ou le pilote n'est pas installé, revient à SendInput.\",\n    \"virtual_sink\": \"Évier virtuel\",\n    \"virtual_sink_desc\": \"Spécifiez manuellement un périphérique audio virtuel à utiliser. Si non défini, le périphérique est choisi automatiquement. Nous vous recommandons fortement de laisser ce champ vide pour utiliser la sélection automatique de l'appareil !\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Installer le pilote de souris virtuelle ?\",\n    \"vmouse_confirm_uninstall\": \"Désinstaller le pilote de souris virtuelle ?\",\n    \"vmouse_install\": \"Installer le pilote\",\n    \"vmouse_installing\": \"Installation...\",\n    \"vmouse_note\": \"Le pilote de souris virtuelle nécessite une installation séparée. Veuillez utiliser le panneau de contrôle Sunshine pour installer ou gérer le pilote.\",\n    \"vmouse_refresh\": \"Actualiser le statut\",\n    \"vmouse_status_installed\": \"Installé (non actif)\",\n    \"vmouse_status_not_installed\": \"Non installé\",\n    \"vmouse_status_running\": \"En cours d'exécution\",\n    \"vmouse_uninstall\": \"Désinstaller le pilote\",\n    \"vmouse_uninstalling\": \"Désinstallation...\",\n    \"vt_coder\": \"Codeur VideoToolbox\",\n    \"vt_realtime\": \"Encodage en temps réel VideoToolbox\",\n    \"vt_software\": \"Encodage logiciel VideoToolbox\",\n    \"vt_software_allowed\": \"Autorisé\",\n    \"vt_software_forced\": \"Forcé\",\n    \"wan_encryption_mode\": \"Mode de chiffrement WAN\",\n    \"wan_encryption_mode_1\": \"Activé pour les clients pris en charge (par défaut)\",\n    \"wan_encryption_mode_2\": \"Obligatoire pour tous les clients\",\n    \"wan_encryption_mode_desc\": \"Ceci détermine quand le chiffrement sera utilisé lors du streaming par Internet. Le chiffrement peut réduire les performances de streaming, en particulier sur les hôtes et clients moins puissants.\",\n    \"webhook_curl_command\": \"Commande\",\n    \"webhook_curl_command_desc\": \"Copiez la commande suivante dans votre terminal pour tester si le webhook fonctionne correctement :\",\n    \"webhook_curl_copy_failed\": \"Échec de la copie, veuillez sélectionner et copier manuellement\",\n    \"webhook_enabled\": \"Notifications Webhook\",\n    \"webhook_enabled_desc\": \"Lorsqu'activé, Sunshine enverra des notifications d'événements à l'URL Webhook spécifiée\",\n    \"webhook_group\": \"Paramètres de notification Webhook\",\n    \"webhook_skip_ssl_verify\": \"Ignorer la vérification du certificat SSL\",\n    \"webhook_skip_ssl_verify_desc\": \"Ignorer la vérification du certificat SSL pour les connexions HTTPS, uniquement pour les tests ou les certificats auto-signés\",\n    \"webhook_test\": \"Tester\",\n    \"webhook_test_failed\": \"Test Webhook échoué\",\n    \"webhook_test_failed_note\": \"Note : Veuillez vérifier si l'URL est correcte, ou consultez la console du navigateur pour plus d'informations.\",\n    \"webhook_test_success\": \"Test Webhook réussi !\",\n    \"webhook_test_success_cors_note\": \"Note : En raison des restrictions CORS, le statut de réponse du serveur ne peut pas être confirmé.\\nLa requête a été envoyée. Si le webhook est correctement configuré, le message devrait avoir été livré.\\n\\nSuggestion : Vérifiez l'onglet Réseau dans les outils de développement de votre navigateur pour les détails de la requête.\",\n    \"webhook_test_url_required\": \"Veuillez d'abord saisir l'URL Webhook\",\n    \"webhook_timeout\": \"Délai d'expiration de la requête\",\n    \"webhook_timeout_desc\": \"Délai d'expiration pour les requêtes Webhook en millisecondes, plage 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"L'URL pour recevoir les notifications d'événements, prend en charge les protocoles HTTP/HTTPS\",\n    \"wgc_checking_mode\": \"Vérification du mode...\",\n    \"wgc_checking_running_mode\": \"Vérification du mode d'exécution...\",\n    \"wgc_control_panel_only\": \"Cette fonctionnalité est uniquement disponible dans le panneau de configuration Sunshine\",\n    \"wgc_mode_switch_failed\": \"Échec du changement de mode\",\n    \"wgc_mode_switch_started\": \"Changement de mode initié. Si une invite UAC apparaît, veuillez cliquer sur 'Oui' pour confirmer.\",\n    \"wgc_service_mode_warning\": \"La capture WGC nécessite une exécution en mode utilisateur. Si vous êtes actuellement en mode service, veuillez cliquer sur le bouton ci-dessus pour passer en mode utilisateur.\",\n    \"wgc_switch_to_service_mode\": \"Passer en mode service\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Actuellement en mode utilisateur. Cliquez pour passer en mode service.\",\n    \"wgc_switch_to_user_mode\": \"Passer en mode utilisateur\",\n    \"wgc_switch_to_user_mode_tooltip\": \"La capture WGC nécessite une exécution en mode utilisateur. Cliquez sur ce bouton pour passer en mode utilisateur.\",\n    \"wgc_user_mode_available\": \"Actualmente en mode utilisateur. La capture WGC est disponible.\",\n    \"window_title\": \"Titre de la fenêtre\",\n    \"window_title_desc\": \"Le titre de la fenêtre à capturer (correspondance partielle, insensible à la casse). Si laissé vide, le nom de l'application en cours d'exécution sera utilisé automatiquement.\",\n    \"window_title_placeholder\": \"par ex., Nom de l'application\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine est un serveur de streaming auto-hébergé pour Moonlight.\",\n    \"download\": \"Télécharger\",\n    \"installed_version_not_stable\": \"Vous utilisez une version pré-publiée de Sunshine. Vous pouvez rencontrer des bugs ou d'autres problèmes. Veuillez signaler tout problème que vous rencontrez. Merci de nous aider à faire de Sunshine un meilleur logiciel!\",\n    \"loading_latest\": \"Chargement de la dernière version...\",\n    \"new_pre_release\": \"Une nouvelle préversion est disponible !\",\n    \"new_stable\": \"Une nouvelle version stable est disponible !\",\n    \"startup_errors\": \"<b>Attention !</b> Sunshine a détecté ces erreurs lors du démarrage. Nous <b>recommandons vivement de</b> les corriger avant de commencer à streamer.\",\n    \"update_download_confirm\": \"Vous êtes sur le point d'ouvrir la page de téléchargement des mises à jour dans votre navigateur. Continuer ?\",\n    \"version_dirty\": \"Merci de contribuer à faire de Sunshine un meilleur logiciel !\",\n    \"version_latest\": \"Vous utilisez la dernière version de Sunshine\",\n    \"view_logs\": \"Voir les journaux\",\n    \"welcome\": \"Bonjour, Sunshine !\"\n  },\n  \"navbar\": {\n    \"applications\": \"Applications\",\n    \"configuration\": \"Configuration\",\n    \"home\": \"Accueil\",\n    \"password\": \"Changer le mot de passe\",\n    \"pin\": \"Pin\",\n    \"theme_auto\": \"Automatique\",\n    \"theme_dark\": \"Sombre\",\n    \"theme_light\": \"Lumière\",\n    \"toggle_theme\": \"Thème\",\n    \"troubleshoot\": \"Dépannage\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Confirmer le mot de passe\",\n    \"current_creds\": \"Identifiants actuels\",\n    \"new_creds\": \"Nouveaux identifiants\",\n    \"new_username_desc\": \"S'il n'est pas spécifié, le nom d'utilisateur ne changera pas\",\n    \"password_change\": \"Changement du mot de passe\",\n    \"success_msg\": \"Le mot de passe a été modifié avec succès ! Cette page sera bientôt rechargée, votre navigateur vous demandera les nouveaux identifiants.\"\n  },\n  \"pin\": {\n    \"actions\": \"Actions\",\n    \"cancel_editing\": \"Annuler l'édition\",\n    \"client_name\": \"Nom\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Confirmer la suppression\",\n    \"delete_client\": \"Supprimer le client\",\n    \"delete_confirm_message\": \"Êtes-vous sûr de vouloir supprimer <strong>{name}</strong> ?\",\n    \"delete_warning\": \"Cette action ne peut pas être annulée.\",\n    \"device_name\": \"Nom de l'appareil\",\n    \"device_size\": \"Taille de l'appareil\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Grand - TV\",\n    \"device_size_medium\": \"Moyen - Tablette\",\n    \"device_size_small\": \"Petit - Téléphone\",\n    \"edit_client_settings\": \"Modifier les paramètres du client\",\n    \"hdr_profile\": \"Profil HDR\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Chargement...\",\n    \"loading_clients\": \"Chargement des clients...\",\n    \"modify_in_gui\": \"Veuillez modifier dans l'interface graphique\",\n    \"none\": \"-- Aucun --\",\n    \"or_manual_pin\": \"ou saisir le PIN manuellement\",\n    \"pair_failure\": \"Échec de l'appairage : Vérifiez si le code PIN est correctement saisi\",\n    \"pair_success\": \"Succès ! Veuillez vérifier Moonlight pour continuer\",\n    \"pin_pairing\": \"Appairage par code PIN\",\n    \"qr_expires_in\": \"Expire dans\",\n    \"qr_generate\": \"Générer le QR Code\",\n    \"qr_paired_success\": \"Appairage réussi !\",\n    \"qr_pairing\": \"Appairage par QR Code\",\n    \"qr_pairing_desc\": \"Générez un QR code pour un appairage rapide. Scannez-le avec le client Moonlight pour l'appairage automatique.\",\n    \"qr_pairing_warning\": \"Fonctionnalité expérimentale. Si l'appairage échoue, veuillez utiliser l'appairage PIN manuel ci-dessous. Note : Cette fonctionnalité ne fonctionne que sur le réseau local.\",\n    \"qr_refresh\": \"Actualiser le QR Code\",\n    \"remove_paired_devices_desc\": \"Supprimez vos appareils appairés.\",\n    \"save_changes\": \"Enregistrer les modifications\",\n    \"save_failed\": \"Échec de l'enregistrement des paramètres du client. Veuillez réessayer.\",\n    \"save_or_cancel_first\": \"Veuillez d'abord enregistrer ou annuler l'édition\",\n    \"send\": \"Envoyer\",\n    \"unknown_client\": \"Client inconnu\",\n    \"unpair_all_confirm\": \"Êtes-vous sûr de vouloir dissocier tous les clients ? Cette action ne peut pas être annulée.\",\n    \"unsaved_changes\": \"Modifications non enregistrées\",\n    \"warning_msg\": \"Assurez-vous que vous avez accès au client avec lequel vous appariez. Ce logiciel peut donner un contrôle total à votre ordinateur, alors soyez prudent !\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android recommandé\",\n    \"client_downloads\": \"Téléchargements clients\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"Discussions GitHub\",\n    \"gpl_license_text_1\": \"Ce logiciel est sous licence GPL-3.0. Vous êtes libre de l'utiliser, de le modifier et de le distribuer.\",\n    \"gpl_license_text_2\": \"Pour protéger l'écosystème open source, veuillez éviter d'utiliser des logiciels qui violent la licence GPL-3.0.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Rejoindre la communauté\",\n    \"join_group_desc\": \"Obtenir de l'aide et partager des expériences\",\n    \"legal\": \"Légal\",\n    \"legal_desc\": \"En continuant à utiliser ce logiciel, vous acceptez les termes et conditions des documents suivants.\",\n    \"license\": \"Licence\",\n    \"lizardbyte_website\": \"Site web de LizardByte\",\n    \"official_website\": \"Site officiel\",\n    \"official_website_title\": \"AlkaidLab - Site officiel\",\n    \"open_source\": \"Open Source\",\n    \"open_source_desc\": \"Star & Fork pour soutenir le projet\",\n    \"quick_start\": \"Démarrage rapide\",\n    \"resources\": \"Ressources\",\n    \"resources_desc\": \"Ressources pour Sunshine !\",\n    \"third_party_desc\": \"Avis sur les composants tiers\",\n    \"third_party_moonlight\": \"Liens amis\",\n    \"third_party_notice\": \"Avis aux tiers\",\n    \"tutorial\": \"Tutoriel\",\n    \"tutorial_desc\": \"Guide détaillé de configuration et d'utilisation\",\n    \"view_license\": \"Voir la licence complète\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Résumé de la configuration\",\n    \"android_client\": \"Client Android\",\n    \"base_display_title\": \"Affichage virtuel\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"La configuration a été enregistrée avec succès.\",\n    \"description\": \"Commençons par une configuration rapide\",\n    \"device_id\": \"ID du périphérique\",\n    \"device_state\": \"État\",\n    \"download_clients\": \"Télécharger les clients\",\n    \"finish\": \"Terminer la configuration\",\n    \"go_to_apps\": \"Configurer les applications\",\n    \"harmony_goto_repo\": \"Aller au dépôt\",\n    \"harmony_modal_desc\": \"Pour HarmonyOS NEXT Moonlight, veuillez rechercher Moonlight V+ dans le HarmonyOS App Store\",\n    \"harmony_modal_link_notice\": \"Ce lien redirigera vers le dépôt du projet\",\n    \"ios_client\": \"Client iOS\",\n    \"load_error\": \"Échec du chargement de la configuration\",\n    \"next\": \"Suivant\",\n    \"physical_display\": \"Écran physique/Émulateur EDID\",\n    \"physical_display_desc\": \"Diffuser vos moniteurs physiques réels\",\n    \"previous\": \"Précédent\",\n    \"restart_countdown_unit\": \"secondes\",\n    \"restart_desc\": \"Configuration enregistrée. Sunshine redémarre pour appliquer les paramètres d'affichage.\",\n    \"restart_go_now\": \"Aller maintenant\",\n    \"restart_title\": \"Redémarrage de Sunshine\",\n    \"save_error\": \"Échec de l'enregistrement de la configuration\",\n    \"select_adapter\": \"Adaptateur graphique\",\n    \"selected_adapter\": \"Adaptateur sélectionné\",\n    \"selected_display\": \"Écran sélectionné\",\n    \"setup_complete\": \"Configuration terminée !\",\n    \"setup_complete_desc\": \"Les paramètres de base sont maintenant actifs. Vous pouvez commencer à streamer avec un client Moonlight immédiatement !\",\n    \"skip\": \"Ignorer l'assistant de configuration\",\n    \"skip_confirm\": \"Êtes-vous sûr de vouloir ignorer l'assistant de configuration ? Vous pourrez configurer ces options plus tard dans la page des paramètres.\",\n    \"skip_confirm_title\": \"Ignorer l'assistant de configuration\",\n    \"skip_error\": \"Échec de l'ignorance\",\n    \"state_active\": \"Actif\",\n    \"state_inactive\": \"Inactif\",\n    \"state_primary\": \"Principal\",\n    \"state_unknown\": \"Inconnu\",\n    \"step0_description\": \"Choisissez la langue de l'interface\",\n    \"step0_title\": \"Langue\",\n    \"step1_description\": \"Choisissez l'écran à diffuser\",\n    \"step1_title\": \"Sélection de l'écran\",\n    \"step1_vdd_intro\": \"L'écran de base (VDD) est l'écran virtuel intelligent intégré de Sunshine Foundation, prenant en charge toute résolution, fréquence d'images et optimisation HDR. C'est le choix privilégié pour le streaming écran éteint et le streaming en écran étendu.\",\n    \"step2_description\": \"Choisissez votre adaptateur graphique\",\n    \"step2_title\": \"Sélectionner l'adaptateur\",\n    \"step3_description\": \"Choisissez la stratégie de préparation du périphérique d'affichage\",\n    \"step3_ensure_active\": \"Assurer l'activation\",\n    \"step3_ensure_active_desc\": \"Active l'écran s'il n'est pas déjà actif\",\n    \"step3_ensure_only_display\": \"Assurer l'écran unique\",\n    \"step3_ensure_only_display_desc\": \"Désactive tous les autres écrans et n'active que l'écran spécifié (recommandé)\",\n    \"step3_ensure_primary\": \"Assurer l'écran principal\",\n    \"step3_ensure_primary_desc\": \"Active l'écran et le définit comme écran principal\",\n    \"step3_ensure_secondary\": \"Streaming secondaire\",\n    \"step3_ensure_secondary_desc\": \"Utilise uniquement l'écran virtuel pour le streaming secondaire étendu\",\n    \"step3_no_operation\": \"Aucune opération\",\n    \"step3_no_operation_desc\": \"Aucune modification de l'état de l'écran ; l'utilisateur doit s'assurer que l'écran est prêt\",\n    \"step3_title\": \"Stratégie d'affichage\",\n    \"step4_title\": \"Terminé\",\n    \"stream_mode\": \"Mode de diffusion\",\n    \"unknown_display\": \"Écran inconnu\",\n    \"virtual_display\": \"Écran virtuel (ZakoHDR)\",\n    \"virtual_display_desc\": \"Diffuser en utilisant un périphérique d'affichage virtuel (nécessite l'installation du pilote ZakoVDD)\",\n    \"welcome\": \"Bienvenue dans Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Avancé\",\n    \"amd\": \"Encodeur AMD AMF\",\n    \"av\": \"Audio/Vidéo\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Fichiers de configuration\",\n    \"general\": \"Général\",\n    \"input\": \"Entrée\",\n    \"network\": \"Réseau\",\n    \"nv\": \"Encodeur NVIDIA NVENC\",\n    \"qsv\": \"Encodeur Intel QuickSync\",\n    \"sw\": \"Encodeur logiciel\",\n    \"vaapi\": \"Encodeur VAAPI\",\n    \"vt\": \"Encodeur VideoToolbox\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analyse...\",\n    \"ai_analyzing_logs\": \"Analyse des journaux en cours, veuillez patienter...\",\n    \"ai_config\": \"Configuration IA\",\n    \"ai_copy_result\": \"Copier\",\n    \"ai_diagnosis\": \"Diagnostic IA\",\n    \"ai_diagnosis_title\": \"Diagnostic IA des journaux\",\n    \"ai_error\": \"Analyse échouée\",\n    \"ai_key_local\": \"La clé API est stockée localement uniquement et jamais téléchargée\",\n    \"ai_model\": \"Modèle\",\n    \"ai_provider\": \"Fournisseur\",\n    \"ai_reanalyze\": \"Réanalyser\",\n    \"ai_result\": \"Résultat du diagnostic\",\n    \"ai_retry\": \"Réessayer\",\n    \"ai_start_diagnosis\": \"Démarrer le diagnostic\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Si vous devez arrêter Sunshine immédiatement, vous pouvez utiliser cette fonction. Notez que vous devrez le redémarrer manuellement après l'arrêt.\",\n    \"boom_sunshine_success\": \"Sunshine a été arrêté\",\n    \"confirm_boom\": \"Vraiment quitter ?\",\n    \"confirm_boom_desc\": \"Vous voulez vraiment quitter ? Eh bien, je ne peux pas vous arrêter, allez-y et cliquez à nouveau\",\n    \"confirm_logout\": \"Confirmer la déconnexion ?\",\n    \"confirm_logout_desc\": \"Vous devrez saisir à nouveau votre mot de passe pour accéder à l'interface Web.\",\n    \"copy_config\": \"Copier la configuration\",\n    \"copy_config_error\": \"Échec de la copie de la configuration\",\n    \"copy_config_success\": \"Configuration copiée dans le presse-papiers !\",\n    \"copy_logs\": \"Copier les journaux\",\n    \"download_logs\": \"Télécharger les journaux\",\n    \"force_close\": \"Fermer de force\",\n    \"force_close_desc\": \"Si Moonlight se plaint d'une application en cours d'exécution, forcer la fermeture de l'application devrait résoudre le problème.\",\n    \"force_close_error\": \"Erreur lors de la fermeture de l'application\",\n    \"force_close_success\": \"L'application à bien été fermée !\",\n    \"ignore_case\": \"Ignorer la casse\",\n    \"logout\": \"Déconnexion\",\n    \"logout_desc\": \"Déconnexion. Vous devrez peut-être vous reconnecter.\",\n    \"logout_localhost_tip\": \"Environnement actuel sans authentification : la déconnexion ne déclenchera pas la demande de mot de passe.\",\n    \"logs\": \"Journaux\",\n    \"logs_desc\": \"Voir les journaux envoyés par Sunshine\",\n    \"logs_find\": \"Rechercher...\",\n    \"match_contains\": \"Contient\",\n    \"match_exact\": \"Exact\",\n    \"match_regex\": \"Expression régulière\",\n    \"reopen_setup_wizard\": \"Rouvrir l'assistant de configuration\",\n    \"reopen_setup_wizard_desc\": \"Rouvrir la page de l'assistant de configuration pour reconfigurer les paramètres initiaux.\",\n    \"reopen_setup_wizard_error\": \"Échec de la réouverture de l'assistant de configuration\",\n    \"reset_display_device_desc_windows\": \"Si Sunshine est bloqué en essayant de restaurer les paramètres de périphérique d'affichage modifiés, vous pouvez réinitialiser les paramètres et procéder à la restauration manuelle de l'état d'affichage.\\nCela peut se produire pour diverses raisons : le périphérique n'est plus disponible, a été branché sur un port différent, etc.\",\n    \"reset_display_device_error_windows\": \"Erreur lors de la réinitialisation de la persistance !\",\n    \"reset_display_device_success_windows\": \"Réinitialisation de la persistance réussie !\",\n    \"reset_display_device_windows\": \"Réinitialiser la mémoire d'affichage\",\n    \"restart_sunshine\": \"Redémarrer Sunshine\",\n    \"restart_sunshine_desc\": \"Si Sunshine ne fonctionne pas correctement, vous pouvez essayer de le redémarrer. Cela mettra fin à toutes les sessions en cours.\",\n    \"restart_sunshine_success\": \"Sunshine redémarre\",\n    \"troubleshooting\": \"Dépannage\",\n    \"unpair_all\": \"Dissocier tous les appareils\",\n    \"unpair_all_error\": \"Erreur lors de la dissociation\",\n    \"unpair_all_success\": \"Désappairage réussi.\",\n    \"unpair_desc\": \"Retirer vos appareils appariés. Les appareils individuellement non appariés avec une session active resteront connectés, mais ne peuvent pas démarrer ou reprendre une session.\",\n    \"unpair_single_no_devices\": \"Il n'y a aucun appareil associé.\",\n    \"unpair_single_success\": \"Cependant, le(s) appareil(s) peuvent toujours être dans une session active. Utilisez le bouton 'Forcer la fermeture' ci-dessus pour mettre fin à toute session ouverte.\",\n    \"unpair_single_unknown\": \"Client inconnu\",\n    \"unpair_title\": \"Délimiter les appareils\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Confirmation du mot de passe\",\n    \"create_creds\": \"Avant de commencer, vous devez créer un nouveau nom d'utilisateur et un nouveau mot de passe pour accéder à l'interface Web.\",\n    \"create_creds_alert\": \"Les identifiants ci-dessous sont nécessaires pour accéder à l'interface Web de Sunshine. Gardez-les en sécurité, car vous ne les reverrez plus jamais !\",\n    \"creds_local_only\": \"Vos identifiants sont stockés localement hors ligne et ne seront jamais téléchargés vers un serveur.\",\n    \"error\": \"Erreur !\",\n    \"greeting\": \"Bienvenue sur Sunshine Foundation !\",\n    \"hide_password\": \"Masquer le mot de passe\",\n    \"login\": \"Connexion\",\n    \"network_error\": \"Erreur réseau, veuillez vérifier votre connexion\",\n    \"password\": \"Mot de passe\",\n    \"password_match\": \"Les mots de passe correspondent\",\n    \"password_mismatch\": \"Les mots de passe ne correspondent pas\",\n    \"server_error\": \"Erreur serveur\",\n    \"show_password\": \"Afficher le mot de passe\",\n    \"success\": \"Succès !\",\n    \"username\": \"Nom d'utilisateur\",\n    \"welcome_success\": \"Cette page se rechargera bientôt, votre navigateur vous demandera les nouveaux identifiants\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/it.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Applica\",\n    \"auto\": \"Automatico\",\n    \"autodetect\": \"Rilevamento automatico (consigliato)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"Annulla\",\n    \"close\": \"Chiudi\",\n    \"copied\": \"Copiato negli appunti\",\n    \"copy\": \"Copia\",\n    \"delete\": \"Elimina\",\n    \"description\": \"Description\",\n    \"disabled\": \"Disattivato\",\n    \"disabled_def\": \"Disabilitato (predefinito)\",\n    \"dismiss\": \"Ignora\",\n    \"do_cmd\": \"Esegui Comando\",\n    \"download\": \"Scarica\",\n    \"edit\": \"Modifica\",\n    \"elevated\": \"Come Admin\",\n    \"enabled\": \"Abilitato\",\n    \"enabled_def\": \"Abilitato (predefinito)\",\n    \"error\": \"Errore!\",\n    \"no_changes\": \"Nessuna modifica\",\n    \"note\": \"Nota:\",\n    \"password\": \"Password\",\n    \"remove\": \"Rimuovi\",\n    \"run_as\": \"Esegui come amministratore\",\n    \"save\": \"Salva\",\n    \"see_more\": \"Vedi Altro\",\n    \"success\": \"Operazione riuscita!\",\n    \"undo_cmd\": \"Comando di Annullamento\",\n    \"username\": \"Nome utente\",\n    \"warning\": \"Attenzione!\"\n  },\n  \"apps\": {\n    \"actions\": \"Azioni\",\n    \"add_cmds\": \"Aggiungi comandi\",\n    \"add_new\": \"Aggiungi nuovo\",\n    \"advanced_options\": \"Opzioni avanzate\",\n    \"app_name\": \"Nome applicazione\",\n    \"app_name_desc\": \"Il Nome dell'applicazione, come mostrato su Moonlight\",\n    \"applications_desc\": \"Le applicazioni vengono aggiornate solo al riavvio del client\",\n    \"applications_title\": \"Applicazioni\",\n    \"auto_detach\": \"Continua lo streaming se l'applicazione chiude improvvisamente\",\n    \"auto_detach_desc\": \"Cerca di individuare le applicazioni di tipo launcher che si chiudono subito dopo aver avviato un altro programma o una propria istanza. Quando viene rilevata un'app di tipo launcher, viene trattata come un'applicazione separata.\",\n    \"basic_info\": \"Informazioni di base\",\n    \"cmd\": \"Comando\",\n    \"cmd_desc\": \"L'applicazione principale da avviare. Se vuoto, non verrà avviata alcuna applicazione.\",\n    \"cmd_examples_title\": \"Esempi comuni:\",\n    \"cmd_note\": \"Se il percorso del comando eseguibile contiene spazi, è necessario racchiuderlo tra doppie virgolette.\",\n    \"cmd_prep_desc\": \"Un elenco di comandi da eseguire prima/dopo questa applicazione. Se uno dei comandi preliminari fallisce, l'avvio dell'applicazione viene interrotto.\",\n    \"cmd_prep_name\": \"Comandi Preliminari\",\n    \"command_settings\": \"Impostazioni comando\",\n    \"covers_found\": \"Copertine trovate\",\n    \"delete\": \"Cancella\",\n    \"delete_confirm\": \"Sei sicuro di voler eliminare \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Comandi Separati\",\n    \"detached_cmds_add\": \"Aggiungi comando separato\",\n    \"detached_cmds_desc\": \"Un elenco di comandi da eseguire in background.\",\n    \"detached_cmds_note\": \"Se il percorso del comando eseguibile contiene spazi, è necessario racchiuderlo tra doppie virgolette.\",\n    \"detached_cmds_remove\": \"Rimuovi comando distaccato\",\n    \"edit\": \"Modifica\",\n    \"env_app_id\": \"ID dell'applicazione\",\n    \"env_app_name\": \"Nome app\",\n    \"env_client_audio_config\": \"La configurazione audio richiesta dal client (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"Se il client ha richiesto l'opzione \\\"Ottimizza le impostazioni del gioco per lo streaming\\\" (TRUE o FALSE)\",\n    \"env_client_fps\": \"FPS richiesti dal client (int)\",\n    \"env_client_gcmap\": \"La maschera del gamepad richiesta, in formato bitset/bitfield (int)\",\n    \"env_client_hdr\": \"Se L'HDR è abilitato dal client (TRUE o FALSE)\",\n    \"env_client_height\": \"L'altezza richiesta dal client (int)\",\n    \"env_client_host_audio\": \"Se Il client ha richiesto l'audio dell'host (TRUE o FALSE)\",\n    \"env_client_name\": \"Nome amichevole del client (stringa)\",\n    \"env_client_width\": \"La larghezza richiesta dal client (int)\",\n    \"env_displayplacer_example\": \"Esempio - displayplacer per l'automazione della risoluzione:\",\n    \"env_qres_example\": \"Esempio - QRes per l'automazione della risoluzione:\",\n    \"env_qres_path\": \"percorso di QRes\",\n    \"env_var_name\": \"Nome Variable\",\n    \"env_vars_about\": \"Informazioni sulle variabili d'ambiente\",\n    \"env_vars_desc\": \"Tutti i comandi ottengono queste variabili d'ambiente per impostazione predefinita:\",\n    \"env_xrandr_example\": \"Esempio - Xrandr per l'automazione della risoluzione:\",\n    \"exit_timeout\": \"Timeout Uscita\",\n    \"exit_timeout_desc\": \"Numero di secondi in cui attendere che tutti i processi delle app si chiudano correttamente quando richiesto. Se disattivato, il valore predefinito è di attendere fino a 5 secondi. Se impostato a zero o a un valore negativo, l'app verrà immediatamente terminata.\",\n    \"file_selector_not_initialized\": \"Selettore file non inizializzato\",\n    \"find_cover\": \"Trova Copertina\",\n    \"form_invalid\": \"Si prega di controllare i campi obbligatori\",\n    \"form_valid\": \"Applicazione valida\",\n    \"global_prep_desc\": \"Abilita/Disabilita l'esecuzione dei Comandi di Preparazione Globali per questa applicazione.\",\n    \"global_prep_name\": \"Comandi di Preparazione Globali\",\n    \"image\": \"Immagine\",\n    \"image_desc\": \"Percorso dell'immagine/icona/foto che verrà inviata al client. L'immagine deve essere un file PNG. Se non impostata, Sunshine invierà l'immagine predefinita.\",\n    \"image_settings\": \"Impostazioni immagine\",\n    \"loading\": \"Caricamento...\",\n    \"menu_cmd_actions\": \"Azioni\",\n    \"menu_cmd_add\": \"Aggiungi comando menu\",\n    \"menu_cmd_command\": \"Comando\",\n    \"menu_cmd_desc\": \"Dopo la configurazione, questi comandi saranno visibili nel menu di ritorno del client, consentendo l'esecuzione rapida di operazioni specifiche senza interrompere lo streaming, come l'avvio di programmi helper.\\nEsempio: Nome visualizzato - Chiudi il computer; Comando - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Nome visualizzato\",\n    \"menu_cmd_drag_sort\": \"Trascina per ordinare\",\n    \"menu_cmd_name\": \"Comandi menu\",\n    \"menu_cmd_placeholder_command\": \"Comando\",\n    \"menu_cmd_placeholder_display_name\": \"Nome visualizzato\",\n    \"menu_cmd_placeholder_execute\": \"Esegui comando\",\n    \"menu_cmd_placeholder_undo\": \"Annulla comando\",\n    \"menu_cmd_remove_menu\": \"Rimuovi comando menu\",\n    \"menu_cmd_remove_prep\": \"Rimuovi comando di preparazione\",\n    \"mouse_mode\": \"Modalità mouse\",\n    \"mouse_mode_auto\": \"Auto (Impostazione globale)\",\n    \"mouse_mode_desc\": \"Seleziona il metodo di input del mouse per questa applicazione. Auto usa l'impostazione globale, Mouse virtuale usa il driver HID, SendInput usa l'API Windows.\",\n    \"mouse_mode_sendinput\": \"SendInput (API Windows)\",\n    \"mouse_mode_vmouse\": \"Mouse virtuale\",\n    \"name\": \"Nome\",\n    \"output_desc\": \"Il file dove viene memorizzato l'output del comando, se non specificato, l'output viene ignorato\",\n    \"output_name\": \"Output\",\n    \"run_as_desc\": \"Questo può essere necessario per alcune applicazioni che richiedono permessi di amministratore per funzionare correttamente.\",\n    \"scan_result_add_all\": \"Aggiungi tutto\",\n    \"scan_result_edit_title\": \"Aggiungi e modifica\",\n    \"scan_result_filter_all\": \"Tutto\",\n    \"scan_result_filter_epic_title\": \"Giochi Epic Games\",\n    \"scan_result_filter_executable\": \"Eseguibile\",\n    \"scan_result_filter_executable_title\": \"File eseguibile\",\n    \"scan_result_filter_gog_title\": \"Giochi GOG Galaxy\",\n    \"scan_result_filter_script\": \"Script\",\n    \"scan_result_filter_script_title\": \"Script batch/comando\",\n    \"scan_result_filter_shortcut\": \"Scorciatoia\",\n    \"scan_result_filter_shortcut_title\": \"Scorciatoia\",\n    \"scan_result_filter_steam_title\": \"Giochi Steam\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Gioco\",\n    \"scan_result_games_only\": \"Solo giochi\",\n    \"scan_result_matched\": \"Corrispondenze: {count}\",\n    \"scan_result_no_apps\": \"Nessuna applicazione trovata da aggiungere\",\n    \"scan_result_no_matches\": \"Nessuna applicazione corrispondente trovata\",\n    \"scan_result_quick_add_title\": \"Aggiunta rapida\",\n    \"scan_result_remove_title\": \"Rimuovi dall'elenco\",\n    \"scan_result_search_placeholder\": \"Cerca nome applicazione, comando o percorso...\",\n    \"scan_result_show_all\": \"Mostra tutto\",\n    \"scan_result_title\": \"Risultati scansione\",\n    \"scan_result_try_different_keywords\": \"Prova a usare diverse parole chiave di ricerca\",\n    \"scan_result_type_batch\": \"Batch\",\n    \"scan_result_type_command\": \"Script comando\",\n    \"scan_result_type_executable\": \"File eseguibile\",\n    \"scan_result_type_shortcut\": \"Scorciatoia\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Cerca applicazioni...\",\n    \"select\": \"Seleziona\",\n    \"test_menu_cmd\": \"Testa comando\",\n    \"test_menu_cmd_empty\": \"Il comando non può essere vuoto\",\n    \"test_menu_cmd_executing\": \"Esecuzione comando...\",\n    \"test_menu_cmd_failed\": \"Esecuzione comando fallita\",\n    \"test_menu_cmd_success\": \"Comando eseguito con successo!\",\n    \"use_desktop_image\": \"Usa lo sfondo del desktop corrente\",\n    \"wait_all\": \"Continua lo streaming fino all'uscita di tutti i processi dell'app\",\n    \"wait_all_desc\": \"Questo continuerà lo streaming fino a quando tutti i processi avviati dall'app non saranno terminati. Quando non è selezionato, lo streaming si fermerà all'uscita del processo iniziale dell'app, anche se altri processi sono ancora in esecuzione.\",\n    \"working_dir\": \"Directory di Lavoro\",\n    \"working_dir_desc\": \"La directory di lavoro che dovrebbe essere passata al processo. Per esempio, alcune applicazioni usano la directory di lavoro per cercare i file di configurazione. Se non impostato, Sunshine userà come predefinita la directory padre del comando\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Nome Adattatore\",\n    \"adapter_name_desc_linux_1\": \"Specifica manualmente una GPU da usare per la cattura.\",\n    \"adapter_name_desc_linux_2\": \"per trovare tutti i dispositivi con capacità VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Sostituisci ``renderD129`` con il dispositivo sopra per elencare il nome e le funzionalità del dispositivo. Per essere supportato da Sunshine, ha bisogno di avere minimo:\",\n    \"adapter_name_desc_windows\": \"Specifica manualmente una GPU da utilizzare per la cattura. Se non impostato, la GPU viene scelta automaticamente. Nota: questa GPU deve avere un display collegato e acceso. Se il tuo laptop non può abilitare l'uscita GPU diretta, imposta su automatico.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Se è installata l'ultima versione del display virtuale, può associarsi automaticamente al binding della GPU\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Aggiungi\",\n    \"address_family\": \"Famiglia di indirizzi\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Imposta la famiglia di indirizzi utilizzata da Sunshine\",\n    \"address_family_ipv4\": \"Solo IPv4\",\n    \"always_send_scancodes\": \"Invia sempre Scancode\",\n    \"always_send_scancodes_desc\": \"L'invio di scancode migliora la compatibilità con i giochi e le applicazioni, ma può risultare in input da tastiera errati da alcuni client che non utilizzano un layout di inglese statunitense. Abilita se l' input della tastiera non funziona affatto in certe applicazioni. Disabilita se i tasti del client generano l'input errato sull'host.\",\n    \"amd_coder\": \"Coder AMF (H264)\",\n    \"amd_coder_desc\": \"Consente di selezionare la codifica dell'entropia per dare la priorità alla qualità o alla velocità di codifica. Solo H.264.\",\n    \"amd_enforce_hrd\": \"Applica Decoder di Riferimento Ipotetico (HRD) per AMF\",\n    \"amd_enforce_hrd_desc\": \"Aumenta i vincoli in materia di controllo della velocità per soddisfare i requisiti del modello HRD. Questo riduce notevolmente l'overflow del bitrate, ma può causare artefatti di codifica o qualità ridotta su determinate schede.\",\n    \"amd_preanalysis\": \"Preanalisi AMF\",\n    \"amd_preanalysis_desc\": \"Ciò consente la preanalisi nel controllo della velocità, che può aumentare la qualità a scapito di una maggiore latenza di codifica.\",\n    \"amd_quality\": \"Qualità AMF\",\n    \"amd_quality_balanced\": \"bilanciato -- bilanciato (predefinito)\",\n    \"amd_quality_desc\": \"Questo controlla l'equilibrio tra velocità di codifica e qualità.\",\n    \"amd_quality_group\": \"Impostazioni Qualità AMF\",\n    \"amd_quality_quality\": \"qualità -- preferisce la qualità\",\n    \"amd_quality_speed\": \"velocità -- preferisce la velocità\",\n    \"amd_qvbr_quality\": \"Livello qualità AMF QVBR\",\n    \"amd_qvbr_quality_desc\": \"Livello di qualità per la modalità di controllo bitrate QVBR. Intervallo: 1-51 (più basso = migliore qualità). Predefinito: 23. Si applica solo quando il controllo bitrate è impostato su 'qvbr'.\",\n    \"amd_rc\": \"Controllo della Velocità AMF\",\n    \"amd_rc_cbr\": \"cbr -- bitrate costante\",\n    \"amd_rc_cqp\": \"cqp -- modalità qp costante\",\n    \"amd_rc_desc\": \"Questo controlla il metodo di controllo della velocità per garantire che non stiamo superando il target di bitrate client. 'cqp' non è adatto per il target di bitrate e altre opzioni oltre 'vbr_latency' dipendono dall'esecuzione HRD per aiutare a limitare i overflow di bitrate.\",\n    \"amd_rc_group\": \"Impostazioni Controllo di Velocità AMF\",\n    \"amd_rc_hqcbr\": \"hqcbr -- bitrate costante alta qualità\",\n    \"amd_rc_hqvbr\": \"hqvbr -- bitrate variabile alta qualità\",\n    \"amd_rc_qvbr\": \"qvbr -- bitrate variabile di qualità (usa livello qualità QVBR)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- bitrate variabile con vincoli di latenza\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- bitrate variabile con vincoli di picco\",\n    \"amd_usage\": \"Utilizzo AMF\",\n    \"amd_usage_desc\": \"Questo imposta il profilo di codifica di base. Tutte le opzioni presentate di seguito sovrascriveranno un sottoinsieme del profilo di utilizzo, ma ci sono ulteriori impostazioni nascoste applicate che non possono essere configurate altrove.\",\n    \"amd_usage_lowlatency\": \"lowlatency - bassa latenza (più veloce)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - bassa latenza, alta qualità (veloce)\",\n    \"amd_usage_transcoding\": \"transcoding -- transcodifica (più lenta)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency - latenza ultra bassa (più veloce)\",\n    \"amd_usage_webcam\": \"webcam -- webcam (lento)\",\n    \"amd_vbaq\": \"Quantizzazione Adattiva Basata Sulla Varianza AMF (VBAQ)\",\n    \"amd_vbaq_desc\": \"Il sistema visivo umano è tipicamente meno sensibile agli artefatti in aree altamente strutturate. In modalità VBAQ, la varianza di pixel è utilizzata per indicare la complessità delle texture spaziali, consentendo al codificatore di allocare più bit ad aree più fluide. Abilitare questa funzione porta a migliorare la qualità visiva soggettiva con alcuni contenuti.\",\n    \"amf_draw_mouse_cursor\": \"Disegna un cursore semplice quando si utilizza il metodo di cattura AMF\",\n    \"amf_draw_mouse_cursor_desc\": \"In alcuni casi, l'utilizzo della cattura AMF non visualizzerà il puntatore del mouse. Abilitando questa opzione verrà disegnato un semplice puntatore del mouse sullo schermo. Nota: la posizione del puntatore del mouse verrà aggiornata solo quando c'è un aggiornamento dello schermo del contenuto, quindi in scenari non di gioco come sul desktop, potresti osservare un movimento lento del puntatore del mouse.\",\n    \"apply_note\": \"Fare clic su 'Applica' per applicare le modifiche e riavviare Sunshine. Questo terminerà qualsiasi sessione in esecuzione.\",\n    \"audio_sink\": \"Uscita Audio\",\n    \"audio_sink_desc_linux\": \"Il nome dell'uscita audio è utilizzato per il Loopback audio. Se non si specifica questa variabile, pulseaudio selezionerà il dispositivo predefinito. È possibile trovare il nome del'uscita audio utilizzando entrambi i comandi:\",\n    \"audio_sink_desc_macos\": \"Il nome dell'uscita audio utilizzata per Audio Loopback. Sunshine può accedere solo ai microfoni su macOS a causa delle limitazioni di sistema. Puoi usare Soundflower o BlackHole per trasmettere l'audio di sistema.\",\n    \"audio_sink_desc_windows\": \"Specifica manualmente un dispositivo audio specifico da catturare. Se disattivato, il dispositivo viene scelto automaticamente. Si consiglia vivamente di lasciare vuoto questo campo per utilizzare la selezione automatica del dispositivo! Se si dispone di più dispositivi audio con nomi identici, è possibile ottenere l'ID dispositivo utilizzando il seguente comando:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Altoparlanti (High Definition Audio Device)\",\n    \"av1_mode\": \"Supporto AV1\",\n    \"av1_mode_0\": \"Sunshine fornirà il supporto per AV1 basandosi sulle funzionalità dell'encoder (raccomandato)\",\n    \"av1_mode_1\": \"Sunshine non fornirà il supporto per AV1\",\n    \"av1_mode_2\": \"Sunshine fornirà il supporto per il profilo AV1 Main a 8 bit\",\n    \"av1_mode_3\": \"Sunshine fornirà il supporto per i profili AV1 Main a 8 bit e 10 bit (HDR)\",\n    \"av1_mode_desc\": \"Consente al client di richiedere flussi video AV1 Main 8-bit o 10-bit. AV1 è più intensivo da codificare per la CPU, quindi abilitandolo, si utilizza la codifica software, si possono ridurre le prestazioni.\",\n    \"back_button_timeout\": \"Timeout Emulazione Home/Tasto Guida\",\n    \"back_button_timeout_desc\": \"Se il pulsante Indietro/Select viene premuto per il numero specificato di millisecondi, viene emulato un pulsante Home/Tasto Guida. Se impostato a un valore < 0 (predefinito), tenendo premuto il pulsante Indietro/Select non verrà emulato il pulsante Home/Tasto Guida.\",\n    \"bind_address\": \"Indirizzo di binding (funzionalità di test)\",\n    \"bind_address_desc\": \"Imposta l'indirizzo IP specifico a cui Sunshine si legherà. Se lasciato vuoto, Sunshine si legherà a tutti gli indirizzi disponibili.\",\n    \"capture\": \"Forza un metodo di acquisizione specifico\",\n    \"capture_desc\": \"In modalità automatica Sunshine userà il primo che funziona. NvFBC richiede driver nvidia patchati.\",\n    \"capture_target\": \"Obiettivo di acquisizione\",\n    \"capture_target_desc\": \"Seleziona il tipo di obiettivo da acquisire. Selezionando 'Finestra', puoi acquisire una specifica finestra dell'applicazione (come il software di interpolazione frame AI) invece dell'intero schermo.\",\n    \"capture_target_display\": \"Schermo\",\n    \"capture_target_window\": \"Finestra\",\n    \"cert\": \"Certificato\",\n    \"cert_desc\": \"Il certificato utilizzato per l'accoppiamento web UI e il client Moonlight. Per la migliore compatibilità, dovrebbe avere una chiave pubblica RSA-2048.\",\n    \"channels\": \"Massimi Client Connessi\",\n    \"channels_desc_1\": \"Sunshine può consentire che una singola sessione di streaming sia condivisa con più client contemporaneamente.\",\n    \"channels_desc_2\": \"Alcuni encoder hardware possono avere limitazioni che riducono le prestazioni con più flussi.\",\n    \"close_verify_safe\": \"Verifica sicura compatibile con client vecchi\",\n    \"close_verify_safe_desc\": \"Client vecchi potrebbero non essere in grado di connettersi a Sunshine, si prega di disabilitare questa opzione o aggiornare il client\",\n    \"coder_cabac\": \"cabac -- codifica aritmetica binaria adattiva contestuale - qualità superiore\",\n    \"coder_cavlc\": \"cavlc -- codifica contestuale adattativa a lunghezza variabile - decodifica più veloce\",\n    \"configuration\": \"Configurazione\",\n    \"controller\": \"Abilita l'input del Gamepad\",\n    \"controller_desc\": \"Permette ai guest di controllare il sistema host con un gamepad / controller\",\n    \"credentials_file\": \"File Credenziali\",\n    \"credentials_file_desc\": \"Memorizza il nome utente/password separatamente dal file di stato di Sunshine.\",\n    \"display_device_options_note_desc_windows\": \"Windows salva varie impostazioni di visualizzazione per ogni combinazione di schermi attualmente attivi.\\nSunshine applica quindi le modifiche a uno o più schermi appartenenti a tale combinazione.\\nSe si disconnette un dispositivo che era attivo quando Sunshine ha applicato le impostazioni, le modifiche non possono essere\\nripristinate a meno che la combinazione non possa essere riattivata quando Sunshine tenta di annullare le modifiche!\",\n    \"display_device_options_note_windows\": \"Nota su come vengono applicate le impostazioni\",\n    \"display_device_options_windows\": \"Opzioni dispositivo di visualizzazione\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Attiva il display se non è già attivo\",\n    \"display_device_prep_ensure_active_windows\": \"Attiva il display automaticamente\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Disattiva tutti gli altri display e attiva solo il display specificato\",\n    \"display_device_prep_ensure_only_display_windows\": \"Disattiva gli altri display e attiva solo il display specificato\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Attiva il display e lo imposta come display principale\",\n    \"display_device_prep_ensure_primary_windows\": \"Attiva il display automaticamente e impostalo come display principale\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Utilizza solo il display virtuale per lo streaming secondario esteso\",\n    \"display_device_prep_ensure_secondary_windows\": \"Streaming display secondario (solo display virtuale)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Nessuna modifica allo stato del display; l'utente deve assicurarsi che il display sia pronto\",\n    \"display_device_prep_no_operation_windows\": \"Disabilitato\",\n    \"display_device_prep_windows\": \"Preparazione del display\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Deve essere specificato almeno un valore \\\"ricevuto\\\" e un valore \\\"finale\\\".\\nUn campo vuoto nella sezione \\\"ricevuto\\\" significa \\\"corrisponde a qualsiasi valore\\\". Un campo vuoto nella sezione \\\"finale\\\" significa \\\"mantieni il valore ricevuto\\\".\\nSe lo desideri, puoi associare un valore FPS specifico a una risoluzione specifica...\\n\\nNota: se l'opzione \\\"Ottimizza impostazioni di gioco\\\" non è abilitata nel client Moonlight, le righe contenenti i valori di risoluzione vengono ignorate.\",\n    \"display_mode_remapping_desc_windows\": \"Specifica come una risoluzione e/o una frequenza di aggiornamento specifica deve essere rimappata ad altri valori.\\nPuoi trasmettere in streaming a una risoluzione inferiore, mentre il rendering avviene a una risoluzione superiore sull'host per un effetto di supersampling.\\nOppure puoi trasmettere in streaming a FPS più alti limitando l'host a una frequenza di aggiornamento inferiore.\\nLa corrispondenza viene eseguita dall'alto verso il basso. Una volta trovata la corrispondenza, le altre voci non vengono più controllate, ma vengono comunque validate.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Frequenza di aggiornamento finale\",\n    \"display_mode_remapping_final_resolution_windows\": \"Risoluzione finale\",\n    \"display_mode_remapping_optional\": \"opzionale\",\n    \"display_mode_remapping_received_fps_windows\": \"FPS ricevuti\",\n    \"display_mode_remapping_received_resolution_windows\": \"Risoluzione ricevuta\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Nota: se l'opzione \\\"Ottimizza impostazioni di gioco\\\" non è abilitata nel client Moonlight, la rimappatura è disabilitata.\",\n    \"display_mode_remapping_windows\": \"Rimappa le modalità di visualizzazione\",\n    \"display_modes\": \"Modalità di visualizzazione\",\n    \"ds4_back_as_touchpad_click\": \"Mappa Indietro/Select come Clic Touchpad\",\n    \"ds4_back_as_touchpad_click_desc\": \"Quando si forza l'emulazione DS4, mappa Indietro/Select come Clic Touchpad\",\n    \"dsu_server_port\": \"DSU Server Port\",\n    \"dsu_server_port_desc\": \"DSU server listening port (default 26760). Sunshine will act as a DSU server to receive client connections and send motion data. Enable DSU server in your client(Yuzu,Ryujinx etc.) and set DSU server address(127.0.0.1) and port(26760)\",\n    \"enable_dsu_server\": \"Abilita server DSU\",\n    \"enable_dsu_server_desc\": \"Enable DSU server to receive client connections and send motion data\",\n    \"encoder\": \"Forza un encoder specifico\",\n    \"encoder_desc\": \"Forza un encoder specifico, altrimenti Sunshine selezionerà l'opzione migliore disponibile. Nota: Se si specifica un codificatore hardware su Windows, deve corrispondere alla GPU a cui è collegato il display.\",\n    \"encoder_software\": \"Software\",\n    \"experimental\": \"Sperimentale\",\n    \"experimental_features\": \"Funzionalità sperimentali\",\n    \"external_ip\": \"IP Esterno\",\n    \"external_ip_desc\": \"Se non viene fornito alcun indirizzo IP esterno, Sunshine lo rileverà automaticamente\",\n    \"fec_percentage\": \"Percentuale FEC\",\n    \"fec_percentage_desc\": \"Percentuale di correzione errore per pacchetto dati in ogni fotogramma video. Valori più elevati possono correggere maggiori perdite di rete, ma al costo di un uso crescente della larghezza di banda.\",\n    \"ffmpeg_auto\": \"auto -- lascia che decida ffmpeg (predefinito)\",\n    \"file_apps\": \"File Applicazioni\",\n    \"file_apps_desc\": \"Il file in cui vengono memorizzate le attuali applicazioni di Sunshine.\",\n    \"file_state\": \"File Stato\",\n    \"file_state_desc\": \"Il file in cui viene memorizzato lo stato attuale di Sunshine\",\n    \"fps\": \"FPS pubblicizzati\",\n    \"gamepad\": \"Tipo di Gamepad Emulato\",\n    \"gamepad_auto\": \"Opzioni di selezione automatica\",\n    \"gamepad_desc\": \"Scegli quale tipo di gamepad emulare sull'host\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 Manual Options\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Opzioni manuali DS4\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Comandi di preparazione\",\n    \"global_prep_cmd_desc\": \"Configura un elenco di comandi da eseguire prima o dopo aver eseguito qualsiasi applicazione. Se uno qualsiasi dei comandi di preparazione specificati fallisce, il processo di avvio dell'applicazione verrà interrotto.\",\n    \"hdr_luminance_analysis\": \"Metadati dinamici HDR (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Abilita l'analisi della luminanza GPU per frame e inietta metadati dinamici HDR10+ (ST 2094-40) e HDR Vivid (CUVA) nel flusso codificato. Fornisce suggerimenti di tone-mapping per frame per display supportati. Aggiunge un leggero overhead GPU (~0,5-1,5ms/frame ad alte risoluzioni). Disabilitare se si verificano cali di framerate con HDR attivato.\",\n    \"hdr_prep_automatic_windows\": \"Switch on/off the HDR mode as requested by the client\",\n    \"hdr_prep_no_operation_windows\": \"Disabled\",\n    \"hdr_prep_windows\": \"HDR state change\",\n    \"hevc_mode\": \"Supporto HEVC\",\n    \"hevc_mode_0\": \"Sunshine fornirà il supporto per HEVC basandosi sulle funzionalità dell'encoder (raccomandato)\",\n    \"hevc_mode_1\": \"Sunshine non fornirà il supporto per HEVC\",\n    \"hevc_mode_2\": \"Sunshine fornirà il supporto per il profilo HEVC Main\",\n    \"hevc_mode_3\": \"Sunshine fornirà il supporto per i profili HEVC Main e Main10 (HDR)\",\n    \"hevc_mode_desc\": \"Consente al client di richiedere flussi video HEVC Main o HEVC Main10. HEVC è più intensivo per la CPU, quindi abilitarlo può ridurre le prestazioni quando si utilizza la codifica software.\",\n    \"high_resolution_scrolling\": \"Supporto Scorrimento Mouse ad Alta Risoluzione\",\n    \"high_resolution_scrolling_desc\": \"Quando abilitato, Sunshine passerà gli eventi di scorrimento ad alta risoluzione dei client Moonlight. Può essere utile disabilitarlo per le vecchie applicazioni che scorrono troppo velocemente con eventi di scorrimento ad alta risoluzione.\",\n    \"install_steam_audio_drivers\": \"Installa i Driver Audio di Steam\",\n    \"install_steam_audio_drivers_desc\": \"Se Steam è installato, installerà automaticamente il driver Steam Streaming Speakers per supportare il suono surround 5.1/7.1 e silenziare l'audio host.\",\n    \"key_repeat_delay\": \"Ritardo Ripetizione Tasti\",\n    \"key_repeat_delay_desc\": \"Controlla quanto velocemente i tasti si ripeteranno. È Il ritardo iniziale in millisecondi prima di ripetere i tasti.\",\n    \"key_repeat_frequency\": \"Frequenza Di Ripetizione Tasti\",\n    \"key_repeat_frequency_desc\": \"Quante volte i tasti si ripetono ogni secondo. Questa opzione supporta i decimali.\",\n    \"key_rightalt_to_key_win\": \"Mappa il tasto Alt destro al tasto Windows\",\n    \"key_rightalt_to_key_win_desc\": \"Potrebbe succedere che non sia possibile inviare il Tasto Windows direttamente da Moonlight. In questi casi può essere utile far credere a Sunshine che il tasto Alt Destro è il Tasto Windows\",\n    \"key_rightalt_to_key_windows\": \"Mappare il tasto Alt destro sul tasto Windows\",\n    \"keyboard\": \"Abilita Input da Tastiera\",\n    \"keyboard_desc\": \"Consente ai guest di controllare il sistema host con la tastiera\",\n    \"lan_encryption_mode\": \"Modalità Crittografia LAN\",\n    \"lan_encryption_mode_1\": \"Abilitato per i client supportati\",\n    \"lan_encryption_mode_2\": \"Obbligatorio per tutti i client\",\n    \"lan_encryption_mode_desc\": \"Questo determina quando la crittografia sarà utilizzata durante lo streaming sulla rete locale. La crittografia può ridurre le prestazioni di streaming, in particolare su host e client meno potenti.\",\n    \"locale\": \"Lingua\",\n    \"locale_desc\": \"La lingua utilizzata per l'interfaccia utente di Sunshine.\",\n    \"log_level\": \"Livello di Log\",\n    \"log_level_0\": \"Dettagliato\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"Informazioni\",\n    \"log_level_3\": \"Avviso\",\n    \"log_level_4\": \"Errore\",\n    \"log_level_5\": \"Critico\",\n    \"log_level_6\": \"Nessuno\",\n    \"log_level_desc\": \"Il livello minimo di log sullo standard output\",\n    \"log_path\": \"Percorso File Di Log\",\n    \"log_path_desc\": \"Il file in cui vengono memorizzati i log attuali di Sunshine.\",\n    \"max_bitrate\": \"Bitrate Massimo\",\n    \"max_bitrate_desc\": \"Il bitrate massimo (in Kbps) in cui Sunshine codificherà il flusso. Se impostato a 0, utilizzerà sempre il bitrate richiesto dal Moonlight.\",\n    \"max_fps_reached\": \"Valori FPS massimi raggiunti\",\n    \"max_resolutions_reached\": \"Numero massimo di risoluzioni raggiunto\",\n    \"mdns_broadcast\": \"Trova questo computer nella rete locale\",\n    \"mdns_broadcast_desc\": \"Se questa opzione è abilitata, Sunshine permetterà a altri dispositivi di trovare questo computer automaticamente. Moonlight deve essere configurato per trovare questo computer automaticamente nella rete locale.\",\n    \"min_threads\": \"Conteggio Minimo Thread CPU\",\n    \"min_threads_desc\": \"Aumentare leggermente il valore riduce l'efficienza di codifica, ma di solito ne vale la pena per guadagnare l'impiego di più core della CPU per la codifica. Il valore ideale è il valore più basso che può codificare in modo affidabile in base le impostazioni di streaming desiderate sul vostro hardware.\",\n    \"minimum_fps_target\": \"Obiettivo FPS minimo\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Opzioni varie\",\n    \"motion_as_ds4\": \"Emula un gamepad DS4 se quello del client segnala che ci sono sensori di movimento\",\n    \"motion_as_ds4_desc\": \"Se disabilitato, i sensori di movimento non saranno presi in considerazione durante la selezione del tipo gamepad.\",\n    \"mouse\": \"Abilita l'Input del Mouse\",\n    \"mouse_desc\": \"Permette ai guest di controllare il sistema host con il mouse\",\n    \"native_pen_touch\": \"Supporto Nativo Della Penna/Touch\",\n    \"native_pen_touch_desc\": \"Se abilitato, Sunshine passerà direttamente gli eventi nativi della penna/touch dal client Moonlight. Può essere utile disabilitarlo per le applicazioni più vecchie senza supporto nativo della penna/touch.\",\n    \"no_fps\": \"Nessun valore FPS aggiunto\",\n    \"no_resolutions\": \"Nessuna risoluzione aggiunta\",\n    \"notify_pre_releases\": \"Notifiche Pre-Rilascio\",\n    \"notify_pre_releases_desc\": \"Indica se notificare o meno le nuove versioni pre-rilascio di Sunshine\",\n    \"nvenc_h264_cavlc\": \"Preferisci CAVLC a CABAC in H.264\",\n    \"nvenc_h264_cavlc_desc\": \"La forma più semplice di codifica dell'entropia. CAVLC ha bisogno di circa il 10% di bitrate in più per la stessa qualità. Rilevante solo per i dispositivi di decodifica molto vecchi.\",\n    \"nvenc_latency_over_power\": \"Prioritizza una latenza di codifica più bassa rispetto al risparmio energetico\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine richiede la massima velocità di clock GPU durante lo streaming per ridurre la latenza di codifica. La disabilitazione non è consigliata in quanto ciò può portare ad un aumento significativo della latenza di codifica.\",\n    \"nvenc_lookahead_depth\": \"Lookahead depth\",\n    \"nvenc_lookahead_depth_desc\": \"Number of frames to look ahead during encoding (0-32). Lookahead improves encoding quality, especially in complex scenes, by providing better motion estimation and bitrate distribution. Higher values improve quality but increase encoding latency. Set to 0 to disable lookahead. Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_lookahead_level\": \"Lookahead level\",\n    \"nvenc_lookahead_level_0\": \"Level 0 (lowest quality, fastest)\",\n    \"nvenc_lookahead_level_1\": \"Level 1\",\n    \"nvenc_lookahead_level_2\": \"Level 2\",\n    \"nvenc_lookahead_level_3\": \"Level 3 (highest quality, slowest)\",\n    \"nvenc_lookahead_level_autoselect\": \"Auto-select (let driver choose optimal level)\",\n    \"nvenc_lookahead_level_desc\": \"Lookahead quality level. Higher levels improve quality at the expense of performance. This option only takes effect when lookahead_depth is greater than 0. Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_lookahead_level_disabled\": \"Disabled (same as level 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Presenta OpenGL/Vulkan sopra DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine non può catturare i programmi OpenGL e Vulkan a schermo intero al massimo frame rate a meno che non presentino sopra DXGI. Questa è una impostazione a livello di sistema che viene ripristinata all'uscita del programma sunshine.\",\n    \"nvenc_preset\": \"Preimpostazione prestazioni\",\n    \"nvenc_preset_1\": \"(più veloce, predefinito)\",\n    \"nvenc_preset_7\": \"(più lento)\",\n    \"nvenc_preset_desc\": \"Numeri più alti migliorano la compressione (qualità a un dato bitrate) a costo di una maggiore latenza di codifica. È consigliata la modifica solo quando c'è un limite di rete o del decoder, altrimenti un effetto simile può essere raggiunto aumentando il bitrate.\",\n    \"nvenc_rate_control\": \"Modalità controllo velocità\",\n    \"nvenc_rate_control_cbr\": \"CBR (Bitrate Costante) - Bassa latenza\",\n    \"nvenc_rate_control_desc\": \"Seleziona la modalità di controllo della velocità. CBR (Bitrate Costante) fornisce un bitrate fisso per lo streaming a bassa latenza. VBR (Bitrate Variabile) consente al bitrate di variare in base alla complessità della scena, fornendo una migliore qualità per scene complesse al costo di un bitrate variabile.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Bitrate Variabile) - Migliore qualità\",\n    \"nvenc_realtime_hags\": \"Usa la priorità in tempo reale nello scheduling hardware dell'accelerazione GPU\",\n    \"nvenc_realtime_hags_desc\": \"Attualmente i driver NVIDIA possono bloccarsi durante la codifica quando HAGS è abilitato, la priorità in tempo reale viene utilizzata e l'utilizzo VRAM è vicino al massimo. Disabilitare questa opzione riduce la priorità ad alta, eludendo il blocco al costo di una riduzione delle prestazioni di acquisizione quando la GPU è pesantemente caricata.\",\n    \"nvenc_spatial_aq\": \"AQ spaziale\",\n    \"nvenc_spatial_aq_desc\": \"Assegna valori QP più alti alle parti piatte del video. È consigliato abilitarlo per lo streaming a bitrate più bassi.\",\n    \"nvenc_spatial_aq_disabled\": \"Disabled (faster, default)\",\n    \"nvenc_spatial_aq_enabled\": \"Enabled (slower)\",\n    \"nvenc_split_encode\": \"Codifica frame divisa\",\n    \"nvenc_split_encode_desc\": \"Split the encoding of each video frame over multiple NVENC hardware units. Significantly reduces encoding latency with a marginal compression efficiency penalty. This option is ignored if your GPU has a singular NVENC unit.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Driver decides (default)\",\n    \"nvenc_split_encode_four_strips\": \"Forza divisione a 4 strisce (richiede 4+ motori NVENC)\",\n    \"nvenc_split_encode_three_strips\": \"Forza divisione a 3 strisce (richiede 3+ motori NVENC)\",\n    \"nvenc_split_encode_two_strips\": \"Forza divisione a 2 strisce (richiede 2+ motori NVENC)\",\n    \"nvenc_target_quality\": \"Qualità target (modalità VBR)\",\n    \"nvenc_target_quality_desc\": \"Livello di qualità target per la modalità VBR (0-51 per H.264/HEVC, 0-63 per AV1). Valori più bassi = qualità più alta. Impostare su 0 per la selezione automatica della qualità. Usato solo quando la modalità di controllo della velocità è VBR.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Modalità a due passaggi\",\n    \"nvenc_twopass_desc\": \"Aggiunge un passaggio di codifica preliminare. Questo permette di rilevare più vettori di movimento, distribuire meglio il bitrate attraverso il frame e rispettare più rigorosamente i limiti di bitrate. Disabilitarlo non è raccomandato in quanto questo può portare a occasionali bitrate overshoot e successiva perdita del pacchetto.\",\n    \"nvenc_twopass_disabled\": \"Disabilitato (più veloce, non consigliato)\",\n    \"nvenc_twopass_full_res\": \"Risoluzione completa (lenta)\",\n    \"nvenc_twopass_quarter_res\": \"Un quarto di risoluzione (più veloce, predefinito)\",\n    \"nvenc_vbv_increase\": \"Incremento percentuale VBV/HRD singolo frame\",\n    \"nvenc_vbv_increase_desc\": \"Per impostazione predefinita, Sunshine utilizza VBV/HRD a singolo frame, in questo modo la dimensione di un frame video codificato non supererà il bitrate richiesto diviso per il frame rate richiesto. Allentare questa restrizione può portare benefici, agendo come un bitrate variabile a bassa latenza, ma può anche portare alla perdita di pacchetti se la rete non ha banda aggiuntiva sufficiente per gestire picchi di bitrate. Il valore massimo accettato è di 400, che corrisponde a 5x la dimensione limite dei frame video codificati.\",\n    \"origin_web_ui_allowed\": \"Origine Web UI Consentita\",\n    \"origin_web_ui_allowed_desc\": \"L'origine dell'indirizzo di endpoint remoto a cui viene consentito l'accesso all'interfaccia utente Web\",\n    \"origin_web_ui_allowed_lan\": \"Solo quelli in LAN possono accedere all'interfaccia utente Web\",\n    \"origin_web_ui_allowed_pc\": \"Solo localhost può accedere all'interfaccia Web\",\n    \"origin_web_ui_allowed_wan\": \"Chiunque può accedere all'interfaccia Web\",\n    \"output_name_desc_unix\": \"Durante l'avvio di Sunshine, dovresti vedere l'elenco dei display rilevati. Nota: devi usare il valore id all'interno della parentesi. Quello in basso è un esempio, la lista effettiva può essere trovata in \\\"Risoluzione dei Problemi\\\".\",\n    \"output_name_desc_windows\": \"Specifica manualmente un display da usare per la cattura. Se lasciato vuoto, viene catturato il display primario. Nota: Se hai specificato una GPU sopra, questo display deve essere collegato a quella GPU. I valori appropriati possono essere trovati usando il seguente comando:\",\n    \"output_name_unix\": \"Numero di Display\",\n    \"output_name_windows\": \"Nome Output\",\n    \"ping_timeout\": \"Timeout Ping\",\n    \"ping_timeout_desc\": \"Per quanti millisecondi attendere dati da Moonlight prima di chiudere lo streaming\",\n    \"pkey\": \"Chiave Privata\",\n    \"pkey_desc\": \"La chiave privata utilizzata per l'accoppiamento web UI e client Moonlight. Per la migliore compatibilità, questa dovrebbe essere una chiave privata RSA-2048.\",\n    \"port\": \"Porta\",\n    \"port_alert_1\": \"Sunshine non può utilizzare porte sotto 1024!\",\n    \"port_alert_2\": \"I porti sopra 65535 non sono disponibili!\",\n    \"port_desc\": \"Imposta la famiglia di porte utilizzati da Sunshine\",\n    \"port_http_port_note\": \"Usa questa porta per connetterti con Moonlight.\",\n    \"port_note\": \"Nota\",\n    \"port_port\": \"Porta\",\n    \"port_protocol\": \"Protocollo\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Esporre l'interfaccia Web su Internet è un rischio per la sicurezza! Procedi a tuo rischio!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Parametro Di Quantizzazione\",\n    \"qp_desc\": \"Alcuni dispositivi potrebbero non supportare Constant Bit Rate. Per questi dispositivi, viene invece utilizzato QP. Valore più alto significa più compressione, ma meno qualità.\",\n    \"qsv_coder\": \"Coder QuickSync (H264)\",\n    \"qsv_preset\": \"Preimpostazione QuickSync\",\n    \"qsv_preset_fast\": \"più veloce (qualità inferiore)\",\n    \"qsv_preset_faster\": \"più veloce (qualità minima)\",\n    \"qsv_preset_medium\": \"medio (predefinito)\",\n    \"qsv_preset_slow\": \"lento (buona qualità)\",\n    \"qsv_preset_slower\": \"più lento (migliore qualità)\",\n    \"qsv_preset_slowest\": \"più lento (migliore qualità)\",\n    \"qsv_preset_veryfast\": \"ancora più veloce (qualità minima)\",\n    \"qsv_slow_hevc\": \"Permetti la codifica lenta in HEVC\",\n    \"qsv_slow_hevc_desc\": \"Questo può abilitare la codifica HEVC su vecchie GPU Intel, al costo di un maggiore utilizzo della GPU e prestazioni peggiori.\",\n    \"refresh_rate_change_automatic_windows\": \"Use FPS value provided by the client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Enter the refresh rate to be used\",\n    \"refresh_rate_change_manual_windows\": \"Use manually entered refresh rate\",\n    \"refresh_rate_change_no_operation_windows\": \"Disabled\",\n    \"refresh_rate_change_windows\": \"FPS change\",\n    \"res_fps_desc\": \"I modalità di visualizzazione pubblicizzate da Sunshine. Alcune versioni di Moonlight, come Moonlight-nx (Switch), si affidano a questi elenchi per garantire che le risoluzioni e fps richiesti siano supportati. Questa impostazione non cambia il modo in cui lo stream viene inviato a Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Use resolution provided by the client\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_manual_windows\": \"Use manually entered resolution\",\n    \"resolution_change_no_operation_windows\": \"Disabled\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_windows\": \"Resolution change\",\n    \"resolutions\": \"Risoluzioni pubblicizzate\",\n    \"restart_note\": \"Sunshine sta riavviando per applicare le modifiche.\",\n    \"sleep_mode\": \"Modalità sospensione\",\n    \"sleep_mode_away\": \"Modalità assente (Display spento, risveglio istantaneo)\",\n    \"sleep_mode_desc\": \"Controlla cosa succede quando il client invia un comando di sospensione. Sospensione (S3): sospensione tradizionale, basso consumo ma richiede WOL per il risveglio. Ibernazione (S4): salva su disco, consumo molto basso. Modalità assente: il display si spegne ma il sistema resta attivo per un risveglio istantaneo - ideale per server di streaming di giochi.\",\n    \"sleep_mode_hibernate\": \"Ibernazione (S4)\",\n    \"sleep_mode_suspend\": \"Sospensione (S3)\",\n    \"stream_audio\": \"Abilita streaming audio\",\n    \"stream_audio_desc\": \"Disabilita questa opzione per interrompere lo streaming audio.\",\n    \"stream_mic\": \"Abilita streaming microfono\",\n    \"stream_mic_desc\": \"Disabilita questa opzione per interrompere lo streaming del microfono.\",\n    \"stream_mic_download_btn\": \"Scarica microfono virtuale\",\n    \"stream_mic_download_confirm\": \"Stai per essere reindirizzato alla pagina di download del microfono virtuale. Continuare?\",\n    \"stream_mic_note\": \"Questa funzionalità richiede l'installazione di un microfono virtuale\",\n    \"sunshine_name\": \"Nome Sunshine\",\n    \"sunshine_name_desc\": \"Il nome visualizzato da Moonlight. Se non specificato, viene utilizzato il nome host del PC\",\n    \"sw_preset\": \"Preset SW\",\n    \"sw_preset_desc\": \"Ottimizza il trade-off tra velocità di codifica (fotogrammi codificati al secondo) e efficienza di compressione (qualità per bit nel bitstream). Predefiniti a superfast.\",\n    \"sw_preset_fast\": \"veloce\",\n    \"sw_preset_faster\": \"più veloce\",\n    \"sw_preset_medium\": \"medio\",\n    \"sw_preset_slow\": \"lento\",\n    \"sw_preset_slower\": \"più lento\",\n    \"sw_preset_superfast\": \"superveloce (predefinito)\",\n    \"sw_preset_ultrafast\": \"ultra veloce\",\n    \"sw_preset_veryfast\": \"molto veloce\",\n    \"sw_preset_veryslow\": \"molto lento\",\n    \"sw_tune\": \"Rifinimento SW\",\n    \"sw_tune_animation\": \"animazione -- buona per i cartoni animati; utilizza un maggiore de-blocking e più frame di riferimento\",\n    \"sw_tune_desc\": \"Opzioni di rifinimento, che vengono applicate dopo la preimpostazione. Predefinite a zerolatency.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- permette una decodifica più veloce disabilitando alcuni filtri\",\n    \"sw_tune_film\": \"film -- uso per contenuti cinematografici di alta qualità; riduce il deblocking\",\n    \"sw_tune_grain\": \"grain -- conserva la struttura della grana nel vecchio materiale di film\",\n    \"sw_tune_stillimage\": \"stillimage -- buono per contenuti simili alle presentazioni\",\n    \"sw_tune_zerolatency\": \"zerolatency -- buono per la codifica veloce e lo streaming a bassa latenza (predefinito)\",\n    \"system_tray\": \"Abilita barra di sistema\",\n    \"system_tray_desc\": \"Se abilitare la barra delle applicazioni. Se abilitato, Sunshine visualizzerà un'icona nella barra delle applicazioni e potrà essere controllato da lì.\",\n    \"touchpad_as_ds4\": \"Emula un gamepad DS4 se il gamepad client segnala che un touchpad è presente\",\n    \"touchpad_as_ds4_desc\": \"Se disabilitata, la presenza del touchpad non sarà presa in considerazione durante la selezione del tipo del gamepad.\",\n    \"unsaved_changes_tooltip\": \"Hai modifiche non salvate. Clicca per salvare.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Configura automaticamente l'inoltro delle porte per lo streaming su Internet\",\n    \"variable_refresh_rate\": \"Frequenza di aggiornamento variabile (VRR)\",\n    \"variable_refresh_rate_desc\": \"Consente al framerate dello stream video di corrispondere al framerate di rendering per il supporto VRR. Quando abilitato, la codifica avviene solo quando sono disponibili nuovi frame, consentendo allo stream di seguire il framerate di rendering effettivo.\",\n    \"vdd_reuse_desc_windows\": \"Quando attivato, tutti i client condivideranno lo stesso VDD (Virtual Display Device). Quando disattivato (predefinito), ogni client ottiene il proprio VDD. Attiva questa opzione per un cambio client più rapido, ma nota che tutti i client condivideranno le stesse impostazioni di visualizzazione.\",\n    \"vdd_reuse_windows\": \"Riutilizza lo stesso VDD per tutti i client\",\n    \"virtual_display\": \"Display virtuale\",\n    \"virtual_mouse\": \"Driver mouse virtuale\",\n    \"virtual_mouse_desc\": \"Quando abilitato, Sunshine utilizzerà il driver Zako Virtual Mouse (se installato) per simulare l'input del mouse a livello HID. Ciò consente ai giochi che usano Raw Input di ricevere eventi del mouse. Quando disabilitato o driver non installato, torna a SendInput.\",\n    \"virtual_sink\": \"Uscita Audio Virtuale\",\n    \"virtual_sink_desc\": \"Specifica manualmente un dispositivo audio virtuale da usare. Se disattivato, il dispositivo viene scelto automaticamente. Si consiglia vivamente di lasciare vuoto questo campo per utilizzare la selezione automatica del dispositivo!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Installare il driver del mouse virtuale?\",\n    \"vmouse_confirm_uninstall\": \"Disinstallare il driver del mouse virtuale?\",\n    \"vmouse_install\": \"Installa driver\",\n    \"vmouse_installing\": \"Installazione...\",\n    \"vmouse_note\": \"Il driver del mouse virtuale richiede installazione separata. Utilizzare il pannello di controllo Sunshine per installare o gestire il driver.\",\n    \"vmouse_refresh\": \"Aggiorna stato\",\n    \"vmouse_status_installed\": \"Installato (non attivo)\",\n    \"vmouse_status_not_installed\": \"Non installato\",\n    \"vmouse_status_running\": \"In esecuzione\",\n    \"vmouse_uninstall\": \"Disinstalla driver\",\n    \"vmouse_uninstalling\": \"Disinstallazione...\",\n    \"vt_coder\": \"Coder VideoToolbox\",\n    \"vt_realtime\": \"Codifica VideoToolbox In Tempo Reale\",\n    \"vt_software\": \"Codifica Software VideoToolbox\",\n    \"vt_software_allowed\": \"Consentita\",\n    \"vt_software_forced\": \"Forzata\",\n    \"wan_encryption_mode\": \"Modalità Crittografia WAN\",\n    \"wan_encryption_mode_1\": \"Abilitato per i client supportati (predefinito)\",\n    \"wan_encryption_mode_2\": \"Obbligatorio per tutti i client\",\n    \"wan_encryption_mode_desc\": \"Questo determina quando la crittografia sarà utilizzata durante lo streaming su Internet. La crittografia può ridurre le prestazioni di streaming, in particolare su host e client meno potenti.\",\n    \"webhook_curl_command\": \"Comando\",\n    \"webhook_curl_command_desc\": \"Copia il seguente comando nel tuo terminale per testare se il webhook funziona correttamente:\",\n    \"webhook_curl_copy_failed\": \"Copia fallita, seleziona e copia manualmente\",\n    \"webhook_enabled\": \"Notifiche Webhook\",\n    \"webhook_enabled_desc\": \"Quando abilitato, Sunshine invierà notifiche di eventi all'URL Webhook specificato\",\n    \"webhook_group\": \"Impostazioni notifiche Webhook\",\n    \"webhook_skip_ssl_verify\": \"Salta verifica certificato SSL\",\n    \"webhook_skip_ssl_verify_desc\": \"Salta la verifica del certificato SSL per le connessioni HTTPS, solo per test o certificati autofirmati\",\n    \"webhook_test\": \"Testa\",\n    \"webhook_test_failed\": \"Test Webhook fallito\",\n    \"webhook_test_failed_note\": \"Nota: Controlla se l'URL è corretto o controlla la console del browser per ulteriori informazioni.\",\n    \"webhook_test_success\": \"Test Webhook riuscito!\",\n    \"webhook_test_success_cors_note\": \"Nota: A causa delle restrizioni CORS, lo stato della risposta del server non può essere confermato.\\nLa richiesta è stata inviata. Se il webhook è configurato correttamente, il messaggio dovrebbe essere stato consegnato.\\n\\nSuggerimento: Controlla la scheda Rete negli strumenti per sviluppatori del tuo browser per i dettagli della richiesta.\",\n    \"webhook_test_url_required\": \"Inserisci prima l'URL Webhook\",\n    \"webhook_timeout\": \"Timeout richiesta\",\n    \"webhook_timeout_desc\": \"Timeout per le richieste Webhook in millisecondi, intervallo 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"L'URL per ricevere notifiche di eventi, supporta protocolli HTTP/HTTPS\",\n    \"wgc_checking_mode\": \"Controllo modalità...\",\n    \"wgc_checking_running_mode\": \"Controllo modalità di esecuzione...\",\n    \"wgc_control_panel_only\": \"Questa funzionalità è disponibile solo nel pannello di controllo di Sunshine\",\n    \"wgc_mode_switch_failed\": \"Cambio modalità fallito\",\n    \"wgc_mode_switch_started\": \"Cambio modalità avviato. Se appare un prompt UAC, clicca su 'Sì' per confermare.\",\n    \"wgc_service_mode_warning\": \"La cattura WGC richiede l'esecuzione in modalità utente. Se attualmente in modalità servizio, clicca sul pulsante sopra per passare alla modalità utente.\",\n    \"wgc_switch_to_service_mode\": \"Passa alla modalità servizio\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Attualmente in modalità utente. Clicca per passare alla modalità servizio.\",\n    \"wgc_switch_to_user_mode\": \"Passa alla modalità utente\",\n    \"wgc_switch_to_user_mode_tooltip\": \"La cattura WGC richiede l'esecuzione in modalità utente. Clicca su questo pulsante per passare alla modalità utente.\",\n    \"wgc_user_mode_available\": \"Attualmente in modalità utente. La cattura WGC è disponibile.\",\n    \"window_title\": \"Titolo Finestra\",\n    \"window_title_desc\": \"Il titolo della finestra da acquisire (corrispondenza parziale, non fa distinzione tra maiuscole e minuscole). Se lasciato vuoto, verrà utilizzato automaticamente il nome dell'applicazione in esecuzione corrente.\",\n    \"window_title_placeholder\": \"es., Nome Applicazione\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine è una piattaforma autonoma di game stream per Moonlight.\",\n    \"download\": \"Download\",\n    \"installed_version_not_stable\": \"Stai eseguendo una versione in anteprima di Sunshine. Potresti riscontrare bug o altri problemi. Si prega di segnalare eventuali problemi incontrati. Grazie per aver contribuito a rendere Sunshine un software migliore!\",\n    \"loading_latest\": \"Caricamento dell'ultima versione...\",\n    \"new_pre_release\": \"Una nuova versione pre-rilascio è disponibile!\",\n    \"new_stable\": \"Una nuova versione Stabile è disponibile!\",\n    \"startup_errors\": \"<b>Attenzione!</b> Sunshine ha rilevato questi errori durante l'avvio. Ti <b>raccomandamo vivamente </b> di risolverli prima dello streaming.\",\n    \"update_download_confirm\": \"Stai per aprire la pagina di download degli aggiornamenti nel browser. Continuare?\",\n    \"version_dirty\": \"Grazie per aver contribuito a rendere Sunshine un software migliore!\",\n    \"version_latest\": \"Stai eseguendo l'ultima versione di Sunshine\",\n    \"view_logs\": \"Visualizza log\",\n    \"welcome\": \"Ciao, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Applicazioni\",\n    \"configuration\": \"Configurazione\",\n    \"home\": \"Home\",\n    \"password\": \"Modifica Password\",\n    \"pin\": \"Pin\",\n    \"theme_auto\": \"Automatico\",\n    \"theme_dark\": \"Scuro\",\n    \"theme_light\": \"Chiaro\",\n    \"toggle_theme\": \"Tema\",\n    \"troubleshoot\": \"Risoluzione Dei Problemi\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Conferma Password\",\n    \"current_creds\": \"Credenziali Attuali\",\n    \"new_creds\": \"Nuove credenziali\",\n    \"new_username_desc\": \"Se non specificato, il nome utente non cambierà\",\n    \"password_change\": \"Cambio Password\",\n    \"success_msg\": \"La password è stata modificata con successo! Questa pagina verrà ricaricata presto, il tuo browser ti chiederà le nuove credenziali.\"\n  },\n  \"pin\": {\n    \"actions\": \"Azioni\",\n    \"cancel_editing\": \"Annulla modifica\",\n    \"client_name\": \"Nome\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Conferma eliminazione\",\n    \"delete_client\": \"Elimina client\",\n    \"delete_confirm_message\": \"Sei sicuro di voler eliminare <strong>{name}</strong>?\",\n    \"delete_warning\": \"Questa azione non può essere annullata.\",\n    \"device_name\": \"Nome del Dispositivo\",\n    \"device_size\": \"Dimensione dispositivo\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Grande - TV\",\n    \"device_size_medium\": \"Medio - Tablet\",\n    \"device_size_small\": \"Piccolo - Telefono\",\n    \"edit_client_settings\": \"Modifica impostazioni client\",\n    \"hdr_profile\": \"Profilo HDR\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Caricamento...\",\n    \"loading_clients\": \"Caricamento client...\",\n    \"modify_in_gui\": \"Si prega di modificare nell'interfaccia grafica\",\n    \"none\": \"-- Nessuno --\",\n    \"or_manual_pin\": \"o inserisci il PIN manualmente\",\n    \"pair_failure\": \"Accoppiamento non riuscito: verificare se il PIN è digitato correttamente\",\n    \"pair_success\": \"Fatto! Controlla Moonlight per proseguire\",\n    \"pin_pairing\": \"Accoppiamento con PIN\",\n    \"qr_expires_in\": \"Scade tra\",\n    \"qr_generate\": \"Genera codice QR\",\n    \"qr_paired_success\": \"Abbinamento riuscito!\",\n    \"qr_pairing\": \"Abbinamento tramite QR Code\",\n    \"qr_pairing_desc\": \"Genera un codice QR per un abbinamento rapido. Scansionalo con il client Moonlight per abbinarti automaticamente.\",\n    \"qr_pairing_warning\": \"Funzione sperimentale. Se l'abbinamento fallisce, usa l'abbinamento manuale con PIN qui sotto. Nota: Questa funzione funziona solo in LAN.\",\n    \"qr_refresh\": \"Aggiorna codice QR\",\n    \"remove_paired_devices_desc\": \"Rimuovi i tuoi dispositivi accoppiati.\",\n    \"save_changes\": \"Salva modifiche\",\n    \"save_failed\": \"Impossibile salvare le impostazioni del client. Riprova.\",\n    \"save_or_cancel_first\": \"Si prega di salvare o annullare la modifica prima\",\n    \"send\": \"Invia\",\n    \"unknown_client\": \"Client sconosciuto\",\n    \"unpair_all_confirm\": \"Sei sicuro di voler disaccoppiare tutti i client? Questa azione non può essere annullata.\",\n    \"unsaved_changes\": \"Modifiche non salvate\",\n    \"warning_msg\": \"Assicurati di avere accesso al client con cui stai accoppiando. Questo software può dare il controllo totale al tuo computer, quindi fai attenzione!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android consigliato\",\n    \"client_downloads\": \"Download client\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"Discussioni di GitHub\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Unisciti alla comunità\",\n    \"join_group_desc\": \"Ottieni aiuto e condividi esperienze\",\n    \"legal\": \"Info legali\",\n    \"legal_desc\": \"Continuando a utilizzare questo software si accettano i termini e le condizioni riportati nei seguenti documenti.\",\n    \"license\": \"Licenza\",\n    \"lizardbyte_website\": \"Sito web di LizardByte\",\n    \"official_website\": \"Sito ufficiale\",\n    \"official_website_title\": \"AlkaidLab - Sito ufficiale\",\n    \"open_source\": \"Open Source\",\n    \"open_source_desc\": \"Star & Fork per supportare il progetto\",\n    \"quick_start\": \"Avvio rapido\",\n    \"resources\": \"Risorse\",\n    \"resources_desc\": \"Risorse per Sunshine!\",\n    \"third_party_desc\": \"Avvisi componenti di terze parti\",\n    \"third_party_moonlight\": \"Link amici\",\n    \"third_party_notice\": \"Avvisi di terze parti\",\n    \"tutorial\": \"Tutorial\",\n    \"tutorial_desc\": \"Guida dettagliata alla configurazione e all'uso\",\n    \"view_license\": \"Visualizza licenza completa\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Display virtuale\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"Vai al repository\",\n    \"harmony_modal_desc\": \"Per HarmonyOS NEXT Moonlight, cerca Moonlight V+ nell'App Store di HarmonyOS\",\n    \"harmony_modal_link_notice\": \"Questo link reindirizzerà al repository del progetto\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"secondi\",\n    \"restart_desc\": \"Configurazione salvata. Sunshine si sta riavviando per applicare le impostazioni di visualizzazione.\",\n    \"restart_go_now\": \"Vai ora\",\n    \"restart_title\": \"Riavvio di Sunshine\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"Le impostazioni di base sono ora attive. Puoi iniziare subito a trasmettere con un client Moonlight!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"Il display base (VDD) è il display virtuale intelligente integrato di Sunshine Foundation, che supporta qualsiasi risoluzione, frame rate e ottimizzazione HDR. È la scelta preferita per lo streaming a schermo spento e lo streaming con display esteso.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Assicura attivazione\",\n    \"step3_ensure_active_desc\": \"Attiva il display se non è già attivo\",\n    \"step3_ensure_only_display\": \"Assicura display unico\",\n    \"step3_ensure_only_display_desc\": \"Disattiva tutti gli altri display e attiva solo il display specificato (consigliato)\",\n    \"step3_ensure_primary\": \"Assicura display principale\",\n    \"step3_ensure_primary_desc\": \"Attiva il display e lo imposta come display principale\",\n    \"step3_ensure_secondary\": \"Streaming secondario\",\n    \"step3_ensure_secondary_desc\": \"Utilizza solo il display virtuale per lo streaming secondario esteso\",\n    \"step3_no_operation\": \"Nessuna operazione\",\n    \"step3_no_operation_desc\": \"Nessuna modifica allo stato del display; l'utente deve assicurarsi che il display sia pronto\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Display virtuale (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Advanced\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Config Files\",\n    \"general\": \"General\",\n    \"input\": \"Input\",\n    \"network\": \"Network\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analisi...\",\n    \"ai_analyzing_logs\": \"Analisi dei log in corso, attendere...\",\n    \"ai_config\": \"Configurazione IA\",\n    \"ai_copy_result\": \"Copia\",\n    \"ai_diagnosis\": \"Diagnosi IA\",\n    \"ai_diagnosis_title\": \"Diagnosi IA dei log\",\n    \"ai_error\": \"Analisi fallita\",\n    \"ai_key_local\": \"La chiave API è memorizzata solo localmente e mai caricata\",\n    \"ai_model\": \"Modello\",\n    \"ai_provider\": \"Fornitore\",\n    \"ai_reanalyze\": \"Rianalizza\",\n    \"ai_result\": \"Risultato della diagnosi\",\n    \"ai_retry\": \"Riprova\",\n    \"ai_start_diagnosis\": \"Avvia diagnosi\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Se è necessario spegnere immediatamente Sunshine, è possibile utilizzare questa funzione. Si noti che sarà necessario avviarlo manualmente dopo lo spegnimento.\",\n    \"boom_sunshine_success\": \"Sunshine è stato spento\",\n    \"confirm_boom\": \"Vuoi davvero uscire?\",\n    \"confirm_boom_desc\": \"Quindi vuoi davvero uscire? Beh, non posso fermarti, vai avanti e clicca di nuovo\",\n    \"confirm_logout\": \"Confermare l'uscita?\",\n    \"confirm_logout_desc\": \"Dovrai inserire di nuovo la password per accedere all'interfaccia web.\",\n    \"copy_config\": \"Copia configurazione\",\n    \"copy_config_error\": \"Impossibile copiare la configurazione\",\n    \"copy_config_success\": \"Configurazione copiata negli appunti!\",\n    \"copy_logs\": \"Copia log\",\n    \"download_logs\": \"Scarica log\",\n    \"force_close\": \"Chiusura forzata\",\n    \"force_close_desc\": \"Se Moonlight si lamenta di un'app attualmente in esecuzione, la chiusura forzata dell'app dovrebbe risolvere il problema.\",\n    \"force_close_error\": \"Errore durante la chiusura dell'applicazione\",\n    \"force_close_success\": \"Applicazione chiusa con successo!\",\n    \"ignore_case\": \"Ignora maiuscole e minuscole\",\n    \"logout\": \"Esci\",\n    \"logout_desc\": \"Esci. Potrebbe essere necessario accedere di nuovo.\",\n    \"logout_localhost_tip\": \"L'ambiente attuale non richiede accesso; il logout non mostrerà la richiesta di password.\",\n    \"logs\": \"Log\",\n    \"logs_desc\": \"Vedi i log caricati da Sunshine\",\n    \"logs_find\": \"Trova...\",\n    \"match_contains\": \"Contiene\",\n    \"match_exact\": \"Esatto\",\n    \"match_regex\": \"Espressione regolare\",\n    \"reopen_setup_wizard\": \"Riapri procedura guidata di configurazione\",\n    \"reopen_setup_wizard_desc\": \"Riapri la pagina della procedura guidata di configurazione per riconfigurare le impostazioni iniziali.\",\n    \"reopen_setup_wizard_error\": \"Errore durante la riapertura della procedura guidata di configurazione\",\n    \"reset_display_device_desc_windows\": \"Se Sunshine è bloccato nel tentativo di ripristinare le impostazioni del dispositivo di visualizzazione modificate, è possibile reimpostare le impostazioni e procedere al ripristino manuale dello stato del display.\\nQuestopuò accadere per vari motivi: il dispositivo non è più disponibile, è stato collegato a una porta diversa, ecc.\",\n    \"reset_display_device_error_windows\": \"Errore durante il ripristino della persistenza!\",\n    \"reset_display_device_success_windows\": \"Ristabilimento della persistenza riuscito!\",\n    \"reset_display_device_windows\": \"Ripristino memoria display\",\n    \"restart_sunshine\": \"Riavvia Sunshine\",\n    \"restart_sunshine_desc\": \"Se Sunshine non funziona correttamente, puoi provare a riavviarlo. Questo terminerà qualsiasi sessione in esecuzione.\",\n    \"restart_sunshine_success\": \"Sunshine sta riavviando\",\n    \"troubleshooting\": \"Risoluzione dei Problemi\",\n    \"unpair_all\": \"Rimuovi Tutto\",\n    \"unpair_all_error\": \"Errore durante la rimozione\",\n    \"unpair_all_success\": \"Rimozione Riuscita.\",\n    \"unpair_desc\": \"Rimuove i dispositivi accoppiati. I dispositivi separati con una sessione attiva rimarranno collegati, ma non potranno avviare o riprendere una sessione.\",\n    \"unpair_single_no_devices\": \"Non ci sono dispositivi accoppiati.\",\n    \"unpair_single_success\": \"Tuttavia, il dispositivo o i dispositivi possono essere ancora in una sessione attiva. Usa il pulsante 'Force Close' sopra per terminare qualsiasi sessione aperta.\",\n    \"unpair_single_unknown\": \"Client Sconosciuto\",\n    \"unpair_title\": \"Rimuovi Dispositivi\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Conferma password\",\n    \"create_creds\": \"Prima di iniziare, è necessario creare un nuovo nome utente e una nuova password per accedere all'interfaccia web.\",\n    \"create_creds_alert\": \"Le credenziali in basso sono necessarie per accedere all'interfaccia web di Sunshine. Tienile al sicuro, poichè non potrai più visualizzarle!\",\n    \"creds_local_only\": \"Le tue credenziali vengono archiviate localmente offline e non verranno mai caricate su alcun server.\",\n    \"error\": \"Errore!\",\n    \"greeting\": \"Benvenuto in Sunshine Foundation!\",\n    \"hide_password\": \"Nascondi password\",\n    \"login\": \"Login\",\n    \"network_error\": \"Errore di rete, controlla la connessione\",\n    \"password\": \"Password\",\n    \"password_match\": \"Le password corrispondono\",\n    \"password_mismatch\": \"Le password non corrispondono\",\n    \"server_error\": \"Errore del server\",\n    \"show_password\": \"Mostra password\",\n    \"success\": \"Operazione riuscita!\",\n    \"username\": \"Nome utente\",\n    \"welcome_success\": \"Questa pagina verrà ricaricata, il tuo browser ti richiederà le nuove credenziali\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/ja.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"適用\",\n    \"auto\": \"自動\",\n    \"autodetect\": \"自動検出 (推奨)\",\n    \"beta\": \"(ベータ版)\",\n    \"cancel\": \"キャンセル\",\n    \"close\": \"閉じる\",\n    \"copied\": \"クリップボードにコピーしました\",\n    \"copy\": \"コピー\",\n    \"delete\": \"削除\",\n    \"description\": \"説明\",\n    \"disabled\": \"無効\",\n    \"disabled_def\": \"無効 (デフォルト)\",\n    \"dismiss\": \"却下する\",\n    \"do_cmd\": \"コマンド実行\",\n    \"download\": \"ダウンロード\",\n    \"edit\": \"編集\",\n    \"elevated\": \"管理者として実行\",\n    \"enabled\": \"有効\",\n    \"enabled_def\": \"有効 (デフォルト)\",\n    \"error\": \"エラー！\",\n    \"no_changes\": \"変更なし\",\n    \"note\": \"メモ:\",\n    \"password\": \"パスワード\",\n    \"remove\": \"削除\",\n    \"run_as\": \"管理者として実行\",\n    \"save\": \"保存\",\n    \"see_more\": \"もっと見る\",\n    \"success\": \"成功！\",\n    \"undo_cmd\": \"元に戻す\",\n    \"username\": \"ユーザー名\",\n    \"warning\": \"警告！\"\n  },\n  \"apps\": {\n    \"actions\": \"アクション\",\n    \"add_cmds\": \"コマンドを追加\",\n    \"add_new\": \"新規追加\",\n    \"advanced_options\": \"詳細オプション\",\n    \"app_name\": \"アプリケーション名\",\n    \"app_name_desc\": \"アプリケーション名（Moonlight に表示させるもの）\",\n    \"applications_desc\": \"アプリケーション一覧はクライアントの再起動時にのみ更新されます\",\n    \"applications_title\": \"アプリケーション一覧\",\n    \"auto_detach\": \"アプリケーションが一瞬終了してもストリーミングを続行します\",\n    \"auto_detach_desc\": \"これにより、別のプログラムまたは別インスタンスを起動してすぐ終了する、ランチャー型アプリを自動的に検出しようとします。 ランチャータイプのアプリが検出されると、切断されたアプリとして扱われます。\",\n    \"basic_info\": \"基本情報\",\n    \"cmd\": \"コマンド\",\n    \"cmd_desc\": \"起動したいメインアプリケーション。空白の場合はアプリケーションは起動しません。\",\n    \"cmd_examples_title\": \"一般的な例：\",\n    \"cmd_note\": \"コマンド実行ファイルへのパスにスペースが含まれている場合は、引用符で囲む必要があります。\",\n    \"cmd_prep_desc\": \"このアプリケーションの前/後に実行するコマンドのリストです。prep-commandsのいずれかに失敗した場合、アプリケーションの起動は中止されます。\",\n    \"cmd_prep_name\": \"コマンドの準備\",\n    \"command_settings\": \"コマンド設定\",\n    \"covers_found\": \"カバー画像が見つかりました\",\n    \"delete\": \"削除\",\n    \"delete_confirm\": \"\\\"{name}\\\" を削除してもよろしいですか？\",\n    \"detached_cmds\": \"切り離されたコマンド\",\n    \"detached_cmds_add\": \"別のコマンドを追加\",\n    \"detached_cmds_desc\": \"バックグラウンドで実行するコマンドのリスト。\",\n    \"detached_cmds_note\": \"コマンド実行ファイルへのパスにスペースが含まれている場合は、引用符で囲む必要があります。\",\n    \"detached_cmds_remove\": \"切り離されたコマンドを削除\",\n    \"edit\": \"編集\",\n    \"env_app_id\": \"アプリ ID\",\n    \"env_app_name\": \"アプリ名\",\n    \"env_client_audio_config\": \"クライアントから要求されたオーディオ設定 (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"クライアントは最適なストリーミングのためにゲームを最適化するオプションを要求しています (true/false)\",\n    \"env_client_fps\": \"クライアントから要求された FPS (整数)\",\n    \"env_client_gcmap\": \"要求されたゲームパッドマスク、ビットセット/ビットフィールド形式にて指定 (int)\",\n    \"env_client_hdr\": \"HDR はクライアントによって有効になっています (true/false)\",\n    \"env_client_height\": \"クライアントから要求された高さ (整数)\",\n    \"env_client_host_audio\": \"クライアントはホストオーディオを要求しています (true/false)\",\n    \"env_client_name\": \"クライアント表示名（文字列）\",\n    \"env_client_width\": \"クライアントから要求された幅 (int)\",\n    \"env_displayplacer_example\": \"例 - 解像度自動化のためのディスプレイ プレーサ：\",\n    \"env_qres_example\": \"例 - 自動解像度用のQRes:\",\n    \"env_qres_path\": \"qresのパス\",\n    \"env_var_name\": \"変数名\",\n    \"env_vars_about\": \"環境変数について\",\n    \"env_vars_desc\": \"すべてのコマンドはデフォルトでこれらの環境変数を取得します:\",\n    \"env_xrandr_example\": \"例 - 解像度自動化のための Xrandr:\",\n    \"exit_timeout\": \"終了タイムアウト\",\n    \"exit_timeout_desc\": \"終了要求時にすべてのアプリプロセスが正常に終了するまで待機する秒数。 設定されていない場合、デフォルトでは5秒まで待機します。ゼロまたはマイナス値に設定されている場合、アプリは直ちに終了します。\",\n    \"file_selector_not_initialized\": \"ファイルセレクターが初期化されていません\",\n    \"find_cover\": \"カバーを見つける\",\n    \"form_invalid\": \"必須フィールドを確認してください\",\n    \"form_valid\": \"有効なアプリケーション\",\n    \"global_prep_desc\": \"このアプリケーションのグローバル準備コマンドの実行を有効/無効にする。\",\n    \"global_prep_name\": \"グローバル準備コマンド\",\n    \"image\": \"画像\",\n    \"image_desc\": \"クライアントに送信されるアプリケーションアイコン/画像/画像パス。画像はPNGファイルである必要があります。設定されていない場合、Sunshineはデフォルトのボックス画像を送信します。\",\n    \"image_settings\": \"画像設定\",\n    \"loading\": \"読み込み中...\",\n    \"menu_cmd_actions\": \"操作\",\n    \"menu_cmd_add\": \"メニューコマンドを追加\",\n    \"menu_cmd_command\": \"コマンド\",\n    \"menu_cmd_desc\": \"設定後、これらのコマンドはクライアントの戻りメニューに表示され、ストリームを中断することなく特定の操作を迅速に実行できます。例：ヘルパープログラムの起動など。\\n例：表示名 - コンピューターを閉じる；コマンド - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"表示名\",\n    \"menu_cmd_drag_sort\": \"ドラッグして並べ替え\",\n    \"menu_cmd_name\": \"メニューコマンド\",\n    \"menu_cmd_placeholder_command\": \"コマンド\",\n    \"menu_cmd_placeholder_display_name\": \"表示名\",\n    \"menu_cmd_placeholder_execute\": \"コマンドを実行\",\n    \"menu_cmd_placeholder_undo\": \"コマンドを元に戻す\",\n    \"menu_cmd_remove_menu\": \"メニューコマンドを削除\",\n    \"menu_cmd_remove_prep\": \"準備コマンドを削除\",\n    \"mouse_mode\": \"マウスモード\",\n    \"mouse_mode_auto\": \"自動（グローバル設定）\",\n    \"mouse_mode_desc\": \"このアプリケーションのマウス入力方法を選択します。自動はグローバル設定を使用、仮想マウスはHIDドライバーを使用、SendInputはWindows APIを使用します。\",\n    \"mouse_mode_sendinput\": \"SendInput（Windows API）\",\n    \"mouse_mode_vmouse\": \"仮想マウス\",\n    \"name\": \"名前\",\n    \"output_desc\": \"コマンドの出力を保存するファイル。指定されない場合、出力は無視されます。\",\n    \"output_name\": \"出力\",\n    \"run_as_desc\": \"これは、管理者権限を必要とするアプリケーションが正常に動作するために必要な場合があります。\",\n    \"scan_result_add_all\": \"すべて追加\",\n    \"scan_result_edit_title\": \"追加と編集\",\n    \"scan_result_filter_all\": \"すべて\",\n    \"scan_result_filter_epic_title\": \"Epic Gamesゲーム\",\n    \"scan_result_filter_executable\": \"実行可能\",\n    \"scan_result_filter_executable_title\": \"実行可能ファイル\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxyゲーム\",\n    \"scan_result_filter_script\": \"スクリプト\",\n    \"scan_result_filter_script_title\": \"バッチ/コマンドスクリプト\",\n    \"scan_result_filter_shortcut\": \"ショートカット\",\n    \"scan_result_filter_shortcut_title\": \"ショートカット\",\n    \"scan_result_filter_steam_title\": \"Steamゲーム\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"ゲーム\",\n    \"scan_result_games_only\": \"ゲームのみ\",\n    \"scan_result_matched\": \"一致: {count}\",\n    \"scan_result_no_apps\": \"追加するアプリケーションが見つかりません\",\n    \"scan_result_no_matches\": \"一致するアプリケーションが見つかりません\",\n    \"scan_result_quick_add_title\": \"クイック追加\",\n    \"scan_result_remove_title\": \"リストから削除\",\n    \"scan_result_search_placeholder\": \"アプリケーション名、コマンド、またはパスを検索...\",\n    \"scan_result_show_all\": \"すべて表示\",\n    \"scan_result_title\": \"スキャン結果\",\n    \"scan_result_try_different_keywords\": \"別の検索キーワードを使用してみてください\",\n    \"scan_result_type_batch\": \"バッチ\",\n    \"scan_result_type_command\": \"コマンドスクリプト\",\n    \"scan_result_type_executable\": \"実行可能ファイル\",\n    \"scan_result_type_shortcut\": \"ショートカット\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"アプリケーションを検索...\",\n    \"select\": \"選択\",\n    \"test_menu_cmd\": \"コマンドをテスト\",\n    \"test_menu_cmd_empty\": \"コマンドは空にできません\",\n    \"test_menu_cmd_executing\": \"コマンドを実行中...\",\n    \"test_menu_cmd_failed\": \"コマンドの実行に失敗しました\",\n    \"test_menu_cmd_success\": \"コマンドが正常に実行されました！\",\n    \"use_desktop_image\": \"現在のデスクトップ壁紙を使用\",\n    \"wait_all\": \"すべてのアプリプロセスが終了するまでストリーミングを続ける\",\n    \"wait_all_desc\": \"これは、アプリによって開始されたすべてのプロセスが終了するまで、ストリーミングを続けます。 チェックを外すと、他のアプリプロセスがまだ実行中であっても、最初のアプリプロセスが終了するとストリーミングは停止します。\",\n    \"working_dir\": \"作業ディレクトリ\",\n    \"working_dir_desc\": \"プロセスに渡される作業ディレクトリ。たとえば、アプリケーションによっては、作業ディレクトリを使用して設定ファイルを検索します。 設定されていない場合、Sunshineはデフォルトでコマンドの親ディレクトリを使用します。\"\n  },\n  \"config\": {\n    \"adapter_name\": \"アダプター名\",\n    \"adapter_name_desc_linux_1\": \"キャプチャに使用する GPU を手動で指定します。\",\n    \"adapter_name_desc_linux_2\": \"VAAPIが可能なすべてのデバイスを検索する\",\n    \"adapter_name_desc_linux_3\": \"``renderD129`` を上記のデバイスに置き換えて、デバイスの名前と機能を一覧表示します。 Sunshineでサポートされるには、最小限にする必要があります。\",\n    \"adapter_name_desc_windows\": \"キャプチャに使用するGPUを手動で指定します。未設定の場合、GPUは自動的に選択されます。注意：このGPUはディスプレイに接続され、電源がオンになっている必要があります。ノートPCで直接GPU出力を有効にできない場合は、自動に設定してください。\",\n    \"adapter_name_desc_windows_vdd_hint\": \"仮想ディスプレイの最新バージョンがインストールされている場合、GPUバインディングと自動的に関連付けることができます\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"追加\",\n    \"address_family\": \"アドレスファミリー\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Sunshineが使用するアドレスファミリーを設定する\",\n    \"address_family_ipv4\": \"IPv4 のみ\",\n    \"always_send_scancodes\": \"常にスキャンコードを送信する\",\n    \"always_send_scancodes_desc\": \"スキャンコードを送信すると、ゲームやアプリとの互換性が向上しますが、米国英語のキーボードレイアウトを使用していない特定のクライアントからのキーボード入力が誤っている可能性があります。 特定のアプリケーションでキーボード入力がまったく動作しない場合に有効にします。 クライアントのキーがホストに間違った入力を生成している場合は無効にします。\",\n    \"amd_coder\": \"AMFコーダー(H264)\",\n    \"amd_coder_desc\": \"エントロピーエンコーディングを選択して品質やエンコーディング速度を優先することができます。H.264 のみ。\",\n    \"amd_enforce_hrd\": \"AMF仮説リファレンスデコーダ(HRD) Enforcement\",\n    \"amd_enforce_hrd_desc\": \"HRDモデル要件を満たすためのレート制御の制約を増やします。 これによりビットレートのオーバーフローが大幅に減少しますが、エンコードアーティファクトや特定のカードの品質が低下する可能性があります。\",\n    \"amd_preanalysis\": \"AMF事前解析\",\n    \"amd_preanalysis_desc\": \"これにより、レート制御の事前分析が可能になり、エンコード待ち時間の増加を犠牲にして品質が向上する可能性があります。\",\n    \"amd_quality\": \"AMF品質\",\n    \"amd_quality_balanced\": \"balance-- balance（デフォルト）\",\n    \"amd_quality_desc\": \"これにより、エンコード速度と品質のバランスを制御します。\",\n    \"amd_quality_group\": \"AMF品質設定\",\n    \"amd_quality_quality\": \"品質 -- 品質を優先\",\n    \"amd_quality_speed\": \"スピード -- 速度を優先\",\n    \"amd_qvbr_quality\": \"AMF QVBR品質レベル\",\n    \"amd_qvbr_quality_desc\": \"QVBRレート制御モードの品質レベル。範囲: 1-51（低い方が高品質）。デフォルト: 23。レート制御が'qvbr'に設定されている場合のみ適用。\",\n    \"amd_rc\": \"AMFレート制御\",\n    \"amd_rc_cbr\": \"cbr -- 固定ビットレート（デフォルト）\",\n    \"amd_rc_cqp\": \"cqp -- 固定qp モード\",\n    \"amd_rc_desc\": \"これは、クライアントのビットレート目標を超えないようにするレート制御方法を制御します。 'cqp'はビットレートターゲティングには適していません。'vbr_latency'以外のオプションはHRDエンフォースメントに依存してビットレートオーバーフローを制限します。\",\n    \"amd_rc_group\": \"AMFレートコントロール設定\",\n    \"amd_rc_hqcbr\": \"hqcbr -- 高品質固定ビットレート\",\n    \"amd_rc_hqvbr\": \"hqvbr -- 高品質可変ビットレート\",\n    \"amd_rc_qvbr\": \"qvbr -- 品質可変ビットレート（QVBR品質レベルを使用）\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- レイテンシ制約付き可変ビットレート\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- ピーク制約可変ビットレート\",\n    \"amd_usage\": \"AMF使用率\",\n    \"amd_usage_desc\": \"これにより、基本エンコーディングプロファイルが設定されます。 以下に表示されるすべてのオプションは、使用状況プロファイルのサブセットを上書きしますが、他の場所では設定できない追加の非表示設定が適用されます。\",\n    \"amd_usage_lowlatency\": \"lowlaterity - 低レイテンシ（最速）\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - 低レイテンシ、高品質 (高速)\",\n    \"amd_usage_transcoding\": \"トランスコード-- トランスコード（最も遅い）\",\n    \"amd_usage_ultralowlatency\": \"超低レイテンシー - 超低レイテンシ（最速）\",\n    \"amd_usage_webcam\": \"ウェブカメラ -- ウェブカメラ (スロー)\",\n    \"amd_vbaq\": \"AMF分散ベース適応型量子化(VBAQ)\",\n    \"amd_vbaq_desc\": \"人間の視覚システムは、高度なテクスチャ領域の人工物には通常、あまり敏感です。 VBAQモードでは、ピクセル分散を使用して空間テクスチャの複雑さを示し、エンコーダがより多くのビットを割り当て、領域をスムーズにすることができます。 この機能を有効にすると、一部のコンテンツで主観的なビジュアル品質が向上します。\",\n    \"amf_draw_mouse_cursor\": \"AMFキャプチャ使用時にシンプルなカーソルを描画する\",\n    \"amf_draw_mouse_cursor_desc\": \"場合によっては、AMF キャプチャを使用するとマウスポインターが表示されないことがあります。このオプションを有効にすると、画面上にシンプルなマウスポインターが描画されます。注意：マウスポインターの位置はコンテンツ画面が更新されたときのみ更新されるため、デスクトップなどのゲーム以外のシナリオではマウスポインターの動きが遅れることがあります。\",\n    \"apply_note\": \"'適用' をクリックして Sunshine を再起動し、変更を適用します。これにより、実行中のセッションはすべて終了します。\",\n    \"audio_sink\": \"音声シンク\",\n    \"audio_sink_desc_linux\": \"オーディオループバックに使用されるオーディオシンクの名前。この変数を指定しない場合、pulseaudio はデフォルトのモニターデバイスを選択します。 いずれかのコマンドを使用して、オーディオシンクの名前を見つけることができます。\",\n    \"audio_sink_desc_macos\": \"Audio Loopback に使用されるオーディオシンクの名前。Sunshineはシステムの制限により、macOSのマイクにのみアクセスできます。 Soundflower または BlackHole を使用してシステムのオーディオをストリーミングする。\",\n    \"audio_sink_desc_windows\": \"キャプチャする特定のオーディオデバイスを手動で指定します。未設定の場合、デバイスは自動的に選択されます。 自動デバイス選択を使用するには、このフィールドを空白のままにすることを強くお勧めします! 同じ名前の複数のオーディオデバイスをお持ちの場合は、次のコマンドを使用してデバイス ID を取得できます。\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"スピーカー (高品位オーディオデバイス)\",\n    \"av1_mode\": \"AV1 サポート\",\n    \"av1_mode_0\": \"サンシャインはエンコーダ機能に基づいてAV1のサポートを宣伝します(推奨)\",\n    \"av1_mode_1\": \"サンシャインはAV1のサポートを宣伝しません\",\n    \"av1_mode_2\": \"SunshineはAV1メイン8ビットプロファイルのサポートを宣伝します\",\n    \"av1_mode_3\": \"SunshineはAV1メイン8ビットと10ビット(HDR)プロファイルのサポートを宣伝します。\",\n    \"av1_mode_desc\": \"クライアントがAV1 Main 8ビットまたは10ビットのビデオストリームを要求できるようにします。 AV1はエンコードにCPU負荷がかかるため、ソフトウェアエンコーディングを使用する際のパフォーマンスが低下する可能性があります。\",\n    \"back_button_timeout\": \"ホーム/ガイドボタンエミュレーションのタイムアウト\",\n    \"back_button_timeout_desc\": \"Back/Selectボタンが指定されたミリ秒の間押し続けられると、Home/Guideボタン押下がエミュレートされる。値＜0（デフォルト）に設定すると、Back/Selectボタンを押し続けてもHome/Guideボタンはエミュレートされない。\",\n    \"bind_address\": \"バインドアドレス（テスト機能）\",\n    \"bind_address_desc\": \"Sunshineがバインドする特定のIPアドレスを設定します。空白の場合、すべての利用可能なアドレスにバインドします。\",\n    \"capture\": \"特定のキャプチャ方法を強制する\",\n    \"capture_desc\": \"自動モードでSunshineは動作する最初のものを使用します. NvFBCはパッチ済み nvidiaドライバが必要です.\",\n    \"capture_target\": \"キャプチャターゲット\",\n    \"capture_target_desc\": \"キャプチャするターゲットの種類を選択します。「ウィンドウ」を選択すると、ディスプレイ全体ではなく、特定のアプリケーションウィンドウ（AIフレーム補間ソフトウェアなど）をキャプチャできます。\",\n    \"capture_target_display\": \"ディスプレイ\",\n    \"capture_target_window\": \"ウィンドウ\",\n    \"cert\": \"証明書\",\n    \"cert_desc\": \"Web UIとMoonlightクライアントのペアリングに使用される証明書。互換性を確保するためには、RSA-2048 公開鍵が必要です。\",\n    \"channels\": \"最大接続クライアント数\",\n    \"channels_desc_1\": \"Sunshineは、単一のストリーミングセッションを複数のクライアントと同時に共有することができます。\",\n    \"channels_desc_2\": \"一部のハードウェアエンコーダには、複数のストリームでパフォーマンスを低下させる制限がある場合があります。\",\n    \"close_verify_safe\": \"安全な検証互換性のある古いクライアント\",\n    \"close_verify_safe_desc\": \"古いクライアントはSunshineに接続できない可能性があります。このオプションを無効にするか、クライアントを更新してください\",\n    \"coder_cabac\": \"cabac -- コンテキスト適応二進数演算符号化 - 高品質\",\n    \"coder_cavlc\": \"cavlc -- コンテキスト適応型可変長符号化 - 高速デコード\",\n    \"configuration\": \"設定\",\n    \"controller\": \"ゲームパッド入力を有効にする\",\n    \"controller_desc\": \"ゲストがゲームパッド/コントローラーでホストシステムを制御できるようにします\",\n    \"credentials_file\": \"資格情報ファイル\",\n    \"credentials_file_desc\": \"ユーザー名/パスワードは、サンシャインのステートファイルとは別に保管してください。\",\n    \"display_device_options_note_desc_windows\": \"Windowsは、現在アクティブなディスプレイの組み合わせごとに、さまざまなディスプレイ設定を保存します。\\nSunshineは、そのようなディスプレイの組み合わせに属するディスプレイに変更を適用します。\\nSunshineが設定を適用したときにアクティブだったデバイスを切断すると、Sunshineが変更を元に戻そうとする時までに\\nその組み合わせを再びアクティブにできない限り、変更を元に戻すことはできません！\",\n    \"display_device_options_note_windows\": \"設定の適用方法に関する注意\",\n    \"display_device_options_windows\": \"ディスプレイデバイスオプション\",\n    \"display_device_prep_ensure_active_desc_windows\": \"ディスプレイがアクティブでない場合はアクティブにします\",\n    \"display_device_prep_ensure_active_windows\": \"ディスプレイを自動的にアクティブにする\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"他のすべてのディスプレイを無効にし、指定されたディスプレイのみを有効にします\",\n    \"display_device_prep_ensure_only_display_windows\": \"他のディスプレイを無効にし、指定されたディスプレイのみをアクティブにする\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"ディスプレイをアクティブにし、プライマリディスプレイとして設定します\",\n    \"display_device_prep_ensure_primary_windows\": \"ディスプレイを自動的にアクティブにし、プライマリディスプレイにする\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"仮想ディスプレイのみを使用してセカンダリ拡張ストリーミングを行います\",\n    \"display_device_prep_ensure_secondary_windows\": \"セカンダリディスプレイストリーミング（仮想ディスプレイのみ）\",\n    \"display_device_prep_no_operation_desc_windows\": \"ディスプレイの状態を変更しません。ユーザーがディスプレイの準備を確認する必要があります\",\n    \"display_device_prep_no_operation_windows\": \"無効\",\n    \"display_device_prep_windows\": \"ディスプレイの準備\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"「受信」と「最終」の値をそれぞれ少なくとも1つ指定する必要があります。\\n「受信」セクションの空欄は「任意の値に一致」を意味します。「最終」セクションの空欄は「受信値を維持」を意味します。\\n必要に応じて、特定のFPS値を特定の解像度に一致させることができます...\\n\\n注: Moonlightクライアントで「ゲーム設定を最適化」オプションが有効になっていない場合、解像度の値を含む行は無視されます。\",\n    \"display_mode_remapping_desc_windows\": \"特定の解像度やリフレッシュレートを別の値に再マッピングする方法を指定します。\\n低い解像度でストリーミングしながら、ホスト上でより高い解像度でレンダリングすることで、スーパーサンプリング効果を得ることができます。\\nまた、より高いFPSでストリーミングしながら、ホストのリフレッシュレートを低く制限することもできます。\\nマッチングは上から下に行われます。エントリが一致すると、他のエントリはチェックされませんが、検証は引き続き行われます。\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"最終リフレッシュレート\",\n    \"display_mode_remapping_final_resolution_windows\": \"最終解像度\",\n    \"display_mode_remapping_optional\": \"任意\",\n    \"display_mode_remapping_received_fps_windows\": \"受信FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"受信解像度\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"注: Moonlightクライアントで「ゲーム設定を最適化」オプションが有効になっていない場合、再マッピングは無効になります。\",\n    \"display_mode_remapping_windows\": \"ディスプレイモードの再マッピング\",\n    \"display_modes\": \"ディスプレイモード\",\n    \"ds4_back_as_touchpad_click\": \"戻る/選択をタッチパッドにマップする\",\n    \"ds4_back_as_touchpad_click_desc\": \"DS4エミュレーションを強制するときは、戻る/選択をタッチパッドにマップする\",\n    \"dsu_server_port\": \"DSU Server Port\",\n    \"dsu_server_port_desc\": \"DSU server listening port (default 26760). Sunshine will act as a DSU server to receive client connections and send motion data. Enable DSU server in your client(Yuzu,Ryujinx etc.) and set DSU server address(127.0.0.1) and port(26760)\",\n    \"enable_dsu_server\": \"DSUサーバーを有効にする\",\n    \"enable_dsu_server_desc\": \"Enable DSU server to receive client connections and send motion data\",\n    \"encoder\": \"特定のエンコーダーを強制する\",\n    \"encoder_desc\": \"特定のエンコーダを強制します。そうでなければ、Sunshineは最良の選択肢を選択します。 注:Windowsでハードウェアエンコーダを指定する場合は、ディスプレイが接続されているGPUと一致する必要があります。\",\n    \"encoder_software\": \"ソフトウェア\",\n    \"experimental\": \"実験的\",\n    \"experimental_features\": \"実験的機能\",\n    \"external_ip\": \"外部 IP\",\n    \"external_ip_desc\": \"外部IPアドレスが指定されていない場合、Sunshineは自動的に外部IPを検出します。\",\n    \"fec_percentage\": \"FECの割合\",\n    \"fec_percentage_desc\": \"各ビデオフレーム内のデータ パケットあたりのパケットを修正するエラー率。 より高い値は、ネットワークパケットの損失を増やすことができますが、帯域幅の使用量を増加させることができます。\",\n    \"ffmpeg_auto\": \"auto -- ffmpegで判断する (デフォルト)\",\n    \"file_apps\": \"アプリファイル\",\n    \"file_apps_desc\": \"Sunshineの現在のアプリが保存されているファイル。\",\n    \"file_state\": \"状態ファイル\",\n    \"file_state_desc\": \"サンシャインの現在の状態が保存されているファイル\",\n    \"fps\": \"公告された FPS\",\n    \"gamepad\": \"エミュレートしたゲームパッドのタイプ\",\n    \"gamepad_auto\": \"自動選択オプション\",\n    \"gamepad_desc\": \"ホスト上でエミュレートするゲームパッドの種類を選択します\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 Manual Options\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"DS4マニュアルオプション\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"コマンドの準備\",\n    \"global_prep_cmd_desc\": \"アプリケーションの実行前または実行後に実行されるコマンドのリストを構成します。 指定された preparation コマンドのいずれかに失敗すると、アプリケーションの起動プロセスは中断されます。\",\n    \"hdr_luminance_analysis\": \"HDRダイナミックメタデータ (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"フレームごとのGPU輝度分析を有効にし、HDR10+ (ST 2094-40)およびHDR Vivid (CUVA)ダイナミックメタデータを符号化ビットストリームに注入します。対応ディスプレイにフレームごとのトーンマッピングヒントを提供します。高解像度で若干のGPUオーバーヘッド（~0.5-1.5ms/フレーム）が追加されます。HDR有効時にフレームレートが低下する場合は無効にしてください。\",\n    \"hdr_prep_automatic_windows\": \"Switch on/off the HDR mode as requested by the client\",\n    \"hdr_prep_no_operation_windows\": \"Disabled\",\n    \"hdr_prep_windows\": \"HDR state change\",\n    \"hevc_mode\": \"HEVC サポート\",\n    \"hevc_mode_0\": \"サンシャインはエンコーダ機能に基づいてHEVCのサポートを宣伝します(推奨)\",\n    \"hevc_mode_1\": \"サンシャインはHEVCのサポートを宣伝しません\",\n    \"hevc_mode_2\": \"サンシャインはHEVCメインプロファイルのサポートを宣伝します\",\n    \"hevc_mode_3\": \"サンシャインはHEVC MainおよびMain10(HDR)プロファイルのサポートを宣伝します\",\n    \"hevc_mode_desc\": \"HEVC MainまたはHEVC Main10ビデオストリームのリクエストをクライアントに許可します。 HEVCはエンコードにCPU負荷がかかるため、ソフトウェアエンコーディングを使用する際のパフォーマンスが低下する可能性があります。\",\n    \"high_resolution_scrolling\": \"高解像度スクロールサポート\",\n    \"high_resolution_scrolling_desc\": \"有効にすると、SunshineはMoonlightのクライアントから高解像度スクロールイベントを通過します。 これは、高解像度スクロールイベントで高速にスクロールする古いアプリケーションでは無効にすることができます。\",\n    \"install_steam_audio_drivers\": \"Steam オーディオドライバをインストール\",\n    \"install_steam_audio_drivers_desc\": \"Steamがインストールされている場合、Steam Streaming Speakersドライバが自動的にインストールされ、5.1/7.1 サラウンドサウンドとホストオーディオのミュートがサポートされます。\",\n    \"key_repeat_delay\": \"キーリピート遅延\",\n    \"key_repeat_delay_desc\": \"キーを繰り返す速度を制御します。キーを繰り返すまでの時間をミリ秒単位で設定します。\",\n    \"key_repeat_frequency\": \"キーの繰り返し周波数\",\n    \"key_repeat_frequency_desc\": \"キーが毎秒繰り返される頻度。この設定可能なオプションは10進数をサポートします。\",\n    \"key_rightalt_to_key_win\": \"右AltキーをWindowsキーにマップ\",\n    \"key_rightalt_to_key_win_desc\": \"Moonlight から Windows キーを直接送信できない可能性があります。 これらの場合、SunshineにRight AltキーがWindowsキーであると考えさせると便利かもしれません。\",\n    \"key_rightalt_to_key_windows\": \"右AltキーをWindowsキーにマップする\",\n    \"keyboard\": \"キーボード入力を有効にする\",\n    \"keyboard_desc\": \"ゲストがキーボードでホストシステムを制御できるようにします\",\n    \"lan_encryption_mode\": \"LAN 暗号化モード\",\n    \"lan_encryption_mode_1\": \"サポートされているクライアントで有効\",\n    \"lan_encryption_mode_2\": \"すべてのクライアントに必要です\",\n    \"lan_encryption_mode_desc\": \"これは、ローカルネットワーク経由でストリーミングする際に暗号化がいつ使用されるかを決定します。暗号化は、特に強力なホストやクライアントでは、ストリーミングのパフォーマンスを低下させることができます。\",\n    \"locale\": \"ロケール\",\n    \"locale_desc\": \"Sunshineのユーザーインターフェースに使用されるロケール。\",\n    \"log_level\": \"ログレベル\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"情報\",\n    \"log_level_3\": \"警告\",\n    \"log_level_4\": \"エラー\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"なし\",\n    \"log_level_desc\": \"標準出力に印刷された最小ログレベル\",\n    \"log_path\": \"ログファイルのパス\",\n    \"log_path_desc\": \"Sunshineの現在のログが保存されているファイル。\",\n    \"max_bitrate\": \"最大ビットレート\",\n    \"max_bitrate_desc\": \"Sunshineがストリームをエンコードする最大ビットレート（Kbps単位）。0に設定すると、Moonlightが要求するビットレートが常に使用されます。\",\n    \"max_fps_reached\": \"最大FPS値に達しました\",\n    \"max_resolutions_reached\": \"最大解像度数に達しました\",\n    \"mdns_broadcast\": \"このコンピュータをローカルネットワークで見つける\",\n    \"mdns_broadcast_desc\": \"このオプションを有効にすると、Sunshineは自動的にこのコンピュータを見つけることができます。Moonlightもローカルネットワークでこのコンピュータを自動的に見つけるように設定する必要があります。\",\n    \"min_threads\": \"最小CPUスレッド数\",\n    \"min_threads_desc\": \"値を大きくするとエンコーディングの効率はわずかに低下しますが、通常はエンコーディングにCPUコアをより多く使用する価値があります。 理想的な値は、ハードウェア上の希望のストリーミング設定で確実にエンコードできる最小値です。\",\n    \"minimum_fps_target\": \"最小 FPS ターゲット\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"その他のオプション\",\n    \"motion_as_ds4\": \"クライアントのゲームパッドがモーションセンサーが存在することを報告する場合、DS4ゲームパッドをエミュレートします\",\n    \"motion_as_ds4_desc\": \"無効にすると、モーションセンサーはゲームパッドの種類選択中に考慮されません。\",\n    \"mouse\": \"マウス入力を有効にする\",\n    \"mouse_desc\": \"ゲストがマウスでホストシステムを制御できるようにします\",\n    \"native_pen_touch\": \"Native Pen/Touch サポート\",\n    \"native_pen_touch_desc\": \"有効にすると、SunshineはMoonlightクライアントからネイティブのペン/タッチイベントを通過します。これはネイティブのペン/タッチサポートがない古いアプリケーションでは無効にするのに便利です。\",\n    \"no_fps\": \"FPS値が追加されていません\",\n    \"no_resolutions\": \"解像度が追加されていません\",\n    \"notify_pre_releases\": \"プレリリース通知\",\n    \"notify_pre_releases_desc\": \"Sunshineの新しいプレリリースバージョンを通知するかどうか\",\n    \"nvenc_h264_cavlc\": \"H.264よりCAVLCを優先する\",\n    \"nvenc_h264_cavlc_desc\": \"単純なエントロピーコーディング形式。CAVLCは同じ品質のために約10%のビットレートを必要とします。本当に古いデコードデバイスにのみ関係します。\",\n    \"nvenc_latency_over_power\": \"省電力よりもエンコーディングのレイテンシを低減したい場合\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine は、エンコーディングのレイテンシを低減するためにストリーミング中に最大GPU クロック速度を要求します。 これを無効にするとエンコード待ち時間が大幅に増加する可能性があるため、推奨されません。\",\n    \"nvenc_lookahead_depth\": \"先読み深度\",\n    \"nvenc_lookahead_depth_desc\": \"エンコード中に先読みするフレーム数（0-32）。先読みにより、動き予測とビットレート配分が改善され、特に複雑なシーンでの画質が向上します。値が高いほど画質は向上しますが、エンコード遅延が増加します。0に設定すると無効になります。NVENC SDK 13.0 (1202) 以上が必要です。\",\n    \"nvenc_lookahead_level\": \"先読みレベル\",\n    \"nvenc_lookahead_level_0\": \"レベル 0（最低品質、最速）\",\n    \"nvenc_lookahead_level_1\": \"レベル 1\",\n    \"nvenc_lookahead_level_2\": \"レベル 2\",\n    \"nvenc_lookahead_level_3\": \"レベル 3（最高品質、最遅）\",\n    \"nvenc_lookahead_level_autoselect\": \"自動選択（ドライバに最適レベルを選択させる）\",\n    \"nvenc_lookahead_level_desc\": \"先読み品質レベル。レベルが高いほど画質は向上しますが、パフォーマンスは低下します。このオプションは lookahead_depth が 0 より大きい場合のみ有効です。NVENC SDK 13.0 (1202) 以上が必要です。\",\n    \"nvenc_lookahead_level_disabled\": \"無効（レベル 0 と同じ）\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"DXGI上に現在のOpenGL/Vulkan\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshineは、DXGIの上に存在しない限り、フルフレームレートでフルスクリーンOpenGLとVulkanプログラムをキャプチャすることはできません。 これはシステム全体の設定であり、サンシャインプログラムの出口に戻ります。\",\n    \"nvenc_preset\": \"パフォーマンスプリセット\",\n    \"nvenc_preset_1\": \"(高速、デフォルト)\",\n    \"nvenc_preset_7\": \"(最も遅い)\",\n    \"nvenc_preset_desc\": \"数値が高いほど、符号化遅延の増加を犠牲にして圧縮(一定のビットレートでの品質)が向上します。 ネットワークまたはデコーダによって制限されている場合にのみ変更することをお勧めします, そうでなければ、ビットレートを増やすことによって、同様の効果を達成することができます.\",\n    \"nvenc_rate_control\": \"レート制御モード\",\n    \"nvenc_rate_control_cbr\": \"CBR (固定ビットレート) - 低遅延\",\n    \"nvenc_rate_control_desc\": \"レート制御モードを選択します。CBR（固定ビットレート）は、低遅延ストリーミング用に固定ビットレートを提供します。VBR（可変ビットレート）は、シーンの複雑さに応じてビットレートを変動させ、複雑なシーンでより良い品質を提供しますが、ビットレートは変動します。\",\n    \"nvenc_rate_control_vbr\": \"VBR (可変ビットレート) - 高品質\",\n    \"nvenc_realtime_hags\": \"ハードウェアアクセラレーションGPUスケジューリングでリアルタイム優先度を使用する\",\n    \"nvenc_realtime_hags_desc\": \"現在、NVIDIAドライバは、HAGSが有効で、リアルタイムプライオリティが使用され、VRAM使用率が最大に近い場合、エンコーダでフリーズすることがあります。このオプションを無効にすると、優先順位が高に下がり、GPUに大きな負荷がかかったときのキャプチャパフォーマンスの低下と引き換えに、フリーズを回避できます。\",\n    \"nvenc_spatial_aq\": \"Spatial AQ\",\n    \"nvenc_spatial_aq_desc\": \"より高いQP値をビデオのフラットリージョンに割り当てます。低ビットレートでストリーミングする際に有効にすることをお勧めします。\",\n    \"nvenc_spatial_aq_disabled\": \"Disabled (faster, default)\",\n    \"nvenc_spatial_aq_enabled\": \"Enabled (slower)\",\n    \"nvenc_split_encode\": \"分割フレームエンコーディング\",\n    \"nvenc_split_encode_desc\": \"Split the encoding of each video frame over multiple NVENC hardware units. Significantly reduces encoding latency with a marginal compression efficiency penalty. This option is ignored if your GPU has a singular NVENC unit.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Driver decides (default)\",\n    \"nvenc_split_encode_four_strips\": \"4ストリップ分割を強制 (4つ以上のNVENCエンジンが必要)\",\n    \"nvenc_split_encode_three_strips\": \"3ストリップ分割を強制 (3つ以上のNVENCエンジンが必要)\",\n    \"nvenc_split_encode_two_strips\": \"2ストリップ分割を強制 (2つ以上のNVENCエンジンが必要)\",\n    \"nvenc_target_quality\": \"ターゲット品質（VBR モード）\",\n    \"nvenc_target_quality_desc\": \"Target quality level for VBR mode (0-51 for H.264/HEVC, 0-63 for AV1). Lower values = higher quality. Set to 0 for automatic quality selection. Only used when rate control mode is VBR.\",\n    \"nvenc_temporal_aq\": \"時間的適応量子化 (Temporal AQ)\",\n    \"nvenc_temporal_aq_desc\": \"時間的適応量子化を有効にします。Temporal AQは時間軸での量子化を最適化し、ビットレート配分を改善し、動きのあるシーンの画質を向上させます。空間AQと併用し、先読み（lookahead_depth > 0）を有効にする必要があります。NVENC SDK 13.0 (1202) 以上が必要です。\",\n    \"nvenc_temporal_filter\": \"時間フィルター\",\n    \"nvenc_temporal_filter_4\": \"レベル 4（最大強度）\",\n    \"nvenc_temporal_filter_desc\": \"エンコード前に適用される時間フィルタリングの強度。時間フィルターはノイズを低減し、特に自然なコンテンツの圧縮効率を向上させます。レベルが高いほどノイズ低減効果は高くなりますが、わずかなぼやけが生じる可能性があります。NVENC SDK 13.0 (1202) 以上が必要です。注意: frameIntervalP >= 5 が必要で、zeroReorderDelay やステレオ MVC とは互換性がありません。\",\n    \"nvenc_temporal_filter_disabled\": \"無効（時間フィルタリングなし）\",\n    \"nvenc_twopass\": \"Two-passモード\",\n    \"nvenc_twopass_desc\": \"予備的なエンコードパスを追加します。これは、より多くのモーションベクトルを検出することができます。より良いフレーム全体でビットレートを分配し、より厳密にビットレート制限に従います。 これは時折ビットレートオーバーシュートやその後のパケット損失につながる可能性があるため、無効にすることは推奨されません。\",\n    \"nvenc_twopass_disabled\": \"無効 (高速、推奨されません)\",\n    \"nvenc_twopass_full_res\": \"フル解像度（低速）\",\n    \"nvenc_twopass_quarter_res\": \"クォーター解像度（速く、デフォルト）\",\n    \"nvenc_vbv_increase\": \"シングルフレーム VBV/HRD パーセンテージ増加\",\n    \"nvenc_vbv_increase_desc\": \"デフォルトでは、単一フレームVBV/HRDを使用しています。つまり、エンコードされたビデオフレームサイズは要求されたビットレートを要求されたフレームレートで割った値を超えないことが予想されます。 この制限を緩和することは有益であり、低レイテンシの可変ビットレートとして機能することができます。 ネットワークにビットレートのスパイクを処理するバッファヘッドルームがない場合、パケットロスを引き起こす可能性があります。 許容可能な最大値は400で、エンコードされたビデオフレームの上限サイズ制限の5倍に相当します。\",\n    \"origin_web_ui_allowed\": \"許可されたオリジンウェブUI\",\n    \"origin_web_ui_allowed_desc\": \"Web UIへのアクセスが拒否されていないリモートエンドポイントアドレスのオリジンです\",\n    \"origin_web_ui_allowed_lan\": \"LAN 内のユーザだけが Web UI にアクセスできます\",\n    \"origin_web_ui_allowed_pc\": \"ローカルホストのみがWebUIにアクセスできます\",\n    \"origin_web_ui_allowed_wan\": \"誰でもWeb UIにアクセスできます\",\n    \"output_name_desc_unix\": \"Sunshineの起動時には、検出されたディスプレイのリストが表示されます。注:括弧内のid値を使用する必要があります。\",\n    \"output_name_desc_windows\": \"キャプチャに使用するディスプレイを手動で指定します。未設定の場合、プライマリディスプレイをキャプチャします。 注意: 上記の GPU を指定した場合、この表示は GPU に接続する必要があります。次のコマンドを使用して適切な値を見つけることができます。\",\n    \"output_name_unix\": \"番号を表示\",\n    \"output_name_windows\": \"出力名\",\n    \"ping_timeout\": \"Pingのタイムアウト\",\n    \"ping_timeout_desc\": \"Moonlightがデータが止まってからストリームをシャットダウンするまで待機時間をミリ秒で指定\",\n    \"pkey\": \"プライベートキー\",\n    \"pkey_desc\": \"ウェブ UI とMoonlight クライアントのペアリングに使用される秘密鍵。互換性を確保するためには、RSA-2048 秘密鍵を使用する必要があります。\",\n    \"port\": \"ポート\",\n    \"port_alert_1\": \"1024以下のポートを使用することはできません!\",\n    \"port_alert_2\": \"65535以上のポートは利用できません!\",\n    \"port_desc\": \"Sunshineが使用するポートのファミリーを設定する\",\n    \"port_http_port_note\": \"Moonlight に接続するには、このポートを使用してください。\",\n    \"port_note\": \"メモ\",\n    \"port_port\": \"ポート\",\n    \"port_protocol\": \"Protocol\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Web UIをインターネットに公開することはセキュリティ上のリスクです! ご自身の責任で進めてください!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"量子化パラメータ\",\n    \"qp_desc\": \"デバイスによっては、Constant Bit Rateをサポートしていない可能性があります。これらのデバイスでは、QPが代わりに使用されます。値が高いほど圧縮が多くなりますが、品質が低下します。\",\n    \"qsv_coder\": \"QuickSync Coder (H264)\",\n    \"qsv_preset\": \"QuickSync Preset\",\n    \"qsv_preset_fast\": \"より速く (低品質)\",\n    \"qsv_preset_faster\": \"最速（低品質）\",\n    \"qsv_preset_medium\": \"ミディアム（デフォルト）\",\n    \"qsv_preset_slow\": \"遅い (良質)\",\n    \"qsv_preset_slower\": \"遅い (より良い品質)\",\n    \"qsv_preset_slowest\": \"最も遅い (最高品質)\",\n    \"qsv_preset_veryfast\": \"最速（低品質）\",\n    \"qsv_slow_hevc\": \"低速HEVCエンコーディングを許可する\",\n    \"qsv_slow_hevc_desc\": \"これにより、GPU 使用率の向上とパフォーマンスの低下を犠牲にして、古い Intel GPU での HEVC エンコーディングを有効にできます。\",\n    \"refresh_rate_change_automatic_windows\": \"Use FPS value provided by the client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Enter the refresh rate to be used\",\n    \"refresh_rate_change_manual_windows\": \"Use manually entered refresh rate\",\n    \"refresh_rate_change_no_operation_windows\": \"Disabled\",\n    \"refresh_rate_change_windows\": \"FPS change\",\n    \"res_fps_desc\": \"Sunshine が公告するディスプレイモード。Moonlight-nx（Switch）などの一部の Moonlight バージョンは、これらのリストに依存して、要求された解像度と FPS がサポートされていることを確認します。この設定は画面ストリームが Moonlight に送信される方法を変更しません。\",\n    \"resolution_change_automatic_windows\": \"Use resolution provided by the client\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_manual_windows\": \"Use manually entered resolution\",\n    \"resolution_change_no_operation_windows\": \"Disabled\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_windows\": \"Resolution change\",\n    \"resolutions\": \"公告された解像度\",\n    \"restart_note\": \"サンシャインは変更を適用するために再起動しています。\",\n    \"sleep_mode\": \"スリープモード\",\n    \"sleep_mode_away\": \"アウェイモード（ディスプレイオフ、即時復帰）\",\n    \"sleep_mode_desc\": \"クライアントがスリープコマンドを送信した時の動作を制御します。サスペンド(S3)：従来のスリープ、低消費電力ですがWOLで起こす必要があります。ハイバネート(S4)：ディスクに保存、超低消費電力。アウェイモード：ディスプレイをオフにしますがシステムは稼働し続け、即座に復帰可能 - ゲームストリーミングサーバーに最適です。\",\n    \"sleep_mode_hibernate\": \"ハイバネート（S4）\",\n    \"sleep_mode_suspend\": \"サスペンド（S3スリープ）\",\n    \"stream_audio\": \"音声ストリーミングを有効にする\",\n    \"stream_audio_desc\": \"このオプションを無効にすると、音声ストリーミングが停止します。\",\n    \"stream_mic\": \"マイクストリーミングを有効にする\",\n    \"stream_mic_desc\": \"このオプションを無効にすると、マイクストリーミングが停止します。\",\n    \"stream_mic_download_btn\": \"仮想マイクをダウンロード\",\n    \"stream_mic_download_confirm\": \"仮想マイクのダウンロードページにリダイレクトされます。続行しますか？\",\n    \"stream_mic_note\": \"この機能を使用するには仮想マイクのインストールが必要です\",\n    \"sunshine_name\": \"サンシャイン名\",\n    \"sunshine_name_desc\": \"Moonlight によって表示される名前。指定されていない場合は、PC のホスト名が使用されます\",\n    \"sw_preset\": \"SWプリセット\",\n    \"sw_preset_desc\": \"エンコード速度（エンコードフレーム/秒）と圧縮効率（ビットストリームのビット毎の品質）のトレードオフを最適化します。デフォルトは超高速です。\",\n    \"sw_preset_fast\": \"速い\",\n    \"sw_preset_faster\": \"より速く\",\n    \"sw_preset_medium\": \"medium\",\n    \"sw_preset_slow\": \"遅い\",\n    \"sw_preset_slower\": \"遅いです\",\n    \"sw_preset_superfast\": \"スーパーファスト（デフォルト）\",\n    \"sw_preset_ultrafast\": \"超高速\",\n    \"sw_preset_veryfast\": \"veryfast\",\n    \"sw_preset_veryslow\": \"veryslow\",\n    \"sw_tune\": \"SWチューン\",\n    \"sw_tune_animation\": \"アニメーションは漫画に適していますより高いデブロッキングや参照フレームを使っています\",\n    \"sw_tune_desc\": \"チューニングオプション。プリセットの後に適用されます。デフォルトはゼロになります。\",\n    \"sw_tune_fastdecode\": \"fastdecode -- 特定のフィルタを無効にすることでより高速なデコードが可能です\",\n    \"sw_tune_film\": \"フィルム-- 高品質の映画コンテンツに使用します。\",\n    \"sw_tune_grain\": \"穀物は、古くて粒状のフィルム素材に保存されています\",\n    \"sw_tune_stillimage\": \"スタイルはスライドショーのようなコンテンツに適しています\",\n    \"sw_tune_zerolatency\": \"zerolatency -- 高速なエンコーディングと低遅延ストリーミングに適しています (デフォルト)\",\n    \"system_tray\": \"システムトレイを有効にする\",\n    \"system_tray_desc\": \"システムトレイを有効にするかどうか。有効にすると、Sunshine はシステムトレイにアイコンを表示し、システムトレイから制御できます。\",\n    \"touchpad_as_ds4\": \"クライアントゲームパッドがタッチパッドが存在することを報告する場合、DS4ゲームパッドをエミュレートします\",\n    \"touchpad_as_ds4_desc\": \"無効にすると、ゲームパッドの種類選択中にタッチパッドの存在が考慮されません。\",\n    \"unsaved_changes_tooltip\": \"保存されていない変更があります。クリックして保存。\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"インターネット経由でストリーミングするポート転送を自動的に設定します\",\n    \"variable_refresh_rate\": \"可変リフレッシュレート (VRR)\",\n    \"variable_refresh_rate_desc\": \"VRR サポートのためにビデオストリームのフレームレートをレンダリングフレームレートに合わせることを許可します。有効にすると、新しいフレームが利用可能な場合のみエンコーディングが行われ、ストリームが実際のレンダリングフレームレートに従うことができます。\",\n    \"vdd_reuse_desc_windows\": \"有効にすると、すべてのクライアントが同じVDD（仮想ディスプレイデバイス）を共有します。無効の場合（デフォルト）、各クライアントは独自のVDDを取得します。クライアントの切り替えを高速化するにはこれを有効にしますが、すべてのクライアントが同じ表示設定を共有することに注意してください。\",\n    \"vdd_reuse_windows\": \"すべてのクライアントで同じVDDを再利用\",\n    \"virtual_display\": \"仮想ディスプレイ\",\n    \"virtual_mouse\": \"仮想マウスドライバー\",\n    \"virtual_mouse_desc\": \"有効にすると、SunshineはZako仮想マウスドライバー（インストール済みの場合）を使用してHIDレベルでマウス入力をシミュレートします。Raw Inputを使用するゲームがマウスイベントを受信できるようになります。無効またはドライバー未インストールの場合、SendInputにフォールバックします。\",\n    \"virtual_sink\": \"仮想オーディオシンク\",\n    \"virtual_sink_desc\": \"使用する仮想オーディオデバイスを手動で指定します。未設定の場合は、デバイスが自動的に選択されます。 自動デバイス選択を使用するには、このフィールドを空白のままにすることを強くお勧めします!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"仮想マウスドライバーをインストールしますか？\",\n    \"vmouse_confirm_uninstall\": \"仮想マウスドライバーをアンインストールしますか？\",\n    \"vmouse_install\": \"ドライバーをインストール\",\n    \"vmouse_installing\": \"インストール中...\",\n    \"vmouse_note\": \"仮想マウスドライバーは別途インストールが必要です。Sunshineコントロールパネルを使用してドライバーのインストールまたは管理を行ってください。\",\n    \"vmouse_refresh\": \"ステータスを更新\",\n    \"vmouse_status_installed\": \"インストール済み（非アクティブ）\",\n    \"vmouse_status_not_installed\": \"未インストール\",\n    \"vmouse_status_running\": \"実行中\",\n    \"vmouse_uninstall\": \"ドライバーをアンインストール\",\n    \"vmouse_uninstalling\": \"アンインストール中...\",\n    \"vt_coder\": \"VideoToolbox Coder\",\n    \"vt_realtime\": \"VideoToolbox リアルタイムエンコーディング\",\n    \"vt_software\": \"VideoToolbox ソフトウェアエンコーディング\",\n    \"vt_software_allowed\": \"許可\",\n    \"vt_software_forced\": \"強制的に\",\n    \"wan_encryption_mode\": \"WAN暗号化モード\",\n    \"wan_encryption_mode_1\": \"サポートされているクライアントで有効になっています（デフォルト）\",\n    \"wan_encryption_mode_2\": \"すべてのクライアントに必要です\",\n    \"wan_encryption_mode_desc\": \"これは、インターネット経由でストリーミングする際に暗号化がいつ使用されるかを決定します。特に強力なホストやクライアントでは、暗号化によりストリーミングパフォーマンスが低下します。\",\n    \"webhook_curl_command\": \"コマンド\",\n    \"webhook_curl_command_desc\": \"以下のコマンドをターミナルにコピーして、webhookが正常に動作するかテストしてください：\",\n    \"webhook_curl_copy_failed\": \"コピーに失敗しました。手動で選択してコピーしてください\",\n    \"webhook_enabled\": \"Webhook通知\",\n    \"webhook_enabled_desc\": \"有効にすると、Sunshineは指定されたWebhook URLにイベント通知を送信します\",\n    \"webhook_group\": \"Webhook通知設定\",\n    \"webhook_skip_ssl_verify\": \"SSL証明書検証をスキップ\",\n    \"webhook_skip_ssl_verify_desc\": \"HTTPS接続のSSL証明書検証をスキップ、テストまたは自己署名証明書のみ\",\n    \"webhook_test\": \"テスト\",\n    \"webhook_test_failed\": \"Webhookテスト失敗\",\n    \"webhook_test_failed_note\": \"注意：URLが正しいか確認するか、ブラウザのコンソールで詳細情報を確認してください。\",\n    \"webhook_test_success\": \"Webhookテスト成功！\",\n    \"webhook_test_success_cors_note\": \"注意：CORS制限により、サーバーの応答ステータスを確認できません。\\nリクエストは送信されました。webhookが正しく設定されている場合、メッセージは配信されているはずです。\\n\\n推奨：ブラウザの開発者ツールのNetworkタブでリクエストの詳細を確認してください。\",\n    \"webhook_test_url_required\": \"まずWebhook URLを入力してください\",\n    \"webhook_timeout\": \"リクエストタイムアウト\",\n    \"webhook_timeout_desc\": \"Webhookリクエストのタイムアウト時間（ミリ秒）、範囲100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"イベント通知を受信するURL、HTTP/HTTPSプロトコルをサポート\",\n    \"wgc_checking_mode\": \"確認中...\",\n    \"wgc_checking_running_mode\": \"実行モードを確認中...\",\n    \"wgc_control_panel_only\": \"この機能は Sunshine Control Panel でのみ利用可能です\",\n    \"wgc_mode_switch_failed\": \"モード切り替えに失敗しました\",\n    \"wgc_mode_switch_started\": \"モード切り替えを開始しました。UACプロンプトが表示された場合は、「はい」をクリックして確認してください。\",\n    \"wgc_service_mode_warning\": \"WGCキャプチャはユーザーモードで実行する必要があります。現在サービスモードで実行中の場合は、上のボタンをクリックしてユーザーモードに切り替えます。\",\n    \"wgc_switch_to_service_mode\": \"サービスモードに切り替え\",\n    \"wgc_switch_to_service_mode_tooltip\": \"現在ユーザーモードで実行中です。クリックしてサービスモードに切り替えます。\",\n    \"wgc_switch_to_user_mode\": \"ユーザーモードに切り替え\",\n    \"wgc_switch_to_user_mode_tooltip\": \"WGCキャプチャはユーザーモードで実行する必要があります。このボタンをクリックしてユーザーモードに切り替えます。\",\n    \"wgc_user_mode_available\": \"現在ユーザーモードで実行中です。WGCキャプチャが利用可能です。\",\n    \"window_title\": \"ウィンドウタイトル\",\n    \"window_title_desc\": \"キャプチャするウィンドウのタイトル（部分一致、大文字小文字区別なし）。空のままにすると、現在実行中のアプリケーション名が自動的に使用されます。\",\n    \"window_title_placeholder\": \"例: アプリケーション名\"\n  },\n  \"index\": {\n    \"description\": \"サンシャインはムーンライトのための自己ホストゲームストリームホストです。\",\n    \"download\": \"ダウンロード\",\n    \"installed_version_not_stable\": \"Sunshineのプレリリース版を実行しています。バグやその他の問題が発生する可能性があります。 問題が発生した場合は報告してください。Sunshineをより良いソフトウェアにしていただきありがとうございます！\",\n    \"loading_latest\": \"最新のリリースを読み込んでいます...\",\n    \"new_pre_release\": \"新しいプレリリースバージョンが利用可能です!\",\n    \"new_stable\": \"新しい安定版が利用可能です！\",\n    \"startup_errors\": \"<b>注意</b>Sunshineは起動時にこれらのエラーを検出しました。ストリーミングの前にこれらのエラーを修正することを<b>強くお勧め</b>します。\",\n    \"update_download_confirm\": \"ブラウザで更新のダウンロードページを開きます。続行しますか？\",\n    \"version_dirty\": \"Sunshineをより良いソフトウェアにしてくれてありがとうございます!\",\n    \"version_latest\": \"サンシャインの最新バージョンを実行しています\",\n    \"view_logs\": \"ログを表示\",\n    \"welcome\": \"こんにちは、サンシャイン！\"\n  },\n  \"navbar\": {\n    \"applications\": \"アプリケーション\",\n    \"configuration\": \"設定\",\n    \"home\": \"ホーム\",\n    \"password\": \"パスワードの変更\",\n    \"pin\": \"Pin\",\n    \"theme_auto\": \"自動\",\n    \"theme_dark\": \"ダーク\",\n    \"theme_light\": \"ライト\",\n    \"toggle_theme\": \"テーマ\",\n    \"troubleshoot\": \"トラブルシューティング\"\n  },\n  \"password\": {\n    \"confirm_password\": \"パスワードの確認\",\n    \"current_creds\": \"現在の資格情報\",\n    \"new_creds\": \"新しい資格情報\",\n    \"new_username_desc\": \"指定しない場合、ユーザー名は変更されません\",\n    \"password_change\": \"パスワードの変更\",\n    \"success_msg\": \"パスワードが正常に変更されました！このページはまもなくリロードされます。ブラウザーは新しい資格情報を要求します。\"\n  },\n  \"pin\": {\n    \"actions\": \"アクション\",\n    \"cancel_editing\": \"編集をキャンセル\",\n    \"client_name\": \"名前\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"削除を確認\",\n    \"delete_client\": \"クライアントを削除\",\n    \"delete_confirm_message\": \"<strong>{name}</strong>を削除してもよろしいですか？\",\n    \"delete_warning\": \"この操作は元に戻せません。\",\n    \"device_name\": \"端末名\",\n    \"device_size\": \"デバイスサイズ\",\n    \"device_size_info\": \"<strong>デバイスサイズ</strong>: クライアントデバイスの画面サイズタイプ（小 - スマートフォン、中 - タブレット、大 - TV）を設定し、ストリーミング体験とタッチ操作を最適化します。\",\n    \"device_size_large\": \"大 - TV\",\n    \"device_size_medium\": \"中 - タブレット\",\n    \"device_size_small\": \"小 - 電話\",\n    \"edit_client_settings\": \"クライアント設定を編集\",\n    \"hdr_profile\": \"HDRプロファイル\",\n    \"hdr_profile_info\": \"<strong>HDRプロファイル</strong>: このクライアントで使用するHDRカラープロファイル（ICCファイル）を選択し、デバイス上でHDRコンテンツが正しく表示されるようにします。最新のクライアントを使用している場合、ホスト仮想スクリーンへの輝度情報の自動同期をサポートしているため、自動同期を有効にするにはこのフィールドを空白のままにしてください。\",\n    \"loading\": \"読み込み中...\",\n    \"loading_clients\": \"クライアントを読み込み中...\",\n    \"modify_in_gui\": \"GUIで変更してください\",\n    \"none\": \"-- なし --\",\n    \"or_manual_pin\": \"またはPINを手動入力\",\n    \"pair_failure\": \"ペアリングに失敗しました：PINが正しく入力されたかどうかを確認します\",\n    \"pair_success\": \"成功！Moonlight を確認して続行してください\",\n    \"pin_pairing\": \"PINペアリング\",\n    \"qr_expires_in\": \"有効期限\",\n    \"qr_generate\": \"QRコードを生成\",\n    \"qr_paired_success\": \"ペアリング成功！\",\n    \"qr_pairing\": \"QRコードペアリング\",\n    \"qr_pairing_desc\": \"QRコードを生成して素早くペアリングします。Moonlightクライアントでスキャンすると自動的にペアリングされます。\",\n    \"qr_pairing_warning\": \"実験的な機能です。ペアリングに失敗した場合は、下の手動PINペアリングを使用してください。注意：この機能はLAN内でのみ動作します。\",\n    \"qr_refresh\": \"QRコードを更新\",\n    \"remove_paired_devices_desc\": \"ペアリングされたデバイスを削除します。\",\n    \"save_changes\": \"変更を保存\",\n    \"save_failed\": \"クライアント設定の保存に失敗しました。もう一度お試しください。\",\n    \"save_or_cancel_first\": \"まず編集を保存またはキャンセルしてください\",\n    \"send\": \"送信\",\n    \"unknown_client\": \"不明なクライアント\",\n    \"unpair_all_confirm\": \"すべてのクライアントのペアリングを解除してもよろしいですか？この操作は元に戻せません。\",\n    \"unsaved_changes\": \"保存されていない変更\",\n    \"warning_msg\": \"ペアリングするクライアントにアクセスできることを確認してください。このソフトウェアは、あなたのコンピュータを完全にコントロールすることができますので、注意してください！\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android おすすめ\",\n    \"client_downloads\": \"クライアントダウンロード\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"GitHub Discussions\",\n    \"gpl_license_text_1\": \"このソフトウェアは GPL-3.0 の下でライセンスされています。自由に使用、変更、配布することができます。\",\n    \"gpl_license_text_2\": \"オープンソースエコシステムを保護するために、GPL-3.0 ライセンスに違反するソフトウェアの使用は避けてください。\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"コミュニティに参加\",\n    \"join_group_desc\": \"ヘルプを受けたり経験を共有\",\n    \"legal\": \"Legal\",\n    \"legal_desc\": \"このソフトウェアの使用を継続することにより、以下のドキュメントの利用規約に同意したことになります。\",\n    \"license\": \"ライセンス\",\n    \"lizardbyte_website\": \"LizardByte ウェブサイト\",\n    \"official_website\": \"公式サイト\",\n    \"official_website_title\": \"AlkaidLab - 公式サイト\",\n    \"open_source\": \"オープンソース\",\n    \"open_source_desc\": \"Star & Fork でプロジェクトを支援\",\n    \"quick_start\": \"クイックスタート\",\n    \"resources\": \"リソース\",\n    \"resources_desc\": \"Sunshineのための資源！\",\n    \"third_party_desc\": \"サードパーティコンポーネント通知\",\n    \"third_party_moonlight\": \"フレンドリーリンク\",\n    \"third_party_notice\": \"第三者通知\",\n    \"tutorial\": \"チュートリアル\",\n    \"tutorial_desc\": \"詳細な設定と使用ガイド\",\n    \"view_license\": \"完全なライセンスを表示\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"仮想ディスプレイ\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"リポジトリへ移動\",\n    \"harmony_modal_desc\": \"HarmonyOS NEXT Moonlight はHarmonyOSストアで Moonlight V+ を検索してください\",\n    \"harmony_modal_link_notice\": \"このリンクはプロジェクトリポジトリにリダイレクトされます\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"秒\",\n    \"restart_desc\": \"設定が保存されました。Sunshineはディスプレイ設定を適用するために再起動しています。\",\n    \"restart_go_now\": \"今すぐ移動\",\n    \"restart_title\": \"Sunshineを再起動中\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"基本設定が有効になりました。Moonlightクライアントですぐにストリーミングを開始できます！\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"ベースディスプレイ（VDD）はSunshine Foundation内蔵のスマート仮想ディスプレイで、任意の解像度・フレームレート・HDR最適化をサポートします。画面オフストリーミングや拡張ディスプレイストリーミングに最適です。\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"アクティブ化を確認\",\n    \"step3_ensure_active_desc\": \"ディスプレイがアクティブでない場合はアクティブにします\",\n    \"step3_ensure_only_display\": \"唯一のディスプレイを確認\",\n    \"step3_ensure_only_display_desc\": \"他のすべてのディスプレイを無効にし、指定されたディスプレイのみを有効にします（推奨）\",\n    \"step3_ensure_primary\": \"プライマリを確認\",\n    \"step3_ensure_primary_desc\": \"ディスプレイをアクティブにし、プライマリディスプレイとして設定します\",\n    \"step3_ensure_secondary\": \"セカンダリストリーミング\",\n    \"step3_ensure_secondary_desc\": \"仮想ディスプレイのみを使用してセカンダリ拡張ストリーミングを行います\",\n    \"step3_no_operation\": \"操作なし\",\n    \"step3_no_operation_desc\": \"ディスプレイの状態を変更しません。ユーザーがディスプレイの準備を確認する必要があります\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Advanced\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Config Files\",\n    \"general\": \"General\",\n    \"input\": \"Input\",\n    \"network\": \"Network\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"分析中...\",\n    \"ai_analyzing_logs\": \"ログを分析中、お待ちください...\",\n    \"ai_config\": \"AI設定\",\n    \"ai_copy_result\": \"コピー\",\n    \"ai_diagnosis\": \"AI診断\",\n    \"ai_diagnosis_title\": \"AIログ診断\",\n    \"ai_error\": \"分析失敗\",\n    \"ai_key_local\": \"APIキーはローカルにのみ保存され、アップロードされません\",\n    \"ai_model\": \"モデル\",\n    \"ai_provider\": \"プロバイダー\",\n    \"ai_reanalyze\": \"再分析\",\n    \"ai_result\": \"診断結果\",\n    \"ai_retry\": \"再試行\",\n    \"ai_start_diagnosis\": \"診断を開始\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Sunshineをすぐにシャットダウンする必要がある場合は、この機能を使用できます。シャットダウン後は手動で再起動する必要があることに注意してください。\",\n    \"boom_sunshine_success\": \"Sunshineがシャットダウンされました\",\n    \"confirm_boom\": \"本当に終了しますか？\",\n    \"confirm_boom_desc\": \"本当に終了したいですか？まあ、止めることはできません。続けてクリックしてください\",\n    \"confirm_logout\": \"Confirm logout?\",\n    \"confirm_logout_desc\": \"You will need to enter your password again to access the web UI.\",\n    \"copy_config\": \"設定をコピー\",\n    \"copy_config_error\": \"設定のコピーに失敗しました\",\n    \"copy_config_success\": \"設定がクリップボードにコピーされました！\",\n    \"copy_logs\": \"ログをコピー\",\n    \"download_logs\": \"ログをダウンロード\",\n    \"force_close\": \"強制閉じる\",\n    \"force_close_desc\": \"Moonlight が現在実行中のアプリについて不満がある場合、強制終了すると問題が修正されます。\",\n    \"force_close_error\": \"アプリケーションを終了中にエラー\",\n    \"force_close_success\": \"申請は正常に終了しました！\",\n    \"ignore_case\": \"大文字と小文字を区別しない\",\n    \"logout\": \"ログアウト\",\n    \"logout_desc\": \"ログアウトします。再度ログインが必要になる場合があります。\",\n    \"logout_localhost_tip\": \"現在の環境ではログインが不要です。ログアウトしても認証プロンプトは表示されません。\",\n    \"logs\": \"ログ\",\n    \"logs_desc\": \"Sunshineによってアップロードされたログを参照してください\",\n    \"logs_find\": \"検索...\",\n    \"match_contains\": \"含む\",\n    \"match_exact\": \"完全に一致\",\n    \"match_regex\": \"正規表現\",\n    \"reopen_setup_wizard\": \"セットアップウィザードを再度開く\",\n    \"reopen_setup_wizard_desc\": \"セットアップウィザードページを再度開いて、初期設定を再構成できます。\",\n    \"reopen_setup_wizard_error\": \"セットアップウィザードの再開に失敗しました\",\n    \"reset_display_device_desc_windows\": \"Sunshine が変更されたディスプレイデバイスの設定を復元しようとしてスタックしている場合、設定をリセットして手動でディスプレイの状態を復元できます。\\nこれは様々な理由で発生する可能性があります：デバイスが利用できなくなった、別のポートに接続されたなど。\",\n    \"reset_display_device_error_windows\": \"持久化のリセット中にエラーが発生しました！\",\n    \"reset_display_device_success_windows\": \"持久化のリセットに成功しました！\",\n    \"reset_display_device_windows\": \"ディスプレイメモリのリセット\",\n    \"restart_sunshine\": \"サンシャインを再起動\",\n    \"restart_sunshine_desc\": \"Sunshineが正常に動作していない場合は、再起動を試みることができます。実行中のセッションはすべて終了します。\",\n    \"restart_sunshine_success\": \"サンシャインが再起動しています\",\n    \"troubleshooting\": \"トラブルシューティング\",\n    \"unpair_all\": \"すべてのペアリングを解除\",\n    \"unpair_all_error\": \"ペアリング解除中のエラー\",\n    \"unpair_all_success\": \"ペアを解除しました！\",\n    \"unpair_desc\": \"ペアリングされたデバイスを削除します。アクティブなセッションを持つペアリングされていないデバイスは、接続されたままですが、セッションを開始または再開することはできません。\",\n    \"unpair_single_no_devices\": \"ペアリングされたデバイスがありません。\",\n    \"unpair_single_success\": \"ただし、デバイスはまだアクティブなセッションにいる可能性があります。上の「強制終了」ボタンを使用して、開いているセッションを終了します。\",\n    \"unpair_single_unknown\": \"不明なクライアント\",\n    \"unpair_title\": \"デバイスのペアリングを解除\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"パスワードの確認\",\n    \"create_creds\": \"始める前に、Web UI にアクセスするための新しいユーザー名とパスワードを作成する必要があります。\",\n    \"create_creds_alert\": \"以下の資格情報は、SunshineのWeb UIにアクセスするために必要です。あなたが二度と見ることはありませんので、安全に保管してください！\",\n    \"creds_local_only\": \"認証情報はローカルにオフラインで保存され、サーバーにアップロードされることはありません。\",\n    \"error\": \"エラー！\",\n    \"greeting\": \"Sunshine Foundation へようこそ！\",\n    \"hide_password\": \"パスワードを隠す\",\n    \"login\": \"ログイン\",\n    \"network_error\": \"ネットワークエラー、接続を確認してください\",\n    \"password\": \"パスワード\",\n    \"password_match\": \"パスワードが一致しました\",\n    \"password_mismatch\": \"パスワードが一致しません\",\n    \"server_error\": \"サーバーエラー\",\n    \"show_password\": \"パスワードを表示\",\n    \"success\": \"成功！\",\n    \"username\": \"ユーザー名\",\n    \"welcome_success\": \"このページはまもなく再読み込みされます。ブラウザーは新しい資格情報を要求します。\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/ko.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"적용\",\n    \"auto\": \"자동 설정\",\n    \"autodetect\": \"자동 감지 (권장)\",\n    \"beta\": \"(베타)\",\n    \"cancel\": \"취소\",\n    \"close\": \"닫기\",\n    \"copied\": \"클립보드에 복사됨\",\n    \"copy\": \"복사\",\n    \"delete\": \"삭제\",\n    \"description\": \"설명\",\n    \"disabled\": \"비활성화\",\n    \"disabled_def\": \"사용 안 함(기본값)\",\n    \"dismiss\": \"무시\",\n    \"do_cmd\": \"명령 수행\",\n    \"download\": \"다운로드\",\n    \"edit\": \"편집\",\n    \"elevated\": \"관리자 권한으로 실행\",\n    \"enabled\": \"활성화\",\n    \"enabled_def\": \"활성화 (기본값)\",\n    \"error\": \"오류!\",\n    \"no_changes\": \"변경 사항 없음\",\n    \"note\": \"참고:\",\n    \"password\": \"비밀번호\",\n    \"remove\": \"제거\",\n    \"run_as\": \"관리자 권한으로 실행\",\n    \"save\": \"저장\",\n    \"see_more\": \"자세히 보기\",\n    \"success\": \"성공!\",\n    \"undo_cmd\": \"명령 실행 취소\",\n    \"username\": \"사용자명\",\n    \"warning\": \"경고!\"\n  },\n  \"apps\": {\n    \"actions\": \"작업\",\n    \"add_cmds\": \"명령어 추가\",\n    \"add_new\": \"신규 추가\",\n    \"advanced_options\": \"고급 옵션\",\n    \"app_name\": \"애플리케이션 이름\",\n    \"app_name_desc\": \"Moonlight에 표시될 애플리케이션 이름\",\n    \"applications_desc\": \"클라이언트를 다시 시작할 때 애플리케이션이 새로 고침 됩니다.\",\n    \"applications_title\": \"애플리케이션\",\n    \"auto_detach\": \"애플리케이션이 빠르게 종료되는 경우에도 스트리밍 계속하기\",\n    \"auto_detach_desc\": \"다른 프로그램이나 인스턴스를 실행한 후 빠르게 종료되는 런처형 앱을 자동으로 감지하려고 시도합니다. 런처형 앱이 감지되면 해당 앱은 분리된 앱으로 처리됩니다.\",\n    \"basic_info\": \"기본 정보\",\n    \"cmd\": \"명령\",\n    \"cmd_desc\": \"시작할 기본 애플리케이션입니다. 비어 있으면 애플리케이션이 시작되지 않습니다.\",\n    \"cmd_examples_title\": \"일반적인 예:\",\n    \"cmd_note\": \"명령 실행 파일의 경로에 공백이 포함되어 있으면 따옴표로 묶어야 합니다.\",\n    \"cmd_prep_desc\": \"이 애플리케이션 전/후에 실행할 명령 목록입니다. 준비 명령 중 하나라도 실패하면 애플리케이션 시작이 중단됩니다.\",\n    \"cmd_prep_name\": \"명령 준비\",\n    \"command_settings\": \"명령 설정\",\n    \"covers_found\": \"커버 발견\",\n    \"delete\": \"삭제\",\n    \"delete_confirm\": \"\\\"{name}\\\"을(를) 삭제하시겠습니까?\",\n    \"detached_cmds\": \"분리된 명령\",\n    \"detached_cmds_add\": \"분리된 명령 추가\",\n    \"detached_cmds_desc\": \"백그라운드에서 실행할 명령어 입니다.\",\n    \"detached_cmds_note\": \"명령 실행 파일의 경로에 공백이 포함되어 있으면 따옴표로 묶어야 합니다.\",\n    \"detached_cmds_remove\": \"분리된 명령 제거\",\n    \"edit\": \"편집\",\n    \"env_app_id\": \"앱 ID\",\n    \"env_app_name\": \"앱 이름\",\n    \"env_client_audio_config\": \"클라이언트가 요청한 오디오 구성(2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"클라이언트가 최적의 스트리밍을 위해 게임 최적화 옵션을 요청했습니다(참/거짓).\",\n    \"env_client_fps\": \"클라이언트가 요청한 FPS(int)\",\n    \"env_client_gcmap\": \"요청된 게임패드 마스크, 비트셋/비트필드 형식(int)\",\n    \"env_client_hdr\": \"클라이언트가 HDR을 활성화합니다(참/거짓).\",\n    \"env_client_height\": \"클라이언트가 요청한 높이(int)\",\n    \"env_client_host_audio\": \"클라이언트가 호스트 오디오를 요청했습니다(참/거짓).\",\n    \"env_client_name\": \"클라이언트 표시 이름 (문자열)\",\n    \"env_client_width\": \"클라이언트가 요청한 너비(int)\",\n    \"env_displayplacer_example\": \"예시) 해상도 자동화를 위한 Displayplacer\",\n    \"env_qres_example\": \"예 - 해결 자동화를 위한 QR:\",\n    \"env_qres_path\": \"Qres 경로\",\n    \"env_var_name\": \"변수 이름\",\n    \"env_vars_about\": \"환경 변수 정보\",\n    \"env_vars_desc\": \"모든 명령은 기본적으로 이러한 환경 변수를 가져옵니다:\",\n    \"env_xrandr_example\": \"예시 - 해상도 자동화를 위한 Xrandr:\",\n    \"exit_timeout\": \"종료 시간 초과\",\n    \"exit_timeout_desc\": \"종료 요청 시 모든 앱 프로세스가 정상적으로 종료될 때까지 기다릴 시간(초)입니다. 설정하지 않으면 기본값은 최대 5초까지 대기하는 것입니다. 0 또는 음수 값으로 설정하면 앱이 즉시 종료됩니다.\",\n    \"file_selector_not_initialized\": \"파일 선택기가 초기화되지 않았습니다\",\n    \"find_cover\": \"표지 찾기\",\n    \"form_invalid\": \"필수 필드를 확인하세요\",\n    \"form_valid\": \"유효한 애플리케이션\",\n    \"global_prep_desc\": \"이 애플리케이션에 대한 글로벌 준비 명령 실행을 활성화/비활성화합니다.\",\n    \"global_prep_name\": \"글로벌 준비 명령\",\n    \"image\": \"이미지\",\n    \"image_desc\": \"클라이언트로 전송할 애플리케이션 아이콘/사진/이미지 경로입니다. 이미지는 PNG 파일이어야 합니다. 설정하지 않으면 Sunshine은 기본 상자 이미지를 전송합니다.\",\n    \"image_settings\": \"이미지 설정\",\n    \"loading\": \"로드 중...\",\n    \"menu_cmd_actions\": \"작업\",\n    \"menu_cmd_add\": \"메뉴 명령 추가\",\n    \"menu_cmd_command\": \"명령\",\n    \"menu_cmd_desc\": \"설정 후 이러한 명령은 클라이언트의 반환 메뉴에 표시되어 스트림을 중단하지 않고 특정 작업을 빠르게 실행할 수 있습니다. 예: 도우미 프로그램 시작.\\n예: 표시 이름 - 컴퓨터 종료; 명령 - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"표시 이름\",\n    \"menu_cmd_drag_sort\": \"드래그하여 정렬\",\n    \"menu_cmd_name\": \"메뉴 명령\",\n    \"menu_cmd_placeholder_command\": \"명령\",\n    \"menu_cmd_placeholder_display_name\": \"표시 이름\",\n    \"menu_cmd_placeholder_execute\": \"명령 실행\",\n    \"menu_cmd_placeholder_undo\": \"명령 실행 취소\",\n    \"menu_cmd_remove_menu\": \"메뉴 명령 제거\",\n    \"menu_cmd_remove_prep\": \"준비 명령 제거\",\n    \"mouse_mode\": \"마우스 모드\",\n    \"mouse_mode_auto\": \"자동 (전역 설정)\",\n    \"mouse_mode_desc\": \"이 애플리케이션의 마우스 입력 방법을 선택합니다. 자동은 전역 설정을 사용하고, 가상 마우스는 HID 드라이버를 사용하며, SendInput은 Windows API를 사용합니다.\",\n    \"mouse_mode_sendinput\": \"SendInput (Windows API)\",\n    \"mouse_mode_vmouse\": \"가상 마우스\",\n    \"name\": \"이름\",\n    \"output_desc\": \"명령의 출력이 저장되는 파일로, 지정하지 않으면 출력이 무시됩니다.\",\n    \"output_name\": \"출력\",\n    \"run_as_desc\": \"이는 제대로 실행하려면 관리자 권한이 필요한 일부 애플리케이션에 필요할 수 있습니다.\",\n    \"scan_result_add_all\": \"모두 추가\",\n    \"scan_result_edit_title\": \"추가 및 편집\",\n    \"scan_result_filter_all\": \"전체\",\n    \"scan_result_filter_epic_title\": \"Epic Games 게임\",\n    \"scan_result_filter_executable\": \"실행 파일\",\n    \"scan_result_filter_executable_title\": \"실행 파일\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxy 게임\",\n    \"scan_result_filter_script\": \"스크립트\",\n    \"scan_result_filter_script_title\": \"배치/명령 스크립트\",\n    \"scan_result_filter_shortcut\": \"바로 가기\",\n    \"scan_result_filter_shortcut_title\": \"바로 가기\",\n    \"scan_result_filter_steam_title\": \"Steam 게임\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"게임\",\n    \"scan_result_games_only\": \"게임만\",\n    \"scan_result_matched\": \"일치: {count}\",\n    \"scan_result_no_apps\": \"추가할 애플리케이션을 찾을 수 없습니다\",\n    \"scan_result_no_matches\": \"일치하는 애플리케이션을 찾을 수 없습니다\",\n    \"scan_result_quick_add_title\": \"빠른 추가\",\n    \"scan_result_remove_title\": \"목록에서 제거\",\n    \"scan_result_search_placeholder\": \"애플리케이션 이름, 명령 또는 경로 검색...\",\n    \"scan_result_show_all\": \"모두 표시\",\n    \"scan_result_title\": \"스캔 결과\",\n    \"scan_result_try_different_keywords\": \"다른 검색 키워드를 사용해 보세요\",\n    \"scan_result_type_batch\": \"배치\",\n    \"scan_result_type_command\": \"명령 스크립트\",\n    \"scan_result_type_executable\": \"실행 파일\",\n    \"scan_result_type_shortcut\": \"바로 가기\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"애플리케이션 검색...\",\n    \"select\": \"선택\",\n    \"test_menu_cmd\": \"명령 테스트\",\n    \"test_menu_cmd_empty\": \"명령은 비어 있을 수 없습니다\",\n    \"test_menu_cmd_executing\": \"명령 실행 중...\",\n    \"test_menu_cmd_failed\": \"명령 실행 실패\",\n    \"test_menu_cmd_success\": \"명령이 성공적으로 실행되었습니다!\",\n    \"use_desktop_image\": \"현재 데스크톱 배경 화면 사용\",\n    \"wait_all\": \"모든 앱 프로세스가 종료될 때까지 스트리밍을 계속합니다.\",\n    \"wait_all_desc\": \"앱에서 시작한 모든 프로세스가 종료될 때까지 스트리밍이 계속됩니다. 이 옵션을 선택하지 않으면 다른 앱 프로세스가 계속 실행 중이더라도 초기 앱 프로세스가 종료되면 스트리밍이 중지됩니다.\",\n    \"working_dir\": \"작업 디렉토리\",\n    \"working_dir_desc\": \"프로세스에 전달할 작업 디렉터리입니다. 예를 들어 일부 애플리케이션은 작업 디렉터리를 사용하여 구성 파일을 검색합니다. 이 옵션을 설정하지 않으면 기본적으로 Sunshine은 다음 명령의 상위 디렉터리로 설정됩니다.\"\n  },\n  \"config\": {\n    \"adapter_name\": \"어댑터 이름\",\n    \"adapter_name_desc_linux_1\": \"캡처에 사용할 GPU를 수동으로 지정합니다.\",\n    \"adapter_name_desc_linux_2\": \"를 검색하여 VAAPI를 지원하는 모든 장치를 찾습니다.\",\n    \"adapter_name_desc_linux_3\": \"'renderD129'를 위의 장치로 바꾸면 장치의 이름과 기능이 나열됩니다. Sunshine에서 지원하려면 최소한 이 기능이 있어야 합니다:\",\n    \"adapter_name_desc_windows\": \"캡처에 사용할 GPU를 수동으로 지정합니다. 설정하지 않으면 GPU가 자동으로 선택됩니다. 참고: 이 GPU는 디스플레이가 연결되어 있고 전원이 켜져 있어야 합니다. 노트북에서 직접 GPU 출력을 활성화할 수 없는 경우 자동으로 설정하세요.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"가상 디스플레이의 최신 버전이 설치되어 있으면 GPU 바인딩과 자동으로 연결할 수 있습니다\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"추가\",\n    \"address_family\": \"주소 가족\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Sunshine이 사용하는 주소 계열 설정\",\n    \"address_family_ipv4\": \"IPv4 전용\",\n    \"always_send_scancodes\": \"항상 스캔 코드 보내기\",\n    \"always_send_scancodes_desc\": \"스캔코드를 전송하면 게임 및 앱과의 호환성이 향상되지만 미국식 영어 키보드 레이아웃을 사용하지 않는 특정 클라이언트에서 키보드 입력이 잘못될 수 있습니다. 특정 애플리케이션에서 키보드 입력이 전혀 작동하지 않는 경우 활성화합니다. 클라이언트의 키가 호스트에서 잘못된 입력을 생성하는 경우 비활성화합니다.\",\n    \"amd_coder\": \"AMF 코더(H264)\",\n    \"amd_coder_desc\": \"엔트로피 인코딩을 선택하여 품질 또는 인코딩 속도의 우선순위를 정할 수 있습니다. H.264만 해당.\",\n    \"amd_enforce_hrd\": \"AMF 가상 참조 디코더(HRD) 시행\",\n    \"amd_enforce_hrd_desc\": \"HRD 모델 요구 사항을 충족하기 위해 속도 제어에 대한 제약 조건을 높입니다. 이렇게 하면 비트레이트 오버플로가 크게 줄어들지만 특정 카드에서 인코딩 아티팩트 또는 품질 저하가 발생할 수 있습니다.\",\n    \"amd_preanalysis\": \"AMF 사전 분석\",\n    \"amd_preanalysis_desc\": \"이렇게 하면 속도 제어 사전 분석이 가능하므로 인코딩 지연 시간이 증가하는 대신 품질이 향상될 수 있습니다.\",\n    \"amd_quality\": \"AMF 품질\",\n    \"amd_quality_balanced\": \"균형 -- 균형 (기본값)\",\n    \"amd_quality_desc\": \"인코딩 속도와 품질 간의 균형을 제어합니다.\",\n    \"amd_quality_group\": \"AMF 품질 설정\",\n    \"amd_quality_quality\": \"품질 - 품질 선호\",\n    \"amd_quality_speed\": \"속도 - 속도 선호\",\n    \"amd_qvbr_quality\": \"AMF QVBR 품질 레벨\",\n    \"amd_qvbr_quality_desc\": \"QVBR 레이트 제어 모드의 품질 레벨. 범위: 1-51 (낮을수록 고품질). 기본값: 23. 레이트 제어가 'qvbr'로 설정된 경우에만 적용됩니다.\",\n    \"amd_rc\": \"AMF 비율 제어\",\n    \"amd_rc_cbr\": \"cbr - 일정한 비트레이트(HRD가 활성화된 경우 권장)\",\n    \"amd_rc_cqp\": \"CQP -- 상수 QP 모드\",\n    \"amd_rc_desc\": \"이는 클라이언트 비트레이트 목표를 초과하지 않도록 비트레이트 제어 방법을 제어합니다. 'cqp'는 비트레이트 타겟팅에 적합하지 않으며, 'vbr_latency' 이외의 다른 옵션은 비트레이트 오버플로를 제한하는 데 도움이 되는 HRD 적용에 의존합니다.\",\n    \"amd_rc_group\": \"AMF 비율 제어 설정\",\n    \"amd_rc_hqcbr\": \"hqcbr -- 고품질 고정 비트레이트\",\n    \"amd_rc_hqvbr\": \"hqvbr -- 고품질 가변 비트레이트\",\n    \"amd_rc_qvbr\": \"qvbr -- 품질 가변 비트레이트 (QVBR 품질 레벨 사용)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency - 지연 시간 제한 가변 비트 전송률(HRD가 비활성화된 경우 권장, 기본값)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak - 피크 제한 가변 비트 전송률\",\n    \"amd_usage\": \"AMF 사용법\",\n    \"amd_usage_desc\": \"기본 인코딩 프로필을 설정합니다. 아래에 제시된 모든 옵션은 사용 프로필의 하위 집합을 재정의하지만 다른 곳에서는 구성할 수 없는 숨겨진 설정이 추가로 적용됩니다.\",\n    \"amd_usage_lowlatency\": \"낮은 지연 시간 - 낮은 지연 시간(가장 빠름)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - 낮은 지연 시간, 높은 품질(빠른)\",\n    \"amd_usage_transcoding\": \"트랜스코딩 -- 트랜스코딩(가장 느림)\",\n    \"amd_usage_ultralowlatency\": \"초저지연 - 초저지연(가장 빠름, 기본값)\",\n    \"amd_usage_webcam\": \"웹캠 -- 웹캠(느림)\",\n    \"amd_vbaq\": \"AMF 분산 기반 적응형 양자화(VBAQ)\",\n    \"amd_vbaq_desc\": \"인간의 시각 시스템은 일반적으로 텍스처가 심한 영역의 아티팩트에 덜 민감합니다. VBAQ 모드에서는 픽셀 분산이 공간 텍스처의 복잡도를 나타내는 데 사용되므로 인코더가 더 부드러운 영역에 더 많은 비트를 할당할 수 있습니다. 이 기능을 활성화하면 일부 콘텐츠에서 주관적인 화질이 개선됩니다.\",\n    \"amf_draw_mouse_cursor\": \"AMF 캡처 방식 사용 시 간단한 커서 그리기\",\n    \"amf_draw_mouse_cursor_desc\": \"일부 경우 AMF 캐편를 사용하면 마우스 포인터가 표시되지 않습니다. 이 옵션을 활성화하면 화면에 간단한 마우스 포인터가 그려집니다. 참고: 마우스 포인터의 위치는 콘텐츠 화면이 업데이트될 때만 업데이트되므로, 데스크톱 등의 비게임 시나리오에서는 마우스 포인터 움직임이 느리게 보일 수 있습니다.\",\n    \"apply_note\": \"'적용'을 클릭하여 설정을 적용합니다. Sunshine이 다시 시작되며 연결된 모든 세션이 종료됩니다.\",\n    \"audio_sink\": \"오디오 싱크\",\n    \"audio_sink_desc_linux\": \"오디오 루프백에 사용되는 오디오 싱크의 이름입니다. 이 변수를 지정하지 않으면 pulseaudio가 기본 모니터 장치를 선택합니다. 오디오 싱크의 이름은 다음 명령을 사용하여 찾을 수 있습니다:\",\n    \"audio_sink_desc_macos\": \"오디오 루프백에 사용되는 오디오 싱크의 이름입니다. Sunshine은 시스템 제한으로 인해 macOS에서만 마이크에 액세스할 수 있습니다. 사운드플라워 또는 블랙홀을 사용하여 시스템 오디오를 스트리밍하려면.\",\n    \"audio_sink_desc_windows\": \"캡처할 특정 오디오 장치를 수동으로 지정합니다. 설정하지 않으면 장치가 자동으로 선택됩니다. 자동 장치 선택을 사용하려면 이 필드를 비워 두는 것이 좋습니다! 동일한 이름의 오디오 장치가 여러 개 있는 경우 다음 명령을 사용하여 장치 ID를 얻을 수 있습니다:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"스피커(고화질 오디오 장치)\",\n    \"av1_mode\": \"AV1 지원\",\n    \"av1_mode_0\": \"Sunshine은 인코더 기능에 따라 AV1 지원을 광고합니다(권장).\",\n    \"av1_mode_1\": \"Sunshine은 AV1에 대한 지원을 광고하지 않습니다.\",\n    \"av1_mode_2\": \"Sunshine은 AV1 메인 8비트 프로파일 지원을 광고합니다.\",\n    \"av1_mode_3\": \"Sunshine은 AV1 메인 8비트 및 10비트(HDR) 프로파일 지원을 광고할 예정입니다.\",\n    \"av1_mode_desc\": \"클라이언트가 AV1 메인 8비트 또는 10비트 비디오 스트림을 요청할 수 있습니다. AV1은 인코딩 시 CPU를 더 많이 사용하므로 이 옵션을 활성화하면 소프트웨어 인코딩을 사용할 때 성능이 저하될 수 있습니다.\",\n    \"back_button_timeout\": \"홈/가이드 버튼 에뮬레이션 시간 초과\",\n    \"back_button_timeout_desc\": \"뒤로/선택 버튼을 지정된 밀리초 동안 누르고 있으면 홈/가이드 버튼 누름이 에뮬레이션됩니다. 값을 0 미만(기본값)으로 설정하면 뒤로/선택 버튼을 길게 눌러도 홈/가이드 버튼이 에뮬레이션되지 않습니다.\",\n    \"bind_address\": \"바인드 주소 (테스트 기능)\",\n    \"bind_address_desc\": \"Sunshine이 바인드할 특정 IP 주소를 설정합니다. 비어 있으면 Sunshine는 모든 사용 가능한 주소에 바인드됩니다.\",\n    \"capture\": \"특정 캡처 방법 강제 적용\",\n    \"capture_desc\": \"자동 모드에서 Sunshine은 가장 먼저 작동하는 것을 사용합니다. NvFBC에는 패치된 엔비디아 드라이버가 필요합니다.\",\n    \"capture_target\": \"캡처 대상\",\n    \"capture_target_desc\": \"캡처할 대상 유형을 선택합니다. '창'을 선택할 때 AI 프레임 보간 소프트웨어와 같은 특정 애플리케이션 창을 전체 화면 대신 캡처할 수 있습니다.\",\n    \"capture_target_display\": \"디스플레이\",\n    \"capture_target_window\": \"창\",\n    \"cert\": \"인증서\",\n    \"cert_desc\": \"웹 UI와 Moonlight 클라이언트 페어링에 사용되는 인증서입니다. 최상의 호환성을 위해 RSA-2048 공개 키가 있어야 합니다.\",\n    \"channels\": \"최대 연결 클라이언트 수\",\n    \"channels_desc_1\": \"Sunshine을 사용하면 하나의 스트리밍 세션을 여러 클라이언트와 동시에 공유할 수 있습니다.\",\n    \"channels_desc_2\": \"일부 하드웨어 인코더에는 다중 스트림에서 성능을 저하시키는 제한이 있을 수 있습니다.\",\n    \"close_verify_safe\": \"안전한 검증 호환 이전 클라이언트\",\n    \"close_verify_safe_desc\": \"이전 클라이언트는 Sunshine에 연결할 수 없을 수 있습니다. 이 옵션을 비활성화하거나 클라이언트를 업데이트하세요\",\n    \"coder_cabac\": \"카박 - 컨텍스트 적응형 이진 산술 코딩 - 더 높은 품질\",\n    \"coder_cavlc\": \"cavlc - 컨텍스트 적응형 가변 길이 코딩 - 더 빠른 디코딩\",\n    \"configuration\": \"설정\",\n    \"controller\": \"게임패드 입력 활성화\",\n    \"controller_desc\": \"게스트가 게임패드/컨트롤러로 호스트 시스템을 제어할 수 있습니다.\",\n    \"credentials_file\": \"자격 증명 파일\",\n    \"credentials_file_desc\": \"사용자 이름/비밀번호를 Sunshine의 상태 파일과 별도로 저장하세요.\",\n    \"display_device_options_note_desc_windows\": \"Windows는 현재 활성화된 디스플레이의 각 조합에 대해 다양한 디스플레이 설정을 저장합니다.\\nSunshine은 해당 디스플레이 조합에 속하는 디스플레이에 변경 사항을 적용합니다.\\nSunshine이 설정을 적용했을 때 활성화되어 있던 장치의 연결을 해제하면, Sunshine이 변경 사항을 되돌리려 할 때까지\\n해당 조합을 다시 활성화할 수 없는 경우 변경 사항을 되돌릴 수 없습니다!\",\n    \"display_device_options_note_windows\": \"설정 적용 방식에 대한 참고 사항\",\n    \"display_device_options_windows\": \"디스플레이 장치 옵션\",\n    \"display_device_prep_ensure_active_desc_windows\": \"디스플레이가 활성화되지 않은 경우 활성화합니다\",\n    \"display_device_prep_ensure_active_windows\": \"디스플레이를 자동으로 활성화\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"다른 모든 디스플레이를 비활성화하고 지정된 디스플레이만 활성화합니다\",\n    \"display_device_prep_ensure_only_display_windows\": \"다른 디스플레이를 비활성화하고 지정된 디스플레이만 활성화\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"디스플레이를 활성화하고 기본 디스플레이로 설정합니다\",\n    \"display_device_prep_ensure_primary_windows\": \"디스플레이를 자동으로 활성화하고 기본 디스플레이로 설정\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"가상 디스플레이만 사용하여 보조 확장 스트리밍을 수행합니다\",\n    \"display_device_prep_ensure_secondary_windows\": \"보조 디스플레이 스트리밍 (가상 디스플레이 전용)\",\n    \"display_device_prep_no_operation_desc_windows\": \"디스플레이 상태를 변경하지 않습니다. 사용자가 직접 준비해야 합니다\",\n    \"display_device_prep_no_operation_windows\": \"비활성화됨\",\n    \"display_device_prep_windows\": \"디스플레이 준비\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"\\\"수신\\\" 및 \\\"최종\\\" 값을 각각 하나 이상 지정해야 합니다.\\n\\\"수신\\\" 섹션의 빈 필드는 \\\"모든 값과 일치\\\"를 의미합니다. \\\"최종\\\" 섹션의 빈 필드는 \\\"수신 값 유지\\\"를 의미합니다.\\n원하는 경우 특정 FPS 값을 특정 해상도에 매칭할 수 있습니다...\\n\\n참고: Moonlight 클라이언트에서 \\\"게임 설정 최적화\\\" 옵션이 활성화되지 않은 경우, 해상도 값이 포함된 행은 무시됩니다.\",\n    \"display_mode_remapping_desc_windows\": \"특정 해상도 및/또는 주사율을 다른 값으로 다시 매핑하는 방법을 지정합니다.\\n낮은 해상도로 스트리밍하면서 호스트에서 더 높은 해상도로 렌더링하여 슈퍼샘플링 효과를 얻을 수 있습니다.\\n또는 더 높은 FPS로 스트리밍하면서 호스트의 주사율을 낮게 제한할 수 있습니다.\\n매칭은 위에서 아래로 수행됩니다. 항목이 매칭되면 다른 항목은 더 이상 확인되지 않지만 여전히 유효성 검사가 수행됩니다.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"최종 주사율\",\n    \"display_mode_remapping_final_resolution_windows\": \"최종 해상도\",\n    \"display_mode_remapping_optional\": \"선택 사항\",\n    \"display_mode_remapping_received_fps_windows\": \"수신 FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"수신 해상도\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"참고: Moonlight 클라이언트에서 \\\"게임 설정 최적화\\\" 옵션이 활성화되지 않은 경우, 다시 매핑이 비활성화됩니다.\",\n    \"display_mode_remapping_windows\": \"디스플레이 모드 다시 매핑\",\n    \"display_modes\": \"디스플레이 모드\",\n    \"ds4_back_as_touchpad_click\": \"지도 뒤로 가기/터치패드 클릭으로 선택\",\n    \"ds4_back_as_touchpad_click_desc\": \"DS4 에뮬레이션을 강제할 때 뒤로/선택을 터치패드 클릭에 매핑합니다.\",\n    \"dsu_server_port\": \"DSU Server Port\",\n    \"dsu_server_port_desc\": \"DSU server listening port (default 26760). Sunshine will act as a DSU server to receive client connections and send motion data. Enable DSU server in your client(Yuzu,Ryujinx etc.) and set DSU server address(127.0.0.1) and port(26760)\",\n    \"enable_dsu_server\": \"DSU 서버 활성화\",\n    \"enable_dsu_server_desc\": \"Enable DSU server to receive client connections and send motion data\",\n    \"encoder\": \"특정 인코더 강제 적용\",\n    \"encoder_desc\": \"특정 인코더를 강제로 지정하지 않으면 Sunshine에서 사용 가능한 최상의 옵션을 선택합니다. 참고: Windows에서 하드웨어 인코더를 지정하는 경우 디스플레이가 연결된 GPU와 일치해야 합니다.\",\n    \"encoder_software\": \"소프트웨어\",\n    \"experimental\": \"실험적\",\n    \"experimental_features\": \"실험적 기능\",\n    \"external_ip\": \"외부 IP\",\n    \"external_ip_desc\": \"외부 IP 주소가 지정되지 않은 경우 Sunshine은 자동으로 외부 IP를 감지합니다.\",\n    \"fec_percentage\": \"FEC 백분율\",\n    \"fec_percentage_desc\": \"각 비디오 프레임의 데이터 패킷당 오류를 수정하는 패킷의 백분율입니다. 값이 높을수록 더 많은 네트워크 패킷 손실을 보정할 수 있지만 대역폭 사용량이 증가합니다.\",\n    \"ffmpeg_auto\": \"자동 -- FFMPEG 결정에 맡김(기본값)\",\n    \"file_apps\": \"앱 파일\",\n    \"file_apps_desc\": \"현재 Sunshine의 앱이 저장되어 있는 파일입니다.\",\n    \"file_state\": \"상태 파일\",\n    \"file_state_desc\": \"Sunshine의 현재 상태가 저장된 파일입니다.\",\n    \"fps\": \"광고된 FPS\",\n    \"gamepad\": \"에뮬레이트된 게임패드 유형\",\n    \"gamepad_auto\": \"자동 선택 옵션\",\n    \"gamepad_desc\": \"호스트에서 에뮬레이트할 게임패드 유형을 선택합니다.\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 선택 옵션\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"DS4 수동 옵션\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"명령 준비\",\n    \"global_prep_cmd_desc\": \"애플리케이션 실행 전 또는 실행 후에 실행할 명령 목록을 구성합니다. 지정된 준비 명령 중 하나라도 실패하면 애플리케이션 실행 프로세스가 중단됩니다.\",\n    \"hdr_luminance_analysis\": \"HDR 동적 메타데이터 (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"프레임별 GPU 휘도 분석을 활성화하고 HDR10+ (ST 2094-40) 및 HDR Vivid (CUVA) 동적 메타데이터를 인코딩된 비트스트림에 주입합니다. 지원 디스플레이에 프레임별 톤 매핑 힌트를 제공합니다. 고해상도에서 약간의 GPU 오버헤드(~0.5-1.5ms/프레임)가 추가됩니다. HDR 활성화 시 프레임 레이트 저하가 발생하면 비활성화하세요.\",\n    \"hdr_prep_automatic_windows\": \"Switch on/off the HDR mode as requested by the client\",\n    \"hdr_prep_no_operation_windows\": \"Disabled\",\n    \"hdr_prep_windows\": \"HDR state change\",\n    \"hevc_mode\": \"HEVC 지원\",\n    \"hevc_mode_0\": \"Sunshine은 인코더 기능에 따라 HEVC 지원을 광고합니다(권장).\",\n    \"hevc_mode_1\": \"Sunshine은 HEVC 지원을 광고하지 않습니다.\",\n    \"hevc_mode_2\": \"Sunshine은 HEVC 메인 프로필에 대한 지원을 광고합니다\",\n    \"hevc_mode_3\": \"Sunshine은 HEVC 메인 및 메인10(HDR) 프로파일 지원을 광고할 예정입니다\",\n    \"hevc_mode_desc\": \"클라이언트가 HEVC 메인 또는 HEVC 메인10 비디오 스트림을 요청할 수 있도록 허용합니다. HEVC는 인코딩에 CPU를 더 많이 사용하므로 이 옵션을 활성화하면 소프트웨어 인코딩을 사용할 때 성능이 저하될 수 있습니다.\",\n    \"high_resolution_scrolling\": \"고해상도 스크롤 지원\",\n    \"high_resolution_scrolling_desc\": \"이 옵션을 활성화하면 Sunshine은 Moonlight 클라이언트의 고해상도 스크롤 이벤트를 통과합니다. 고해상도 스크롤 이벤트로 너무 빠르게 스크롤하는 구형 애플리케이션의 경우 이 옵션을 비활성화하면 유용할 수 있습니다.\",\n    \"install_steam_audio_drivers\": \"Steam 오디오 드라이버 설치\",\n    \"install_steam_audio_drivers_desc\": \"Steam이 설치되어 있으면 5.1/7.1 서라운드 사운드와 호스트 오디오 음소거를 지원하는 Steam Streaming Speakers 드라이버가 자동으로 설치됩니다. (수동으로 설치할 일이 없어요)\",\n    \"key_repeat_delay\": \"키 반복 지연\",\n    \"key_repeat_delay_desc\": \"키가 반복되는 속도를 제어합니다. 키가 반복되기 전 초기 지연 시간 (ms) 입니다.\",\n    \"key_repeat_frequency\": \"키 반복 빈도\",\n    \"key_repeat_frequency_desc\": \"매 초마다 키가 반복되는 빈도입니다. (이 옵션은 소수점 입력이 가능합니다.)\",\n    \"key_rightalt_to_key_win\": \"오른쪽 Alt 키를 Windows 키로 매핑\",\n    \"key_rightalt_to_key_win_desc\": \"달빛에서 Windows 키를 직접 보낼 수 없는 경우가 있을 수 있습니다. 이러한 경우 Sunshine이 오른쪽 Alt 키를 Windows 키로 인식하도록 하는 것이 유용할 수 있습니다.\",\n    \"key_rightalt_to_key_windows\": \"Map Right Alt key to Windows key\",\n    \"keyboard\": \"키보드 입력 활성화\",\n    \"keyboard_desc\": \"게스트가 키보드로 호스트 시스템을 제어할 수 있습니다.\",\n    \"lan_encryption_mode\": \"LAN 암호화 모드\",\n    \"lan_encryption_mode_1\": \"지원되는 클라이언트에서 사용 가능\",\n    \"lan_encryption_mode_2\": \"모든 사용자에게 필수\",\n    \"lan_encryption_mode_desc\": \"로컬 네트워크를 통해 스트리밍할 때 암호화를 사용할 시기를 결정합니다. 암호화는 특히 성능이 낮은 호스트와 클라이언트에서 스트리밍 성능을 저하시킬 수 있습니다.\",\n    \"locale\": \"언어\",\n    \"locale_desc\": \"Sunshine의 사용자 인터페이스에 사용 및 표시되는 언어입니다.\",\n    \"log_level\": \"로그 레벨\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"Info\",\n    \"log_level_3\": \"Warning\",\n    \"log_level_4\": \"Error\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"None\",\n    \"log_level_desc\": \"표준 출력으로 인쇄되는 최소 로그 수준\",\n    \"log_path\": \"로그 파일 경로\",\n    \"log_path_desc\": \"Sunshine의 현재 로그가 저장된 파일입니다.\",\n    \"max_bitrate\": \"최대 비트\",\n    \"max_bitrate_desc\": \"Sunshine이 스트림을 인코딩할 최대 비트 전송률(Kbps)입니다. 0으로 설정하면 항상 Moonlight에서 요청한 비트레이트를 사용합니다.\",\n    \"max_fps_reached\": \"최대 FPS 값에 도달했습니다\",\n    \"max_resolutions_reached\": \"최대 해상도 수에 도달했습니다\",\n    \"mdns_broadcast\": \"이 컴퓨터를 로컬 네트워크에서 찾기\",\n    \"mdns_broadcast_desc\": \"이 옵션을 활성화하면 Sunshine이 자동으로 이 컴퓨터를 찾을 수 있도록 합니다. Moonlight도 로컬 네트워크에서 이 컴퓨터를 자동으로 찾도록 설정해야 합니다.\",\n    \"min_threads\": \"최소 CPU 스레드 수\",\n    \"min_threads_desc\": \"이 값을 높이면 인코딩 효율이 약간 떨어지지만, 일반적으로 인코딩에 더 많은 CPU 코어를 사용할 수 있다는 점에서 그만한 가치가 있습니다. 이상적인 값은 하드웨어에서 원하는 스트리밍 설정으로 안정적으로 인코딩할 수 있는 가장 낮은 값입니다.\",\n    \"minimum_fps_target\": \"최소 FPS 목표\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"기타 옵션\",\n    \"motion_as_ds4\": \"클라이언트의 게임패드가 모션 기능을 보유중인 경우 DS4 게임패드로 에뮬레이트\",\n    \"motion_as_ds4_desc\": \"비활성화시, 게임패드를 선택할 때 모션 센서 유무를 확인하지 않습니다.\",\n    \"mouse\": \"마우스 입력 활성화\",\n    \"mouse_desc\": \"게스트가 마우스로 호스트 시스템을 제어할 수 있습니다.\",\n    \"native_pen_touch\": \"기본 펜/터치 지원\",\n    \"native_pen_touch_desc\": \"활성화하면 Sunshine은 Moonlight 클라이언트의 기본 펜/터치 이벤트를 통과합니다. 이 기능은 기본 펜/터치를 지원하지 않는 구형 애플리케이션에서 비활성화하면 유용할 수 있습니다.\",\n    \"no_fps\": \"FPS 값이 추가되지 않았습니다\",\n    \"no_resolutions\": \"해상도가 추가되지 않았습니다\",\n    \"notify_pre_releases\": \"사전 출시 알림\",\n    \"notify_pre_releases_desc\": \"Sunshine의 새 사전 출시 버전에 대한 알림 여부입니다.\",\n    \"nvenc_h264_cavlc\": \"H.264에서 CABAC보다 CAVLC 선호\",\n    \"nvenc_h264_cavlc_desc\": \"더 간단한 형태의 엔트로피 코딩. CAVLC는 동일한 화질을 위해 약 10% 더 많은 비트 전송률이 필요합니다. 아주 오래된 디코딩 장치에만 해당됩니다.\",\n    \"nvenc_latency_over_power\": \"전력 절감보다 낮은 인코딩 지연 시간 선호\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine은 인코딩 지연 시간을 줄이기 위해 스트리밍 중에 최대 GPU 클럭 속도를 요청합니다. 이 기능을 비활성화하면 인코딩 지연 시간이 크게 늘어날 수 있으므로 비활성화하는 것은 권장하지 않습니다.\",\n    \"nvenc_lookahead_depth\": \"Lookahead 깊이\",\n    \"nvenc_lookahead_depth_desc\": \"인코딩 중 미리 볼 프레임 수(0-32). Lookahead는 특히 복잡한 장면에서 더 나은 모션 추정 및 비트레이트 분배를 제공하여 인코딩 품질을 향상시킵니다. 값이 높을수록 품질은 향상되지만 인코딩 지연 시간이 늘어납니다. Lookahead를 비활성화하려면 0으로 설정하십시오. NVENC SDK 13.0 (1202) 이상이 필요합니다.\",\n    \"nvenc_lookahead_level\": \"Lookahead 레벨\",\n    \"nvenc_lookahead_level_0\": \"레벨 0 (최저 품질, 가장 빠름)\",\n    \"nvenc_lookahead_level_1\": \"레벨 1\",\n    \"nvenc_lookahead_level_2\": \"레벨 2\",\n    \"nvenc_lookahead_level_3\": \"레벨 3 (최고 품질, 가장 느림)\",\n    \"nvenc_lookahead_level_autoselect\": \"자동 선택 (드라이버가 최적의 레벨 선택)\",\n    \"nvenc_lookahead_level_desc\": \"Lookahead 품질 레벨. 레벨이 높을수록 품질은 향상되지만 성능은 저하됩니다. 이 옵션은 lookahead_depth가 0보다 클 때만 적용됩니다. NVENC SDK 13.0 (1202) 이상이 필요합니다.\",\n    \"nvenc_lookahead_level_disabled\": \"비활성화됨 (레벨 0과 동일)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"DXGI 위에 OpenGL/Vulkan 제공\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine은 DXGI 위에 표시되지 않는 한 전체 화면 OpenGL 및 Vulkan 프로그램을 풀 프레임 속도로 캡처할 수 없습니다. 이는 시스템 전체 설정으로, Sunshine 프로그램 종료 시 되돌려집니다.\",\n    \"nvenc_preset\": \"성능 프리셋\",\n    \"nvenc_preset_1\": \"(가장 빠름, 기본값)\",\n    \"nvenc_preset_7\": \"(가장 느림)\",\n    \"nvenc_preset_desc\": \"수치가 높을수록 인코딩 지연 시간이 증가하는 대신 압축(주어진 비트 전송률에서의 품질)이 향상됩니다. 네트워크 또는 디코더에 의해 제한되는 경우에만 변경하는 것이 좋으며, 그렇지 않은 경우 비트 전송률을 높여도 비슷한 효과를 얻을 수 있습니다.\",\n    \"nvenc_rate_control\": \"비트레이트 제어 모드\",\n    \"nvenc_rate_control_cbr\": \"CBR (고정 비트레이트) - 낮은 지연 시간\",\n    \"nvenc_rate_control_desc\": \"비트레이트 제어 모드를 선택합니다. CBR(고정 비트레이트)은 낮은 지연 시간 스트리밍을 위해 고정된 비트레이트를 제공합니다. VBR(가변 비트레이트)은 장면 복잡도에 따라 비트레이트가 변동하여, 가변 비트레이트의 비용으로 복잡한 장면에서 더 나은 품질을 제공합니다.\",\n    \"nvenc_rate_control_vbr\": \"VBR (가변 비트레이트) - 더 나은 품질\",\n    \"nvenc_realtime_hags\": \"하드웨어 가속 GPU 스케줄링에서 실시간 우선순위 사용\",\n    \"nvenc_realtime_hags_desc\": \"현재 NVIDIA 드라이버는 HAGS가 활성화되어 있고 실시간 우선순위가 사용되며 VRAM 사용률이 최대에 가까울 때 인코더에서 멈출 수 있습니다. 이 옵션을 비활성화하면 우선순위가 높음으로 낮아져 GPU가 과부하 상태일 때 캡처 성능이 저하되는 대신 프리즈를 방지할 수 있습니다.\",\n    \"nvenc_spatial_aq\": \"공간 AQ\",\n    \"nvenc_spatial_aq_desc\": \"동영상의 평평한 영역에 더 높은 QP 값을 할당합니다. 낮은 비트레이트에서 스트리밍할 때 활성화하는 것이 좋습니다.\",\n    \"nvenc_spatial_aq_disabled\": \"Disabled (faster, default)\",\n    \"nvenc_spatial_aq_enabled\": \"Enabled (slower)\",\n    \"nvenc_split_encode\": \"분할 프레임 인코딩\",\n    \"nvenc_split_encode_desc\": \"Split the encoding of each video frame over multiple NVENC hardware units. Significantly reduces encoding latency with a marginal compression efficiency penalty. This option is ignored if your GPU has a singular NVENC unit.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Driver decides (default)\",\n    \"nvenc_split_encode_four_strips\": \"4-스트립 분할 강제 (4개 이상의 NVENC 엔진 필요)\",\n    \"nvenc_split_encode_three_strips\": \"3-스트립 분할 강제 (3개 이상의 NVENC 엔진 필요)\",\n    \"nvenc_split_encode_two_strips\": \"2-스트립 분할 강제 (2개 이상의 NVENC 엔진 필요)\",\n    \"nvenc_target_quality\": \"목표 품질 (VBR 모드)\",\n    \"nvenc_target_quality_desc\": \"Target quality level for VBR mode (0-51 for H.264/HEVC, 0-63 for AV1). Lower values = higher quality. Set to 0 for automatic quality selection. Only used when rate control mode is VBR.\",\n    \"nvenc_temporal_aq\": \"시간적 적응형 양자화 (Temporal AQ)\",\n    \"nvenc_temporal_aq_desc\": \"시간적 적응형 양자화를 활성화합니다. Temporal AQ는 시간 차원에서 양자화를 최적화하여 더 나은 비트레이트 분배를 제공하고 움직임이 많은 장면의 품질을 향상시킵니다. 공간 AQ와 함께 작동하며 Lookahead(lookahead_depth > 0)가 활성화되어 있어야 합니다. NVENC SDK 13.0 (1202) 이상이 필요합니다.\",\n    \"nvenc_temporal_filter\": \"시간적 필터\",\n    \"nvenc_temporal_filter_4\": \"레벨 4 (최대 강도)\",\n    \"nvenc_temporal_filter_desc\": \"인코딩 전에 적용되는 시간적 필터링 강도. 시간적 필터는 노이즈를 줄이고 압축 효율성을 향상시킵니다(특히 자연스러운 콘텐츠의 경우). 레벨이 높을수록 노이즈 감소 효과는 좋지만 약간의 흐림 현상이 발생할 수 있습니다. NVENC SDK 13.0 (1202) 이상이 필요합니다. 참고: frameIntervalP >= 5가 필요하며 zeroReorderDelay 또는 스테레오 MVC와 호환되지 않습니다.\",\n    \"nvenc_temporal_filter_disabled\": \"비활성화됨 (시간적 필터링 없음)\",\n    \"nvenc_twopass\": \"투패스 모드\",\n    \"nvenc_twopass_desc\": \"예비 인코딩 패스를 추가합니다. 이를 통해 더 많은 모션 벡터를 감지하고 프레임 전체에 비트 전송률을 더 잘 분배하며 비트 전송률 제한을 더 엄격하게 준수할 수 있습니다. 이 기능을 비활성화하면 가끔 비트레이트 오버슈팅과 그에 따른 패킷 손실이 발생할 수 있으므로 비활성화하는 것은 권장하지 않습니다.\",\n    \"nvenc_twopass_disabled\": \"사용 안 함(가장 빠름, 권장하지 않음)\",\n    \"nvenc_twopass_full_res\": \"전체 해상도(느림)\",\n    \"nvenc_twopass_quarter_res\": \"분기 해상도(더 빠른, 기본값)\",\n    \"nvenc_vbv_increase\": \"싱글 프레임 VBV/HRD 비율 증가\",\n    \"nvenc_vbv_increase_desc\": \"기본적으로 Sunshine은 단일 프레임 VBV/HRD를 사용하므로 인코딩된 동영상 프레임 크기가 요청된 비트레이트를 요청된 프레임 전송률로 나눈 값을 초과하지 않아야 합니다. 이 제한을 완화하면 지연 시간이 짧은 가변 비트레이트의 이점이 있지만 네트워크에 비트레이트 급증을 처리할 수 있는 버퍼 헤드룸이 없는 경우 패킷 손실이 발생할 수도 있습니다. 허용되는 최대 값은 400이며, 이는 인코딩된 동영상 프레임 상한 크기를 5배 늘린 것에 해당합니다.\",\n    \"origin_web_ui_allowed\": \"웹 UI 접근 권한\",\n    \"origin_web_ui_allowed_desc\": \"웹 UI에 대한 액세스가 거부되지 않은 원격 엔드포인트 주소의 원본입니다.\",\n    \"origin_web_ui_allowed_lan\": \"같은 LAN에 있는 사용자만 접근 허용\",\n    \"origin_web_ui_allowed_pc\": \"로컬 호스트만 접근 허용\",\n    \"origin_web_ui_allowed_wan\": \"누구든지 접근 허용 (보안이 취약해질 수 있습니다.)\",\n    \"output_name_desc_unix\": \"Sunshine이 시작되면 감지된 디스플레이 목록이 표시됩니다. 참고: 괄호 안의 id 값을 사용해야 합니다. 아래는 예시이며, 실제 출력은 문제 해결 탭에서 확인할 수 있습니다.\",\n    \"output_name_desc_windows\": \"캡처에 사용할 디스플레이 디바이스 ID를 수동으로 지정합니다. 설정하지 않으면 기본 디스플레이가 캡처됩니다. 참고: 위에서 GPU를 지정한 경우 이 디스플레이는 해당 GPU에 연결되어 있어야 합니다. Sunshine이 시작되면 감지된 디스플레이 목록이 표시됩니다. 아래는 예시이며, 실제 출력은 문제 해결 탭에서 확인할 수 있습니다.\",\n    \"output_name_unix\": \"표시 번호\",\n    \"output_name_windows\": \"장치 ID 표시\",\n    \"ping_timeout\": \"핑 시간 초과\",\n    \"ping_timeout_desc\": \"스트림을 종료하기 전에 달빛의 데이터를 대기하는 시간(밀리초)\",\n    \"pkey\": \"개인 키\",\n    \"pkey_desc\": \"웹 UI와 Moonlight 클라이언트 페어링에 사용되는 개인 키입니다. 최상의 호환성을 위해 RSA-2048 개인키를 사용해야 합니다.\",\n    \"port\": \"포트\",\n    \"port_alert_1\": \"Sunshine은 1024 이하의 포트를 사용할 수 없습니다, 다시 확인하세요.\",\n    \"port_alert_2\": \"65535 이상의 포트는 사용할 수 없습니다, 다시 확인하세요.\",\n    \"port_desc\": \"Sunshine에서 사용하는 포트 제품군 설정\",\n    \"port_http_port_note\": \"이 포트를 사용하여 Moonlight로 연결할 수 있습니다.\",\n    \"port_note\": \"참고\",\n    \"port_port\": \"포트\",\n    \"port_protocol\": \"프로토콜\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"웹 UI를 '인터넷'에 노출하는 것은 보안상 \\\"매우\\\" 위험합니다! 다시 한번 생각하고 진행하세요.\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"양자화 파라미터\",\n    \"qp_desc\": \"일부 디바이스는 고정 비트 전송률을 지원하지 않을 수 있습니다. 이러한 디바이스에서는 대신 QP가 사용됩니다. 값이 높을수록 압축률이 높아지지만 품질은 떨어집니다.\",\n    \"qsv_coder\": \"퀵싱크 코더(H264)\",\n    \"qsv_preset\": \"퀵싱크 프리셋\",\n    \"qsv_preset_fast\": \"빠름 (낮은 품질)\",\n    \"qsv_preset_faster\": \"더 빠름 (더 낮은 품질)\",\n    \"qsv_preset_medium\": \"중간 (기본값)\",\n    \"qsv_preset_slow\": \"느림 (좋은 품질)\",\n    \"qsv_preset_slower\": \"더 느림 (더 좋은 품질)\",\n    \"qsv_preset_slowest\": \"가장 느림 (최고 품질)\",\n    \"qsv_preset_veryfast\": \"가장 빠름 (최저 품질)\",\n    \"qsv_slow_hevc\": \"느린 HEVC 인코딩 허용\",\n    \"qsv_slow_hevc_desc\": \"이렇게 하면 구형 인텔 GPU에서 HEVC 인코딩이 가능하지만, GPU 사용량이 증가하고 성능이 저하될 수 있습니다.\",\n    \"refresh_rate_change_automatic_windows\": \"Use FPS value provided by the client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Enter the refresh rate to be used\",\n    \"refresh_rate_change_manual_windows\": \"Use manually entered refresh rate\",\n    \"refresh_rate_change_no_operation_windows\": \"Disabled\",\n    \"refresh_rate_change_windows\": \"FPS change\",\n    \"res_fps_desc\": \"Sunshine이 광고하는 디스플레이 모드입니다. Moonlight-nx(Switch) 등 일부 Moonlight 버전은 이 목록을 사용하여 요청된 해상도와 FPS가 지원되는지 확인합니다. 이 설정은 화면 스트림이 Moonlight로 전송되는 방식을 변경하지 않습니다.\",\n    \"resolution_change_automatic_windows\": \"Use resolution provided by the client\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_manual_windows\": \"Use manually entered resolution\",\n    \"resolution_change_no_operation_windows\": \"Disabled\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_windows\": \"Resolution change\",\n    \"resolutions\": \"광고된 해상도\",\n    \"restart_note\": \"변경 사항을 적용하기 위해 Sunshine이 다시 시작됩니다.\",\n    \"sleep_mode\": \"절전 모드\",\n    \"sleep_mode_away\": \"자리 비움 모드 (디스플레이 끄기, 즉시 깨우기)\",\n    \"sleep_mode_desc\": \"클라이언트가 절전 명령을 보낼 때의 동작을 제어합니다. 대기 모드(S3): 기존 절전, 저전력이지만 WOL로 깨워야 합니다. 최대 절전(S4): 디스크에 저장, 매우 저전력. 자리 비움 모드: 디스플레이는 꺼지지만 시스템은 계속 실행되어 즉시 깨울 수 있습니다 - 게임 스트리밍 서버에 이상적입니다.\",\n    \"sleep_mode_hibernate\": \"최대 절전 (S4)\",\n    \"sleep_mode_suspend\": \"대기 모드 (S3 절전)\",\n    \"stream_audio\": \"오디오 스트리밍 활성화\",\n    \"stream_audio_desc\": \"이 옵션을 비활성화하면 오디오 스트리밍이 중지됩니다.\",\n    \"stream_mic\": \"마이크 스트리밍 활성화\",\n    \"stream_mic_desc\": \"이 옵션을 비활성화하면 마이크 스트리밍이 중지됩니다.\",\n    \"stream_mic_download_btn\": \"가상 마이크 다운로드\",\n    \"stream_mic_download_confirm\": \"가상 마이크 다운로드 페이지로 이동합니다. 계속하시겠습니까?\",\n    \"stream_mic_note\": \"이 기능을 사용하려면 가상 마이크를 설치해야 합니다\",\n    \"sunshine_name\": \"Sunshine 이름\",\n    \"sunshine_name_desc\": \"Moonlight에서 표시되는 이름입니다. 지정하지 않으면 PC의 이름이 사용됩니다.\",\n    \"sw_preset\": \"소프트웨어 프리셋\",\n    \"sw_preset_desc\": \"인코딩 속도(초당 인코딩 프레임 수)와 압축 효율성(비트스트림의 비트당 품질) 간의 절충점을 최적화합니다. 기본값은 아주 빠름입니다.\",\n    \"sw_preset_fast\": \"빠름\",\n    \"sw_preset_faster\": \"더 빠름\",\n    \"sw_preset_medium\": \"중간\",\n    \"sw_preset_slow\": \"느림\",\n    \"sw_preset_slower\": \"더 느림\",\n    \"sw_preset_superfast\": \"아주 빠름 (기본값)\",\n    \"sw_preset_ultrafast\": \"가장 빠름\",\n    \"sw_preset_veryfast\": \"매우 빠름\",\n    \"sw_preset_veryslow\": \"매우 느림\",\n    \"sw_tune\": \"SW Tune\",\n    \"sw_tune_animation\": \"애니메이션 - 만화에 적합하며 더 높은 디블럭킹과 더 많은 기준 프레임을 사용합니다.\",\n    \"sw_tune_desc\": \"사전 설정 후에 적용되는 튜닝 옵션입니다. 기본값은 제로 레이턴시입니다.\",\n    \"sw_tune_fastdecode\": \"fastdecode - 특정 필터를 비활성화하여 더 빠르게 디코딩할 수 있습니다.\",\n    \"sw_tune_film\": \"영화 - 고품질 영화 콘텐츠에 사용, 차단 해제 감소\",\n    \"sw_tune_grain\": \"그레인 - 오래되고 거친 필름 소재의 그레인 구조를 보존합니다.\",\n    \"sw_tune_stillimage\": \"스틸 이미지 - 슬라이드쇼와 같은 콘텐츠에 적합\",\n    \"sw_tune_zerolatency\": \"제로 레이턴시 - 빠른 인코딩 및 저지연 스트리밍에 적합(기본값)\",\n    \"system_tray\": \"시스템 트레이 활성화\",\n    \"system_tray_desc\": \"시스템 트레이를 활성화할지 여부입니다. 활성화하면 Sunshine이 시스템 트레이에 아이콘을 표시하고 시스템 트레이에서 제어할 수 있습니다.\",\n    \"touchpad_as_ds4\": \"클라이언트 게임패드가 터치패드가 있다고 보고하는 경우 DS4 게임패드 에뮬레이션\",\n    \"touchpad_as_ds4_desc\": \"비활성화하면 게임패드 유형을 선택할 때 터치패드의 존재 여부가 고려되지 않습니다.\",\n    \"unsaved_changes_tooltip\": \"저장되지 않은 변경 사항이 있습니다. 클릭하여 저장하십시오。\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"인터넷을 통한 포트 포워딩 자동 구성 기능입니다.\",\n    \"variable_refresh_rate\": \"가변 재생빈도 (VRR)\",\n    \"variable_refresh_rate_desc\": \"VRR 지원을 위해 비디오 스트림 프레임레이트가 렌더 프레임레이트와 일치하도록 허용합니다. 활성화하면 새 프레임이 사용 가능할 때만 인코딩이 발생하여 스트림이 실제 렌더 프레임레이트를 따를 수 있습니다.\",\n    \"vdd_reuse_desc_windows\": \"활성화하면 모든 클라이언트가 동일한 VDD(가상 디스플레이 장치)를 공유합니다. 비활성화 시(기본값) 각 클라이언트는 고유한 VDD를 받습니다. 빠른 클라이언트 전환을 위해 이 옵션을 활성화하되, 모든 클라이언트가 동일한 디스플레이 설정을 공유한다는 점에 유의하세요.\",\n    \"vdd_reuse_windows\": \"모든 클라이언트에 동일한 VDD 재사용\",\n    \"virtual_display\": \"가상 디스플레이\",\n    \"virtual_mouse\": \"가상 마우스 드라이버\",\n    \"virtual_mouse_desc\": \"활성화하면 Sunshine은 Zako 가상 마우스 드라이버(설치된 경우)를 사용하여 HID 레벨에서 마우스 입력을 시뮬레이션합니다. Raw Input을 사용하는 게임이 마우스 이벤트를 수신할 수 있게 합니다. 비활성화되거나 드라이버가 설치되지 않은 경우 SendInput으로 대체됩니다.\",\n    \"virtual_sink\": \"가상 오디오 싱크\",\n    \"virtual_sink_desc\": \"사용할 가상 오디오 장치를 수동으로 지정합니다. 설정하지 않으면 자동으로 장치가 선택됩니다. 자동으로 장치를 선택할려면 이 칸을 비우는 것을 권장드립니다.\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"가상 마우스 드라이버를 설치하시겠습니까?\",\n    \"vmouse_confirm_uninstall\": \"가상 마우스 드라이버를 제거하시겠습니까?\",\n    \"vmouse_install\": \"드라이버 설치\",\n    \"vmouse_installing\": \"설치 중...\",\n    \"vmouse_note\": \"가상 마우스 드라이버는 별도 설치가 필요합니다. Sunshine 제어판을 사용하여 드라이버를 설치하거나 관리하세요.\",\n    \"vmouse_refresh\": \"상태 새로고침\",\n    \"vmouse_status_installed\": \"설치됨 (비활성)\",\n    \"vmouse_status_not_installed\": \"설치되지 않음\",\n    \"vmouse_status_running\": \"실행 중\",\n    \"vmouse_uninstall\": \"드라이버 제거\",\n    \"vmouse_uninstalling\": \"제거 중...\",\n    \"vt_coder\": \"비디오 툴박스 코더\",\n    \"vt_realtime\": \"비디오툴박스 실시간 인코딩\",\n    \"vt_software\": \"비디오툴박스 소프트웨어 인코딩\",\n    \"vt_software_allowed\": \"허용됨\",\n    \"vt_software_forced\": \"강제\",\n    \"wan_encryption_mode\": \"WAN 암호화 모드\",\n    \"wan_encryption_mode_1\": \"지원되는 클라이언트에 사용(기본값)\",\n    \"wan_encryption_mode_2\": \"모든 사용자에게 필수\",\n    \"wan_encryption_mode_desc\": \"인터넷을 통해 스트리밍할 때 암호화를 사용할 시기를 결정합니다. 암호화는 특히 성능이 낮은 호스트와 클라이언트에서 스트리밍 성능을 저하시킬 수 있습니다.\",\n    \"webhook_curl_command\": \"명령\",\n    \"webhook_curl_command_desc\": \"다음 명령을 터미널에 복사하여 webhook이 제대로 작동하는지 테스트하세요:\",\n    \"webhook_curl_copy_failed\": \"복사 실패, 수동으로 선택하고 복사하세요\",\n    \"webhook_enabled\": \"Webhook 알림\",\n    \"webhook_enabled_desc\": \"활성화되면 Sunshine이 지정된 Webhook URL로 이벤트 알림을 보냅니다\",\n    \"webhook_group\": \"Webhook 알림 설정\",\n    \"webhook_skip_ssl_verify\": \"SSL 인증서 확인 건너뛰기\",\n    \"webhook_skip_ssl_verify_desc\": \"HTTPS 연결에 대한 SSL 인증서 확인을 건너뛰기, 테스트 또는 자체 서명된 인증서에만 사용\",\n    \"webhook_test\": \"테스트\",\n    \"webhook_test_failed\": \"Webhook 테스트 실패\",\n    \"webhook_test_failed_note\": \"참고: URL이 올바른지 확인하거나 브라우저 콘솔에서 자세한 정보를 확인하세요.\",\n    \"webhook_test_success\": \"Webhook 테스트 성공!\",\n    \"webhook_test_success_cors_note\": \"참고: CORS 제한으로 인해 서버 응답 상태를 확인할 수 없습니다.\\n요청이 전송되었습니다. webhook이 올바르게 구성된 경우 메시지가 전달되었어야 합니다.\\n\\n제안: 브라우저 개발자 도구의 네트워크 탭에서 요청 세부 정보를 확인하세요.\",\n    \"webhook_test_url_required\": \"먼저 Webhook URL을 입력하세요\",\n    \"webhook_timeout\": \"요청 시간 초과\",\n    \"webhook_timeout_desc\": \"Webhook 요청의 시간 초과(밀리초), 범위 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"이벤트 알림을 받을 URL, HTTP/HTTPS 프로토콜 지원\",\n    \"wgc_checking_mode\": \"확인 중...\",\n    \"wgc_checking_running_mode\": \"실행 모드 확인 중...\",\n    \"wgc_control_panel_only\": \"이 기능은 Sunshine Control Panel에서만 사용할 수 있습니다\",\n    \"wgc_mode_switch_failed\": \"모드 전환 실패\",\n    \"wgc_mode_switch_started\": \"모드 전환이 시작되었습니다. UAC 프롬프트가 나타나면 '예'를 클릭하여 확인하십시오.\",\n    \"wgc_service_mode_warning\": \"WGC 캡처는 사용자 모드에서 실행해야 합니다. 현재 서비스 모드에서 실행 중인 경우 위의 버튼을 클릭하여 사용자 모드로 전환하십시오.\",\n    \"wgc_switch_to_service_mode\": \"서비스 모드로 전환\",\n    \"wgc_switch_to_service_mode_tooltip\": \"현재 사용자 모드에서 실행 중입니다. 서비스 모드로 전환하려면 클릭하십시오.\",\n    \"wgc_switch_to_user_mode\": \"사용자 모드로 전환\",\n    \"wgc_switch_to_user_mode_tooltip\": \"WGC 캡처는 사용자 모드에서 실행해야 합니다. 사용자 모드로 전환하려면 이 버튼을 클릭하십시오.\",\n    \"wgc_user_mode_available\": \"현재 사용자 모드에서 실행 중입니다. WGC 캡처를 사용할 수 있습니다.\",\n    \"window_title\": \"창 제목\",\n    \"window_title_desc\": \"캡처할 창의 제목(부분 일치, 대소문자 구분 안 함). 비워두면 현재 실행 중인 응용 프로그램 이름이 자동으로 사용됩니다.\",\n    \"window_title_placeholder\": \"예: 응용 프로그램 이름\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine은 Moonlight의 게임 스트리밍 프로그램입니다.\",\n    \"download\": \"다운로드\",\n    \"installed_version_not_stable\": \"Sunshine의 정식 출시 이전 버전을 실행 중입니다. 버그나 기타 문제가 발생할 수 있습니다. 문제가 발생하면 신고해 주세요. Sunshine을 더 나은 소프트웨어로 만드는 데 도움을 주셔서 감사합니다!\",\n    \"loading_latest\": \"새로운 업데이트를 확인하는 중...\",\n    \"new_pre_release\": \"새로운 사전 출시 버전이 출시되었습니다!\",\n    \"new_stable\": \"새로운 안정 버전이 출시되었습니다!\",\n    \"startup_errors\": \"<b>주의!</b> 시작하는 중에 오류가 감지되었습니다. 스트리밍하기 전에 이 오류를 수정할 것을 <b>강력히 권장합니다.</b>\",\n    \"update_download_confirm\": \"브라우저에서 업데이트 다운로드 페이지를 엽니다. 계속하시겠습니까?\",\n    \"version_dirty\": \"Sunshine이 더 나은 소프트웨어가 될 수 있도록 도와주셔서 감사합니다!\",\n    \"version_latest\": \"최신 버전의 Sunshine을 실행 중입니다.\",\n    \"view_logs\": \"로그 보기\",\n    \"welcome\": \"반가워요, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"애플리케이션\",\n    \"configuration\": \"설정\",\n    \"home\": \"홈\",\n    \"password\": \"비밀번호 변경\",\n    \"pin\": \"새 기기 연결\",\n    \"theme_auto\": \"자동\",\n    \"theme_dark\": \"다크 테마\",\n    \"theme_light\": \"라이트 테마\",\n    \"toggle_theme\": \"테마\",\n    \"troubleshoot\": \"문제 해결\"\n  },\n  \"password\": {\n    \"confirm_password\": \"비밀번호 확인\",\n    \"current_creds\": \"현재 자격 증명\",\n    \"new_creds\": \"새 자격 증명\",\n    \"new_username_desc\": \"지정하지 않으면 사용자 아이디가 변경되지 않습니다.\",\n    \"password_change\": \"비밀번호 변경\",\n    \"success_msg\": \"비밀번호가 성공적으로 변경되었습니다! 이 페이지가 곧 다시 로드되며 브라우저에서 새 자격 증명을 입력하라는 메시지가 표시됩니다.\"\n  },\n  \"pin\": {\n    \"actions\": \"작업\",\n    \"cancel_editing\": \"편집 취소\",\n    \"client_name\": \"이름\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"삭제 확인\",\n    \"delete_client\": \"클라이언트 삭제\",\n    \"delete_confirm_message\": \"<strong>{name}</strong>을(를) 삭제하시겠습니까?\",\n    \"delete_warning\": \"이 작업은 실행 취소할 수 없습니다.\",\n    \"device_name\": \"기기 이름\",\n    \"device_size\": \"기기 크기\",\n    \"device_size_info\": \"<strong>장치 크기</strong>: 클라이언트 장치의 화면 크기 유형(작음 - 휴대폰, 중간 - 태블릿, 큼 - TV)을 설정하여 스트리밍 경험과 터치 조작을 최적화합니다。\",\n    \"device_size_large\": \"대형 - TV\",\n    \"device_size_medium\": \"중형 - 태블릿\",\n    \"device_size_small\": \"소형 - 전화\",\n    \"edit_client_settings\": \"클라이언트 설정 편집\",\n    \"hdr_profile\": \"HDR 프로필\",\n    \"hdr_profile_info\": \"<strong>HDR 프로필</strong>: 이 클라이언트에 사용할 HDR 색상 프로필(ICC 파일)을 선택하여 HDR 콘텐츠가 장치에서 올바르게 표시되도록 합니다. 최신 클라이언트를 사용하는 경우 호스트 가상 화면으로의 밝기 정보 자동 동기화를 지원하므로, 자동 동기화를 활성화하려면 이 필드를 비워 두십시오。\",\n    \"loading\": \"로딩 중...\",\n    \"loading_clients\": \"클라이언트 로딩 중...\",\n    \"modify_in_gui\": \"GUI에서 수정하세요\",\n    \"none\": \"-- 없음 --\",\n    \"or_manual_pin\": \"또는 PIN을 수동 입력\",\n    \"pair_failure\": \"연결할 수 없습니다, PIN이 올바른지 확인하세요.\",\n    \"pair_success\": \"성공! 계속하려면 Moonlight를 확인해 주세요.\",\n    \"pin_pairing\": \"PIN 번호로 연결\",\n    \"qr_expires_in\": \"만료까지\",\n    \"qr_generate\": \"QR 코드 생성\",\n    \"qr_paired_success\": \"페어링 성공!\",\n    \"qr_pairing\": \"QR 코드 페어링\",\n    \"qr_pairing_desc\": \"빠른 페어링을 위해 QR 코드를 생성합니다. Moonlight 클라이언트로 스캔하여 자동으로 페어링합니다.\",\n    \"qr_pairing_warning\": \"실험적 기능입니다. 페어링에 실패하면 아래의 수동 PIN 페어링을 사용하세요. 참고: 이 기능은 LAN에서만 작동합니다.\",\n    \"qr_refresh\": \"QR 코드 새로고침\",\n    \"remove_paired_devices_desc\": \"페어링된 기기를 제거하세요.\",\n    \"save_changes\": \"변경 사항 저장\",\n    \"save_failed\": \"클라이언트 설정 저장에 실패했습니다. 다시 시도해 주세요.\",\n    \"save_or_cancel_first\": \"먼저 편집을 저장하거나 취소하세요\",\n    \"send\": \"보내기\",\n    \"unknown_client\": \"알 수 없는 클라이언트\",\n    \"unpair_all_confirm\": \"모든 클라이언트의 페어링을 해제하시겠습니까? 이 작업은 실행 취소할 수 없습니다.\",\n    \"unsaved_changes\": \"저장되지 않은 변경 사항\",\n    \"warning_msg\": \"페어링하려는 클라이언트에 대한 액세스 권한이 있는지 확인하세요. 이 소프트웨어는 컴퓨터를 완전히 제어할 수 있으므로 주의하세요! \"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android 추천\",\n    \"client_downloads\": \"클라이언트 다운로드\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"GitHub 토론\",\n    \"gpl_license_text_1\": \"이 소프트웨어는 GPL-3.0 라이선스를 따릅니다. 자유롭게 사용, 수정 및 배포할 수 있습니다.\",\n    \"gpl_license_text_2\": \"오픈 소스 생태계를 보호하기 위해 GPL-3.0 라이선스를 위반하는 소프트웨어 사용을 자제해 주십시오。\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"커뮤니티 참여\",\n    \"join_group_desc\": \"도움을 받고 경험을 공유하세요\",\n    \"legal\": \"법률\",\n    \"legal_desc\": \"이 소프트웨어를 계속 사용함으로써 귀하는 다음 문서의 이용 약관에 동의하게 됩니다.\",\n    \"license\": \"라이선스\",\n    \"lizardbyte_website\": \"LizardByte 웹사이트\",\n    \"official_website\": \"공식 웹사이트\",\n    \"official_website_title\": \"AlkaidLab - 공식 웹사이트\",\n    \"open_source\": \"오픈 소스\",\n    \"open_source_desc\": \"Star & Fork로 프로젝트를 지원하세요\",\n    \"quick_start\": \"빠른 시작\",\n    \"resources\": \"리소스\",\n    \"resources_desc\": \"Sunshine을 위한 리소스!\",\n    \"third_party_desc\": \"서드파티 컴포넌트 공지\",\n    \"third_party_moonlight\": \"친구 링크\",\n    \"third_party_notice\": \"제3자 프로그램 안내\",\n    \"tutorial\": \"튜토리얼\",\n    \"tutorial_desc\": \"상세한 구성 및 사용 가이드\",\n    \"view_license\": \"전체 라이선스 보기\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"가상 디스플레이\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"저장소로 이동\",\n    \"harmony_modal_desc\": \"HarmonyOS NEXT Moonlight는 HarmonyOS 스토어에서 Moonlight V+를 검색하세요\",\n    \"harmony_modal_link_notice\": \"이 링크는 프로젝트 저장소로 이동합니다\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"초\",\n    \"restart_desc\": \"설정이 저장되었습니다. 디스플레이 설정을 적용하기 위해 Sunshine이 재시작되고 있습니다.\",\n    \"restart_go_now\": \"지금 이동\",\n    \"restart_title\": \"Sunshine 재시작 중\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"기본 설정이 활성화되었습니다. 이제 Moonlight 클라이언트로 바로 스트리밍을 시작할 수 있습니다!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"기지 디스플레이(VDD)는 Sunshine Foundation에 내장된 스마트 가상 디스플레이로, 모든 해상도, 프레임 레이트 및 HDR 최적화를 지원합니다. 화면 끄기 스트리밍 및 확장 디스플레이 스트리밍에 최적의 선택입니다.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"활성화 확인\",\n    \"step3_ensure_active_desc\": \"디스플레이가 활성화되지 않은 경우 활성화합니다\",\n    \"step3_ensure_only_display\": \"단일 디스플레이 확인\",\n    \"step3_ensure_only_display_desc\": \"다른 모든 디스플레이를 비활성화하고 지정된 디스플레이만 활성화합니다 (권장)\",\n    \"step3_ensure_primary\": \"기본 디스플레이 확인\",\n    \"step3_ensure_primary_desc\": \"디스플레이를 활성화하고 기본 디스플레이로 설정합니다\",\n    \"step3_ensure_secondary\": \"보조 스트리밍\",\n    \"step3_ensure_secondary_desc\": \"가상 디스플레이만 사용하여 보조 확장 스트리밍을 수행합니다\",\n    \"step3_no_operation\": \"작업 없음\",\n    \"step3_no_operation_desc\": \"디스플레이 상태를 변경하지 않습니다. 사용자가 직접 준비해야 합니다\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Advanced\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Config Files\",\n    \"general\": \"General\",\n    \"input\": \"Input\",\n    \"network\": \"Network\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"분석 중...\",\n    \"ai_analyzing_logs\": \"로그를 분석하는 중입니다. 잠시 기다려 주세요...\",\n    \"ai_config\": \"AI 구성\",\n    \"ai_copy_result\": \"복사\",\n    \"ai_diagnosis\": \"AI 진단\",\n    \"ai_diagnosis_title\": \"AI 로그 진단\",\n    \"ai_error\": \"분석 실패\",\n    \"ai_key_local\": \"API 키는 로컬에만 저장되며 업로드되지 않습니다\",\n    \"ai_model\": \"모델\",\n    \"ai_provider\": \"공급자\",\n    \"ai_reanalyze\": \"재분석\",\n    \"ai_result\": \"진단 결과\",\n    \"ai_retry\": \"재시도\",\n    \"ai_start_diagnosis\": \"진단 시작\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Sunshine을 즉시 종료해야 하는 경우 이 기능을 사용할 수 있습니다. 종료 후 수동으로 다시 시작해야 합니다.\",\n    \"boom_sunshine_success\": \"Sunshine이 종료되었습니다\",\n    \"confirm_boom\": \"정말 종료하시겠습니까?\",\n    \"confirm_boom_desc\": \"정말 종료하고 싶으신가요? 음, 막을 수는 없지만 계속해서 다시 클릭하세요\",\n    \"confirm_logout\": \"Confirm logout?\",\n    \"confirm_logout_desc\": \"You will need to enter your password again to access the web UI.\",\n    \"copy_config\": \"구성 복사\",\n    \"copy_config_error\": \"구성 복사 실패\",\n    \"copy_config_success\": \"구성이 클립보드에 복사되었습니다!\",\n    \"copy_logs\": \"Copy logs\",\n    \"download_logs\": \"Download logs\",\n    \"force_close\": \"강제 종료\",\n    \"force_close_desc\": \"Moonlight가 사용 도중 문제가 발생했을 경우, 앱을 강제로 종료하면 해결될 수 있습니다.\",\n    \"force_close_error\": \"애플리케이션을 닫는 동안 오류가 발생했습니다.\",\n    \"force_close_success\": \"앱이 \\\"강제로\\\" 종료되었습니다.\",\n    \"ignore_case\": \"대소문자 구분 안 함\",\n    \"logout\": \"로그아웃\",\n    \"logout_desc\": \"로그아웃합니다. 다시 로그인해야 할 수 있습니다.\",\n    \"logout_localhost_tip\": \"현재 환경은 로그인이 필요하지 않습니다. 로그아웃해도 자격 증명 프롬프트가 표시되지 않습니다.\",\n    \"logs\": \"로그\",\n    \"logs_desc\": \"Sunshine이 업로드한 로그 보기\",\n    \"logs_find\": \"찾기...\",\n    \"match_contains\": \"포함\",\n    \"match_exact\": \"정확히\",\n    \"match_regex\": \"정규 표현식\",\n    \"reopen_setup_wizard\": \"설정 마법사 다시 열기\",\n    \"reopen_setup_wizard_desc\": \"설정 마법사 페이지를 다시 열어 초기 설정을 다시 구성할 수 있습니다.\",\n    \"reopen_setup_wizard_error\": \"설정 마법사 다시 열기 실패\",\n    \"reset_display_device_desc_windows\": \"Sunshine이 변경된 디스플레이 장치 설정을 복원하려고 멈춰 있는 경우, 설정을 재설정하고 수동으로 디스플레이 상태를 복원할 수 있습니다.\\n이는 다양한 이유로 발생할 수 있습니다: 장치를 더 이상 사용할 수 없거나, 다른 포트에 연결되었거나 등.\",\n    \"reset_display_device_error_windows\": \"Error while resetting persistence!\",\n    \"reset_display_device_success_windows\": \"메모리 초기화 성공!\",\n    \"reset_display_device_windows\": \"디스플레이 메모리 초기화\",\n    \"restart_sunshine\": \"Sunshine 다시 시작\",\n    \"restart_sunshine_desc\": \"Sunshine이 제대로 작동하지 않는다면 다시 시작해 보세요. 실행 중인 모든 세션이 종료됩니다.\",\n    \"restart_sunshine_success\": \"Sunshine이 다시 시작됩니다.\",\n    \"troubleshooting\": \"문제 해결\",\n    \"unpair_all\": \"모두 페어링 해제\",\n    \"unpair_all_error\": \"페어링 해제 중 오류\",\n    \"unpair_all_success\": \"페어링되지 않은 모든 장치.\",\n    \"unpair_desc\": \"페어링된 장치를 제거합니다. 활성 세션이 있는 개별적으로 페어링되지 않은 장치는 연결된 상태로 유지되지만 세션을 시작하거나 다시 시작할 수는 없습니다.\",\n    \"unpair_single_no_devices\": \"페어링된 장치가 없습니다.\",\n    \"unpair_single_success\": \"그러나 디바이스가 여전히 활성 세션에 있을 수 있습니다. 열려 있는 세션을 종료하려면 위의 '강제 종료' 버튼을 사용하세요.\",\n    \"unpair_single_unknown\": \"알 수 없는 클라이언트\",\n    \"unpair_title\": \"장치 페어링 해제\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"비밀번호 확인\",\n    \"create_creds\": \"시작하기 전에 웹 UI에 액세스하기 위한 새 사용자 아이디와 비밀번호를 만들어야 합니다.\",\n    \"create_creds_alert\": \"Sunshine의 웹 UI에 액세스하려면 아래 자격 증명이 필요합니다. 다시는 볼 수 없으니 안전하게 보관하세요!\\n(잊어버리면 이 화면을 또 볼 수 있을꺼에요!)\",\n    \"creds_local_only\": \"자격 증명은 로컬에 오프라인으로 저장되며 어떤 서버에도 업로드되지 않습니다.\",\n    \"error\": \"오류!\",\n    \"greeting\": \"Sunshine Foundation에 오신 것을 환영합니다!\",\n    \"hide_password\": \"비밀번호 숨기기\",\n    \"login\": \"로그인\",\n    \"network_error\": \"네트워크 오류, 연결을 확인하세요\",\n    \"password\": \"비밀번호\",\n    \"password_match\": \"비밀번호가 일치합니다\",\n    \"password_mismatch\": \"비밀번호가 일치하지 않습니다\",\n    \"server_error\": \"서버 오류\",\n    \"show_password\": \"비밀번호 표시\",\n    \"success\": \"성공!\",\n    \"username\": \"사용자명\",\n    \"welcome_success\": \"이 페이지는 곧 새로고침 될 것이며, 브라우저에서 다시 로그인을 해야 합니다.\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/pl.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Zastosuj\",\n    \"auto\": \"Automatyczny\",\n    \"autodetect\": \"Autowykrywanie (zalecane)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"Anuluj\",\n    \"close\": \"Zamknij\",\n    \"copied\": \"Skopiowano do schowka\",\n    \"copy\": \"Kopiuj\",\n    \"delete\": \"Usuń\",\n    \"description\": \"Opis\",\n    \"disabled\": \"Wyłączony\",\n    \"disabled_def\": \"Wyłączone (domyślnie)\",\n    \"dismiss\": \"Odrzuć\",\n    \"do_cmd\": \"Polecenie Do\",\n    \"download\": \"Pobierz\",\n    \"edit\": \"Edytuj\",\n    \"elevated\": \"Podwyższone\",\n    \"enabled\": \"Włączone\",\n    \"enabled_def\": \"Włączone (domyślnie)\",\n    \"error\": \"Błąd!\",\n    \"no_changes\": \"Brak zmian\",\n    \"note\": \"Uwaga:\",\n    \"password\": \"Hasło\",\n    \"remove\": \"Usuń\",\n    \"run_as\": \"Uruchom jako administrator\",\n    \"save\": \"Zapisz\",\n    \"see_more\": \"Zobacz więcej\",\n    \"success\": \"Sukces!\",\n    \"undo_cmd\": \"Polecenie Undo\",\n    \"username\": \"Nazwa użytkownika\",\n    \"warning\": \"Ostrzeżenie!\"\n  },\n  \"apps\": {\n    \"actions\": \"Działania\",\n    \"add_cmds\": \"Dodaj polecenia\",\n    \"add_new\": \"Dodaj nowy\",\n    \"advanced_options\": \"Opcje zaawansowane\",\n    \"app_name\": \"Nazwa aplikacji\",\n    \"app_name_desc\": \"Nazwa aplikacji wyświetlana w aplikacji Moonlight\",\n    \"applications_desc\": \"Aplikacje są odświeżane tylko po ponownym uruchomieniu klienta\",\n    \"applications_title\": \"Aplikacje\",\n    \"auto_detach\": \"Kontynuuj przesyłanie strumieniowe, jeśli aplikacja szybko się wyłączy\",\n    \"auto_detach_desc\": \"Spowoduje to próbę automatycznego wykrycia aplikacji typu launcher, które zamykają się szybko po uruchomieniu innego programu lub własnej instancji. Po wykryciu aplikacji typu launcher jest ona traktowana jako aplikacja odłączona.\",\n    \"basic_info\": \"Podstawowe informacje\",\n    \"cmd\": \"Polecenie\",\n    \"cmd_desc\": \"Główna aplikacja do uruchomienia. Jeśli puste, żadna aplikacja nie zostanie uruchomiona.\",\n    \"cmd_examples_title\": \"Typowe przykłady:\",\n    \"cmd_note\": \"Jeśli ścieżka do pliku wykonywalnego zawiera spacje, należy ująć ją w cudzysłów.\",\n    \"cmd_prep_desc\": \"Lista poleceń do uruchomienia przed/po tej aplikacji. Jeśli którekolwiek z poleceń wstępnych nie powiedzie się, uruchomienie aplikacji zostanie przerwane.\",\n    \"cmd_prep_name\": \"Polecenia przygotowujące\",\n    \"command_settings\": \"Ustawienia poleceń\",\n    \"covers_found\": \"Znalezione okładki\",\n    \"delete\": \"Usuń\",\n    \"delete_confirm\": \"Are you sure you want to delete \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Polecenia odłączone\",\n    \"detached_cmds_add\": \"Dodaj odłączone polecenie\",\n    \"detached_cmds_desc\": \"Lista poleceń uruchamianych w tle.\",\n    \"detached_cmds_note\": \"Jeśli ścieżka do pliku wykonywalnego zawiera spacje, należy ująć ją w cudzysłów.\",\n    \"detached_cmds_remove\": \"Usuń odłączone polecenie\",\n    \"edit\": \"Edytuj\",\n    \"env_app_id\": \"Identyfikator aplikacji\",\n    \"env_app_name\": \"Nazwa aplikacji\",\n    \"env_client_audio_config\": \"Konfiguracja audio wymagana przez klienta (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"Klient zażądał opcji optymalizacji gry pod kątem optymalnego strumienia (prawda/fałsz)\",\n    \"env_client_fps\": \"FPS żądane przez klienta (liczba całkowita)\",\n    \"env_client_gcmap\": \"Żądana maska kontrolera w formacie zestawu bitów/pola bitów (liczba całkowita)\",\n    \"env_client_hdr\": \"HDR jest włączony przez klienta (prawda/fałsz)\",\n    \"env_client_height\": \"Wysokość żądana przez klienta (liczba całkowita)\",\n    \"env_client_host_audio\": \"Klient zażądał dźwięku hosta (prawda/fałsz)\",\n    \"env_client_name\": \"Przyjazna nazwa klienta (ciąg)\",\n    \"env_client_width\": \"Szerokość żądana przez klienta (liczba całkowita)\",\n    \"env_displayplacer_example\": \"Przykład - displayplacer dla automatycznej rozdzielczości:\",\n    \"env_qres_example\": \"Przykład - QRes dla automatycznej rozdzielczości:\",\n    \"env_qres_path\": \"ścieżka qres\",\n    \"env_var_name\": \"Nazwa Var\",\n    \"env_vars_about\": \"Informacje o zmiennych środowiskowych\",\n    \"env_vars_desc\": \"Wszystkie polecenia domyślnie pobierają te zmienne środowiskowe:\",\n    \"env_xrandr_example\": \"Przykład - Xrandr dla automatycznej rozdzielczości:\",\n    \"exit_timeout\": \"Limit czasu wyjścia\",\n    \"exit_timeout_desc\": \"Liczba sekund oczekiwania, aż wszystkie procesy aplikacji zakończą działanie po żądaniu zakończenia. Jeśli nie jest ustawiona, domyślnie odczekiwane jest do 5 sekund. Jeśli ustawiona na 0 lub wartość ujemną, aplikacja zostanie natychmiast zakończona.\",\n    \"file_selector_not_initialized\": \"Selektor plików nie został zainicjalizowany\",\n    \"find_cover\": \"Znajdź okładkę\",\n    \"form_invalid\": \"Proszę sprawdzić wymagane pola\",\n    \"form_valid\": \"Prawidłowa aplikacja\",\n    \"global_prep_desc\": \"Włączenie/wyłączenie wykonywania globalnych poleceń przygotowawczych dla tej aplikacji.\",\n    \"global_prep_name\": \"Globalne polecenia przygotowawcze\",\n    \"image\": \"Obraz\",\n    \"image_desc\": \"Ścieżka ikony/obrazu/ścieżki aplikacji, która zostanie wysłany do klienta. Obraz musi być plikiem PNG. Jeśli nie zostanie ustawiony, Sunshine wyśle domyślny obraz pudełka.\",\n    \"image_settings\": \"Ustawienia obrazu\",\n    \"loading\": \"Ładowanie...\",\n    \"menu_cmd_actions\": \"Akcje\",\n    \"menu_cmd_add\": \"Dodaj polecenie menu\",\n    \"menu_cmd_command\": \"Polecenie\",\n    \"menu_cmd_desc\": \"Po konfiguracji te polecenia będą widoczne w menu powrotu klienta, umożliwiając szybkie wykonywanie określonych operacji bez przerywania strumienia, takich jak uruchamianie programów pomocniczych.\\nPrzykład: Nazwa wyświetlana - Zamknij komputer; Polecenie - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Nazwa wyświetlana\",\n    \"menu_cmd_drag_sort\": \"Przeciągnij, aby posortować\",\n    \"menu_cmd_name\": \"Polecenia menu\",\n    \"menu_cmd_placeholder_command\": \"Polecenie\",\n    \"menu_cmd_placeholder_display_name\": \"Nazwa wyświetlana\",\n    \"menu_cmd_placeholder_execute\": \"Wykonaj polecenie\",\n    \"menu_cmd_placeholder_undo\": \"Cofnij polecenie\",\n    \"menu_cmd_remove_menu\": \"Usuń polecenie menu\",\n    \"menu_cmd_remove_prep\": \"Usuń polecenie przygotowania\",\n    \"mouse_mode\": \"Tryb myszy\",\n    \"mouse_mode_auto\": \"Auto (Ustawienie globalne)\",\n    \"mouse_mode_desc\": \"Wybierz metodę wprowadzania myszy dla tej aplikacji. Auto używa ustawienia globalnego, Wirtualna mysz używa sterownika HID, SendInput używa API Windows.\",\n    \"mouse_mode_sendinput\": \"SendInput (API Windows)\",\n    \"mouse_mode_vmouse\": \"Wirtualna mysz\",\n    \"name\": \"Nazwa\",\n    \"output_desc\": \"Plik, w którym przechowywane są dane wyjściowe polecenia, jeśli nie zostanie określony, dane wyjściowe zostaną zignorowane\",\n    \"output_name\": \"Wyjście\",\n    \"run_as_desc\": \"Może to być konieczne w przypadku niektórych aplikacji, które wymagają uprawnień administratora do prawidłowego działania.\",\n    \"scan_result_add_all\": \"Dodaj wszystko\",\n    \"scan_result_edit_title\": \"Dodaj i edytuj\",\n    \"scan_result_filter_all\": \"Wszystkie\",\n    \"scan_result_filter_epic_title\": \"Gry Epic Games\",\n    \"scan_result_filter_executable\": \"Wykonywalny\",\n    \"scan_result_filter_executable_title\": \"Plik wykonywalny\",\n    \"scan_result_filter_gog_title\": \"Gry GOG Galaxy\",\n    \"scan_result_filter_script\": \"Skrypt\",\n    \"scan_result_filter_script_title\": \"Skrypt wsadowy/Polecenie\",\n    \"scan_result_filter_shortcut\": \"Skrót\",\n    \"scan_result_filter_shortcut_title\": \"Skrót\",\n    \"scan_result_filter_steam_title\": \"Gry Steam\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Gra\",\n    \"scan_result_games_only\": \"Tylko gry\",\n    \"scan_result_matched\": \"Dopasowania: {count}\",\n    \"scan_result_no_apps\": \"Nie znaleziono aplikacji do dodania\",\n    \"scan_result_no_matches\": \"Nie znaleziono pasujących aplikacji\",\n    \"scan_result_quick_add_title\": \"Szybkie dodanie\",\n    \"scan_result_remove_title\": \"Usuń z listy\",\n    \"scan_result_search_placeholder\": \"Szukaj nazwy aplikacji, polecenia lub ścieżki...\",\n    \"scan_result_show_all\": \"Pokaż wszystko\",\n    \"scan_result_title\": \"Wyniki skanowania\",\n    \"scan_result_try_different_keywords\": \"Spróbuj użyć innych słów kluczowych do wyszukiwania\",\n    \"scan_result_type_batch\": \"Wsadowy\",\n    \"scan_result_type_command\": \"Skrypt polecenia\",\n    \"scan_result_type_executable\": \"Plik wykonywalny\",\n    \"scan_result_type_shortcut\": \"Skrót\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Szukaj aplikacji...\",\n    \"select\": \"Wybierz\",\n    \"test_menu_cmd\": \"Testuj polecenie\",\n    \"test_menu_cmd_empty\": \"Polecenie nie może być puste\",\n    \"test_menu_cmd_executing\": \"Wykonywanie polecenia...\",\n    \"test_menu_cmd_failed\": \"Wykonanie polecenia nie powiodło się\",\n    \"test_menu_cmd_success\": \"Polecenie zostało pomyślnie wykonane!\",\n    \"use_desktop_image\": \"Użyj aktualnej tapety pulpitu\",\n    \"wait_all\": \"Kontynuuj przesyłanie strumieniowe, aż wszystkie procesy aplikacji zakończą działanie\",\n    \"wait_all_desc\": \"Spowoduje to kontynuowanie przesyłania strumieniowego do momentu zakończenia wszystkich procesów uruchomionych przez aplikację. Jeśli opcja nie jest zaznaczona, strumień zostanie zatrzymany po zakończeniu początkowego procesu aplikacji, nawet jeśli inne procesy aplikacji są nadal uruchomione.\",\n    \"working_dir\": \"Katalog roboczy\",\n    \"working_dir_desc\": \"Katalog roboczy, który powinien zostać przekazany do procesu. Na przykład, niektóre aplikacje używają katalogu roboczego do wyszukiwania plików konfiguracyjnych. Jeśli nie zostanie ustawiony, Sunshine domyślnie wybierze katalog nadrzędny polecenia\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Nazwa adaptera\",\n    \"adapter_name_desc_linux_1\": \"Ręczne określenie procesora graficznego używanego do przechwytywania.\",\n    \"adapter_name_desc_linux_2\": \"aby znaleźć wszystkie urządzenia obsługujące VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Zastąp ``renderD129`` urządzeniem z powyższej listy, aby wyświetlić nazwę i możliwości urządzenia. Aby być obsługiwanym przez Sunshine, musi mieć co najmniej:\",\n    \"adapter_name_desc_windows\": \"Ręcznie określ GPU do użycia do przechwytywania. Jeśli nie ustawiono, GPU jest wybierane automatycznie. Uwaga: Ten GPU musi mieć podłączony i włączony wyświetlacz. Jeśli Twój laptop nie może włączyć bezpośredniego wyjścia GPU, ustaw na automatyczny.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Jeśli zainstalowana jest najnowsza wersja wirtualnego wyświetlacza, może ona automatycznie powiązać się z przypisaniem GPU\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Dodaj\",\n    \"address_family\": \"Rodzina adresów\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Ustaw rodzinę adresów używaną przez Sunshine\",\n    \"address_family_ipv4\": \"Tylko IPv4\",\n    \"always_send_scancodes\": \"Zawsze wysyłaj kody skanowania\",\n    \"always_send_scancodes_desc\": \"Wysyłanie kody skanów zwiększa kompatybilność z grami i aplikacjami, ale może powodować nieprawidłowe wprowadzanie danych z klawiatury przez niektórych klientów, którzy nie używają układu klawiatury Angielski (Stany Zjednoczone). Włącz, jeśli wprowadzanie danych z klawiatury w ogóle nie działa w niektórych aplikacjach. Wyłącz, jeśli klawisze na kliencie generują nieprawidłowe dane wejściowe na hoście.\",\n    \"amd_coder\": \"AMF Coder (H264)\",\n    \"amd_coder_desc\": \"Umożliwia wybór kodowania entropijnego w celu nadania priorytetu jakości lub szybkości kodowania. Tylko H.264.\",\n    \"amd_enforce_hrd\": \"Wymuszanie AMF Hypothetical Reference Decoder (HRD)\",\n    \"amd_enforce_hrd_desc\": \"Zwiększa ograniczenia kontroli szybkości, aby spełnić wymagania modelu HRD. Zmniejsza to znacznie przepełnienie bitrate, ale może powodować artefakty kodowania lub obniżenie jakości na niektórych kartach.\",\n    \"amd_preanalysis\": \"Wstępna analiza AMF\",\n    \"amd_preanalysis_desc\": \"Umożliwia to wstępną analizę kontroli szybkości, która może zwiększyć jakość kosztem zwiększonego opóźnienia kodowania.\",\n    \"amd_quality\": \"Jakość AMF\",\n    \"amd_quality_balanced\": \"balanced -- zrównoważony (domyślnie)\",\n    \"amd_quality_desc\": \"Kontroluje to równowagę między szybkością kodowania a jakością.\",\n    \"amd_quality_group\": \"Ustawienia jakości AMF\",\n    \"amd_quality_quality\": \"quality - preferuj jakość\",\n    \"amd_quality_speed\": \"speed - preferuj szybkość\",\n    \"amd_qvbr_quality\": \"Poziom jakości AMF QVBR\",\n    \"amd_qvbr_quality_desc\": \"Poziom jakości dla trybu kontroli bitratu QVBR. Zakres: 1-51 (niższy = lepsza jakość). Domyślnie: 23. Ma zastosowanie tylko gdy kontrola bitratu jest ustawiona na 'qvbr'.\",\n    \"amd_rc\": \"Kontrola prędkości AMF\",\n    \"amd_rc_cbr\": \"cbr -- stały bitrate (zalecane, jeśli włączona jest funkcja HRD)\",\n    \"amd_rc_cqp\": \"cqp -- stały tryb qp\",\n    \"amd_rc_desc\": \"Kontroluje to metodę kontroli szybkości, aby upewnić się, że nie przekraczamy docelowej przepływności klienta. \\\"cqp\\\" nie nadaje się do kierowania bitrate, a inne opcje poza \\\"vbr_latency\\\" zależą od wymuszenia HRD, aby pomóc ograniczyć przepełnienia bitrate.\",\n    \"amd_rc_group\": \"Ustawienia kontroli prędkości AMF\",\n    \"amd_rc_hqcbr\": \"hqcbr -- stały bitrate wysokiej jakości\",\n    \"amd_rc_hqvbr\": \"hqvbr -- zmienny bitrate wysokiej jakości\",\n    \"amd_rc_qvbr\": \"qvbr -- zmienny bitrate jakości (używa poziomu jakości QVBR)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- zmienna szybkość bitrate z ograniczonym opóźnieniem (zalecane, jeśli HRD jest wyłączone; domyślne)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- zmienna przepływność ograniczona wartością szczytową\",\n    \"amd_usage\": \"Użycie AMF\",\n    \"amd_usage_desc\": \"Ustawia to podstawowy profil kodowania. Wszystkie opcje przedstawione poniżej zastąpią podzbiór profilu użytkowania, ale zastosowane zostaną dodatkowe ukryte ustawienia, których nie można skonfigurować w innym miejscu.\",\n    \"amd_usage_lowlatency\": \"lowlatency - niskie opóźnienie (najszybsze)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - niskie opóźnienie, wysoka jakość (szybko)\",\n    \"amd_usage_transcoding\": \"transcoding -- transkodowanie (najwolniejsze)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency - ultra niskie opóźnienie (najszybsze; domyślne)\",\n    \"amd_usage_webcam\": \"webcam -- kamera internetowa (wolno)\",\n    \"amd_vbaq\": \"AMF Adaptacyjna kwantyzacja oparta na wariancji (VBAQ)\",\n    \"amd_vbaq_desc\": \"Ludzki system wizualny jest zazwyczaj mniej wrażliwy na artefakty w obszarach o wysokiej teksturze. W trybie VBAQ wariancja pikseli jest używana do wskazania złożoności tekstur przestrzennych, umożliwiając koderowi przydzielenie większej liczby bitów do gładszych obszarów. Włączenie tej funkcji prowadzi do poprawy subiektywnej jakości wizualnej w przypadku niektórych treści.\",\n    \"amf_draw_mouse_cursor\": \"Rysuj prosty kursor podczas używania metody przechwytywania AMF\",\n    \"amf_draw_mouse_cursor_desc\": \"W niektórych przypadkach użycie przechwytywania AMF nie wyświetli wskaźnika myszy. Włączenie tej opcji narysuje prosty wskaźnik myszy na ekranie. Uwaga: Pozycja wskaźnika myszy będzie aktualizowana tylko wtedy, gdy nastąpi aktualizacja ekranu zawartości, więc w scenariuszach innych niż gry, takich jak pulpit, możesz zaobserwować powolny ruch wskaźnika myszy.\",\n    \"apply_note\": \"Kliknij \\\"Zastosuj\\\", aby ponownie uruchomić Sunshine i zastosować zmiany. Spowoduje to zakończenie wszystkich uruchomionych sesji.\",\n    \"audio_sink\": \"Wejście audio\",\n    \"audio_sink_desc_linux\": \"Nazwa odbiornika audio używanego dla Audio Loopback. Jeśli nie określisz tej zmiennej, pulseaudio wybierze domyślne urządzenie monitorujące. Nazwę urządzenia audio można znaleźć za pomocą polecenia:\",\n    \"audio_sink_desc_macos\": \"Nazwa odbiornika audio używanego dla Audio Loopback. Sunshine może uzyskać dostęp do mikrofonów tylko w systemie macOS ze względu na ograniczenia systemowe. Aby przesyłać strumieniowo dźwięk systemowy za pomocą Soundflower lub BlackHole.\",\n    \"audio_sink_desc_windows\": \"Ręczne określenie konkretnego urządzenia audio do przechwytywania. Jeśli nie jest ustawione, urządzenie zostanie wybrane automatycznie. Zdecydowanie zalecamy pozostawienie tego pola pustego, aby korzystać z automatycznego wyboru urządzenia! Jeśli masz wiele urządzeń audio o identycznych nazwach, możesz uzyskać identyfikator urządzenia za pomocą następującego polecenia:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Głośniki (High Definition Audio Device)\",\n    \"av1_mode\": \"Wsparcie AV1\",\n    \"av1_mode_0\": \"Sunshine będzie reklamować obsługę AV1 w oparciu o możliwości enkodera (zalecane)\",\n    \"av1_mode_1\": \"Sunshine nie będzie reklamować wsparcia dla AV1\",\n    \"av1_mode_2\": \"Sunshine będzie zalecać wsparcie dla 8-bitowego profilu AV1 Main\",\n    \"av1_mode_3\": \"Sunshine będzie zalecać obsługę profili AV1 Main 8-bit i 10-bit (HDR)\",\n    \"av1_mode_desc\": \"Umożliwia klientowi żądanie 8-bitowych lub 10-bitowych strumieni wideo AV1 Main. Kodowanie AV1 jest bardziej obciążające dla procesora, więc włączenie tej opcji może zmniejszyć wydajność podczas korzystania z kodowania programowego.\",\n    \"back_button_timeout\": \"Limit czasu emulacji przycisku Home/Guide\",\n    \"back_button_timeout_desc\": \"Jeśli przycisk Wstecz/Wybierz zostanie przytrzymany przez określoną liczbę milisekund, nastąpi emulacja naciśnięcia przycisku Home/Guide. Jeśli ustawiono wartość < 0 (domyślnie), przytrzymanie przycisku Wstecz/Wybierz nie będzie emulować przycisku Home/Guide.\",\n    \"bind_address\": \"Adres wiązania (funkcja testowa)\",\n    \"bind_address_desc\": \"Ustaw konkretny adres IP, do którego Sunshine będzie się wiązać. Jeśli pozostawisz puste, Sunshine będzie wiązać się ze wszystkimi dostępnymi adresami.\",\n    \"capture\": \"Wymuś określoną metodę przechwytywania\",\n    \"capture_desc\": \"W trybie automatycznym Sunshine użyje pierwszego działającego sterownika. NvFBC wymaga poprawionych sterowników NVIDIA.\",\n    \"capture_target\": \"Cel przechwytywania\",\n    \"capture_target_desc\": \"Wybierz typ celu do przechwycenia. Wybierając 'Okno', możesz przechwycić określone okno aplikacji (np. oprogramowanie do interpolacji klatek AI) zamiast całego wyświetlacza.\",\n    \"capture_target_display\": \"Wyświetlacz\",\n    \"capture_target_window\": \"Okno\",\n    \"cert\": \"Certyfikat\",\n    \"cert_desc\": \"Certyfikat używany do parowania interfejsu użytkownika i klienta Moonlight. Aby uzyskać najlepszą kompatybilność, powinien on mieć klucz publiczny RSA-2048.\",\n    \"channels\": \"Maksymalna liczba połączonych klientów\",\n    \"channels_desc_1\": \"Sunshine pozwala na udostępnianie pojedynczej sesji streamingowej wielu klientom jednocześnie.\",\n    \"channels_desc_2\": \"Niektóre kodery sprzętowe mogą mieć ograniczenia, które zmniejszają wydajność przy wielu strumieniach.\",\n    \"close_verify_safe\": \"Bezpieczne weryfikowanie zgodne z starszymi klientami\",\n    \"close_verify_safe_desc\": \"Starsze klienty mogą nie łączyć się z Sunshine, proszę wyłączyć tę opcję lub zaktualizować klienta\",\n    \"coder_cabac\": \"cabac -- adaptacyjne binarne kodowanie arytmetyczne - wyższa jakość\",\n    \"coder_cavlc\": \"cavlc - adaptacyjne kodowanie kontekstowe o zmiennej długości - szybsze dekodowanie\",\n    \"configuration\": \"Konfiguracja\",\n    \"controller\": \"Włącz wejście kontrolera\",\n    \"controller_desc\": \"Umożliwia gościom kontrolowanie systemu hosta za pomocą gamepada / kontrolera\",\n    \"credentials_file\": \"Plik poświadczeń\",\n    \"credentials_file_desc\": \"Przechowuj nazwę użytkownika/hasło oddzielnie od pliku stanu Sunshine.\",\n    \"display_device_options_note_desc_windows\": \"System Windows zapisuje różne ustawienia wyświetlania dla każdej kombinacji aktualnie aktywnych wyświetlaczy.\\nSunshine następnie stosuje zmiany do wyświetlacza(-y) należącego do danej kombinacji wyświetlaczy.\\nJeśli odłączysz urządzenie, które było aktywne, gdy Sunshine zastosował ustawienia, zmiany nie mogą zostać\\ncofnięte, chyba że kombinacja może zostać ponownie aktywowana zanim Sunshine spróbuje cofnąć zmiany!\",\n    \"display_device_options_note_windows\": \"Uwaga dotycząca sposobu stosowania ustawień\",\n    \"display_device_options_windows\": \"Opcje urządzenia wyświetlającego\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Aktywuje wyświetlacz, jeśli nie jest jeszcze aktywny\",\n    \"display_device_prep_ensure_active_windows\": \"Automatycznie aktywuj wyświetlacz\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Wyłącza wszystkie inne wyświetlacze i włącza tylko określony\",\n    \"display_device_prep_ensure_only_display_windows\": \"Dezaktywuj inne wyświetlacze i aktywuj tylko wskazany wyświetlacz\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Aktywuje wyświetlacz i ustawia go jako główny\",\n    \"display_device_prep_ensure_primary_windows\": \"Automatycznie aktywuj wyświetlacz i ustaw jako główny\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Używa tylko wirtualnego wyświetlacza do rozszerzonego strumieniowania drugorzędnego\",\n    \"display_device_prep_ensure_secondary_windows\": \"Strumieniowanie wyświetlacza drugorzędnego (tylko wirtualny wyświetlacz)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Brak zmian stanu wyświetlacza; użytkownik musi sam upewnić się, że wyświetlacz jest gotowy\",\n    \"display_device_prep_no_operation_windows\": \"Wyłączone\",\n    \"display_device_prep_windows\": \"Przygotowanie wyświetlacza\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Należy określić co najmniej jedną wartość \\\"odebraną\\\" i jedną \\\"końcową\\\".\\nPuste pole w sekcji \\\"odebrane\\\" oznacza \\\"dopasuj dowolną wartość\\\". Puste pole w sekcji \\\"końcowe\\\" oznacza \\\"zachowaj odebraną wartość\\\".\\nMożesz dopasować konkretną wartość FPS do konkretnej rozdzielczości, jeśli chcesz...\\n\\nUwaga: jeśli opcja \\\"Optymalizuj ustawienia gry\\\" nie jest włączona w kliencie Moonlight, wiersze zawierające wartości rozdzielczości są ignorowane.\",\n    \"display_mode_remapping_desc_windows\": \"Określ, jak konkretna rozdzielczość i/lub częstotliwość odświeżania powinna być przemapowana na inne wartości.\\nMożesz streamować w niższej rozdzielczości, jednocześnie renderując w wyższej rozdzielczości na hoście, uzyskując efekt supersamplingu.\\nMożesz też streamować z wyższym FPS, ograniczając hosta do niższej częstotliwości odświeżania.\\nDopasowywanie odbywa się od góry do dołu. Gdy wpis zostanie dopasowany, pozostałe nie są już sprawdzane, ale nadal są walidowane.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Końcowa częstotliwość odświeżania\",\n    \"display_mode_remapping_final_resolution_windows\": \"Końcowa rozdzielczość\",\n    \"display_mode_remapping_optional\": \"opcjonalnie\",\n    \"display_mode_remapping_received_fps_windows\": \"Odebrane FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"Odebrana rozdzielczość\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Uwaga: jeśli opcja \\\"Optymalizuj ustawienia gry\\\" nie jest włączona w kliencie Moonlight, przemapowanie jest wyłączone.\",\n    \"display_mode_remapping_windows\": \"Przemapuj tryby wyświetlania\",\n    \"display_modes\": \"Tryby wyświetlania\",\n    \"ds4_back_as_touchpad_click\": \"Mapuj przycisk Wstecz/Wybierz na kliknięcie panelu dotykowego\",\n    \"ds4_back_as_touchpad_click_desc\": \"Podczas wymuszania emulacji DS4, mapuj Back/Select na kliknięcie panelu dotykowego\",\n    \"dsu_server_port\": \"DSU Server Port\",\n    \"dsu_server_port_desc\": \"DSU server listening port (default 26760). Sunshine will act as a DSU server to receive client connections and send motion data. Enable DSU server in your client(Yuzu,Ryujinx etc.) and set DSU server address(127.0.0.1) and port(26760)\",\n    \"enable_dsu_server\": \"Włącz serwer DSU\",\n    \"enable_dsu_server_desc\": \"Enable DSU server to receive client connections and send motion data\",\n    \"encoder\": \"Wymuś określony koder\",\n    \"encoder_desc\": \"Wymuś określony koder, w przeciwnym razie Sunshine wybierze najlepszą dostępną opcję. Uwaga: Jeśli określisz koder sprzętowy w systemie Windows, musi on być zgodny z procesorem graficznym, do którego podłączony jest wyświetlacz.\",\n    \"encoder_software\": \"Oprogramowanie\",\n    \"experimental\": \"Eksperymentalne\",\n    \"experimental_features\": \"Funkcje eksperymentalne\",\n    \"external_ip\": \"Zewnętrzny adres IP\",\n    \"external_ip_desc\": \"Jeśli nie podano zewnętrznego adresu IP, Sunshine automatycznie wykryje zewnętrzny adres IP\",\n    \"fec_percentage\": \"Procent FEC\",\n    \"fec_percentage_desc\": \"Procent pakietów korekcji błędów na pakiet danych w każdej klatce wideo. Wyższe wartości mogą skorygować większą utratę pakietów sieciowych, ale kosztem zwiększenia wykorzystania przepustowości.\",\n    \"ffmpeg_auto\": \"auto -- niech ffmpeg zdecyduje (domyślnie)\",\n    \"file_apps\": \"Plik aplikacji\",\n    \"file_apps_desc\": \"Plik, w którym przechowywane są bieżące aplikacje Sunshine.\",\n    \"file_state\": \"Plik stanu\",\n    \"file_state_desc\": \"Plik, w którym przechowywany jest aktualny stan Sunshine\",\n    \"fps\": \"Reklamowane FPS\",\n    \"gamepad\": \"Emulowany typ kontrolera\",\n    \"gamepad_auto\": \"Opcje automatycznego wyboru\",\n    \"gamepad_desc\": \"Wybierz typ kontrolera, który ma być emulowany na hoście\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"Opcje wyboru DS4\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Ustawienia DS4\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Polecenia przygotowujące\",\n    \"global_prep_cmd_desc\": \"Konfiguruje listę poleceń do wykonania przed lub po uruchomieniu dowolnej aplikacji. Jeśli którekolwiek z określonych poleceń przygotowawczych nie powiedzie się, proces uruchamiania aplikacji zostanie przerwany.\",\n    \"hdr_luminance_analysis\": \"Dynamiczne metadane HDR (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Włącza analizę luminancji GPU na klatkę i wstrzykuje dynamiczne metadane HDR10+ (ST 2094-40) i HDR Vivid (CUVA) do zakodowanego strumienia. Zapewnia wskazówki mapowania tonalnego na klatkę dla obsługiwanych wyświetlaczy. Dodaje niewielkie obciążenie GPU (~0,5-1,5ms/klatkę przy wysokich rozdzielczościach). Wyłącz gdy spadki klatek z HDR.\",\n    \"hdr_prep_automatic_windows\": \"Switch on/off the HDR mode as requested by the client\",\n    \"hdr_prep_no_operation_windows\": \"Disabled\",\n    \"hdr_prep_windows\": \"HDR state change\",\n    \"hevc_mode\": \"Obsługa HEVC\",\n    \"hevc_mode_0\": \"Sunshine będzie zalecać obsługę HEVC w oparciu o możliwości kodera (zalecane)\",\n    \"hevc_mode_1\": \"Sunshine nie będzie zalecać wsparcia dla HEVC\",\n    \"hevc_mode_2\": \"Sunshine będzie zalecać wsparcie dla głównego profilu HEVC\",\n    \"hevc_mode_3\": \"Sunshine będzie zalecać obsługę profili HEVC Main i Main10 (HDR)\",\n    \"hevc_mode_desc\": \"Umożliwia klientowi żądanie strumieni wideo HEVC Main lub HEVC Main10. Kodowanie HEVC jest bardziej obciążające dla procesora, więc włączenie tej opcji może zmniejszyć wydajność podczas korzystania z kodowania programowego.\",\n    \"high_resolution_scrolling\": \"Obsługa przewijania w wysokiej rozdzielczości\",\n    \"high_resolution_scrolling_desc\": \"Po włączeniu Sunshine będzie przepuszczać zdarzenia przewijania w wysokiej rozdzielczości od klientów Moonlight. Może to być przydatne do wyłączenia w starszych aplikacjach, które przewijają zbyt szybko zdarzenia przewijania w wysokiej rozdzielczości.\",\n    \"install_steam_audio_drivers\": \"Zainstaluj sterowniki audio Steam\",\n    \"install_steam_audio_drivers_desc\": \"Jeśli zainstalowany jest Steam, automatycznie zainstalowany zostanie sterownik Steam Streaming Speakers do obsługi dźwięku przestrzennego 5.1/7.1 i wyciszania dźwięku hosta.\",\n    \"key_repeat_delay\": \"Opóźnienie powtarzania klawiszy\",\n    \"key_repeat_delay_desc\": \"Kontroluje szybkość powtarzania klawiszy. Początkowe opóźnienie w milisekundach przed powtarzaniem klawiszy.\",\n    \"key_repeat_frequency\": \"Częstotliwość powtarzania klawiszy\",\n    \"key_repeat_frequency_desc\": \"Jak często klawisze powtarzają się co sekundę. Ta konfigurowalna opcja obsługuje wartości dziesiętne.\",\n    \"key_rightalt_to_key_win\": \"Mapuj prawy Alt na klawisz Windows\",\n    \"key_rightalt_to_key_win_desc\": \"Może się zdarzyć, że nie można wysłać klawisza Windows bezpośrednio z Moonlight. W takich przypadkach przydatne może być sprawienie, by Sunshine myślał, że prawy Alt jest klawiszem Windows\",\n    \"key_rightalt_to_key_windows\": \"Map Right Alt key to Windows key\",\n    \"keyboard\": \"Włącz wejście klawiatury\",\n    \"keyboard_desc\": \"Umożliwia gościom kontrolowanie systemu hosta za pomocą klawiatury\",\n    \"lan_encryption_mode\": \"Tryb szyfrowania LAN\",\n    \"lan_encryption_mode_1\": \"Włączone dla obsługiwanych klientów\",\n    \"lan_encryption_mode_2\": \"Wymagane dla wszystkich klientów\",\n    \"lan_encryption_mode_desc\": \"Określa, kiedy szyfrowanie będzie używane podczas przesyłania strumieniowego przez sieć lokalną. Szyfrowanie może zmniejszyć wydajność przesyłania strumieniowego, szczególnie na mniej wydajnych hostach i klientach.\",\n    \"locale\": \"Język\",\n    \"locale_desc\": \"Ustawienia językowe używane w interfejsie użytkownika Sunshine.\",\n    \"log_level\": \"Poziom raportowania\",\n    \"log_level_0\": \"Rozszerzony\",\n    \"log_level_1\": \"Debugowanie\",\n    \"log_level_2\": \"Informacyjne\",\n    \"log_level_3\": \"Ostrzeżenia\",\n    \"log_level_4\": \"Błąd\",\n    \"log_level_5\": \"Krytyczny\",\n    \"log_level_6\": \"Brak\",\n    \"log_level_desc\": \"Minimalny poziom logów wyświetlany w konsoli\",\n    \"log_path\": \"Ścieżka pliku dziennika\",\n    \"log_path_desc\": \"Plik, w którym przechowywane są bieżące dzienniki Sunshine.\",\n    \"max_bitrate\": \"Maksymalny Bitrate\",\n    \"max_bitrate_desc\": \"Maksymalny bitrate (w Kbps), który Sunshine zakoduje stream. Jeśli ustawiony na 0, zawsze będzie używał bitrate żądany przez Moonlight.\",\n    \"max_fps_reached\": \"Osiągnięto maksymalne wartości FPS\",\n    \"max_resolutions_reached\": \"Osiągnięto maksymalną liczbę rozdzielczości\",\n    \"mdns_broadcast\": \"Znajdź ten komputer w lokalnej sieci\",\n    \"mdns_broadcast_desc\": \"Jeśli ta opcja jest włączona, Sunshine pozwoli innym urządzeniom znaleźć ten komputer automatycznie. Moonlight musi być skonfigurowany, aby automatycznie znaleźć ten komputer w lokalnej sieci.\",\n    \"min_threads\": \"Minimalna liczba wątków procesora\",\n    \"min_threads_desc\": \"Zwiększenie wartości nieznacznie zmniejsza wydajność kodowania, ale kompromis jest zwykle warty tego, aby uzyskać wykorzystanie większej liczby rdzeni procesora do kodowania. Idealną wartością jest najniższa wartość, która pozwala na niezawodne kodowanie przy pożądanych ustawieniach strumieniowania na posiadanym sprzęcie.\",\n    \"minimum_fps_target\": \"Minimalny cel FPS\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Różne opcje\",\n    \"motion_as_ds4\": \"Emulacja kontrolera DS4, jeśli kliencki kontroler zgłasza obecność czujników ruchu\",\n    \"motion_as_ds4_desc\": \"Jeśli opcja ta jest wyłączona, czujniki ruchu nie będą brane pod uwagę podczas wyboru typu kontrolera.\",\n    \"mouse\": \"Włącz wejście myszy\",\n    \"mouse_desc\": \"Umożliwia gościom kontrolowanie systemu hosta za pomocą myszy\",\n    \"native_pen_touch\": \"Natywna obsługa pióra/dotyku\",\n    \"native_pen_touch_desc\": \"Po włączeniu Sunshine będzie przekazywać natywne zdarzenia pióra/dotyku z klienta Moonlight. Może to być przydatne do wyłączenia w starszych aplikacjach bez natywnej obsługi pióra/dotyku.\",\n    \"no_fps\": \"Nie dodano wartości FPS\",\n    \"no_resolutions\": \"Nie dodano rozdzielczości\",\n    \"notify_pre_releases\": \"Powiadomienia o wydaniu wstępnym\",\n    \"notify_pre_releases_desc\": \"Czy otrzymywać powiadomienia o nowych przedpremierowych wersjach Sunshine\",\n    \"nvenc_h264_cavlc\": \"Preferowanie CAVLC nad CABAC w H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Prostsza forma kodowania entropijnego. CAVLC wymaga około 10% więcej bitrate dla tej samej jakości. Dotyczy tylko naprawdę starych urządzeń dekodujących.\",\n    \"nvenc_latency_over_power\": \"Niższe opóźnienie kodowania przedkładane nad oszczędność energii\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine żąda maksymalnej prędkości zegara GPU podczas strumieniowania, aby zmniejszyć opóźnienie kodowania. Wyłączenie tej funkcji nie jest zalecane, ponieważ może to prowadzić do znacznego zwiększenia opóźnień kodowania.\",\n    \"nvenc_lookahead_depth\": \"Głębokość analizy (Lookahead)\",\n    \"nvenc_lookahead_depth_desc\": \"Liczba klatek do analizy w przód podczas kodowania (0-32). Lookahead poprawia jakość kodowania, zwłaszcza w złożonych scenach, zapewniając lepszą estymację ruchu i dystrybucję bitrate'u. Wyższe wartości poprawiają jakość, ale zwiększają opóźnienie kodowania. Ustaw 0, aby wyłączyć. Wymaga NVENC SDK 13.0 (1202) lub nowszego.\",\n    \"nvenc_lookahead_level\": \"Poziom analizy (Lookahead)\",\n    \"nvenc_lookahead_level_0\": \"Poziom 0 (najniższa jakość, najszybciej)\",\n    \"nvenc_lookahead_level_1\": \"Poziom 1\",\n    \"nvenc_lookahead_level_2\": \"Poziom 2\",\n    \"nvenc_lookahead_level_3\": \"Poziom 3 (najwyższa jakość, najwolniej)\",\n    \"nvenc_lookahead_level_autoselect\": \"Wybór automatyczny (pozwól sterownikowi wybrać optymalny poziom)\",\n    \"nvenc_lookahead_level_desc\": \"Poziom jakości Lookahead. Wyższe poziomy poprawiają jakość kosztem wydajności. Ta opcja działa tylko wtedy, gdy lookahead_depth jest większe niż 0. Wymaga NVENC SDK 13.0 (1202) lub nowszego.\",\n    \"nvenc_lookahead_level_disabled\": \"Wyłączone (to samo co poziom 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Prezentacja OpenGL/Vulkan na DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine nie może przechwytywać pełnoekranowych programów OpenGL i Vulkan z pełną liczbą klatek na sekundę, chyba że są one wyświetlane na DXGI. Jest to ustawienie ogólnosystemowe, które jest przywracane po wyjściu z programu Sunshine.\",\n    \"nvenc_preset\": \"Wstępne ustawienia wydajności\",\n    \"nvenc_preset_1\": \"(najszybszy, domyślny)\",\n    \"nvenc_preset_7\": \"(najwolniejszy)\",\n    \"nvenc_preset_desc\": \"Wyższe liczby poprawiają kompresję (jakość przy danym bitrate) kosztem zwiększonego opóźnienia kodowania. Zaleca się zmianę tylko wtedy, gdy jest to ograniczone przez sieć lub dekoder, w przeciwnym razie podobny efekt można osiągnąć poprzez zwiększenie bitrate.\",\n    \"nvenc_rate_control\": \"Tryb kontroli bitrate'u\",\n    \"nvenc_rate_control_cbr\": \"CBR (Stały bitrate) - Niskie opóźnienie\",\n    \"nvenc_rate_control_desc\": \"Wybierz tryb kontroli bitrate'u. CBR (Stały bitrate) zapewnia stały bitrate dla strumieniowania z niskim opóźnieniem. VBR (Zmienny bitrate) pozwala na zmianę bitrate'u w zależności od złożoności sceny, zapewniając lepszą jakość dla złożonych scen kosztem zmiennego bitrate'u.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Zmienny bitrate) - Lepsza jakość\",\n    \"nvenc_realtime_hags\": \"Użycie priorytetu czasu rzeczywistego w harmonogramie sprzętowej akceleracji procesora graficznego\",\n    \"nvenc_realtime_hags_desc\": \"Obecnie sterowniki NVIDIA mogą zawieszać się w koderze, gdy włączony jest HAGS, używany jest priorytet czasu rzeczywistego, a wykorzystanie pamięci VRAM jest bliskie maksimum. Wyłączenie tej opcji obniża priorytet do wysokiego, omijając zamrożenie kosztem zmniejszonej wydajności przechwytywania, gdy GPU jest mocno obciążony.\",\n    \"nvenc_spatial_aq\": \"Spatial AQ\",\n    \"nvenc_spatial_aq_desc\": \"Przypisuje wyższe wartości QP do płaskich regionów wideo. Zalecane włączenie podczas streamowania przy niższych przepływnościach.\",\n    \"nvenc_spatial_aq_disabled\": \"Disabled (faster, default)\",\n    \"nvenc_spatial_aq_enabled\": \"Enabled (slower)\",\n    \"nvenc_split_encode\": \"Podzielone kodowanie klatek\",\n    \"nvenc_split_encode_desc\": \"Split the encoding of each video frame over multiple NVENC hardware units. Significantly reduces encoding latency with a marginal compression efficiency penalty. This option is ignored if your GPU has a singular NVENC unit.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Driver decides (default)\",\n    \"nvenc_split_encode_four_strips\": \"Wymuś podział na 4 pasy (wymaga 4+ silników NVENC)\",\n    \"nvenc_split_encode_three_strips\": \"Wymuś podział na 3 pasy (wymaga 3+ silników NVENC)\",\n    \"nvenc_split_encode_two_strips\": \"Wymuś podział na 2 pasy (wymaga 2+ silników NVENC)\",\n    \"nvenc_target_quality\": \"Jakość docelowa (tryb VBR)\",\n    \"nvenc_target_quality_desc\": \"Docelowy poziom jakości dla trybu VBR (0-51 dla H.264/HEVC, 0-63 dla AV1). Niższe wartości = wyższa jakość. Ustaw 0 dla automatycznego wyboru jakości. Używane tylko wtedy, gdy tryb kontroli bitrate'u to VBR.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Tryb dwuprzebiegowy\",\n    \"nvenc_twopass_desc\": \"Dodaje wstępne przejście kodowania. Pozwala to wykryć więcej wektorów ruchu, lepiej rozłożyć przepływność w ramce i ściślej przestrzegać limitów przepływności. Wyłączenie tej funkcji nie jest zalecane, ponieważ może to prowadzić do sporadycznych przekroczeń przepływności i późniejszej utraty pakietów.\",\n    \"nvenc_twopass_disabled\": \"Wyłączony (najszybszy, niezalecany)\",\n    \"nvenc_twopass_full_res\": \"Pełna rozdzielczość (wolniej)\",\n    \"nvenc_twopass_quarter_res\": \"Rozdzielczość ćwiartki (szybsza, domyślna)\",\n    \"nvenc_vbv_increase\": \"Procentowy wzrost VBV/HRD w pojedynczej ramce\",\n    \"nvenc_vbv_increase_desc\": \"Domyślnie Sunshine używa jednoklatkowego VBV/HRD, co oznacza, że żaden zakodowany rozmiar klatki wideo nie powinien przekraczać żądanej przepływności podzielonej przez żądaną liczbę klatek na sekundę. Złagodzenie tego ograniczenia może być korzystne i działać jako zmienna przepływność o niskim opóźnieniu, ale może również prowadzić do utraty pakietów, jeśli sieć nie ma bufora, aby obsłużyć skoki przepływności. Maksymalna akceptowana wartość to 400, co odpowiada 5-krotnemu zwiększeniu górnego limitu rozmiaru zakodowanej ramki wideo.\",\n    \"origin_web_ui_allowed\": \"Interfejs Origin Web UI dozwolony\",\n    \"origin_web_ui_allowed_desc\": \"Pochodzenie adresu zdalnego punktu końcowego, któremu nie odmówiono dostępu do interfejsu Web UI\",\n    \"origin_web_ui_allowed_lan\": \"Tylko osoby w sieci LAN mogą uzyskać dostęp do interfejsu użytkownika\",\n    \"origin_web_ui_allowed_pc\": \"Tylko localhost może uzyskać dostęp do Web UI\",\n    \"origin_web_ui_allowed_wan\": \"Każdy może uzyskać dostęp do Web UI\",\n    \"output_name_desc_unix\": \"Podczas uruchamiania Sunshine powinieneś zobaczyć listę wykrytych wyświetlaczy. Uwaga: Należy użyć wartości id wewnątrz nawiasu. Poniżej znajduje się przykład; rzeczywiste dane wyjściowe można znaleźć w zakładce Rozwiązywanie problemów.\",\n    \"output_name_desc_windows\": \"Ręczne określenie identyfikatora urządzenia wyświetlającego, które ma być używane do przechwytywania. Jeśli nie zostanie ustawione, przechwytywany będzie główny wyświetlacz. Uwaga: Jeśli powyżej określono procesor graficzny, ten wyświetlacz musi być do niego podłączony. Podczas uruchamiania Sunshine powinna zostać wyświetlona lista wykrytych wyświetlaczy. Poniżej znajduje się przykład; rzeczywisty wynik można znaleźć w zakładce Rozwiązywanie problemów.\",\n    \"output_name_unix\": \"Wyświetlany numer\",\n    \"output_name_windows\": \"Wyświetl identyfikator urządzenia\",\n    \"ping_timeout\": \"Limit czasu ping\",\n    \"ping_timeout_desc\": \"Jak długo czekać w milisekundach na dane z Moonlight przed zamknięciem strumienia\",\n    \"pkey\": \"Klucz prywatny\",\n    \"pkey_desc\": \"Klucz prywatny używany do parowania interfejsu użytkownika i klienta Moonlight. Aby zapewnić najlepszą kompatybilność, powinien to być klucz prywatny RSA-2048.\",\n    \"port\": \"Port\",\n    \"port_alert_1\": \"Sunshine nie może używać portów poniżej 1024!\",\n    \"port_alert_2\": \"Porty powyżej 65535 nie są dostępne!\",\n    \"port_desc\": \"Ustaw rodzinę portów używanych przez Sunshine\",\n    \"port_http_port_note\": \"Ten port służy do łączenia się z Moonlight.\",\n    \"port_note\": \"Uwaga\",\n    \"port_port\": \"Port\",\n    \"port_protocol\": \"Protokół\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Wystawienie interfejsu użytkownika na Internet stanowi zagrożenie dla bezpieczeństwa! Postępuj na własne ryzyko!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Parametr kwantyzacji\",\n    \"qp_desc\": \"Niektóre urządzenia mogą nie obsługiwać stałej szybkości transmisji. W przypadku tych urządzeń zamiast tego używana jest wartość QP. Wyższa wartość oznacza większą kompresję, ale niższą jakość.\",\n    \"qsv_coder\": \"Koder QuickSync (H264)\",\n    \"qsv_preset\": \"Ustawienie wstępne QuickSync\",\n    \"qsv_preset_fast\": \"szybki (niska jakość)\",\n    \"qsv_preset_faster\": \"szybciej (niższa jakość)\",\n    \"qsv_preset_medium\": \"średni (domyślnie)\",\n    \"qsv_preset_slow\": \"wolny (dobra jakość)\",\n    \"qsv_preset_slower\": \"wolniej (lepsza jakość)\",\n    \"qsv_preset_slowest\": \"najwolniejszy (najlepsza jakość)\",\n    \"qsv_preset_veryfast\": \"najszybszy (najniższa jakość)\",\n    \"qsv_slow_hevc\": \"Zezwalaj na wolne kodowanie HEVC\",\n    \"qsv_slow_hevc_desc\": \"Może to umożliwić kodowanie HEVC na starszych procesorach graficznych Intel, kosztem wyższego wykorzystania GPU i gorszej wydajności.\",\n    \"refresh_rate_change_automatic_windows\": \"Use FPS value provided by the client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Enter the refresh rate to be used\",\n    \"refresh_rate_change_manual_windows\": \"Use manually entered refresh rate\",\n    \"refresh_rate_change_no_operation_windows\": \"Disabled\",\n    \"refresh_rate_change_windows\": \"FPS change\",\n    \"res_fps_desc\": \"Tryby wyświetlania reklamowane przez Sunshine. Niektóre wersje Moonlight, takie jak Moonlight-nx (Switch), polegają na tych listach, aby upewnić się, że żądane rozdzielczości i fps są obsługiwane. To ustawienie nie zmienia sposobu wysyłania strumienia ekranu do Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Use resolution provided by the client\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_manual_windows\": \"Use manually entered resolution\",\n    \"resolution_change_no_operation_windows\": \"Disabled\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_windows\": \"Resolution change\",\n    \"resolutions\": \"Reklamowane rozdzielczości\",\n    \"restart_note\": \"Sunshine uruchamia się ponownie, aby zastosować zmiany.\",\n    \"sleep_mode\": \"Tryb uśpienia\",\n    \"sleep_mode_away\": \"Tryb nieobecności (Ekran wyłączony, natychmiastowe wybudzenie)\",\n    \"sleep_mode_desc\": \"Kontroluje, co się dzieje, gdy klient wysyła polecenie uśpienia. Wstrzymanie (S3): tradycyjne uśpienie, niskie zużycie energii, ale wymaga WOL do wybudzenia. Hibernacja (S4): zapis na dysk, bardzo niskie zużycie energii. Tryb nieobecności: ekran się wyłącza, ale system działa dalej dla natychmiastowego wybudzenia - idealny dla serwerów strumieniowania gier.\",\n    \"sleep_mode_hibernate\": \"Hibernacja (S4)\",\n    \"sleep_mode_suspend\": \"Wstrzymanie (S3)\",\n    \"stream_audio\": \"Włącz przesyłanie audio\",\n    \"stream_audio_desc\": \"Wyłącz tę opcję, aby zatrzymać przesyłanie audio.\",\n    \"stream_mic\": \"Włącz przesyłanie mikrofonu\",\n    \"stream_mic_desc\": \"Wyłącz tę opcję, aby zatrzymać przesyłanie mikrofonu.\",\n    \"stream_mic_download_btn\": \"Pobierz wirtualny mikrofon\",\n    \"stream_mic_download_confirm\": \"Zostaniesz przekierowany na stronę pobierania wirtualnego mikrofonu. Kontynuować?\",\n    \"stream_mic_note\": \"Ta funkcja wymaga zainstalowania wirtualnego mikrofonu\",\n    \"sunshine_name\": \"Nazwa Sunshine\",\n    \"sunshine_name_desc\": \"Nazwa wyświetlana przez Moonlight. Jeśli nie zostanie określona, używana jest nazwa hosta komputera\",\n    \"sw_preset\": \"Ustawienia wstępne SW\",\n    \"sw_preset_desc\": \"Optymalizacja kompromisu między szybkością kodowania (zakodowane klatki na sekundę) a wydajnością kompresji (jakość na bit w strumieniu bitów). Domyślnie superszybki.\",\n    \"sw_preset_fast\": \"szybki\",\n    \"sw_preset_faster\": \"szybciej\",\n    \"sw_preset_medium\": \"średni\",\n    \"sw_preset_slow\": \"wolny\",\n    \"sw_preset_slower\": \"wolniejszy\",\n    \"sw_preset_superfast\": \"superszybki (domyślnie)\",\n    \"sw_preset_ultrafast\": \"ultraszybki\",\n    \"sw_preset_veryfast\": \"bardzo szybki\",\n    \"sw_preset_veryslow\": \"bardzo wolny\",\n    \"sw_tune\": \"Dostrajanie SW\",\n    \"sw_tune_animation\": \"animacja - dobra do kreskówek; wykorzystuje wyższe odblokowanie i więcej klatek referencyjnych\",\n    \"sw_tune_desc\": \"Opcje strojenia, które są stosowane po ustawieniu wstępnym. Domyślne ustawienie to zerolatency.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- umożliwia szybsze dekodowanie poprzez wyłączenie niektórych filtrów\",\n    \"sw_tune_film\": \"Film - użycie dla wysokiej jakości treści filmowych; obniża deblocking\",\n    \"sw_tune_grain\": \"grain - zachowuje strukturę ziarna w starym, ziarnistym materiale filmowym\",\n    \"sw_tune_stillimage\": \"stillimage - dobre dla zawartości podobnej do pokazu slajdów\",\n    \"sw_tune_zerolatency\": \"zerolatency -- dobre dla szybkiego kodowania i strumieniowania z niskim opóźnieniem (domyślnie)\",\n    \"system_tray\": \"Włącz zasobnik systemowy\",\n    \"system_tray_desc\": \"Czy włączyć zasobnik systemowy. Jeśli włączone, Sunshine wyświetli ikonę w zasobniku systemowym i można nim sterować z zasobnika systemowego.\",\n    \"touchpad_as_ds4\": \"Emulacja kontrolera DS4, jeśli kliencki kontroler zgłasza obecność touchpada\",\n    \"touchpad_as_ds4_desc\": \"Jeśli opcja ta jest wyłączona, obecność touchpada nie będzie brana pod uwagę podczas wyboru typu kontrolera.\",\n    \"unsaved_changes_tooltip\": \"Masz niezapisane zmiany. Kliknij, aby zapisać.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Automatycznie skonfiguruj przekierowanie portów do przesyłania strumieniowego przez Internet\",\n    \"variable_refresh_rate\": \"Zmienna częstotliwość odświeżania (VRR)\",\n    \"variable_refresh_rate_desc\": \"Pozwól, aby liczba klatek strumienia wideo odpowiadała liczbie klatek renderowania dla obsługi VRR. Po włączeniu kodowanie odbywa się tylko wtedy, gdy dostępne są nowe klatki, co pozwala strumieniowi podążać za rzeczywistą liczbą klatek renderowania.\",\n    \"vdd_reuse_desc_windows\": \"Po włączeniu wszyscy klienci będą współdzielić ten sam VDD (Virtual Display Device). Po wyłączeniu (domyślnie) każdy klient otrzymuje własny VDD. Włącz tę opcję dla szybszego przełączania klientów, ale pamiętaj, że wszyscy klienci będą współdzielić te same ustawienia wyświetlania.\",\n    \"vdd_reuse_windows\": \"Ponowne użycie tego samego VDD dla wszystkich klientów\",\n    \"virtual_display\": \"Wirtualny wyświetlacz\",\n    \"virtual_mouse\": \"Sterownik wirtualnej myszy\",\n    \"virtual_mouse_desc\": \"Po włączeniu Sunshine użyje sterownika Zako Virtual Mouse (jeśli zainstalowany) do symulacji wejścia myszy na poziomie HID. Pozwala grom używającym Raw Input odbierać zdarzenia myszy. Po wyłączeniu lub braku sterownika, powrót do SendInput.\",\n    \"virtual_sink\": \"Wirtualne wyjście audio\",\n    \"virtual_sink_desc\": \"Ręczne określenie używanego wirtualnego urządzenia audio. Jeśli nie jest ustawione, urządzenie zostanie wybrane automatycznie. Zdecydowanie zalecamy pozostawienie tego pola pustego, aby korzystać z automatycznego wyboru urządzenia!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Zainstalować sterownik wirtualnej myszy?\",\n    \"vmouse_confirm_uninstall\": \"Odinstalować sterownik wirtualnej myszy?\",\n    \"vmouse_install\": \"Zainstaluj sterownik\",\n    \"vmouse_installing\": \"Instalowanie...\",\n    \"vmouse_note\": \"Sterownik wirtualnej myszy wymaga osobnej instalacji. Użyj panelu sterowania Sunshine do instalacji lub zarządzania sterownikiem.\",\n    \"vmouse_refresh\": \"Odśwież status\",\n    \"vmouse_status_installed\": \"Zainstalowany (nieaktywny)\",\n    \"vmouse_status_not_installed\": \"Nie zainstalowany\",\n    \"vmouse_status_running\": \"Uruchomiony\",\n    \"vmouse_uninstall\": \"Odinstaluj sterownik\",\n    \"vmouse_uninstalling\": \"Odinstalowywanie...\",\n    \"vt_coder\": \"Koder VideoToolbox\",\n    \"vt_realtime\": \"Kodowanie w czasie rzeczywistym VideoToolbox\",\n    \"vt_software\": \"Kodowanie oprogramowania VideoToolbox\",\n    \"vt_software_allowed\": \"Dozwolone\",\n    \"vt_software_forced\": \"Wymuszone\",\n    \"wan_encryption_mode\": \"Tryb szyfrowania WAN\",\n    \"wan_encryption_mode_1\": \"Włączone dla obsługiwanych klientów (domyślnie)\",\n    \"wan_encryption_mode_2\": \"Wymagane dla wszystkich klientów\",\n    \"wan_encryption_mode_desc\": \"Określa, kiedy szyfrowanie będzie używane podczas przesyłania strumieniowego przez Internet. Szyfrowanie może zmniejszyć wydajność przesyłania strumieniowego, szczególnie na mniej wydajnych hostach i klientach.\",\n    \"webhook_curl_command\": \"Polecenie\",\n    \"webhook_curl_command_desc\": \"Skopiuj następujące polecenie do terminala, aby przetestować, czy webhook działa poprawnie:\",\n    \"webhook_curl_copy_failed\": \"Kopiowanie nie powiodło się, wybierz i skopiuj ręcznie\",\n    \"webhook_enabled\": \"Powiadomienia Webhook\",\n    \"webhook_enabled_desc\": \"Po włączeniu, Sunshine będzie wysyłać powiadomienia o zdarzeniach na określony URL Webhook\",\n    \"webhook_group\": \"Ustawienia powiadomień Webhook\",\n    \"webhook_skip_ssl_verify\": \"Pomiń weryfikację certyfikatu SSL\",\n    \"webhook_skip_ssl_verify_desc\": \"Pomiń weryfikację certyfikatu SSL dla połączeń HTTPS, tylko do testowania lub certyfikatów z podpisem własnym\",\n    \"webhook_test\": \"Test\",\n    \"webhook_test_failed\": \"Test Webhook nie powiódł się\",\n    \"webhook_test_failed_note\": \"Uwaga: Sprawdź, czy URL jest poprawny, lub sprawdź konsolę przeglądarki, aby uzyskać więcej informacji.\",\n    \"webhook_test_success\": \"Test Webhook zakończony sukcesem!\",\n    \"webhook_test_success_cors_note\": \"Uwaga: Ze względu na ograniczenia CORS, status odpowiedzi serwera nie może być potwierdzony.\\nŻądanie zostało wysłane. Jeśli webhook jest poprawnie skonfigurowany, wiadomość powinna zostać dostarczona.\\n\\nSugestia: Sprawdź kartę Sieć w narzędziach deweloperskich przeglądarki, aby zobaczyć szczegóły żądania.\",\n    \"webhook_test_url_required\": \"Proszę najpierw wprowadzić URL Webhook\",\n    \"webhook_timeout\": \"Timeout żądania\",\n    \"webhook_timeout_desc\": \"Timeout dla żądań Webhook w milisekundach, zakres 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"URL do odbierania powiadomień o zdarzeniach, obsługuje protokoły HTTP/HTTPS\",\n    \"wgc_checking_mode\": \"Sprawdzanie trybu...\",\n    \"wgc_checking_running_mode\": \"Sprawdzanie trybu działania...\",\n    \"wgc_control_panel_only\": \"Ta funkcja jest dostępna tylko w Panelu sterowania Sunshine\",\n    \"wgc_mode_switch_failed\": \"Nie udało się przełączyć trybu\",\n    \"wgc_mode_switch_started\": \"Zainicjowano przełączanie trybu. Jeśli pojawi się monit UAC, kliknij 'Tak', aby potwierdzić.\",\n    \"wgc_service_mode_warning\": \"Przechwytywanie WGC wymaga działania w trybie użytkownika. Jeśli aktualnie działasz w trybie usługi, kliknij powyższy przycisk, aby przełączyć się na tryb użytkownika.\",\n    \"wgc_switch_to_service_mode\": \"Przełącz na tryb usługi\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Aktualnie działa w trybie użytkownika. Kliknij, aby przełączyć na tryb usługi.\",\n    \"wgc_switch_to_user_mode\": \"Przełącz na tryb użytkownika\",\n    \"wgc_switch_to_user_mode_tooltip\": \"Przechwytywanie WGC wymaga działania w trybie użytkownika. Kliknij ten przycisk, aby przełączyć się na tryb użytkownika.\",\n    \"wgc_user_mode_available\": \"Aktualnie działa w trybie użytkownika. Przechwytywanie WGC jest dostępne.\",\n    \"window_title\": \"Tytuł okna\",\n    \"window_title_desc\": \"Tytuł okna do przechwycenia (częściowe dopasowanie, bez rozróżniania wielkości liter). Jeśli pozostawisz puste, nazwa aktualnie uruchomionej aplikacji zostanie użyta automatycznie.\",\n    \"window_title_placeholder\": \"np. Nazwa Aplikacji\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine jest samodzielnym hostem strumienia gry dla Moonlight.\",\n    \"download\": \"Pobierz\",\n    \"installed_version_not_stable\": \"Korzystasz z przedpremierowej wersji Sunshine. Mogą wystąpić błędy lub inne problemy. Prosimy o zgłaszanie wszelkich napotkanych problemów. Dziękujemy za pomoc w ulepszaniu oprogramowania Sunshine!\",\n    \"loading_latest\": \"Ładowanie najnowszej wersji...\",\n    \"new_pre_release\": \"Dostępna jest nowa wersja przedpremierowa!\",\n    \"new_stable\": \"Nowa stabilna wersja jest już dostępna!\",\n    \"startup_errors\": \"<b>Uwaga!</b> Sunshine wykrył te błędy podczas uruchamiania. <b>ZDECYDOWANIE ZALECAMY</b> ich naprawienie przed rozpoczęciem streamowania.\",\n    \"update_download_confirm\": \"Za chwilę otworzysz stronę pobierania aktualizacji w przeglądarce. Kontynuować?\",\n    \"version_dirty\": \"Dziękujemy za pomoc w ulepszaniu oprogramowania Sunshine!\",\n    \"version_latest\": \"Korzystasz z najnowszej wersji Sunshine\",\n    \"view_logs\": \"Wyświetl dzienniki\",\n    \"welcome\": \"Witaj, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Aplikacje\",\n    \"configuration\": \"Konfiguracja\",\n    \"home\": \"Strona główna\",\n    \"password\": \"Zmień hasło\",\n    \"pin\": \"Pin\",\n    \"theme_auto\": \"Auto\",\n    \"theme_dark\": \"Ciemny\",\n    \"theme_light\": \"Jasny\",\n    \"toggle_theme\": \"Wygląd\",\n    \"troubleshoot\": \"Rozwiązywanie problemów\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Potwierdź hasło\",\n    \"current_creds\": \"Aktualne dane logowania\",\n    \"new_creds\": \"Nowe dane logowania\",\n    \"new_username_desc\": \"Jeśli nie zostanie podane, nazwa użytkownika nie ulegnie zmianie\",\n    \"password_change\": \"Zmiana hasła\",\n    \"success_msg\": \"Hasło zostało pomyślnie zmienione! Strona zostanie wkrótce przeładowana, a przeglądarka poprosi o podanie nowych danych uwierzytelniających.\"\n  },\n  \"pin\": {\n    \"actions\": \"Akcje\",\n    \"cancel_editing\": \"Anuluj edycję\",\n    \"client_name\": \"Nazwa\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Potwierdź usunięcie\",\n    \"delete_client\": \"Usuń klienta\",\n    \"delete_confirm_message\": \"Czy na pewno chcesz usunąć <strong>{name}</strong>?\",\n    \"delete_warning\": \"Tej akcji nie można cofnąć.\",\n    \"device_name\": \"Nazwa urządzenia\",\n    \"device_size\": \"Rozmiar urządzenia\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Duże - TV\",\n    \"device_size_medium\": \"Średnie - Tablet\",\n    \"device_size_small\": \"Małe - Telefon\",\n    \"edit_client_settings\": \"Edytuj ustawienia klienta\",\n    \"hdr_profile\": \"Profil HDR\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Ładowanie...\",\n    \"loading_clients\": \"Ładowanie klientów...\",\n    \"modify_in_gui\": \"Proszę zmodyfikować w interfejsie graficznym\",\n    \"none\": \"-- Brak --\",\n    \"or_manual_pin\": \"lub wprowadź PIN ręcznie\",\n    \"pair_failure\": \"Parowanie nie powiodło się: Sprawdź, czy kod PIN został wpisany poprawnie\",\n    \"pair_success\": \"Sukces! Sprawdź Moonlight, aby kontynuować\",\n    \"pin_pairing\": \"Parowanie PIN\",\n    \"qr_expires_in\": \"Wygasa za\",\n    \"qr_generate\": \"Wygeneruj kod QR\",\n    \"qr_paired_success\": \"Sparowano pomyślnie!\",\n    \"qr_pairing\": \"Parowanie kodem QR\",\n    \"qr_pairing_desc\": \"Wygeneruj kod QR do szybkiego parowania. Zeskanuj go klientem Moonlight, aby sparować automatycznie.\",\n    \"qr_pairing_warning\": \"Funkcja eksperymentalna. Jeśli parowanie się nie powiedzie, użyj ręcznego parowania PIN poniżej. Uwaga: Ta funkcja działa tylko w sieci LAN.\",\n    \"qr_refresh\": \"Odśwież kod QR\",\n    \"remove_paired_devices_desc\": \"Usuń sparowane urządzenia.\",\n    \"save_changes\": \"Zapisz zmiany\",\n    \"save_failed\": \"Nie udało się zapisać ustawień klienta. Proszę spróbować ponownie.\",\n    \"save_or_cancel_first\": \"Najpierw zapisz lub anuluj edycję\",\n    \"send\": \"Wyślij\",\n    \"unknown_client\": \"Nieznany klient\",\n    \"unpair_all_confirm\": \"Czy na pewno chcesz rozłączyć wszystkich klientów? Tej akcji nie można cofnąć.\",\n    \"unsaved_changes\": \"Niezapisane zmiany\",\n    \"warning_msg\": \"Upewnij się, że masz dostęp do klienta, z którym się łączysz. To oprogramowanie może dać całkowitą kontrolę nad komputerem, więc bądź ostrożny!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android zalecany\",\n    \"client_downloads\": \"Pobieranie klientów\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"Dyskusje GitHub\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Dołącz do społeczności\",\n    \"join_group_desc\": \"Uzyskaj pomoc i dziel się doświadczeniami\",\n    \"legal\": \"Legal\",\n    \"legal_desc\": \"Kontynuując korzystanie z tego oprogramowania, użytkownik wyraża zgodę na warunki określone w poniższych dokumentach.\",\n    \"license\": \"Licencja\",\n    \"lizardbyte_website\": \"Strona internetowa LizardByte\",\n    \"official_website\": \"Official Website\",\n    \"official_website_title\": \"AlkaidLab - Oficjalna strona\",\n    \"open_source\": \"Otwarty kod źródłowy\",\n    \"open_source_desc\": \"Star & Fork aby wesprzeć projekt\",\n    \"quick_start\": \"Szybki start\",\n    \"resources\": \"Zasoby\",\n    \"resources_desc\": \"Zasoby dla Sunshine!\",\n    \"third_party_desc\": \"Powiadomienia o komponentach firm trzecich\",\n    \"third_party_moonlight\": \"Przyjazne linki\",\n    \"third_party_notice\": \"Powiadomienia strony trzeciej\",\n    \"tutorial\": \"Poradnik\",\n    \"tutorial_desc\": \"Szczegółowy przewodnik konfiguracji i użytkowania\",\n    \"view_license\": \"Zobacz pełną licencję\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Virtual Display\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"Go to Repository\",\n    \"harmony_modal_desc\": \"For HarmonyOS NEXT Moonlight, please search for Moonlight V+ in the HarmonyOS App Store\",\n    \"harmony_modal_link_notice\": \"This link will redirect to the project repository\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"sekund\",\n    \"restart_desc\": \"Konfiguracja zapisana. Sunshine restartuje się, aby zastosować ustawienia wyświetlania.\",\n    \"restart_go_now\": \"Idź teraz\",\n    \"restart_title\": \"Ponowne uruchamianie Sunshine\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"Podstawowe ustawienia są teraz aktywne. Możesz od razu rozpocząć strumieniowanie za pomocą klienta Moonlight!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"Wyświetlacz bazowy (VDD) to wbudowany inteligentny wirtualny wyświetlacz Sunshine Foundation, obsługujący dowolną rozdzielczość, liczbę klatek na sekundę i optymalizację HDR. Jest preferowanym wyborem do streamingu z wyłączonym ekranem i streamingu na rozszerzony wyświetlacz.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Zapewnij aktywację\",\n    \"step3_ensure_active_desc\": \"Aktywuje wyświetlacz, jeśli nie jest jeszcze aktywny\",\n    \"step3_ensure_only_display\": \"Zapewnij jedyny wyświetlacz\",\n    \"step3_ensure_only_display_desc\": \"Wyłącza wszystkie inne wyświetlacze i włącza tylko określony (zalecane)\",\n    \"step3_ensure_primary\": \"Zapewnij wyświetlacz główny\",\n    \"step3_ensure_primary_desc\": \"Aktywuje wyświetlacz i ustawia go jako główny\",\n    \"step3_ensure_secondary\": \"Strumieniowanie drugorzędne\",\n    \"step3_ensure_secondary_desc\": \"Używa tylko wirtualnego wyświetlacza do rozszerzonego strumieniowania drugorzędnego\",\n    \"step3_no_operation\": \"Brak operacji\",\n    \"step3_no_operation_desc\": \"Brak zmian stanu wyświetlacza; użytkownik musi sam upewnić się, że wyświetlacz jest gotowy\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Advanced\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Config Files\",\n    \"general\": \"General\",\n    \"input\": \"Input\",\n    \"network\": \"Network\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analizowanie...\",\n    \"ai_analyzing_logs\": \"Analizowanie logów, proszę czekać...\",\n    \"ai_config\": \"Konfiguracja AI\",\n    \"ai_copy_result\": \"Kopiuj\",\n    \"ai_diagnosis\": \"Diagnostyka AI\",\n    \"ai_diagnosis_title\": \"Diagnostyka AI logów\",\n    \"ai_error\": \"Analiza nieudana\",\n    \"ai_key_local\": \"Klucz API jest przechowywany tylko lokalnie i nigdy nie jest przesyłany\",\n    \"ai_model\": \"Model\",\n    \"ai_provider\": \"Dostawca\",\n    \"ai_reanalyze\": \"Ponowna analiza\",\n    \"ai_result\": \"Wynik diagnostyki\",\n    \"ai_retry\": \"Ponów\",\n    \"ai_start_diagnosis\": \"Rozpocznij diagnostykę\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Jeśli musisz natychmiast zamknąć Sunshine, możesz użyć tej funkcji. Pamiętaj, że będziesz musiał ręcznie uruchomić go ponownie po zamknięciu.\",\n    \"boom_sunshine_success\": \"Sunshine został zamknięty\",\n    \"confirm_boom\": \"Naprawdę chcesz wyjść?\",\n    \"confirm_boom_desc\": \"Więc naprawdę chcesz wyjść? Cóż, nie mogę cię zatrzymać, śmiało i kliknij ponownie\",\n    \"confirm_logout\": \"Potwierdzić wylogowanie?\",\n    \"confirm_logout_desc\": \"Aby uzyskać dostęp do interfejsu webowego, będziesz musiał ponownie wpisać hasło.\",\n    \"copy_config\": \"Kopiuj konfigurację\",\n    \"copy_config_error\": \"Nie udało się skopiować konfiguracji\",\n    \"copy_config_success\": \"Konfiguracja skopiowana do schowka!\",\n    \"copy_logs\": \"Kopiuj dzienniki\",\n    \"download_logs\": \"Pobierz dzienniki\",\n    \"force_close\": \"Wymuś zamknięcie\",\n    \"force_close_desc\": \"Jeśli Moonlight skarży się na aktualnie uruchomioną aplikację, wymuszenie jej zamknięcia powinno rozwiązać problem.\",\n    \"force_close_error\": \"Błąd podczas zamykania aplikacji\",\n    \"force_close_success\": \"Aplikacja zamknięta pomyślnie!\",\n    \"ignore_case\": \"Ignoruj wielkość liter\",\n    \"logout\": \"Wyloguj się\",\n    \"logout_desc\": \"Wyloguj się. Może być konieczne ponowne logowanie.\",\n    \"logout_localhost_tip\": \"Obecne środowisko nie wymaga logowania; wylogowanie nie wyświetli monitu o hasło.\",\n    \"logs\": \"Dzienniki\",\n    \"logs_desc\": \"Zobacz dzienniki przesłane przez Sunshine\",\n    \"logs_find\": \"Znajdź...\",\n    \"match_contains\": \"Zawiera\",\n    \"match_exact\": \"Dokładnie\",\n    \"match_regex\": \"Wyrażenie regularne\",\n    \"reopen_setup_wizard\": \"Ponownie otwórz kreator konfiguracji\",\n    \"reopen_setup_wizard_desc\": \"Ponownie otwórz stronę kreatora konfiguracji, aby ponownie skonfigurować ustawienia początkowe.\",\n    \"reopen_setup_wizard_error\": \"Nie udało się ponownie otworzyć kreatora konfiguracji\",\n    \"reset_display_device_desc_windows\": \"Jeśli Sunshine utknął próbując przywrócić zmienione ustawienia urządzenia wyświetlającego, możesz zresetować ustawienia i ręcznie przywrócić stan wyświetlacza.\\nMoże to wystąpić z różnych powodów: urządzenie nie jest już dostępne, zostało podłączone do innego portu itp.\",\n    \"reset_display_device_error_windows\": \"Błąd podczas resetowania persistencji!\",\n    \"reset_display_device_success_windows\": \"Resetowanie persistencji zakończone sukcesem!\",\n    \"reset_display_device_windows\": \"Resetowanie pamięci wyświetlacza\",\n    \"restart_sunshine\": \"Restart Sunshine\",\n    \"restart_sunshine_desc\": \"Jeśli Sunshine nie działa poprawnie, możesz spróbować uruchomić go ponownie. Spowoduje to zakończenie wszystkich uruchomionych sesji.\",\n    \"restart_sunshine_success\": \"Sunshine uruchamia się ponownie\",\n    \"troubleshooting\": \"Rozwiązywanie problemów\",\n    \"unpair_all\": \"Rozparuj wszystko\",\n    \"unpair_all_error\": \"Błąd podczas rozłączania pary\",\n    \"unpair_all_success\": \"Wszystkie urządzenia rozłączone.\",\n    \"unpair_desc\": \"Usuń sparowane urządzenia. Indywidualnie niesparowane urządzenia z aktywną sesją pozostaną połączone, ale nie będą mogły rozpocząć ani wznowić sesji.\",\n    \"unpair_single_no_devices\": \"Nie ma sparowanych urządzeń.\",\n    \"unpair_single_success\": \"Urządzenia mogą być jednak nadal w aktywnej sesji. Użyj przycisku \\\"Wymuś zamknięcie\\\" powyżej, aby zakończyć wszystkie otwarte sesje.\",\n    \"unpair_single_unknown\": \"Nieznany klient\",\n    \"unpair_title\": \"Odparuj urządzenia\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Potwierdź hasło\",\n    \"create_creds\": \"Przed rozpoczęciem pracy należy utworzyć nową nazwę użytkownika i hasło dostępu do interfejsu użytkownika.\",\n    \"create_creds_alert\": \"Poniższe dane uwierzytelniające są potrzebne do uzyskania dostępu do interfejsu użytkownika Sunshine. Zachowaj je w bezpiecznym miejscu, ponieważ nigdy więcej ich nie zobaczysz!\",\n    \"creds_local_only\": \"Twoje dane logowania są przechowywane lokalnie w trybie offline i nigdy nie zostaną przesłane na żaden serwer.\",\n    \"error\": \"Błąd!\",\n    \"greeting\": \"Witamy w Sunshine Foundation!\",\n    \"hide_password\": \"Ukryj hasło\",\n    \"login\": \"Login\",\n    \"network_error\": \"Błąd sieci, sprawdź połączenie\",\n    \"password\": \"Hasło\",\n    \"password_match\": \"Hasła się zgadzają\",\n    \"password_mismatch\": \"Hasła nie pasują\",\n    \"server_error\": \"Błąd serwera\",\n    \"show_password\": \"Pokaż hasło\",\n    \"success\": \"Sukces!\",\n    \"username\": \"Nazwa użytkownika\",\n    \"welcome_success\": \"Strona zostanie wkrótce przeładowana, a przeglądarka poprosi o podanie nowych danych uwierzytelniających\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/pt.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Aplicar\",\n    \"auto\": \"Automático\",\n    \"autodetect\": \"Detetar automaticamente (recomendado)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"Cancelar\",\n    \"close\": \"Fechar\",\n    \"copied\": \"Copiado para a área de transferência\",\n    \"copy\": \"Copiar\",\n    \"delete\": \"Excluir\",\n    \"description\": \"Descrição\",\n    \"disabled\": \"Desabilitado\",\n    \"disabled_def\": \"Desativado (padrão)\",\n    \"dismiss\": \"Descartar\",\n    \"do_cmd\": \"Faça o Comando\",\n    \"download\": \"Descarregar\",\n    \"edit\": \"Editar\",\n    \"elevated\": \"Elevado\",\n    \"enabled\": \"Ativado\",\n    \"enabled_def\": \"Ativado (padrão)\",\n    \"error\": \"Erro!\",\n    \"no_changes\": \"Nenhuma alteração\",\n    \"note\": \"Nota:\",\n    \"password\": \"Palavra-passe\",\n    \"remove\": \"Remover\",\n    \"run_as\": \"Executar como Administrador\",\n    \"save\": \"Guardar\",\n    \"see_more\": \"Ver mais\",\n    \"success\": \"Sucesso!\",\n    \"undo_cmd\": \"Desfazer Comando\",\n    \"username\": \"Usuário:\",\n    \"warning\": \"Aviso!\"\n  },\n  \"apps\": {\n    \"actions\": \"Ações.\",\n    \"add_cmds\": \"Adicionar Comandos\",\n    \"add_new\": \"Adicionar novo\",\n    \"advanced_options\": \"Opções avançadas\",\n    \"app_name\": \"Nome da aplicação\",\n    \"app_name_desc\": \"Nome do aplicativo, como mostrado no Moonlight\",\n    \"applications_desc\": \"Aplicações só são atualizadas quando o Cliente for reiniciado\",\n    \"applications_title\": \"Aplicações\",\n    \"auto_detach\": \"Continue transmitindo se o aplicativo fechar rapidamente\",\n    \"auto_detach_desc\": \"Isso tentará detectar automaticamente aplicativos de tipo launcher que fecham rapidamente após a inicialização de outro programa ou instância de si mesmos. Quando um aplicativo de tipo launcher é detectado, ele é tratado como um aplicativo destacado.\",\n    \"basic_info\": \"Informação básica\",\n    \"cmd\": \"Comando\",\n    \"cmd_desc\": \"O aplicativo principal a ser iniciado. Se em branco, nenhum aplicativo será iniciado.\",\n    \"cmd_examples_title\": \"Exemplos comuns:\",\n    \"cmd_note\": \"Se o caminho para o comando conter espaços, você deve colocá-lo entre aspas.\",\n    \"cmd_prep_desc\": \"Uma lista de comandos a serem executados antes / depois desta aplicação. Se algum dos comandos de predefinição falhar, iniciar o aplicativo é abortado.\",\n    \"cmd_prep_name\": \"Preparações do Comando\",\n    \"command_settings\": \"Configurações de comando\",\n    \"covers_found\": \"Capas encontradas\",\n    \"delete\": \"excluir\",\n    \"delete_confirm\": \"Tem certeza de que deseja excluir \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Comandos desanexados\",\n    \"detached_cmds_add\": \"Adicionar Comando Desanexado\",\n    \"detached_cmds_desc\": \"Uma lista de comandos a serem executados em segundo plano.\",\n    \"detached_cmds_note\": \"Se o caminho para o comando conter espaços, você deve colocá-lo entre aspas.\",\n    \"detached_cmds_remove\": \"Remover comando destacado\",\n    \"edit\": \"Alterar\",\n    \"env_app_id\": \"ID do aplicativo\",\n    \"env_app_name\": \"Nome do aplicativo\",\n    \"env_client_audio_config\": \"A configuração de áudio solicitada pelo cliente (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"O cliente solicitou a opção de otimizar o jogo para uma transmissão ideal (verdadeiro/falso)\",\n    \"env_client_fps\": \"O FPS solicitado pelo cliente (int)\",\n    \"env_client_gcmap\": \"A máscara de gamepad solicitada, em formato bitset/bitfield (int)\",\n    \"env_client_hdr\": \"O HDR está ativado pelo cliente (verdadeiro/falso)\",\n    \"env_client_height\": \"A altura solicitada pelo cliente (int)\",\n    \"env_client_host_audio\": \"O cliente solicitou áudio de host (verdadeiro/falso)\",\n    \"env_client_name\": \"Nome amigável do cliente (string)\",\n    \"env_client_width\": \"A largura solicitada pelo cliente (int)\",\n    \"env_displayplacer_example\": \"Exemplo - displayplacer para Automação de Resolução:\",\n    \"env_qres_example\": \"Exemplo - QRes para Automação de Resolução:\",\n    \"env_qres_path\": \"Caminho das configurações rápidas\",\n    \"env_var_name\": \"Nome da Var\",\n    \"env_vars_about\": \"Sobre Variáveis de Ambiente\",\n    \"env_vars_desc\": \"Todos os comandos obtêm essas variáveis de ambiente por padrão:\",\n    \"env_xrandr_example\": \"Exemplo - Xrandr para Automação de Resolução:\",\n    \"exit_timeout\": \"Tempo Esgotado\",\n    \"exit_timeout_desc\": \"Número de segundos para esperar que todos os processos do aplicativo saiam graciosamente quando solicitado a sair. Se não definido, o padrão é esperar até 5 segundos. Se definido como zero ou negativo, o aplicativo será encerrado imediatamente.\",\n    \"file_selector_not_initialized\": \"Seletor de arquivo não inicializado\",\n    \"find_cover\": \"Encontrar capa\",\n    \"form_invalid\": \"Por favor, verifique os campos obrigatórios\",\n    \"form_valid\": \"Aplicação válida\",\n    \"global_prep_desc\": \"Ativar/desativar a execução de comandos de preparação global para este aplicativo.\",\n    \"global_prep_name\": \"Comandos de Preparação Global\",\n    \"image\": \"Imagem:\",\n    \"image_desc\": \"Caminho da aplicação icon/imagem/imagem que será enviado para o cliente. Imagem deve ser um arquivo PNG. Se não estiver definido, Sunshine irá enviar a imagem da caixa padrão.\",\n    \"image_settings\": \"Configurações de imagem\",\n    \"loading\": \"Carregandochar@@0\",\n    \"menu_cmd_actions\": \"Ações\",\n    \"menu_cmd_add\": \"Adicionar comando de menu\",\n    \"menu_cmd_command\": \"Comando\",\n    \"menu_cmd_desc\": \"Após a configuração, estes comandos estarão visíveis no menu de retorno do cliente, permitindo a execução rápida de operações específicas sem interromper a transmissão, como iniciar programas auxiliares.\\nExemplo: Nome de exibição - Fechar o computador; Comando - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Nome de exibição\",\n    \"menu_cmd_drag_sort\": \"Arrastar para ordenar\",\n    \"menu_cmd_name\": \"Comandos de menu\",\n    \"menu_cmd_placeholder_command\": \"Comando\",\n    \"menu_cmd_placeholder_display_name\": \"Nome de exibição\",\n    \"menu_cmd_placeholder_execute\": \"Executar comando\",\n    \"menu_cmd_placeholder_undo\": \"Desfazer comando\",\n    \"menu_cmd_remove_menu\": \"Remover comando de menu\",\n    \"menu_cmd_remove_prep\": \"Remover comando de preparação\",\n    \"mouse_mode\": \"Modo do rato\",\n    \"mouse_mode_auto\": \"Auto (Definição global)\",\n    \"mouse_mode_desc\": \"Selecione o método de entrada do rato para esta aplicação. Auto usa a definição global, Rato virtual usa o controlador HID, SendInput usa a API do Windows.\",\n    \"mouse_mode_sendinput\": \"SendInput (API do Windows)\",\n    \"mouse_mode_vmouse\": \"Rato virtual\",\n    \"name\": \"Nome\",\n    \"output_desc\": \"O arquivo onde a saída do comando é armazenada, se não for especificado, a saída é ignorada\",\n    \"output_name\": \"Saída\",\n    \"run_as_desc\": \"Isto pode ser necessário para que alguns aplicativos que requerem permissões de administrador sejam executados corretamente.\",\n    \"scan_result_add_all\": \"Adicionar tudo\",\n    \"scan_result_edit_title\": \"Adicionar e editar\",\n    \"scan_result_filter_all\": \"Tudo\",\n    \"scan_result_filter_epic_title\": \"Jogos Epic Games\",\n    \"scan_result_filter_executable\": \"Executável\",\n    \"scan_result_filter_executable_title\": \"Arquivo executável\",\n    \"scan_result_filter_gog_title\": \"Jogos GOG Galaxy\",\n    \"scan_result_filter_script\": \"Script\",\n    \"scan_result_filter_script_title\": \"Script de lote/comando\",\n    \"scan_result_filter_shortcut\": \"Atalho\",\n    \"scan_result_filter_shortcut_title\": \"Atalho\",\n    \"scan_result_filter_steam_title\": \"Jogos Steam\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Jogo\",\n    \"scan_result_games_only\": \"Apenas jogos\",\n    \"scan_result_matched\": \"Correspondências: {count}\",\n    \"scan_result_no_apps\": \"Nenhuma aplicação encontrada para adicionar\",\n    \"scan_result_no_matches\": \"Nenhuma aplicação correspondente encontrada\",\n    \"scan_result_quick_add_title\": \"Adição rápida\",\n    \"scan_result_remove_title\": \"Remover da lista\",\n    \"scan_result_search_placeholder\": \"Pesquisar nome da aplicação, comando ou caminho...\",\n    \"scan_result_show_all\": \"Mostrar tudo\",\n    \"scan_result_title\": \"Resultados da pesquisa\",\n    \"scan_result_try_different_keywords\": \"Tente usar palavras-chave de pesquisa diferentes\",\n    \"scan_result_type_batch\": \"Lote\",\n    \"scan_result_type_command\": \"Script de comando\",\n    \"scan_result_type_executable\": \"Arquivo executável\",\n    \"scan_result_type_shortcut\": \"Atalho\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Pesquisar aplicações...\",\n    \"select\": \"Selecionar\",\n    \"test_menu_cmd\": \"Testar comando\",\n    \"test_menu_cmd_empty\": \"O comando não pode estar vazio\",\n    \"test_menu_cmd_executing\": \"A executar comando...\",\n    \"test_menu_cmd_failed\": \"Falha ao executar comando\",\n    \"test_menu_cmd_success\": \"Comando executado com sucesso!\",\n    \"use_desktop_image\": \"Usar papel de parede atual do ambiente de trabalho\",\n    \"wait_all\": \"Continue transmitindo até que todos os processos de app saiam\",\n    \"wait_all_desc\": \"Isso continuará transmitindo até que todos os processos iniciados pelo aplicativo tenham sido encerrados. Quando desmarcado, a transmissão será interrompida quando o processo inicial do aplicativo terminar, mesmo que outros processos de aplicativo ainda estejam em execução.\",\n    \"working_dir\": \"Diretório de trabalho\",\n    \"working_dir_desc\": \"O diretório de trabalho que deve ser passado para o processo. Por exemplo, alguns aplicativos usam o diretório de trabalho para procurar arquivos de configuração. Se não estiver definido, o Sunshine será o padrão para o diretório pai do comando\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Nome do adaptador\",\n    \"adapter_name_desc_linux_1\": \"Especifique manualmente uma GPU para usar na captura.\",\n    \"adapter_name_desc_linux_2\": \"para encontrar todos os dispositivos capazes do VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Substitua ``renderD129`` pelo dispositivo de cima para listar o nome e os recursos do dispositivo. Para ser apoiado pelo Sol, ele precisa ter no mínimo:\",\n    \"adapter_name_desc_windows\": \"Especifique manualmente uma GPU para usar na captura. Se não definido, a GPU é escolhida automaticamente. Nota: Esta GPU deve ter um display conectado e ligado. Se o seu laptop não puder habilitar a saída direta da GPU, defina como automático.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Se a versão mais recente do monitor virtual estiver instalada, ela poderá ser associada automaticamente à vinculação da GPU\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Adicionar\",\n    \"address_family\": \"Família de endereços\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Definir a família de endereços usada pelo Sunshine\",\n    \"address_family_ipv4\": \"Apenas IPv4\",\n    \"always_send_scancodes\": \"Sempre enviar Scancodes\",\n    \"always_send_scancodes_desc\": \"O envio de códigos de verificação melhora a compatibilidade com jogos e aplicativos, mas pode resultar em uma entrada incorreta de teclado de certos clientes que não estão usando um layout de teclado inglês dos EUA. Habilitar se a entrada de teclado não estiver funcionando em certas aplicações. Desative se as chaves no cliente estão gerando a entrada errada no host.\",\n    \"amd_coder\": \"Codificador AMF (H264)\",\n    \"amd_coder_desc\": \"Permite que você selecione a codificação entropia para priorizar a qualidade ou a velocidade de codificação. Somente H.264.\",\n    \"amd_enforce_hrd\": \"Aplicação de Decodificador de Referência Hipotetica (HRD) AMF\",\n    \"amd_enforce_hrd_desc\": \"Aumenta as restrições de controle de taxa para atender aos requisitos do modelo de hash. Isso reduz consideravelmente os transbordos de bitrato, mas pode causar a codificação de artefatos ou uma redução de qualidade em certas cartas.\",\n    \"amd_preanalysis\": \"Pré-análise AMF\",\n    \"amd_preanalysis_desc\": \"Isto permite a pré-análise de controle, que pode aumentar a qualidade em detrimento de uma maior latência de codificação.\",\n    \"amd_quality\": \"Qualidade AMF\",\n    \"amd_quality_balanced\": \"Balanceado - balanceado (padrão)\",\n    \"amd_quality_desc\": \"Isto controla o equilíbrio entre a velocidade de codificação e a qualidade.\",\n    \"amd_quality_group\": \"Configurações de qualidade AMF\",\n    \"amd_quality_quality\": \"qualidade -- preferir qualidade\",\n    \"amd_quality_speed\": \"velocidade -- preferir velocidade\",\n    \"amd_qvbr_quality\": \"Nível de qualidade AMF QVBR\",\n    \"amd_qvbr_quality_desc\": \"Nível de qualidade para o modo de controlo de taxa QVBR. Intervalo: 1-51 (menor = melhor qualidade). Predefinição: 23. Aplica-se apenas quando o controlo de taxa está definido como 'qvbr'.\",\n    \"amd_rc\": \"Controle de Taxa AMF\",\n    \"amd_rc_cbr\": \"cbr -- taxa de bits constante (padrão)\",\n    \"amd_rc_cqp\": \"cqp -- modo qp constante\",\n    \"amd_rc_desc\": \"Isto controla o método de controle da taxa para garantir que não estamos a exceder o alvo da taxa de bits do cliente. 'cqp' não é adequado para segmentação de taxa de bits e outras opções além de 'vbr_latency' dependem da aplicação HRD para ajudar a restringir os fluxos de taxa de bits.\",\n    \"amd_rc_group\": \"Configurações de controle de taxa AMF\",\n    \"amd_rc_hqcbr\": \"hqcbr -- taxa de bits constante alta qualidade\",\n    \"amd_rc_hqvbr\": \"hqvbr -- taxa de bits variável alta qualidade\",\n    \"amd_rc_qvbr\": \"qvbr -- taxa de bits variável de qualidade (usa nível de qualidade QVBR)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- bitrate variável limitado pela latência (recomendado se o HDR estiver desabilitado; padrão)\",\n    \"amd_rc_vbr_peak\": \"vbr_pico -- pico de taxa de bits variável restrita\",\n    \"amd_usage\": \"Uso do AMF\",\n    \"amd_usage_desc\": \"Isso define o perfil de codificação base. Todas as opções apresentadas abaixo substituirão um subconjunto do perfil de uso, mas há configurações ocultas adicionais aplicadas que não podem ser configuradas em outro lugar.\",\n    \"amd_usage_lowlatency\": \"baixa latência - baixa latência (mais rápido)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - baixa latência, alta qualidade (rápido)\",\n    \"amd_usage_transcoding\": \"transcodificação -- transcodificando (mais lento)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatência - latência ultra baixa (mais rápida)\",\n    \"amd_usage_webcam\": \"webcam -- câmera (lenta)\",\n    \"amd_vbaq\": \"Variação da Variação Baseada na Quantização Adaptativa (VBAQ)\",\n    \"amd_vbaq_desc\": \"O sistema visual humano é tipicamente menos sensível a artefatos em áreas altamente texturadas. No modo VBAQ, a variação de pixel é usada para indicar a complexidade das texturas espaciais, permitindo que o codificador aloce mais bits em áreas mais suaves. Habilitar este recurso leva a melhorias na qualidade visual subjetiva com algum conteúdo.\",\n    \"amf_draw_mouse_cursor\": \"Desenhar um cursor simples ao usar o método de captura AMF\",\n    \"amf_draw_mouse_cursor_desc\": \"Em alguns casos, usar a captura AMF não exibirá o ponteiro do rato. Ativar esta opção desenhará um ponteiro do rato simples no ecrã. Nota: A posição do ponteiro do rato só será atualizada quando houver uma atualização do ecrã de conteúdo, portanto, em cenários que não sejam jogos, como no ambiente de trabalho, pode observar movimento lento do ponteiro do rato.\",\n    \"apply_note\": \"Clique em 'Aplicar' para reiniciar o Sunshine e aplicar as alterações. Isto encerrará todas as sessões em execução.\",\n    \"audio_sink\": \"Pia de Áudio\",\n    \"audio_sink_desc_linux\": \"O nome do afundamento de áudio usado para o loop de áudio. Se você não especificar esta variável, o pulseaudio selecionará o dispositivo de monitor padrão. Você pode encontrar o nome do sumidouro de áudio usando qualquer comando:\",\n    \"audio_sink_desc_macos\": \"O nome do sumidouro de áudio usado para o loop de áudio. O Sunshine só pode acessar microfones no macOS devido a limitações do sistema. Para fazer streaming de áudio do sistema usando Soundflower ou BlackHole.\",\n    \"audio_sink_desc_windows\": \"Especifique manualmente um dispositivo de áudio específico para capturar. Se não for definido, o dispositivo será escolhido automaticamente. Recomendamos fortemente deixar este campo em branco para usar a seleção automática de dispositivo! Se você tiver vários dispositivos de áudio com nomes idênticos, você pode obter o ID do dispositivo usando o seguinte comando:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Alto-falantes (Dispositivo de Áudio de Alta Definição)\",\n    \"av1_mode\": \"Suporte AV1\",\n    \"av1_mode_0\": \"O Sunshine anunciará o suporte para a AV1 com base nos recursos do codificador (recomendado)\",\n    \"av1_mode_1\": \"O sol não anunciará o suporte para a AV1\",\n    \"av1_mode_2\": \"O Sunshine anunciará o suporte para o perfil AV1 de 8 bits\",\n    \"av1_mode_3\": \"A luz do sol anunciará o suporte para os perfis AV1 (8-bit principal) e de 10 bits (HDR)\",\n    \"av1_mode_desc\": \"Permite ao cliente solicitar fluxos de vídeo AV1 principal de 8 bits ou de 10 bits. AV1 usa mais CPU para codificar, então permite que isso reduza o desempenho ao usar a codificação do software.\",\n    \"back_button_timeout\": \"Tempo de Emulação do Botão Home/Guia\",\n    \"back_button_timeout_desc\": \"Se o botão Voltar / Selecionar for mantido pressionado para o número especificado de milissegundos, um botão Home/Guia será pressionado. Se definido como um valor < 0 (padrão), segurar o botão Voltar/Selecionar não irá simular o botão Home/Guia.\",\n    \"bind_address\": \"Endereço de vinculação (recurso de teste)\",\n    \"bind_address_desc\": \"Defina o endereço IP específico ao qual o Sunshine se vinculará. Se deixado em branco, o Sunshine se vinculará a todos os endereços disponíveis.\",\n    \"capture\": \"Forçar um Método de Captura Específica\",\n    \"capture_desc\": \"Modo automático Sunshine usará o primeiro que funciona. NvFBC requer drivers nvidia corrigidos.\",\n    \"capture_target\": \"Alvo de Captura\",\n    \"capture_target_desc\": \"Selecione o tipo de alvo para capturar. Ao selecionar 'Janela', você pode capturar uma janela de aplicativo específica (como um software de interpolação de quadros de IA) em vez de toda a tela.\",\n    \"capture_target_display\": \"Tela\",\n    \"capture_target_window\": \"Janela\",\n    \"cert\": \"Certificado\",\n    \"cert_desc\": \"O certificado usado para a interface do usuário da web e o pareamento do cliente Moonlight. Para a melhor compatibilidade, isso deve ter uma chave pública RSA-2048.\",\n    \"channels\": \"Máximo de Clientes Conectados\",\n    \"channels_desc_1\": \"Sunshine pode permitir que uma única sessão de streaming seja compartilhada com vários clientes simultaneamente.\",\n    \"channels_desc_2\": \"Alguns codificadores de hardware podem ter limitações que reduzem o desempenho com vários fluxos.\",\n    \"close_verify_safe\": \"Verificação segura compatível com clientes antigos\",\n    \"close_verify_safe_desc\": \"Clientes antigos podem não se conectar ao Sunshine, por favor, desative esta opção ou atualize o cliente\",\n    \"coder_cabac\": \"cabac -- contexto adaptável de programação aritmética binária - qualidade superior\",\n    \"coder_cavlc\": \"cavlc -- código adaptável de comprimento de variável de contexto - decodificação mais rápida\",\n    \"configuration\": \"Configuração\",\n    \"controller\": \"Enable Gamepad Input\",\n    \"controller_desc\": \"Permite que os convidados controlem o sistema de host com controle / controle do gamepad\",\n    \"credentials_file\": \"Arquivo de credenciais\",\n    \"credentials_file_desc\": \"Armazenar Usuário/Senha separadamente do arquivo de estado da Sunshine.\",\n    \"display_device_options_note_desc_windows\": \"O Windows guarda várias definições de ecrã para cada combinação de ecrãs atualmente ativos.\\nO Sunshine aplica então as alterações a um (ou mais) ecrã(s) pertencente(s) a tal combinação de ecrãs.\\nSe desligar um dispositivo que estava ativo quando o Sunshine aplicou as definições, as alterações não podem ser\\nrevertidas a menos que a combinação possa ser ativada novamente quando o Sunshine tentar reverter as alterações!\",\n    \"display_device_options_note_windows\": \"Nota sobre como as definições são aplicadas\",\n    \"display_device_options_windows\": \"Opções do dispositivo de exibição\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Ativa o ecrã se não estiver já ativo\",\n    \"display_device_prep_ensure_active_windows\": \"Ativar o ecrã automaticamente\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Desativa todos os outros ecrãs e ativa apenas o ecrã especificado\",\n    \"display_device_prep_ensure_only_display_windows\": \"Desativar outros ecrãs e ativar apenas o ecrã especificado\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Ativa o ecrã e define-o como ecrã principal\",\n    \"display_device_prep_ensure_primary_windows\": \"Ativar o ecrã automaticamente e torná-lo o ecrã principal\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Usa apenas o ecrã virtual para streaming secundário estendido\",\n    \"display_device_prep_ensure_secondary_windows\": \"Streaming de ecrã secundário (apenas ecrã virtual)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Sem alterações no estado do ecrã; o utilizador deve garantir que o ecrã está pronto\",\n    \"display_device_prep_no_operation_windows\": \"Desativado\",\n    \"display_device_prep_windows\": \"Preparação do ecrã\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Pelo menos um valor \\\"recebido\\\" e um valor \\\"final\\\" devem ser especificados.\\nUm campo vazio na seção \\\"recebido\\\" significa \\\"corresponder a qualquer valor\\\". Um campo vazio na seção \\\"final\\\" significa \\\"manter o valor recebido\\\".\\nVocê pode associar um valor de FPS específico a uma resolução específica, se desejar...\\n\\nNota: se a opção \\\"Otimizar configurações do jogo\\\" não estiver habilitada no cliente Moonlight, as linhas contendo valores de resolução serão ignoradas.\",\n    \"display_mode_remapping_desc_windows\": \"Especifique como uma resolução e/ou taxa de atualização específica deve ser remapeada para outros valores.\\nVocê pode transmitir em resolução mais baixa, enquanto renderiza em resolução mais alta no host para um efeito de superamostragem.\\nOu você pode transmitir em FPS mais alto, limitando o host a uma taxa de atualização mais baixa.\\nA correspondência é realizada de cima para baixo. Uma vez que a entrada é correspondida, as outras não são mais verificadas, mas ainda são validadas.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Taxa de atualização final\",\n    \"display_mode_remapping_final_resolution_windows\": \"Resolução final\",\n    \"display_mode_remapping_optional\": \"opcional\",\n    \"display_mode_remapping_received_fps_windows\": \"FPS recebido\",\n    \"display_mode_remapping_received_resolution_windows\": \"Resolução recebida\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Nota: se a opção \\\"Otimizar configurações do jogo\\\" não estiver habilitada no cliente Moonlight, o remapeamento é desabilitado.\",\n    \"display_mode_remapping_windows\": \"Remapear modos de exibição\",\n    \"display_modes\": \"Modos de exibição\",\n    \"ds4_back_as_touchpad_click\": \"Mapear Voltar/Selecionar para o Touchpad Clique\",\n    \"ds4_back_as_touchpad_click_desc\": \"Ao forçar a emulação do DS4, selecione um Voltar/Selecione para o Touchpad Clique\",\n    \"dsu_server_port\": \"DSU Server Port\",\n    \"dsu_server_port_desc\": \"DSU server listening port (default 26760). Sunshine will act as a DSU server to receive client connections and send motion data. Enable DSU server in your client(Yuzu,Ryujinx etc.) and set DSU server address(127.0.0.1) and port(26760)\",\n    \"enable_dsu_server\": \"Ativar servidor DSU\",\n    \"enable_dsu_server_desc\": \"Enable DSU server to receive client connections and send motion data\",\n    \"encoder\": \"Forçar um Codificador Específico\",\n    \"encoder_desc\": \"Força um codificador específico, caso contrário, Sunshine selecionará a melhor opção disponível. Nota: Se você especificar um codificador de hardware no Windows, ele deve coincidir com a GPU onde a tela está conectada.\",\n    \"encoder_software\": \"Software\",\n    \"experimental\": \"Experimental\",\n    \"experimental_features\": \"Funcionalidades experimentais\",\n    \"external_ip\": \"IP externo\",\n    \"external_ip_desc\": \"Se nenhum endereço IP externo for dado, Sunshine detectará automaticamente IP externo\",\n    \"fec_percentage\": \"Porcentagem FEC\",\n    \"fec_percentage_desc\": \"Porcentagem de erro corrigindo pacotes por pacote de dados em cada quadro de vídeo. Valores mais altos podem corrigir para mais perda de pacotes de rede, mas ao custo de aumentar o uso de largura de banda.\",\n    \"ffmpeg_auto\": \"auto -- let ffmpeg decide (padrão)\",\n    \"file_apps\": \"Arquivo de apps\",\n    \"file_apps_desc\": \"O arquivo onde os aplicativos atuais de Sunshine são armazenados.\",\n    \"file_state\": \"Arquivo de estado\",\n    \"file_state_desc\": \"O arquivo onde o estado atual de Sunshine é armazenado\",\n    \"fps\": \"FPS anunciados\",\n    \"gamepad\": \"Tipo de controle emulado\",\n    \"gamepad_auto\": \"Opções de seleção automáticas\",\n    \"gamepad_desc\": \"Escolha qual tipo de controle será emulado no host\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 Manual Options\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Opções de DS4 manual\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Preparações do Comando\",\n    \"global_prep_cmd_desc\": \"Configure uma lista de comandos a serem executados antes ou depois de executar qualquer aplicativo. Se algum dos comandos de preparação especificados falhar, o processo de lançamento do aplicativo será abortado.\",\n    \"hdr_luminance_analysis\": \"Metadados dinâmicos HDR (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Ativa a análise de luminância GPU por fotograma e injeta metadados dinâmicos HDR10+ (ST 2094-40) e HDR Vivid (CUVA) no fluxo codificado. Fornece indicações de mapeamento de tons por fotograma para ecrãs suportados. Adiciona uma pequena sobrecarga GPU (~0,5-1,5ms/fotograma em resoluções altas). Desative se tiver quedas de framerate com HDR ativado.\",\n    \"hdr_prep_automatic_windows\": \"Switch on/off the HDR mode as requested by the client\",\n    \"hdr_prep_no_operation_windows\": \"Disabled\",\n    \"hdr_prep_windows\": \"HDR state change\",\n    \"hevc_mode\": \"Suporte ao HEVC\",\n    \"hevc_mode_0\": \"Sunshine anunciará suporte para o HEVC com base em recursos de codificador (recomendado)\",\n    \"hevc_mode_1\": \"O sol não anunciará o suporte ao HEVC\",\n    \"hevc_mode_2\": \"O sol anunciará o suporte para o perfil principal do HEVC\",\n    \"hevc_mode_3\": \"A luz do sol anunciará o suporte para os perfis HEVC Main e Main10 (HDR)\",\n    \"hevc_mode_desc\": \"Permite ao cliente solicitar fluxos de vídeo HEVC principal ou HEVC Main10. HEVC é mais intenso em CPU para codificar, então permitir que isso possa reduzir o desempenho ao usar a codificação do software.\",\n    \"high_resolution_scrolling\": \"Suporte a Alta Resolução\",\n    \"high_resolution_scrolling_desc\": \"Quando habilitado, o Sunshine irá passar através de eventos de rolagem de alta resolução a partir de clientes de luz Lunar. Isso pode ser útil para desativar para aplicativos mais antigos que rolam muito rápido com eventos de rolagem de alta resolução.\",\n    \"install_steam_audio_drivers\": \"Instalar drivers de áudio Steam\",\n    \"install_steam_audio_drivers_desc\": \"Se o Steam estiver instalado, isso irá instalar automaticamente o driver de Alto-falantes de Streaming do Steam para suportar o som Surround 5.1/7.1 e silenciar o áudio do host.\",\n    \"key_repeat_delay\": \"Atraso da repetição da chave\",\n    \"key_repeat_delay_desc\": \"Controla a rapidez com que as teclas se irão repetir. O atraso inicial em milissegundos antes de repetir as chaves.\",\n    \"key_repeat_frequency\": \"Frequência de repetição de chave\",\n    \"key_repeat_frequency_desc\": \"Com que frequência as chaves se repetem a cada segundo. Esta opção configurável suporta decimais.\",\n    \"key_rightalt_to_key_win\": \"Mapear tecla Alt direita para tecla Windows\",\n    \"key_rightalt_to_key_win_desc\": \"É possível que você não possa enviar diretamente a chave Windows do Moonlight. Nesses casos, pode ser útil fazer Sunshine pensar que a tecla Alt direita é a tecla Windows\",\n    \"key_rightalt_to_key_windows\": \"Tecla Alt Right Map para a tecla Windows\",\n    \"keyboard\": \"Habilitar Entrada de Teclado\",\n    \"keyboard_desc\": \"Permite aos convidados controlar o sistema de host com o teclado\",\n    \"lan_encryption_mode\": \"Modo de Criptografia LAN\",\n    \"lan_encryption_mode_1\": \"Habilitado para clientes suportados\",\n    \"lan_encryption_mode_2\": \"Obrigatório para todos os clientes\",\n    \"lan_encryption_mode_desc\": \"Isso determina quando a criptografia será usada no streaming em sua rede local. A criptografia pode reduzir o desempenho do streaming, particularmente em hosts e clientes menos poderosos.\",\n    \"locale\": \"Localidade\",\n    \"locale_desc\": \"A localidade usada para a interface de usuário do Sunshine.\",\n    \"log_level\": \"Nível do Registro\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"Informações\",\n    \"log_level_3\": \"ATENÇÃO\",\n    \"log_level_4\": \"ERRO\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"Nenhuma\",\n    \"log_level_desc\": \"O nível mínimo de log impresso no padrão\",\n    \"log_path\": \"Caminho do Logfile\",\n    \"log_path_desc\": \"O arquivo onde os logs atuais de Sunshine são armazenados.\",\n    \"max_bitrate\": \"Bitrate Máximo\",\n    \"max_bitrate_desc\": \"A taxa de bits máxima (em Kbps) que Sunshine irá codificar o stream. Se definido como 0, ele sempre usará a bitrate solicitada pela luar.\",\n    \"max_fps_reached\": \"Valores máximos de FPS atingidos\",\n    \"max_resolutions_reached\": \"Número máximo de resoluções atingido\",\n    \"mdns_broadcast\": \"Encontrar este computador na rede local\",\n    \"mdns_broadcast_desc\": \"Se esta opção estiver ativada, o Sunshine permitirá que outros dispositivos encontrem este computador automaticamente. O Moonlight deve ser configurado para encontrar este computador automaticamente na rede local.\",\n    \"min_threads\": \"Contagem mínima de tópicos da CPU\",\n    \"min_threads_desc\": \"Aumentar o valor reduz ligeiramente a eficiência da codificação, mas a troca geralmente vale a pena para ganhar o uso de mais núcleos da CPU para codificação. O valor ideal é o mais baixo que pode codificar, de forma confiável, as configurações de streaming desejadas no seu hardware.\",\n    \"minimum_fps_target\": \"Objetivo de FPS mínimo\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Opções diversas\",\n    \"motion_as_ds4\": \"Emular um gamepad DS4 se o cliente reportar sensores de movimento estiverem presentes\",\n    \"motion_as_ds4_desc\": \"Se desativado, os sensores de movimento não serão tidos em conta durante a seleção de tipo gamepad\",\n    \"mouse\": \"Habilitar Entrada do Mouse\",\n    \"mouse_desc\": \"Permite aos convidados controlar o sistema de host com o mouse\",\n    \"native_pen_touch\": \"Suporte nativo para Pen/Toque\",\n    \"native_pen_touch_desc\": \"Quando ativado, o Sunshine irá passar por eventos nativos de caneta/toque de clientes de lua. Isto pode ser útil para desativar aplicações mais antigas sem o suporte nativo ao canal/toque.\",\n    \"no_fps\": \"Nenhum valor de FPS adicionado\",\n    \"no_resolutions\": \"Nenhuma resolução adicionada\",\n    \"notify_pre_releases\": \"Pré-Lançar notificações\",\n    \"notify_pre_releases_desc\": \"Se deve ser notificado de novas versões de lançamento do Sunshine\",\n    \"nvenc_h264_cavlc\": \"Preferir CAVLC ao CABAC no H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Forma simples de codificação de entrope. CAVLC precisa de cerca de 10% mais bitrate para a mesma qualidade. Somente relevante para dispositivos de decodificação realmente antigos.\",\n    \"nvenc_latency_over_power\": \"Prefere latência de codificação inferior sobre economia de energia\",\n    \"nvenc_latency_over_power_desc\": \"O Sunshine solicita o máximo de velocidade de relógio com GPU durante a transmissão, para reduzir a latência de codificação. Desativação não é recomendado, uma vez que isso pode levar a um aumento significativo da latência de codificação.\",\n    \"nvenc_lookahead_depth\": \"Profundidade de Lookahead\",\n    \"nvenc_lookahead_depth_desc\": \"Número de quadros para olhar à frente durante a codificação (0-32). O Lookahead melhora a qualidade da codificação, especialmente em cenas complexas, fornecendo melhor estimativa de movimento e distribuição de taxa de bits. Valores mais altos melhoram a qualidade, mas aumentam a latência de codificação. Defina como 0 para desativar. Requer NVENC SDK 13.0 (1202) ou mais recente.\",\n    \"nvenc_lookahead_level\": \"Nível de Lookahead\",\n    \"nvenc_lookahead_level_0\": \"Nível 0 (qualidade mais baixa, mais rápido)\",\n    \"nvenc_lookahead_level_1\": \"Nível 1\",\n    \"nvenc_lookahead_level_2\": \"Nível 2\",\n    \"nvenc_lookahead_level_3\": \"Nível 3 (qualidade mais alta, mais lento)\",\n    \"nvenc_lookahead_level_autoselect\": \"Seleção automática (deixe o driver escolher o nível ideal)\",\n    \"nvenc_lookahead_level_desc\": \"Nível de qualidade de Lookahead. Níveis mais altos melhoram a qualidade às custas do desempenho. Esta opção só tem efeito quando lookahead_depth é maior que 0. Requer NVENC SDK 13.0 (1202) ou mais recente.\",\n    \"nvenc_lookahead_level_disabled\": \"Desativado (igual ao nível 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Apresentar OpenGL/Vulkan em cima de DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"O Sunshine não pode capturar programas OpenGL e Vulkan de tela cheia a uma taxa de quadros completa, a menos que eles apresentem em cima do DXGI. Essa configuração é de todo o sistema que é revertida na saída sunshine do programa.\",\n    \"nvenc_preset\": \"Predefinição de desempenho\",\n    \"nvenc_preset_1\": \"(mais rápido, padrão)\",\n    \"nvenc_preset_7\": \"(mais lento)\",\n    \"nvenc_preset_desc\": \"Valores maiores melhoram a compressão (qualidade em taxa de bits dada) ao custo de maior latência de codificação. Recomendado para mudar apenas quando limitado por rede ou descodificador, caso contrário, o efeito semelhante pode ser alcançado aumentando a taxa de bits.\",\n    \"nvenc_rate_control\": \"Modo de controle de taxa\",\n    \"nvenc_rate_control_cbr\": \"CBR (Taxa de bits constante) - Baixa latência\",\n    \"nvenc_rate_control_desc\": \"Selecione o modo de controle de taxa. CBR (Taxa de bits constante) fornece taxa de bits fixa para streaming de baixa latência. VBR (Taxa de bits variável) permite que a taxa de bits varie com base na complexidade da cena, proporcionando melhor qualidade para cenas complexas ao custo de taxa de bits variável.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Taxa de bits variável) - Melhor qualidade\",\n    \"nvenc_realtime_hags\": \"Use prioridade em tempo real em agendamento de gpu acelerado por hardware\",\n    \"nvenc_realtime_hags_desc\": \"Atualmente os motoristas da NVIDIA podem congelar no codificador quando o HAGS estiver ativado, a prioridade em tempo real é usada e a utilização da VRAM está próxima do máximo. Desabilitar esta opção reduz a prioridade ao alto, contornando o congelamento ao custo de desempenho reduzido quando a GPU está fortemente carregada.\",\n    \"nvenc_spatial_aq\": \"Spatial AQ\",\n    \"nvenc_spatial_aq_desc\": \"Atribuir valores mais elevados de QP a regiões planas do vídeo. Recomendado para permitir o streaming em taxas de bits mais baixas.\",\n    \"nvenc_spatial_aq_disabled\": \"Disabled (faster, default)\",\n    \"nvenc_spatial_aq_enabled\": \"Enabled (slower)\",\n    \"nvenc_split_encode\": \"Codificação de quadros dividida\",\n    \"nvenc_split_encode_desc\": \"Split the encoding of each video frame over multiple NVENC hardware units. Significantly reduces encoding latency with a marginal compression efficiency penalty. This option is ignored if your GPU has a singular NVENC unit.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Driver decides (default)\",\n    \"nvenc_split_encode_four_strips\": \"Forçar divisão em 4 faixas (requer 4+ motores NVENC)\",\n    \"nvenc_split_encode_three_strips\": \"Forçar divisão em 3 faixas (requer 3+ motores NVENC)\",\n    \"nvenc_split_encode_two_strips\": \"Forçar divisão em 2 faixas (requer 2+ motores NVENC)\",\n    \"nvenc_target_quality\": \"Qualidade alvo (modo VBR)\",\n    \"nvenc_target_quality_desc\": \"Nível de qualidade alvo para o modo VBR (0-51 para H.264/HEVC, 0-63 para AV1). Valores mais baixos = maior qualidade. Defina como 0 para seleção automática de qualidade. Usado apenas quando o modo de controle de taxa é VBR.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Modo de duas passagens\",\n    \"nvenc_twopass_desc\": \"Adiciona passe de codificação preliminar. Isso permite detectar mais vetores de movimento, distribuir melhor a taxa de bits pelo quadro e aderir de forma mais rigorosa aos limites de bits. Desabilitar não é recomendado uma vez que isso pode levar a uma superação de bits ocasional e a perda de pacotes subsequentes.\",\n    \"nvenc_twopass_disabled\": \"Desativado (mais rápido, não recomendado)\",\n    \"nvenc_twopass_full_res\": \"Resolução completa (mais lento)\",\n    \"nvenc_twopass_quarter_res\": \"Resolução de trimestre (mais rápido, padrão)\",\n    \"nvenc_vbv_increase\": \"Porcentagem de VBV/HRD de Um-frame\",\n    \"nvenc_vbv_increase_desc\": \"Por padrão, o sunshine usa um simples frame VBV/HRD, o que significa que qualquer tamanho de quadro de vídeo codificado não é esperado exceder a bitrate solicitada dividida pela taxa de quadros solicitada. Relaxar esta restrição pode ser benéfico e agir como taxa de bits variável de baixa latência, mas também pode levar à perda de pacotes se a rede não tiver espaço de armazenamento para manipular espinhos de taxa de bits. O valor máximo aceito é 400, o que corresponde a 5x de aumento no limite máximo do quadro de vídeo codificado.\",\n    \"origin_web_ui_allowed\": \"Interface de Origem Web Permitida\",\n    \"origin_web_ui_allowed_desc\": \"A origem do endereço do endpoint remoto que não é negado o acesso à Web UI\",\n    \"origin_web_ui_allowed_lan\": \"Somente aqueles em LAN podem acessar a interface Web\",\n    \"origin_web_ui_allowed_pc\": \"Somente localhost pode acessar a Web UI\",\n    \"origin_web_ui_allowed_wan\": \"Alguém pode acessar a interface web\",\n    \"output_name_desc_unix\": \"Durante a inicialização do sol, você deve ver a lista de telas detectadas. Nota: Você precisa usar o valor do id dentro dos parênteses.\",\n    \"output_name_desc_windows\": \"Especifique manualmente um display a ser usado para captura. Se não for definido, o display primário é capturado. Nota: Se você especificou uma GPU acima, essa tela deve estar conectada à GPU. Os valores apropriados podem ser encontrados usando o seguinte comando:\",\n    \"output_name_unix\": \"Mostrar número\",\n    \"output_name_windows\": \"Nome da saída\",\n    \"ping_timeout\": \"Tempo limite\",\n    \"ping_timeout_desc\": \"Quanto tempo esperar em milissegundos por dados do luar antes de desligar o fluxo\",\n    \"pkey\": \"Chave Privada\",\n    \"pkey_desc\": \"A chave privada usada para a interface do usuário da web e o pareamento do cliente Moonlight. Para a melhor compatibilidade, esta deve ser uma chave privada RSA-2048.\",\n    \"port\": \"Porta\",\n    \"port_alert_1\": \"O sol não pode usar portas abaixo de 1024!\",\n    \"port_alert_2\": \"Portas acima de 65535 não estão disponíveis!\",\n    \"port_desc\": \"Definir a família dos portos usados pelo Sunshine\",\n    \"port_http_port_note\": \"Use esta porta para conectar com o Luar.\",\n    \"port_note\": \"Observação\",\n    \"port_port\": \"Porta\",\n    \"port_protocol\": \"Protocol\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Expor a interface da web à internet é um risco de segurança! Proceda por sua própria conta e risco!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Parâmetro de Quantização\",\n    \"qp_desc\": \"Alguns dispositivos podem não suportar Taxa de Bits Constante. Para esses dispositivos, QP é usado. Valores maiores significam mais compressão, mas menos qualidade.\",\n    \"qsv_coder\": \"Programador QuickSync (H264)\",\n    \"qsv_preset\": \"QuickSync Preset\",\n    \"qsv_preset_fast\": \"rápido (baixa qualidade)\",\n    \"qsv_preset_faster\": \"mais rápido (menor qualidade)\",\n    \"qsv_preset_medium\": \"médio (padrão)\",\n    \"qsv_preset_slow\": \"lento (boa qualidade)\",\n    \"qsv_preset_slower\": \"mais lento (melhor qualidade)\",\n    \"qsv_preset_slowest\": \"mais lento (melhor qualidade)\",\n    \"qsv_preset_veryfast\": \"mais rápido (menor qualidade)\",\n    \"qsv_slow_hevc\": \"Permitir codificação lenta do HEVC\",\n    \"qsv_slow_hevc_desc\": \"Isto pode habilitar a codificação HEVC em GPUs mais antigas, ao custo de maior uso da GPU e pior desempenho.\",\n    \"refresh_rate_change_automatic_windows\": \"Use FPS value provided by the client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Enter the refresh rate to be used\",\n    \"refresh_rate_change_manual_windows\": \"Use manually entered refresh rate\",\n    \"refresh_rate_change_no_operation_windows\": \"Disabled\",\n    \"refresh_rate_change_windows\": \"FPS change\",\n    \"res_fps_desc\": \"Os modos de exibição anunciados pelo Sunshine. Algumas versões do Moonlight, como o Moonlight-nx (Switch), dependem destas listas para garantir que as resoluções e fps solicitados sejam suportados. Esta configuração não altera como o fluxo de ecrã é enviado ao Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Use resolution provided by the client\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_manual_windows\": \"Use manually entered resolution\",\n    \"resolution_change_no_operation_windows\": \"Disabled\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_windows\": \"Resolution change\",\n    \"resolutions\": \"Resoluções anunciadas\",\n    \"restart_note\": \"O sol está reiniciando para aplicar mudanças.\",\n    \"sleep_mode\": \"Modo de suspensão\",\n    \"sleep_mode_away\": \"Modo ausente (Ecrã desligado, despertar instantâneo)\",\n    \"sleep_mode_desc\": \"Controla o que acontece quando o cliente envia um comando de suspensão. Suspensão (S3): suspensão tradicional, baixo consumo mas requer WOL para despertar. Hibernação (S4): guarda no disco, consumo muito baixo. Modo ausente: o ecrã desliga mas o sistema continua a funcionar para despertar instantâneo - ideal para servidores de streaming de jogos.\",\n    \"sleep_mode_hibernate\": \"Hibernação (S4)\",\n    \"sleep_mode_suspend\": \"Suspensão (S3)\",\n    \"stream_audio\": \"Ativar transmissão de áudio\",\n    \"stream_audio_desc\": \"Desative esta opção para parar a transmissão de áudio.\",\n    \"stream_mic\": \"Ativar transmissão de microfone\",\n    \"stream_mic_desc\": \"Desative esta opção para parar a transmissão do microfone.\",\n    \"stream_mic_download_btn\": \"Descarregar microfone virtual\",\n    \"stream_mic_download_confirm\": \"Está prestes a ser redirecionado para a página de descarga do microfone virtual. Continuar?\",\n    \"stream_mic_note\": \"Esta funcionalidade requer a instalação de um microfone virtual\",\n    \"sunshine_name\": \"Nome do Sol\",\n    \"sunshine_name_desc\": \"O nome exibido pela luz da lua. Se não for especificado, o nome do host do PC é usado\",\n    \"sw_preset\": \"Predefinições SW\",\n    \"sw_preset_desc\": \"Otimize a troca entre a velocidade de codificação (quadros codificados por segundo) e a eficiência de compressão (qualidade por bit no bitstream). O padrão é super rápido.\",\n    \"sw_preset_fast\": \"rápido\",\n    \"sw_preset_faster\": \"mais rápido\",\n    \"sw_preset_medium\": \"Médio\",\n    \"sw_preset_slow\": \"devagar\",\n    \"sw_preset_slower\": \"lento\",\n    \"sw_preset_superfast\": \"super rápido (padrão)\",\n    \"sw_preset_ultrafast\": \"anular\",\n    \"sw_preset_veryfast\": \"veryfast\",\n    \"sw_preset_veryslow\": \"veryslow\",\n    \"sw_tune\": \"Ajuste SW\",\n    \"sw_tune_animation\": \"animação -- boa para desenhos; usa maior debargamento e mais quadros de referência\",\n    \"sw_tune_desc\": \"Ajuste as opções que são aplicadas após a predefinição. O padrão é zero.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- permite uma decodificação mais rápida desabilitando certos filtros\",\n    \"sw_tune_film\": \"filme - usado para conteúdo de filmes de alta qualidade; reduz o deblocking\",\n    \"sw_tune_grain\": \"grãos - preserva a estrutura de grãos em material cinematográfico antigo e cinzento\",\n    \"sw_tune_stillimage\": \"ainda - bom para conteúdo parecido com a apresentação de slides\",\n    \"sw_tune_zerolatency\": \"zerolatência -- bom para codificação rápida e streaming de baixa latência (padrão)\",\n    \"system_tray\": \"Ativar bandeja do sistema\",\n    \"system_tray_desc\": \"Se deve ativar o tabuleiro do sistema. Se ativado, o Sunshine exibirá um ícone no tabuleiro do sistema e pode ser controlado a partir do tabuleiro do sistema.\",\n    \"touchpad_as_ds4\": \"Emule um gamepad DS4 se o cliente controla um touchpad estiver presente\",\n    \"touchpad_as_ds4_desc\": \"Se desativada, a presença de touchpad não será tida em conta durante a seleção de tipos de controle.\",\n    \"unsaved_changes_tooltip\": \"Você tem alterações não salvas. Clique para salvar.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Configurar automaticamente o encaminhamento de portas para transmissão na Internet\",\n    \"variable_refresh_rate\": \"Taxa de atualização variável (VRR)\",\n    \"variable_refresh_rate_desc\": \"Permitir que a taxa de quadros do fluxo de vídeo corresponda à taxa de quadros de renderização para suporte VRR. Quando ativado, a codificação ocorre apenas quando novos quadros estão disponíveis, permitindo que o fluxo siga a taxa de quadros de renderização real.\",\n    \"vdd_reuse_desc_windows\": \"Quando ativado, todos os clientes partilharão o mesmo VDD (Virtual Display Device). Quando desativado (padrão), cada cliente obtém o seu próprio VDD. Ative isto para uma troca de cliente mais rápida, mas note que todos os clientes partilharão as mesmas definições de ecrã.\",\n    \"vdd_reuse_windows\": \"Reutilizar o mesmo VDD para todos os clientes\",\n    \"virtual_display\": \"Exibição virtual\",\n    \"virtual_mouse\": \"Controlador de rato virtual\",\n    \"virtual_mouse_desc\": \"Quando ativado, o Sunshine utilizará o controlador Zako Virtual Mouse (se instalado) para simular a entrada do rato ao nível HID. Permite que jogos com Raw Input recebam eventos do rato. Quando desativado ou controlador não instalado, recorre ao SendInput.\",\n    \"virtual_sink\": \"Saída de áudio virtual\",\n    \"virtual_sink_desc\": \"Especifique manualmente um dispositivo de áudio virtual para usar. Se não for definido, o dispositivo é escolhido automaticamente. Recomendamos fortemente deixar este campo em branco para usar a seleção automática de dispositivo!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Instalar o controlador de rato virtual?\",\n    \"vmouse_confirm_uninstall\": \"Desinstalar o controlador de rato virtual?\",\n    \"vmouse_install\": \"Instalar controlador\",\n    \"vmouse_installing\": \"A instalar...\",\n    \"vmouse_note\": \"O controlador de rato virtual requer instalação separada. Utilize o painel de controlo do Sunshine para instalar ou gerir o controlador.\",\n    \"vmouse_refresh\": \"Atualizar estado\",\n    \"vmouse_status_installed\": \"Instalado (não ativo)\",\n    \"vmouse_status_not_installed\": \"Não instalado\",\n    \"vmouse_status_running\": \"Em execução\",\n    \"vmouse_uninstall\": \"Desinstalar controlador\",\n    \"vmouse_uninstalling\": \"A desinstalar...\",\n    \"vt_coder\": \"VideoToolbox Coder\",\n    \"vt_realtime\": \"Codificação em Tempo Real VideoToolbox\",\n    \"vt_software\": \"Codificação VideoToolbox Software\",\n    \"vt_software_allowed\": \"Permitido\",\n    \"vt_software_forced\": \"Forçado\",\n    \"wan_encryption_mode\": \"Modo de Criptografia WAN\",\n    \"wan_encryption_mode_1\": \"Habilitado para clientes suportados (padrão)\",\n    \"wan_encryption_mode_2\": \"Obrigatório para todos os clientes\",\n    \"wan_encryption_mode_desc\": \"Isso determina quando a criptografia será usada no streaming pela internet. A criptografia pode reduzir o desempenho do streaming, particularmente em hosts e clientes menos poderosos.\",\n    \"webhook_curl_command\": \"Comando\",\n    \"webhook_curl_command_desc\": \"Copie o seguinte comando para o seu terminal para testar se o webhook está funcionando corretamente:\",\n    \"webhook_curl_copy_failed\": \"Falha ao copiar, por favor selecione e copie manualmente\",\n    \"webhook_enabled\": \"Notificações Webhook\",\n    \"webhook_enabled_desc\": \"Quando habilitado, o Sunshine enviará notificações de eventos para a URL Webhook especificada\",\n    \"webhook_group\": \"Configurações de notificação Webhook\",\n    \"webhook_skip_ssl_verify\": \"Pular verificação de certificado SSL\",\n    \"webhook_skip_ssl_verify_desc\": \"Pular verificação de certificado SSL para conexões HTTPS, apenas para testes ou certificados auto-assinados\",\n    \"webhook_test\": \"Testar\",\n    \"webhook_test_failed\": \"Teste Webhook falhou\",\n    \"webhook_test_failed_note\": \"Nota: Por favor, verifique se a URL está correta ou verifique o console do navegador para mais informações.\",\n    \"webhook_test_success\": \"Teste Webhook bem-sucedido!\",\n    \"webhook_test_success_cors_note\": \"Nota: Devido às restrições CORS, o status de resposta do servidor não pode ser confirmado.\\nA solicitação foi enviada. Se o webhook estiver configurado corretamente, a mensagem deve ter sido entregue.\\n\\nSugestão: Verifique a guia Rede nas ferramentas de desenvolvedor do seu navegador para obter detalhes da solicitação.\",\n    \"webhook_test_url_required\": \"Por favor, insira primeiro a URL Webhook\",\n    \"webhook_timeout\": \"Timeout da solicitação\",\n    \"webhook_timeout_desc\": \"Timeout para solicitações Webhook em milissegundos, intervalo de 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"A URL para receber notificações de eventos, suporta protocolos HTTP/HTTPS\",\n    \"wgc_checking_mode\": \"Verificando modo...\",\n    \"wgc_checking_running_mode\": \"Verificando modo de execução...\",\n    \"wgc_control_panel_only\": \"Este recurso está disponível apenas no Painel de Controle Sunshine\",\n    \"wgc_mode_switch_failed\": \"Falha ao alternar modo\",\n    \"wgc_mode_switch_started\": \"Alternância de modo iniciada. Se um prompt do UAC aparecer, clique em 'Sim' para confirmar.\",\n    \"wgc_service_mode_warning\": \"A captura WGC requer execução no modo de usuário. Se estiver executando no modo de serviço, clique no botão acima para alternar para o modo de usuário.\",\n    \"wgc_switch_to_service_mode\": \"Alternar para Modo de Serviço\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Atualmente em execução no modo de usuário. Clique para alternar para o modo de serviço.\",\n    \"wgc_switch_to_user_mode\": \"Alternar para Modo de Usuário\",\n    \"wgc_switch_to_user_mode_tooltip\": \"A captura WGC requer execução no modo de usuário. Clique neste botão para alternar para o modo de usuário.\",\n    \"wgc_user_mode_available\": \"Atualmente em execução no modo de usuário. A captura WGC está disponível.\",\n    \"window_title\": \"Título da Janela\",\n    \"window_title_desc\": \"O título da janela a ser capturada (correspondência parcial, não diferencia maiúsculas de minúsculas). Se deixado vazio, o nome do aplicativo em execução atual será usado automaticamente.\",\n    \"window_title_placeholder\": \"ex: Nome do Aplicativo\"\n  },\n  \"index\": {\n    \"description\": \"O sol é um anfitrião de jogos auto-hospedado para o Moonlight.\",\n    \"download\": \"BAIXAR\",\n    \"installed_version_not_stable\": \"Você está executando uma versão de pré-lançamento do Sol. Você pode enfrentar erros ou outros problemas. Por favor, reporte qualquer problema que você encontrar. Obrigado por ajudar a fazer do sol um software melhor!\",\n    \"loading_latest\": \"Carregando a última versão...\",\n    \"new_pre_release\": \"Uma nova versão de pré-lançamento está disponível!\",\n    \"new_stable\": \"Uma nova versão Stable está disponível!\",\n    \"startup_errors\": \"<b>Atenção!</b> A Sunshine detectou estes erros durante o arranque. Recomendamos <b>vivamente que</b> os corrija antes de transmitir.\",\n    \"update_download_confirm\": \"Está prestes a abrir a página de transferência de atualizações no seu navegador. Continuar?\",\n    \"version_dirty\": \"Obrigado por ajudar a fazer do sol um software melhor!\",\n    \"version_latest\": \"Você está executando a última versão do Sunshine\",\n    \"view_logs\": \"Ver registos\",\n    \"welcome\": \"Olá, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Aplicações\",\n    \"configuration\": \"Configuração\",\n    \"home\": \"Residencial\",\n    \"password\": \"Mudar a senha\",\n    \"pin\": \"PIN\",\n    \"theme_auto\": \"Automático\",\n    \"theme_dark\": \"Escuro\",\n    \"theme_light\": \"Fino\",\n    \"toggle_theme\": \"Tema\",\n    \"troubleshoot\": \"Solução de problemas\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Confirmar senha\",\n    \"current_creds\": \"Credenciais atuais\",\n    \"new_creds\": \"Novas Credenciais\",\n    \"new_username_desc\": \"Se não for especificado, o nome de usuário não irá mudar\",\n    \"password_change\": \"Alteração de senha\",\n    \"success_msg\": \"A senha foi alterada com sucesso! Essa página será recarregada em breve, seu navegador irá pedir as novas credenciais.\"\n  },\n  \"pin\": {\n    \"actions\": \"Ações\",\n    \"cancel_editing\": \"Cancelar edição\",\n    \"client_name\": \"Nome\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Confirmar exclusão\",\n    \"delete_client\": \"Excluir cliente\",\n    \"delete_confirm_message\": \"Tem certeza de que deseja excluir <strong>{name}</strong>?\",\n    \"delete_warning\": \"Esta ação não pode ser desfeita.\",\n    \"device_name\": \"Nome do dispositivo\",\n    \"device_size\": \"Tamanho do dispositivo\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Grande - TV\",\n    \"device_size_medium\": \"Médio - Tablet\",\n    \"device_size_small\": \"Pequeno - Telefone\",\n    \"edit_client_settings\": \"Editar configurações do cliente\",\n    \"hdr_profile\": \"Perfil HDR\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Carregando...\",\n    \"loading_clients\": \"Carregando clientes...\",\n    \"modify_in_gui\": \"Por favor, modifique na interface gráfica\",\n    \"none\": \"-- Nenhum --\",\n    \"or_manual_pin\": \"ou introduzir PIN manualmente\",\n    \"pair_failure\": \"Pareamento Falhou: Verifique se o PIN foi digitado corretamente\",\n    \"pair_success\": \"Sucesso! Por favor, verifique a Lua Lunar para continuar\",\n    \"pin_pairing\": \"PIN Pairing\",\n    \"qr_expires_in\": \"Expira em\",\n    \"qr_generate\": \"Gerar código QR\",\n    \"qr_paired_success\": \"Emparelhado com sucesso!\",\n    \"qr_pairing\": \"Emparelhamento por QR Code\",\n    \"qr_pairing_desc\": \"Gere um código QR para emparelhamento rápido. Digitalize-o com o cliente Moonlight para emparelhar automaticamente.\",\n    \"qr_pairing_warning\": \"Funcionalidade experimental. Se o emparelhamento falhar, utilize o emparelhamento manual por PIN abaixo. Nota: Esta funcionalidade só funciona em LAN.\",\n    \"qr_refresh\": \"Atualizar código QR\",\n    \"remove_paired_devices_desc\": \"Remova seus dispositivos emparelhados.\",\n    \"save_changes\": \"Salvar alterações\",\n    \"save_failed\": \"Falha ao salvar as configurações do cliente. Por favor, tente novamente.\",\n    \"save_or_cancel_first\": \"Por favor, salve ou cancele a edição primeiro\",\n    \"send\": \"Mandar\",\n    \"unknown_client\": \"Cliente desconhecido\",\n    \"unpair_all_confirm\": \"Tem certeza de que deseja desemparelhar todos os clientes? Esta ação não pode ser desfeita.\",\n    \"unsaved_changes\": \"Alterações não salvas\",\n    \"warning_msg\": \"Certifique-se de que você tem acesso ao cliente com o qual está emparelhando. Este software pode dar controle total ao seu computador, então tenha cuidado!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android recomendado\",\n    \"client_downloads\": \"Downloads de clientes\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"GitHub Discussions\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Juntar-se à comunidade\",\n    \"join_group_desc\": \"Obter ajuda e partilhar experiências\",\n    \"legal\": \"Informações\",\n    \"legal_desc\": \"Ao continuar a usar este software, você concorda com os termos e condições nos seguintes documentos.\",\n    \"license\": \"Tipo:\",\n    \"lizardbyte_website\": \"Portal LizardByte\",\n    \"official_website\": \"Site oficial\",\n    \"official_website_title\": \"AlkaidLab - Site oficial\",\n    \"open_source\": \"Código aberto\",\n    \"open_source_desc\": \"Star & Fork para apoiar o projeto\",\n    \"quick_start\": \"Início rápido\",\n    \"resources\": \"Recursos\",\n    \"resources_desc\": \"Recursos para luz solar!\",\n    \"third_party_desc\": \"Avisos de componentes de terceiros\",\n    \"third_party_moonlight\": \"Links amigos\",\n    \"third_party_notice\": \"Aviso de terceiros\",\n    \"tutorial\": \"Tutorial\",\n    \"tutorial_desc\": \"Guia detalhado de configuração e utilização\",\n    \"view_license\": \"Ver licença completa\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Display virtual\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"Ir para o repositório\",\n    \"harmony_modal_desc\": \"Para HarmonyOS NEXT Moonlight, pesquise Moonlight V+ na loja HarmonyOS\",\n    \"harmony_modal_link_notice\": \"Este link redirecionará para o repositório do projeto\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"segundos\",\n    \"restart_desc\": \"Configuração guardada. O Sunshine está a reiniciar para aplicar as definições de visualização.\",\n    \"restart_go_now\": \"Ir agora\",\n    \"restart_title\": \"A reiniciar o Sunshine\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"As configurações básicas estão ativas. Você pode começar a transmitir com um cliente Moonlight imediatamente!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"O ecrã base (VDD) é o ecrã virtual inteligente integrado do Sunshine Foundation, que suporta qualquer resolução, taxa de fotogramas e otimização HDR. É a escolha preferida para streaming com ecrã desligado e streaming de ecrã estendido.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Garantir ativação\",\n    \"step3_ensure_active_desc\": \"Ativa o ecrã se não estiver já ativo\",\n    \"step3_ensure_only_display\": \"Garantir ecrã único\",\n    \"step3_ensure_only_display_desc\": \"Desativa todos os outros ecrãs e ativa apenas o ecrã especificado (recomendado)\",\n    \"step3_ensure_primary\": \"Garantir ecrã principal\",\n    \"step3_ensure_primary_desc\": \"Ativa o ecrã e define-o como ecrã principal\",\n    \"step3_ensure_secondary\": \"Streaming secundário\",\n    \"step3_ensure_secondary_desc\": \"Usa apenas o ecrã virtual para streaming secundário estendido\",\n    \"step3_no_operation\": \"Sem operação\",\n    \"step3_no_operation_desc\": \"Sem alterações no estado do ecrã; o utilizador deve garantir que o ecrã está pronto\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Advanced\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Config Files\",\n    \"general\": \"General\",\n    \"input\": \"Input\",\n    \"network\": \"Network\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"A analisar...\",\n    \"ai_analyzing_logs\": \"A analisar registos, por favor aguarde...\",\n    \"ai_config\": \"Configuração IA\",\n    \"ai_copy_result\": \"Copiar\",\n    \"ai_diagnosis\": \"Diagnóstico IA\",\n    \"ai_diagnosis_title\": \"Diagnóstico IA dos registos\",\n    \"ai_error\": \"Análise falhada\",\n    \"ai_key_local\": \"A chave API é armazenada apenas localmente e nunca carregada\",\n    \"ai_model\": \"Modelo\",\n    \"ai_provider\": \"Fornecedor\",\n    \"ai_reanalyze\": \"Reanalisar\",\n    \"ai_result\": \"Resultado do diagnóstico\",\n    \"ai_retry\": \"Tentar novamente\",\n    \"ai_start_diagnosis\": \"Iniciar diagnóstico\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Se precisar desligar o Sunshine imediatamente, pode usar esta função. Note que terá de o iniciar manualmente após o desligamento.\",\n    \"boom_sunshine_success\": \"Sunshine foi desligado\",\n    \"confirm_boom\": \"Realmente quer sair?\",\n    \"confirm_boom_desc\": \"Então realmente quer sair? Bem, não posso impedi-lo, vá em frente e clique novamente\",\n    \"confirm_logout\": \"Confirmar saída?\",\n    \"confirm_logout_desc\": \"Terá de introduzir a palavra-passe novamente para aceder à interface web.\",\n    \"copy_config\": \"Copiar configuração\",\n    \"copy_config_error\": \"Falha ao copiar configuração\",\n    \"copy_config_success\": \"Configuração copiada para a área de transferência!\",\n    \"copy_logs\": \"Copy logs\",\n    \"download_logs\": \"Download logs\",\n    \"force_close\": \"Forçar fechamento\",\n    \"force_close_desc\": \"Se o Moonlight reclamar de um aplicativo em execução, forçar o fechamento do aplicativo deve resolver o problema.\",\n    \"force_close_error\": \"Erro ao fechar o aplicativo\",\n    \"force_close_success\": \"Aplicativo fechado com sucesso!\",\n    \"ignore_case\": \"Ignorar maiúsculas e minúsculas\",\n    \"logout\": \"Sair\",\n    \"logout_desc\": \"Sair. Pode ser necessário fazer login novamente.\",\n    \"logout_localhost_tip\": \"O ambiente atual não requer login; o logout não irá solicitar credenciais.\",\n    \"logs\": \"Registros\",\n    \"logs_desc\": \"Veja os logs carregados por Sunshine\",\n    \"logs_find\": \"Localizar...\",\n    \"match_contains\": \"Contém\",\n    \"match_exact\": \"Exato\",\n    \"match_regex\": \"Regex\",\n    \"reopen_setup_wizard\": \"Reabrir assistente de configuração\",\n    \"reopen_setup_wizard_desc\": \"Reabrir a página do assistente de configuração para reconfigurar as configurações iniciais.\",\n    \"reopen_setup_wizard_error\": \"Erro ao reabrir o assistente de configuração\",\n    \"reset_display_device_desc_windows\": \"Se o Sunshine estiver preso a tentar restaurar as configurações alteradas do dispositivo de exibição, pode repor as configurações e prosseguir para restaurar o estado do ecrã manualmente.\\nIsto pode acontecer por vários motivos: o dispositivo já não está disponível, foi ligado a uma porta diferente, etc.\",\n    \"reset_display_device_error_windows\": \"Erro ao resetar a persistência!\",\n    \"reset_display_device_success_windows\": \"Success resetting persistence!\",\n    \"reset_display_device_windows\": \"Reset Display Memory\",\n    \"restart_sunshine\": \"Reiniciar o Sunshine\",\n    \"restart_sunshine_desc\": \"Se o sol não estiver funcionando corretamente, você pode tentar reiniciá-lo. Isso encerrará todas as sessões em execução.\",\n    \"restart_sunshine_success\": \"A luz do sol está reiniciando\",\n    \"troubleshooting\": \"Solução de problemas\",\n    \"unpair_all\": \"Desconectar todos\",\n    \"unpair_all_error\": \"Erro ao desemparelhar\",\n    \"unpair_all_success\": \"Desemparelhado com sucesso!\",\n    \"unpair_desc\": \"Remova seus dispositivos emparelhados. Dispositivos desemparelhados individualmente com uma sessão ativa permanecerão conectados, mas não podem iniciar ou retomar uma sessão.\",\n    \"unpair_single_no_devices\": \"Não há dispositivos emparelhados.\",\n    \"unpair_single_success\": \"No entanto, os dispositivos ainda podem estar em uma sessão ativa. Use o botão 'Forçar Fechar' acima para finalizar todas as sessões abertas.\",\n    \"unpair_single_unknown\": \"Cliente Desconhecido\",\n    \"unpair_title\": \"Desconectar dispositivos\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Confirmar a senha\",\n    \"create_creds\": \"Antes de começar, precisamos que você crie um novo nome de usuário e senha para acessar a interface da web.\",\n    \"create_creds_alert\": \"As credenciais abaixo são necessárias para acessar a interface da Web do Sunshine. Mantenha-as seguras, já que você nunca vai vê-las novamente!\",\n    \"creds_local_only\": \"As suas credenciais são armazenadas localmente offline e nunca serão enviadas para nenhum servidor.\",\n    \"error\": \"Erro!\",\n    \"greeting\": \"Bem-vindo ao Sunshine Foundation!\",\n    \"hide_password\": \"Ocultar palavra-passe\",\n    \"login\": \"Conectar\",\n    \"network_error\": \"Erro de rede, verifique sua conexão\",\n    \"password\": \"Palavra-passe\",\n    \"password_match\": \"As senhas coincidem\",\n    \"password_mismatch\": \"As senhas não coincidem\",\n    \"server_error\": \"Erro do servidor\",\n    \"show_password\": \"Mostrar palavra-passe\",\n    \"success\": \"Sucesso!\",\n    \"username\": \"Usuário:\",\n    \"welcome_success\": \"Esta página será recarregada em breve, seu navegador irá pedir novas credenciais\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/pt_BR.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Aplicar\",\n    \"auto\": \"Automático\",\n    \"autodetect\": \"Autodetecção (recomendado)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"Cancelar\",\n    \"close\": \"Fechar\",\n    \"copied\": \"Copiado para a área de transferência\",\n    \"copy\": \"Copiar\",\n    \"delete\": \"Excluir\",\n    \"description\": \"Descrição\",\n    \"disabled\": \"Desativado\",\n    \"disabled_def\": \"Desativado (padrão)\",\n    \"dismiss\": \"Dispensar\",\n    \"do_cmd\": \"Comando Do\",\n    \"download\": \"Baixar\",\n    \"edit\": \"Editar\",\n    \"elevated\": \"Elevado\",\n    \"enabled\": \"Ativado\",\n    \"enabled_def\": \"Ativado (padrão)\",\n    \"error\": \"Erro!\",\n    \"no_changes\": \"Nenhuma alteração\",\n    \"note\": \"Observação:\",\n    \"password\": \"Senha\",\n    \"remove\": \"Remover\",\n    \"run_as\": \"Executar como administrador\",\n    \"save\": \"Salvar\",\n    \"see_more\": \"Veja mais\",\n    \"success\": \"Sucesso!\",\n    \"undo_cmd\": \"Comando Desfazer\",\n    \"username\": \"Nome de usuário\",\n    \"warning\": \"Atenção!\"\n  },\n  \"apps\": {\n    \"actions\": \"Ações\",\n    \"add_cmds\": \"Adicionar comandos\",\n    \"add_new\": \"Adicionar novo\",\n    \"advanced_options\": \"Opções avançadas\",\n    \"app_name\": \"Nome do aplicativo\",\n    \"app_name_desc\": \"Nome do aplicativo, conforme mostrado no Moonlight\",\n    \"applications_desc\": \"Os aplicativos são atualizados somente quando o Cliente é reiniciado\",\n    \"applications_title\": \"Aplicativos\",\n    \"auto_detach\": \"Continuar a transmissão se o aplicativo for encerrado rapidamente\",\n    \"auto_detach_desc\": \"Isso tentará detectar automaticamente os aplicativos do tipo lançador que fecham rapidamente após iniciar outro programa ou instância deles mesmos. Quando um aplicativo do tipo lançador é detectado, ele é tratado como um aplicativo desvinculado.\",\n    \"basic_info\": \"Informações básicas\",\n    \"cmd\": \"Comando\",\n    \"cmd_desc\": \"O aplicativo principal a ser iniciado. Se estiver em branco, nenhum aplicativo será iniciado.\",\n    \"cmd_examples_title\": \"Exemplos comuns:\",\n    \"cmd_note\": \"Se o caminho para o executável do comando contiver espaços, você deverá colocá-lo entre aspas.\",\n    \"cmd_prep_desc\": \"Uma lista de comandos a serem executados antes/depois desse aplicativo. Se algum dos comandos de preparação falhar, a inicialização do aplicativo será abortada.\",\n    \"cmd_prep_name\": \"Preparativos para o comando\",\n    \"command_settings\": \"Configurações de comando\",\n    \"covers_found\": \"Capas encontradas\",\n    \"delete\": \"Excluir\",\n    \"delete_confirm\": \"Tem certeza de que deseja excluir \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Comandos destacados\",\n    \"detached_cmds_add\": \"Adicionar comando destacado\",\n    \"detached_cmds_desc\": \"Uma lista de comandos a serem executados em segundo plano.\",\n    \"detached_cmds_note\": \"Se o caminho para o executável do comando contiver espaços, você deverá colocá-lo entre aspas.\",\n    \"detached_cmds_remove\": \"Remover comando destacado\",\n    \"edit\": \"Editar\",\n    \"env_app_id\": \"ID do aplicativo\",\n    \"env_app_name\": \"Nome do aplicativo\",\n    \"env_client_audio_config\": \"A configuração de áudio solicitada pelo cliente (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"O cliente solicitou a opção de otimizar o jogo para otimizar a transmissão (verdadeiro/falso)\",\n    \"env_client_fps\": \"O FPS solicitado pelo cliente (int)\",\n    \"env_client_gcmap\": \"A máscara de gamepad solicitada, em um formato de conjunto de bits/campo de bits (int)\",\n    \"env_client_hdr\": \"O HDR é ativado pelo cliente (verdadeiro/falso)\",\n    \"env_client_height\": \"A altura solicitada pelo cliente (int)\",\n    \"env_client_host_audio\": \"O cliente solicitou o áudio do host (verdadeiro/falso)\",\n    \"env_client_name\": \"Nome amigável do cliente (string)\",\n    \"env_client_width\": \"A largura solicitada pelo cliente (int)\",\n    \"env_displayplacer_example\": \"Exemplo - displayplacer para Resolution Automation:\",\n    \"env_qres_example\": \"Exemplo - QRes para automação de resolução:\",\n    \"env_qres_path\": \"caminho do qres\",\n    \"env_var_name\": \"Nome da Var\",\n    \"env_vars_about\": \"Sobre as variáveis de ambiente\",\n    \"env_vars_desc\": \"Todos os comandos obtêm essas variáveis de ambiente por padrão:\",\n    \"env_xrandr_example\": \"Exemplo - Xrandr para automação de resolução:\",\n    \"exit_timeout\": \"Tempo limite de saída\",\n    \"exit_timeout_desc\": \"Número de segundos para aguardar que todos os processos do aplicativo saiam graciosamente quando solicitado a sair. Se não for definido, o padrão é aguardar até 5 segundos. Se for definido como zero ou um valor negativo, o aplicativo será encerrado imediatamente.\",\n    \"file_selector_not_initialized\": \"Seletor de arquivo não inicializado\",\n    \"find_cover\": \"Encontrar cobertura\",\n    \"form_invalid\": \"Por favor, verifique os campos obrigatórios\",\n    \"form_valid\": \"Aplicativo válido\",\n    \"global_prep_desc\": \"Ativar/desativar a execução de comandos de preparação global para esse aplicativo.\",\n    \"global_prep_name\": \"Comandos globais de preparação\",\n    \"image\": \"Imagem\",\n    \"image_desc\": \"Caminho do ícone/figura/imagem do aplicativo que será enviado ao cliente. A imagem deve ser um arquivo PNG. Se não for definida, o Sunshine enviará a imagem padrão da caixa.\",\n    \"image_settings\": \"Configurações de imagem\",\n    \"loading\": \"Carregando...\",\n    \"menu_cmd_actions\": \"Ações\",\n    \"menu_cmd_add\": \"Adicionar comando de menu\",\n    \"menu_cmd_command\": \"Comando\",\n    \"menu_cmd_desc\": \"Após a configuração, estes comandos estarão visíveis no menu de retorno do cliente, permitindo a execução rápida de operações específicas sem interromper a transmissão, como iniciar programas auxiliares.\\nExemplo: Nome de exibição - Fechar o computador; Comando - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Nome de exibição\",\n    \"menu_cmd_drag_sort\": \"Arrastar para ordenar\",\n    \"menu_cmd_name\": \"Comandos de menu\",\n    \"menu_cmd_placeholder_command\": \"Comando\",\n    \"menu_cmd_placeholder_display_name\": \"Nome de exibição\",\n    \"menu_cmd_placeholder_execute\": \"Executar comando\",\n    \"menu_cmd_placeholder_undo\": \"Desfazer comando\",\n    \"menu_cmd_remove_menu\": \"Remover comando de menu\",\n    \"menu_cmd_remove_prep\": \"Remover comando de preparação\",\n    \"mouse_mode\": \"Modo do mouse\",\n    \"mouse_mode_auto\": \"Auto (Configuração global)\",\n    \"mouse_mode_desc\": \"Selecione o método de entrada do mouse para este aplicativo. Auto usa a configuração global, Mouse virtual usa o driver HID, SendInput usa a API do Windows.\",\n    \"mouse_mode_sendinput\": \"SendInput (API do Windows)\",\n    \"mouse_mode_vmouse\": \"Mouse virtual\",\n    \"name\": \"Nome\",\n    \"output_desc\": \"O arquivo em que a saída do comando é armazenada; se não for especificado, a saída será ignorada\",\n    \"output_name\": \"Saída\",\n    \"run_as_desc\": \"Isso pode ser necessário para alguns aplicativos que exigem permissões de administrador para serem executados corretamente.\",\n    \"scan_result_add_all\": \"Adicionar tudo\",\n    \"scan_result_edit_title\": \"Adicionar e editar\",\n    \"scan_result_filter_all\": \"Tudo\",\n    \"scan_result_filter_epic_title\": \"Jogos Epic Games\",\n    \"scan_result_filter_executable\": \"Executável\",\n    \"scan_result_filter_executable_title\": \"Arquivo executável\",\n    \"scan_result_filter_gog_title\": \"Jogos GOG Galaxy\",\n    \"scan_result_filter_script\": \"Script\",\n    \"scan_result_filter_script_title\": \"Script de lote/comando\",\n    \"scan_result_filter_shortcut\": \"Atalho\",\n    \"scan_result_filter_shortcut_title\": \"Atalho\",\n    \"scan_result_filter_steam_title\": \"Jogos Steam\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Jogo\",\n    \"scan_result_games_only\": \"Apenas jogos\",\n    \"scan_result_matched\": \"Correspondências: {count}\",\n    \"scan_result_no_apps\": \"Nenhum aplicativo encontrado para adicionar\",\n    \"scan_result_no_matches\": \"Nenhum aplicativo correspondente encontrado\",\n    \"scan_result_quick_add_title\": \"Adição rápida\",\n    \"scan_result_remove_title\": \"Remover da lista\",\n    \"scan_result_search_placeholder\": \"Pesquisar nome do aplicativo, comando ou caminho...\",\n    \"scan_result_show_all\": \"Mostrar tudo\",\n    \"scan_result_title\": \"Resultados da varredura\",\n    \"scan_result_try_different_keywords\": \"Tente usar palavras-chave de pesquisa diferentes\",\n    \"scan_result_type_batch\": \"Lote\",\n    \"scan_result_type_command\": \"Script de comando\",\n    \"scan_result_type_executable\": \"Arquivo executável\",\n    \"scan_result_type_shortcut\": \"Atalho\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Pesquisar aplicativos...\",\n    \"select\": \"Selecionar\",\n    \"test_menu_cmd\": \"Testar comando\",\n    \"test_menu_cmd_empty\": \"O comando não pode estar vazio\",\n    \"test_menu_cmd_executing\": \"Executando comando...\",\n    \"test_menu_cmd_failed\": \"Falha ao executar comando\",\n    \"test_menu_cmd_success\": \"Comando executado com sucesso!\",\n    \"use_desktop_image\": \"Usar papel de parede atual da área de trabalho\",\n    \"wait_all\": \"Continuar a transmissão até que todos os processos do aplicativo sejam encerrados\",\n    \"wait_all_desc\": \"Isso continuará a transmissão até que todos os processos iniciados pelo aplicativo tenham sido encerrados. Quando desmarcada, a transmissão será interrompida quando o processo inicial do aplicativo for encerrado, mesmo que outros processos do aplicativo ainda estejam em execução.\",\n    \"working_dir\": \"Diretório de trabalho\",\n    \"working_dir_desc\": \"O diretório de trabalho que deve ser passado para o processo. Por exemplo, alguns aplicativos usam o diretório de trabalho para procurar arquivos de configuração. Se não for definido, o padrão do Sunshine será o diretório pai do comando\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Nome do adaptador\",\n    \"adapter_name_desc_linux_1\": \"Especificar manualmente uma GPU a ser usada para captura.\",\n    \"adapter_name_desc_linux_2\": \"para encontrar todos os dispositivos compatíveis com VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Substitua ``renderD129`` pelo dispositivo acima para listar o nome e os recursos do dispositivo. Para ser suportado pelo Sunshine, ele precisa ter, no mínimo:\",\n    \"adapter_name_desc_windows\": \"Especifique manualmente uma GPU para usar na captura. Se não definido, a GPU é escolhida automaticamente. Nota: Esta GPU deve ter um display conectado e ligado. Se o seu laptop não puder habilitar a saída direta da GPU, defina como automático.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Se a versão mais recente do monitor virtual estiver instalada, ela poderá ser associada automaticamente à vinculação da GPU\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Adicionar\",\n    \"address_family\": \"Endereço da família\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Definir a família de endereços usada pelo Sunshine\",\n    \"address_family_ipv4\": \"Somente IPv4\",\n    \"always_send_scancodes\": \"Sempre enviar códigos de barras\",\n    \"always_send_scancodes_desc\": \"O envio de códigos de barras aumenta a compatibilidade com jogos e aplicativos, mas pode resultar em entrada incorreta do teclado de determinados clientes que não estejam usando um layout de teclado em inglês dos EUA. Ative se a entrada do teclado não estiver funcionando em determinados aplicativos. Desative se as teclas do cliente estiverem gerando a entrada incorreta no host.\",\n    \"amd_coder\": \"Codificador AMF (H264)\",\n    \"amd_coder_desc\": \"Permite que você selecione a codificação de entropia para priorizar a qualidade ou a velocidade de codificação. Somente H.264.\",\n    \"amd_enforce_hrd\": \"Aplicação do decodificador de referência hipotético (HRD) da AMF\",\n    \"amd_enforce_hrd_desc\": \"Aumenta as restrições do controle de taxa para atender aos requisitos do modelo HRD. Isso reduz bastante os excessos de taxa de bits, mas pode causar artefatos de codificação ou qualidade reduzida em determinadas placas.\",\n    \"amd_preanalysis\": \"Pré-análise da AMF\",\n    \"amd_preanalysis_desc\": \"Isso permite a pré-análise de controle de taxa, que pode aumentar a qualidade às custas de uma maior latência de codificação.\",\n    \"amd_quality\": \"Qualidade AMF\",\n    \"amd_quality_balanced\": \"balanced -- balanceado (padrão)\",\n    \"amd_quality_desc\": \"Isso controla o equilíbrio entre a velocidade e a qualidade da codificação.\",\n    \"amd_quality_group\": \"Configurações de qualidade do AMF\",\n    \"amd_quality_quality\": \"qualidade -- prefere qualidade\",\n    \"amd_quality_speed\": \"velocidade -- prefere velocidade\",\n    \"amd_qvbr_quality\": \"Nível de qualidade AMF QVBR\",\n    \"amd_qvbr_quality_desc\": \"Nível de qualidade para o modo de controle de taxa QVBR. Faixa: 1-51 (menor = melhor qualidade). Padrão: 23. Aplica-se apenas quando o controle de taxa está definido como 'qvbr'.\",\n    \"amd_rc\": \"Controle de taxa AMF\",\n    \"amd_rc_cbr\": \"cbr -- taxa de bits constante (recomendado se o HRD estiver ativado)\",\n    \"amd_rc_cqp\": \"cqp -- modo qp constante\",\n    \"amd_rc_desc\": \"Isso controla o método de controle de taxa para garantir que não estamos excedendo a meta de taxa de bits do cliente. O \\\"cqp\\\" não é adequado para o direcionamento da taxa de bits, e outras opções além do \\\"vbr_latency\\\" dependem da aplicação do HRD para ajudar a restringir os excessos de taxa de bits.\",\n    \"amd_rc_group\": \"Configurações do controle de taxa AMF\",\n    \"amd_rc_hqcbr\": \"hqcbr -- taxa de bits constante alta qualidade\",\n    \"amd_rc_hqvbr\": \"hqvbr -- taxa de bits variável alta qualidade\",\n    \"amd_rc_qvbr\": \"qvbr -- taxa de bits variável de qualidade (usa nível de qualidade QVBR)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- taxa de bits variável com restrição de latência (recomendado se o HRD estiver desativado; padrão)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- taxa de bits variável com restrição de pico\",\n    \"amd_usage\": \"Uso do AMF\",\n    \"amd_usage_desc\": \"Isso define o perfil de codificação básico. Todas as opções apresentadas abaixo substituirão um subconjunto do perfil de uso, mas há configurações ocultas adicionais aplicadas que não podem ser configuradas em outro lugar.\",\n    \"amd_usage_lowlatency\": \"lowlatency - baixa latência (mais rápida)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - baixa latência, alta qualidade (rápida)\",\n    \"amd_usage_transcoding\": \"transcoding -- transcodificação (mais lento)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency - latência ultrabaixa (mais rápida; padrão)\",\n    \"amd_usage_webcam\": \"webcam -- webcam (lenta)\",\n    \"amd_vbaq\": \"Quantização adaptativa baseada em variância AMF (VBAQ)\",\n    \"amd_vbaq_desc\": \"Em geral, o sistema visual humano é menos sensível a artefatos em áreas altamente texturizadas. No modo VBAQ, a variação de pixels é usada para indicar a complexidade das texturas espaciais, permitindo que o codificador aloque mais bits para áreas mais suaves. A ativação desse recurso leva a melhorias na qualidade visual subjetiva com alguns conteúdos.\",\n    \"amf_draw_mouse_cursor\": \"Desenhar um cursor simples ao usar o método de captura AMF\",\n    \"amf_draw_mouse_cursor_desc\": \"Em alguns casos, usar a captura AMF não exibirá o ponteiro do mouse. Ativar esta opção desenhará um ponteiro do mouse simples na tela. Nota: A posição do ponteiro do mouse só será atualizada quando houver uma atualização da tela de conteúdo, portanto, em cenários que não sejam jogos, como na área de trabalho, você pode observar movimento lento do ponteiro do mouse.\",\n    \"apply_note\": \"Clique em \\\"Apply\\\" (Aplicar) para reiniciar o Sunshine e aplicar as alterações. Isso encerrará todas as sessões em execução.\",\n    \"audio_sink\": \"Dissipador de áudio\",\n    \"audio_sink_desc_linux\": \"O nome do coletor de áudio usado para Loopback de áudio. Se você não especificar essa variável, o pulseaudio selecionará o dispositivo de monitor padrão. Você pode encontrar o nome do coletor de áudio usando qualquer um dos comandos:\",\n    \"audio_sink_desc_macos\": \"O nome do coletor de áudio usado para Loopback de áudio. O Sunshine só pode acessar microfones no macOS devido a limitações do sistema. Para transmitir o áudio do sistema usando o Soundflower ou o BlackHole.\",\n    \"audio_sink_desc_windows\": \"Especificar manualmente um dispositivo de áudio específico para captura. Se não for definido, o dispositivo será escolhido automaticamente. É altamente recomendável deixar esse campo em branco para usar a seleção automática de dispositivos! Se você tiver vários dispositivos de áudio com nomes idênticos, poderá obter o ID do dispositivo usando o seguinte comando:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Alto-falantes (dispositivo de áudio de alta definição)\",\n    \"av1_mode\": \"Suporte AV1\",\n    \"av1_mode_0\": \"O Sunshine anunciará o suporte para AV1 com base nos recursos do codificador (recomendado)\",\n    \"av1_mode_1\": \"A Sunshine não fará propaganda do suporte ao AV1\",\n    \"av1_mode_2\": \"A Sunshine anunciará o suporte ao perfil AV1 Main de 8 bits\",\n    \"av1_mode_3\": \"A Sunshine anunciará o suporte aos perfis AV1 Main de 8 e 10 bits (HDR)\",\n    \"av1_mode_desc\": \"Permite que o cliente solicite fluxos de vídeo AV1 Main de 8 ou 10 bits. A codificação do AV1 consome mais CPU, portanto, a ativação dessa opção pode reduzir o desempenho ao usar a codificação de software.\",\n    \"back_button_timeout\": \"Tempo limite de emulação do botão Início/Guia\",\n    \"back_button_timeout_desc\": \"Se o botão Voltar/Selecionar for mantido pressionado pelo número especificado de milissegundos, um pressionamento do botão Início/Guia será emulado. Se definido com um valor < 0 (padrão), manter pressionado o botão Voltar/Selecionar não emulará o botão Início/Guia.\",\n    \"bind_address\": \"Endereço de vinculação (recurso de teste)\",\n    \"bind_address_desc\": \"Defina o endereço IP específico ao qual o Sunshine será vinculado. Se deixado em branco, o Sunshine será vinculado a todos os endereços disponíveis.\",\n    \"capture\": \"Forçar método de captura específico\",\n    \"capture_desc\": \"No modo automático, o Sunshine usará o primeiro que funcionar. NvFBC requer drivers NVIDIA corrigidos.\",\n    \"capture_target\": \"Destino de Captura\",\n    \"capture_target_desc\": \"Selecione o tipo de destino a ser capturado. Ao selecionar 'Janela', você pode capturar uma janela de aplicativo específica (como software de interpolação de quadros de IA) em vez de toda a tela.\",\n    \"capture_target_display\": \"Tela\",\n    \"capture_target_window\": \"Janela\",\n    \"cert\": \"Certificado\",\n    \"cert_desc\": \"O certificado usado para a interface do usuário da Web e o emparelhamento do cliente Moonlight. Para melhor compatibilidade, ele deve ter uma chave pública RSA-2048.\",\n    \"channels\": \"Máximo de clientes conectados\",\n    \"channels_desc_1\": \"O Sunshine pode permitir que uma única sessão de streaming seja compartilhada com vários clientes simultaneamente.\",\n    \"channels_desc_2\": \"Alguns codificadores de hardware podem ter limitações que reduzem o desempenho com vários fluxos.\",\n    \"close_verify_safe\": \"Verificação segura compatível com clientes antigos\",\n    \"close_verify_safe_desc\": \"Clientes antigos podem não se conectar ao Sunshine, por favor, desative esta opção ou atualize o cliente\",\n    \"coder_cabac\": \"cabac -- codificação aritmética binária adaptável ao contexto - qualidade superior\",\n    \"coder_cavlc\": \"cavlc -- codificação de comprimento variável adaptável ao contexto - decodificação mais rápida\",\n    \"configuration\": \"Configuração\",\n    \"controller\": \"Ativar entrada do controle de jogo\",\n    \"controller_desc\": \"Permite que os convidados controlem o sistema host com um gamepad/controlador\",\n    \"credentials_file\": \"Arquivo de credenciais\",\n    \"credentials_file_desc\": \"Armazene o nome de usuário/senha separadamente do arquivo de estado do Sunshine.\",\n    \"display_device_options_note_desc_windows\": \"O Windows salva várias configurações de vídeo para cada combinação de monitores atualmente ativos.\\nO Sunshine então aplica alterações a um monitor (ou monitores) pertencente a tal combinação de vídeo.\\nSe você desconectar um dispositivo que estava ativo quando o Sunshine aplicou as configurações, as alterações não poderão ser revertidas, a menos que a combinação possa ser ativada novamente quando o Sunshine tentar reverter as alterações!\",\n    \"display_device_options_note_windows\": \"Nota sobre como as configurações são aplicadas\",\n    \"display_device_options_windows\": \"Opções do dispositivo de exibição\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Ativa o monitor se ele não estiver ativo\",\n    \"display_device_prep_ensure_active_windows\": \"Ativar a tela automaticamente\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Desativa todos os outros monitores e ativa apenas o monitor especificado\",\n    \"display_device_prep_ensure_only_display_windows\": \"Desativar outras telas e ativar apenas a tela especificada\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Ativa o monitor e o define como monitor principal\",\n    \"display_device_prep_ensure_primary_windows\": \"Ativar a tela automaticamente e torná-la uma tela primária\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Usa apenas o monitor virtual para streaming secundário estendido\",\n    \"display_device_prep_ensure_secondary_windows\": \"Streaming de monitor secundário (apenas monitor virtual)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Sem alterações no estado do monitor; o usuário deve garantir que o monitor esteja pronto\",\n    \"display_device_prep_no_operation_windows\": \"Desativado\",\n    \"display_device_prep_windows\": \"Preparação de exibição\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Pelo menos um valor \\\"recebido\\\" e um valor \\\"final\\\" devem ser especificados.\\nO campo vazio na seção \\\"recebido\\\" significa \\\"corresponder a qualquer um\\\". O campo vazio na seção \\\"final\\\" significa \\\"manter o valor recebido\\\".\\nVocê pode corresponder um valor de FPS específico a uma resolução específica, se desejar...\\n\\nNota: se a opção \\\"Otimizar configurações do jogo\\\" não estiver ativada no cliente Moonlight, as linhas contendo valor(es) de resolução serão ignoradas.\",\n    \"display_mode_remapping_desc_windows\": \"Especifique como uma resolução específica e/ou taxa de atualização deve ser remapeada para outros valores.\\nVocê pode transmitir em resolução mais baixa, enquanto renderiza em resolução mais alta no host para um efeito de supersampling.\\nOu você pode transmitir em FPS mais alto enquanto limita o host à taxa de atualização mais baixa.\\nA correspondência é executada de cima para baixo. Depois que a entrada for correspondida, outras não serão mais verificadas, mas ainda serão validadas.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Taxa de atualização final\",\n    \"display_mode_remapping_final_resolution_windows\": \"Resolução final\",\n    \"display_mode_remapping_optional\": \"opcional\",\n    \"display_mode_remapping_received_fps_windows\": \"FPS recebido\",\n    \"display_mode_remapping_received_resolution_windows\": \"Resolução recebida\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Nota: se a opção \\\"Otimizar configurações do jogo\\\" não estiver ativada no cliente Moonlight, o remapeamento será desativado.\",\n    \"display_mode_remapping_windows\": \"Remapear modos de exibição\",\n    \"display_modes\": \"Modos de Exibição\",\n    \"ds4_back_as_touchpad_click\": \"Mapear Voltar/Selecionar para clicar no touchpad\",\n    \"ds4_back_as_touchpad_click_desc\": \"Ao forçar a emulação DS4, mapeie Back/Select para Touchpad Click\",\n    \"dsu_server_port\": \"Porta do servidor DSU\",\n    \"dsu_server_port_desc\": \"Porta de escuta do servidor DSU (padrão 26760). O Sunshine atuará como um servidor DSU para receber conexões de cliente e enviar dados de movimento. Ative o servidor DSU no seu cliente (Yuzu, Ryujinx, etc.) e defina o endereço do servidor DSU (127.0.0.1) e a porta (26760).\",\n    \"enable_dsu_server\": \"Ativar servidor DSU\",\n    \"enable_dsu_server_desc\": \"Ativar servidor DSU para receber conexões de cliente e enviar dados de movimento\",\n    \"encoder\": \"Forçar um codificador específico\",\n    \"encoder_desc\": \"Force um codificador específico; caso contrário, o Sunshine selecionará a melhor opção disponível. Observação: se você especificar um codificador de hardware no Windows, ele deverá corresponder à GPU em que o monitor está conectado.\",\n    \"encoder_software\": \"Software\",\n    \"experimental\": \"Experimental\",\n    \"experimental_features\": \"Funcionalidades experimentais\",\n    \"external_ip\": \"IP externo\",\n    \"external_ip_desc\": \"Se nenhum endereço IP externo for fornecido, o Sunshine detectará automaticamente o IP externo\",\n    \"fec_percentage\": \"Porcentagem de FEC\",\n    \"fec_percentage_desc\": \"Porcentagem de pacotes de correção de erros por pacote de dados em cada quadro de vídeo. Valores mais altos podem corrigir mais perdas de pacotes na rede, mas ao custo de aumentar o uso da largura de banda.\",\n    \"ffmpeg_auto\": \"auto -- deixa o ffmpeg decidir (padrão)\",\n    \"file_apps\": \"Arquivo de aplicativos\",\n    \"file_apps_desc\": \"O arquivo em que os aplicativos atuais do Sunshine são armazenados.\",\n    \"file_state\": \"Arquivo estadual\",\n    \"file_state_desc\": \"O arquivo em que o estado atual do Sunshine está armazenado\",\n    \"fps\": \"FPS anunciados\",\n    \"gamepad\": \"Tipo de gamepad emulado\",\n    \"gamepad_auto\": \"Opções de seleção automática\",\n    \"gamepad_desc\": \"Escolha o tipo de gamepad a ser emulado no host\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"Opções de seleção DS4\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Opções manuais do DS4\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Preparativos para o comando\",\n    \"global_prep_cmd_desc\": \"Configure uma lista de comandos a serem executados antes ou depois da execução de qualquer aplicativo. Se algum dos comandos de preparação especificados falhar, o processo de inicialização do aplicativo será abortado.\",\n    \"hdr_luminance_analysis\": \"Metadados dinâmicos HDR (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Ativa a análise de luminância GPU por quadro e injeta metadados dinâmicos HDR10+ (ST 2094-40) e HDR Vivid (CUVA) no fluxo codificado. Fornece dicas de mapeamento de tons por quadro para telas suportadas. Adiciona uma pequena sobrecarga GPU (~0,5-1,5ms/quadro em altas resoluções). Desative se tiver quedas de framerate com HDR ativado.\",\n    \"hdr_prep_automatic_windows\": \"Ligar/desligar o modo HDR conforme solicitado pelo cliente\",\n    \"hdr_prep_no_operation_windows\": \"Desativado\",\n    \"hdr_prep_windows\": \"Mudança de estado HDR\",\n    \"hevc_mode\": \"Suporte a HEVC\",\n    \"hevc_mode_0\": \"A Sunshine anunciará o suporte para HEVC com base nos recursos do codificador (recomendado)\",\n    \"hevc_mode_1\": \"A Sunshine não anunciará suporte para HEVC\",\n    \"hevc_mode_2\": \"A Sunshine anunciará o suporte ao perfil principal HEVC\",\n    \"hevc_mode_3\": \"A Sunshine anunciará o suporte aos perfis HEVC Main e Main10 (HDR)\",\n    \"hevc_mode_desc\": \"Permite que o cliente solicite fluxos de vídeo HEVC Main ou HEVC Main10. A codificação do HEVC consome mais CPU, portanto, ativar essa opção pode reduzir o desempenho ao usar a codificação de software.\",\n    \"high_resolution_scrolling\": \"Suporte à rolagem de alta resolução\",\n    \"high_resolution_scrolling_desc\": \"Quando ativado, o Sunshine transmitirá os eventos de rolagem de alta resolução dos clientes do Moonlight. Isso pode ser útil para desativar aplicativos mais antigos que rolam muito rápido com eventos de rolagem de alta resolução.\",\n    \"install_steam_audio_drivers\": \"Instalar os drivers de áudio do Steam\",\n    \"install_steam_audio_drivers_desc\": \"Se o Steam estiver instalado, isso instalará automaticamente o driver Steam Streaming Speakers para oferecer suporte a som surround 5.1/7.1 e silenciar o áudio do host.\",\n    \"key_repeat_delay\": \"Atraso de repetição de tecla\",\n    \"key_repeat_delay_desc\": \"Controle a velocidade com que as teclas se repetirão. O atraso inicial em milissegundos antes da repetição das teclas.\",\n    \"key_repeat_frequency\": \"Frequência de repetição da tecla\",\n    \"key_repeat_frequency_desc\": \"A frequência com que as teclas se repetem a cada segundo. Essa opção configurável aceita decimais.\",\n    \"key_rightalt_to_key_win\": \"Mapear tecla Alt direita para tecla Windows\",\n    \"key_rightalt_to_key_win_desc\": \"Pode ser que você não consiga enviar a tecla Windows diretamente do Moonlight. Nesses casos, pode ser útil fazer com que o Sunshine pense que a tecla Alt. direita é a tecla Windows\",\n    \"key_rightalt_to_key_windows\": \"Map Right Alt key to Windows key\",\n    \"keyboard\": \"Ativar entrada de teclado\",\n    \"keyboard_desc\": \"Permite que os convidados controlem o sistema host com o teclado\",\n    \"lan_encryption_mode\": \"Modo de criptografia de LAN\",\n    \"lan_encryption_mode_1\": \"Ativado para clientes compatíveis\",\n    \"lan_encryption_mode_2\": \"Necessário para todos os clientes\",\n    \"lan_encryption_mode_desc\": \"Isso determina quando a criptografia será usada durante a transmissão pela rede local. A criptografia pode reduzir o desempenho do streaming, principalmente em hosts e clientes menos potentes.\",\n    \"locale\": \"Local\",\n    \"locale_desc\": \"A localidade usada na interface de usuário do Sunshine.\",\n    \"log_level\": \"Nível de registro\",\n    \"log_level_0\": \"Verboso\",\n    \"log_level_1\": \"Depurar\",\n    \"log_level_2\": \"Informações\",\n    \"log_level_3\": \"Advertência\",\n    \"log_level_4\": \"Erro\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"Nenhum\",\n    \"log_level_desc\": \"O nível mínimo de registro impresso na saída padrão\",\n    \"log_path\": \"Caminho do arquivo de registro\",\n    \"log_path_desc\": \"O arquivo em que os registros atuais do Sunshine são armazenados.\",\n    \"max_bitrate\": \"Bitrate Máximo\",\n    \"max_bitrate_desc\": \"A taxa de bits máxima (em Kbps) que Sunshine irá codificar o stream. Se definido como 0, ele sempre usará a bitrate solicitada pela luar.\",\n    \"max_fps_reached\": \"Valores máximos de FPS atingidos\",\n    \"max_resolutions_reached\": \"Número máximo de resoluções atingido\",\n    \"mdns_broadcast\": \"Encontrar este computador na rede local\",\n    \"mdns_broadcast_desc\": \"Se esta opção estiver ativada, o Sunshine permitirá que outros dispositivos encontrem este computador automaticamente. O Moonlight deve ser configurado para encontrar este computador automaticamente na rede local.\",\n    \"min_threads\": \"Contagem mínima de threads da CPU\",\n    \"min_threads_desc\": \"Aumentar o valor reduz ligeiramente a eficiência da codificação, mas a troca geralmente vale a pena para obter o uso de mais núcleos de CPU para codificação. O valor ideal é o menor valor que pode ser codificado de forma confiável nas configurações de streaming desejadas em seu hardware.\",\n    \"minimum_fps_target\": \"Objetivo de FPS mínimo\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Opções diversas\",\n    \"motion_as_ds4\": \"Emular um gamepad DS4 se o gamepad do cliente informar que há sensores de movimento presentes\",\n    \"motion_as_ds4_desc\": \"Se estiver desativado, os sensores de movimento não serão levados em conta durante a seleção do tipo de gamepad.\",\n    \"mouse\": \"Ativar entrada do mouse\",\n    \"mouse_desc\": \"Permite que os convidados controlem o sistema host com o mouse\",\n    \"native_pen_touch\": \"Suporte nativo a caneta/toque\",\n    \"native_pen_touch_desc\": \"Quando ativado, o Sunshine transmitirá os eventos nativos de caneta/toque dos clientes Moonlight. Isso pode ser útil para desativar aplicativos mais antigos sem suporte nativo a caneta/toque.\",\n    \"no_fps\": \"Nenhum valor de FPS adicionado\",\n    \"no_resolutions\": \"Nenhuma resolução adicionada\",\n    \"notify_pre_releases\": \"Notificações de pré-lançamento\",\n    \"notify_pre_releases_desc\": \"Se deseja ser notificado sobre novas versões de pré-lançamento do Sunshine\",\n    \"nvenc_h264_cavlc\": \"Prefira o CAVLC ao CABAC em H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Forma mais simples de codificação de entropia. O CAVLC precisa de cerca de 10% a mais de taxa de bits para obter a mesma qualidade. Só é relevante para dispositivos de decodificação muito antigos.\",\n    \"nvenc_latency_over_power\": \"Prefere uma latência de codificação menor do que a economia de energia\",\n    \"nvenc_latency_over_power_desc\": \"O Sunshine solicita a velocidade máxima do clock da GPU durante o streaming para reduzir a latência da codificação. Não é recomendável desativá-lo, pois isso pode levar a um aumento significativo da latência de codificação.\",\n    \"nvenc_lookahead_depth\": \"Profundidade de Lookahead\",\n    \"nvenc_lookahead_depth_desc\": \"Número de quadros para olhar à frente durante a codificação (0-32). O Lookahead melhora a qualidade de codificação, especialmente em cenas complexas, fornecendo melhor estimativa de movimento e distribuição de taxa de bits. Valores mais altos melhoram a qualidade, mas aumentam a latência de codificação. Defina como 0 para desativar o lookahead. Requer NVENC SDK 13.0 (1202) ou mais recente.\",\n    \"nvenc_lookahead_level\": \"Nível de Lookahead\",\n    \"nvenc_lookahead_level_0\": \"Nível 0 (qualidade mais baixa, mais rápido)\",\n    \"nvenc_lookahead_level_1\": \"Nível 1\",\n    \"nvenc_lookahead_level_2\": \"Nível 2\",\n    \"nvenc_lookahead_level_3\": \"Nível 3 (qualidade mais alta, mais lento)\",\n    \"nvenc_lookahead_level_autoselect\": \"Seleção automática (deixe o driver escolher o nível ideal)\",\n    \"nvenc_lookahead_level_desc\": \"Nível de qualidade do Lookahead. Níveis mais altos melhoram a qualidade em detrimento do desempenho. Esta opção só tem efeito quando lookahead_depth é maior que 0. Requer NVENC SDK 13.0 (1202) ou mais recente.\",\n    \"nvenc_lookahead_level_disabled\": \"Desativado (igual ao nível 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Apresentar OpenGL/Vulkan sobre o DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"O Sunshine não pode capturar programas OpenGL e Vulkan em tela cheia com taxa de quadros total, a menos que eles sejam apresentados na parte superior do DXGI. Essa é uma configuração de todo o sistema que é revertida ao sair do programa Sunshine.\",\n    \"nvenc_preset\": \"Predefinição de desempenho\",\n    \"nvenc_preset_1\": \"(mais rápido, padrão)\",\n    \"nvenc_preset_7\": \"(mais lento)\",\n    \"nvenc_preset_desc\": \"Números mais altos melhoram a compactação (qualidade em uma determinada taxa de bits) ao custo de uma maior latência de codificação. Recomenda-se alterar somente quando limitado pela rede ou pelo decodificador; caso contrário, é possível obter um efeito semelhante aumentando a taxa de bits.\",\n    \"nvenc_rate_control\": \"Modo de controle de taxa\",\n    \"nvenc_rate_control_cbr\": \"CBR (Taxa de Bits Constante) - Baixa latência\",\n    \"nvenc_rate_control_desc\": \"Selecione o modo de controle de taxa. CBR (Taxa de Bits Constante) fornece taxa de bits fixa para transmissão de baixa latência. VBR (Taxa de Bits Variável) permite que a taxa de bits varie com base na complexidade da cena, proporcionando melhor qualidade para cenas complexas ao custo de taxa de bits variável.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Taxa de Bits Variável) - Melhor qualidade\",\n    \"nvenc_realtime_hags\": \"Usar prioridade em tempo real no agendamento de gpu acelerado por hardware\",\n    \"nvenc_realtime_hags_desc\": \"Atualmente, os drivers da NVIDIA podem travar no codificador quando o HAGS está ativado, a prioridade em tempo real é usada e a utilização da VRAM está próxima do máximo. A desativação dessa opção reduz a prioridade para alta, evitando o congelamento ao custo de um desempenho de captura reduzido quando a GPU está muito carregada.\",\n    \"nvenc_spatial_aq\": \"AQ Espacial\",\n    \"nvenc_spatial_aq_desc\": \"Atribui valores de QP mais altos a regiões planas do vídeo. Recomenda-se ativar ao transmitir com taxas de bits mais baixas.\",\n    \"nvenc_spatial_aq_disabled\": \"Desativado (mais rápido, padrão)\",\n    \"nvenc_spatial_aq_enabled\": \"Ativado (mais lento)\",\n    \"nvenc_split_encode\": \"Codificação de quadro dividido\",\n    \"nvenc_split_encode_desc\": \"Divida a codificação de cada quadro de vídeo em várias unidades de hardware NVENC. Reduz significativamente a latência de codificação com uma penalidade marginal de eficiência de compressão. Esta opção é ignorada se sua GPU tiver uma unidade NVENC singular.\",\n    \"nvenc_split_encode_driver_decides_def\": \"O driver decide (padrão)\",\n    \"nvenc_split_encode_four_strips\": \"Forçar divisão em 4 faixas (requer 4+ mecanismos NVENC)\",\n    \"nvenc_split_encode_three_strips\": \"Forçar divisão em 3 faixas (requer 3+ mecanismos NVENC)\",\n    \"nvenc_split_encode_two_strips\": \"Forçar divisão em 2 faixas (requer 2+ mecanismos NVENC)\",\n    \"nvenc_target_quality\": \"Qualidade alvo (modo VBR)\",\n    \"nvenc_target_quality_desc\": \"Nível de qualidade alvo para o modo VBR (0-51 para H.264/HEVC, 0-63 para AV1). Valores mais baixos = maior qualidade. Defina como 0 para seleção automática de qualidade. Usado apenas quando o modo de controle de taxa é VBR.\",\n    \"nvenc_temporal_aq\": \"Quantização adaptativa temporal\",\n    \"nvenc_temporal_aq_desc\": \"Ativar quantização adaptativa temporal. O Temporal AQ otimiza a quantização ao longo do tempo, proporcionando melhor distribuição de taxa de bits e melhor qualidade em cenas de movimento. Este recurso funciona em conjunto com o AQ espacial e requer que o lookahead esteja ativado (lookahead_depth > 0). Requer NVENC SDK 13.0 (1202) ou mais recente.\",\n    \"nvenc_temporal_filter\": \"Filtro temporal\",\n    \"nvenc_temporal_filter_4\": \"Nível 4 (força máxima)\",\n    \"nvenc_temporal_filter_desc\": \"Força de filtragem temporal aplicada antes da codificação. O filtro temporal reduz o ruído e melhora a eficiência da compressão, especialmente para conteúdo natural. Níveis mais altos fornecem melhor redução de ruído, mas podem introduzir um leve desfoque. Requer NVENC SDK 13.0 (1202) ou mais recente. Nota: Requer frameIntervalP >= 5, não compatível com zeroReorderDelay ou estéreo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Desativado (sem filtragem temporal)\",\n    \"nvenc_twopass\": \"Modo de duas passagens\",\n    \"nvenc_twopass_desc\": \"Adiciona uma passagem de codificação preliminar. Isso permite detectar mais vetores de movimento, distribuir melhor a taxa de bits pelo quadro e aderir mais rigorosamente aos limites de taxa de bits. Não é recomendável desativá-lo, pois isso pode levar a um excesso ocasional de taxa de bits e à subsequente perda de pacotes.\",\n    \"nvenc_twopass_disabled\": \"Desativado (mais rápido, não recomendado)\",\n    \"nvenc_twopass_full_res\": \"Resolução total (mais lenta)\",\n    \"nvenc_twopass_quarter_res\": \"Resolução de um quarto (mais rápida, padrão)\",\n    \"nvenc_vbv_increase\": \"Aumento percentual de VBV/HRD em um único quadro\",\n    \"nvenc_vbv_increase_desc\": \"Por padrão, o sunshine usa VBV/HRD de quadro único, o que significa que não se espera que o tamanho do quadro de vídeo codificado exceda a taxa de bits solicitada dividida pela taxa de quadros solicitada. O relaxamento dessa restrição pode ser benéfico e atuar como taxa de bits variável de baixa latência, mas também pode levar à perda de pacotes se a rede não tiver espaço no buffer para lidar com picos de taxa de bits. O valor máximo aceito é 400, o que corresponde a um limite superior de tamanho de quadro de vídeo codificado 5x maior.\",\n    \"origin_web_ui_allowed\": \"IU da Web de origem permitida\",\n    \"origin_web_ui_allowed_desc\": \"A origem do endereço do ponto de extremidade remoto ao qual não foi negado acesso à interface do usuário da Web\",\n    \"origin_web_ui_allowed_lan\": \"Somente as pessoas na LAN podem acessar a interface do usuário da Web\",\n    \"origin_web_ui_allowed_pc\": \"Somente o localhost pode acessar a interface do usuário da Web\",\n    \"origin_web_ui_allowed_wan\": \"Qualquer pessoa pode acessar a Web UI\",\n    \"output_name_desc_unix\": \"Durante a inicialização do Sunshine, você deverá ver a lista de monitores detectados. Observação: você precisa usar o valor de id dentro do parêntese. Abaixo está um exemplo; a saída real pode ser encontrada na guia Solução de problemas.\",\n    \"output_name_desc_windows\": \"Especifique manualmente um ID de dispositivo de exibição a ser usado para captura. Se não for definido, a tela principal será capturada. Observação: se você especificou uma GPU acima, esse monitor deverá estar conectado a essa GPU. Durante a inicialização do Sunshine, você deverá ver a lista de monitores detectados. Abaixo está um exemplo; a saída real pode ser encontrada na guia Solução de problemas.\",\n    \"output_name_unix\": \"Número de exibição\",\n    \"output_name_windows\": \"Exibir ID do dispositivo\",\n    \"ping_timeout\": \"Tempo limite de ping\",\n    \"ping_timeout_desc\": \"Quanto tempo esperar, em milissegundos, pelos dados do moonlight antes de encerrar o fluxo\",\n    \"pkey\": \"Chave privada\",\n    \"pkey_desc\": \"A chave privada usada para a interface do usuário da Web e o emparelhamento do cliente Moonlight. Para melhor compatibilidade, essa deve ser uma chave privada RSA-2048.\",\n    \"port\": \"Porto\",\n    \"port_alert_1\": \"O Sunshine não pode usar portas abaixo de 1024!\",\n    \"port_alert_2\": \"As portas acima de 65535 não estão disponíveis!\",\n    \"port_desc\": \"Definir a família de portas usadas pelo Sunshine\",\n    \"port_http_port_note\": \"Use essa porta para se conectar ao Moonlight.\",\n    \"port_note\": \"Observação\",\n    \"port_port\": \"Porto\",\n    \"port_protocol\": \"Protocolo\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Expor a interface do usuário da Web à Internet é um risco à segurança! Prossiga por sua própria conta e risco!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Parâmetro de quantização\",\n    \"qp_desc\": \"Alguns dispositivos podem não suportar Constant Bit Rate. Para esses dispositivos, o QP é usado em seu lugar. Um valor mais alto significa mais compactação, mas menos qualidade.\",\n    \"qsv_coder\": \"Codificador QuickSync (H264)\",\n    \"qsv_preset\": \"Predefinição de QuickSync\",\n    \"qsv_preset_fast\": \"rápido (baixa qualidade)\",\n    \"qsv_preset_faster\": \"mais rápido (qualidade inferior)\",\n    \"qsv_preset_medium\": \"médio (padrão)\",\n    \"qsv_preset_slow\": \"lento (boa qualidade)\",\n    \"qsv_preset_slower\": \"mais lento (melhor qualidade)\",\n    \"qsv_preset_slowest\": \"mais lento (melhor qualidade)\",\n    \"qsv_preset_veryfast\": \"mais rápido (qualidade mais baixa)\",\n    \"qsv_slow_hevc\": \"Permitir codificação lenta de HEVC\",\n    \"qsv_slow_hevc_desc\": \"Isso pode permitir a codificação HEVC em GPUs Intel mais antigas, ao custo de maior uso da GPU e pior desempenho.\",\n    \"refresh_rate_change_automatic_windows\": \"Usar valor de FPS fornecido pelo cliente\",\n    \"refresh_rate_change_manual_desc_windows\": \"Digite a taxa de atualização a ser usada\",\n    \"refresh_rate_change_manual_windows\": \"Usar taxa de atualização inserida manualmente\",\n    \"refresh_rate_change_no_operation_windows\": \"Desativado\",\n    \"refresh_rate_change_windows\": \"Mudança de FPS\",\n    \"res_fps_desc\": \"Os modos de exibição anunciados pelo Sunshine. Algumas versões do Moonlight, como o Moonlight-nx (Switch), dependem dessas listas para garantir que as resoluções e fps solicitados sejam suportados. Esta configuração não altera como o fluxo de tela é enviado ao Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Usar resolução fornecida pelo cliente\",\n    \"resolution_change_manual_desc_windows\": \"A opção \\\"Otimizar configurações do jogo\\\" deve estar ativada no cliente Moonlight para que isso funcione.\",\n    \"resolution_change_manual_windows\": \"Usar resolução inserida manualmente\",\n    \"resolution_change_no_operation_windows\": \"Desativado\",\n    \"resolution_change_ogs_desc_windows\": \"A opção \\\"Otimizar configurações do jogo\\\" deve estar ativada no cliente Moonlight para que isso funcione.\",\n    \"resolution_change_windows\": \"Mudança de resolução\",\n    \"resolutions\": \"Resoluções Anunciadas\",\n    \"restart_note\": \"O Sunshine está sendo reiniciado para aplicar as alterações.\",\n    \"sleep_mode\": \"Modo de suspensão\",\n    \"sleep_mode_away\": \"Modo ausente (Tela desligada, despertar instantâneo)\",\n    \"sleep_mode_desc\": \"Controla o que acontece quando o cliente envia um comando de suspensão. Suspensão (S3): suspensão tradicional, baixo consumo mas requer WOL para despertar. Hibernação (S4): salva no disco, consumo muito baixo. Modo ausente: a tela desliga mas o sistema continua funcionando para despertar instantâneo - ideal para servidores de streaming de jogos.\",\n    \"sleep_mode_hibernate\": \"Hibernação (S4)\",\n    \"sleep_mode_suspend\": \"Suspensão (S3)\",\n    \"stream_audio\": \"Ativar transmissão de áudio\",\n    \"stream_audio_desc\": \"Desative esta opção para parar a transmissão de áudio.\",\n    \"stream_mic\": \"Ativar transmissão de microfone\",\n    \"stream_mic_desc\": \"Desative esta opção para parar a transmissão do microfone.\",\n    \"stream_mic_download_btn\": \"Baixar microfone virtual\",\n    \"stream_mic_download_confirm\": \"Você está prestes a ser redirecionado para a página de download do microfone virtual. Continuar?\",\n    \"stream_mic_note\": \"Esta funcionalidade requer a instalação de um microfone virtual\",\n    \"sunshine_name\": \"Nome Sunshine\",\n    \"sunshine_name_desc\": \"O nome exibido pelo Moonlight. Se não for especificado, será usado o nome do host do PC\",\n    \"sw_preset\": \"Predefinições de SW\",\n    \"sw_preset_desc\": \"Otimiza o equilíbrio entre a velocidade de codificação (quadros codificados por segundo) e a eficiência da compactação (qualidade por bit no fluxo de bits). O padrão é super-rápido.\",\n    \"sw_preset_fast\": \"rápido\",\n    \"sw_preset_faster\": \"mais rápido\",\n    \"sw_preset_medium\": \"médio\",\n    \"sw_preset_slow\": \"lento\",\n    \"sw_preset_slower\": \"mais lento\",\n    \"sw_preset_superfast\": \"superfast (padrão)\",\n    \"sw_preset_ultrafast\": \"ultrarrápido\",\n    \"sw_preset_veryfast\": \"muito rápido\",\n    \"sw_preset_veryslow\": \"muito lento\",\n    \"sw_tune\": \"SW Tune\",\n    \"sw_tune_animation\": \"animação -- bom para desenhos animados; usa deblocking mais alto e mais quadros de referência\",\n    \"sw_tune_desc\": \"Opções de ajuste, que são aplicadas após a predefinição. O padrão é zerolatência.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- permite uma decodificação mais rápida ao desativar determinados filtros\",\n    \"sw_tune_film\": \"filme - use para conteúdo de filme de alta qualidade; reduz o desbloqueio\",\n    \"sw_tune_grain\": \"granulação -- preserva a estrutura de granulação em material de filme antigo e granulado\",\n    \"sw_tune_stillimage\": \"stillimage -- bom para conteúdo do tipo apresentação de slides\",\n    \"sw_tune_zerolatency\": \"zerolatency -- bom para codificação rápida e streaming de baixa latência (padrão)\",\n    \"system_tray\": \"Ativar bandeja do sistema\",\n    \"system_tray_desc\": \"Se deve habilitar a bandeja do sistema. Se habilitado, o Sunshine exibirá um ícone na bandeja do sistema e pode ser controlado a partir da bandeja do sistema.\",\n    \"touchpad_as_ds4\": \"Emular um gamepad DS4 se o gamepad do cliente informar que há um touchpad presente\",\n    \"touchpad_as_ds4_desc\": \"Se estiver desativado, a presença do touchpad não será levada em conta durante a seleção do tipo de gamepad.\",\n    \"unsaved_changes_tooltip\": \"Você tem alterações não salvas. Clique para salvar.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Configurar automaticamente o encaminhamento de portas para streaming pela Internet\",\n    \"variable_refresh_rate\": \"Taxa de atualização variável (VRR)\",\n    \"variable_refresh_rate_desc\": \"Permitir que a taxa de quadros do fluxo de vídeo corresponda à taxa de quadros de renderização para suporte VRR. Quando habilitado, a codificação ocorre apenas quando novos quadros estão disponíveis, permitindo que o fluxo siga a taxa de quadros de renderização real.\",\n    \"vdd_reuse_desc_windows\": \"Quando ativado, todos os clientes compartilharão o mesmo VDD (Virtual Display Device). Quando desativado (padrão), cada cliente obtém seu próprio VDD. Ative isso para uma troca de cliente mais rápida, mas observe que todos os clientes compartilharão as mesmas configurações de tela.\",\n    \"vdd_reuse_windows\": \"Reutilizar o mesmo VDD para todos os clientes\",\n    \"virtual_display\": \"Exibição virtual\",\n    \"virtual_mouse\": \"Driver de mouse virtual\",\n    \"virtual_mouse_desc\": \"Quando ativado, o Sunshine usará o driver Zako Virtual Mouse (se instalado) para simular a entrada do mouse no nível HID. Permite que jogos com Raw Input recebam eventos do mouse. Quando desativado ou driver não instalado, volta ao SendInput.\",\n    \"virtual_sink\": \"Pia virtual\",\n    \"virtual_sink_desc\": \"Especificar manualmente um dispositivo de áudio virtual a ser usado. Se não for definido, o dispositivo será escolhido automaticamente. É altamente recomendável deixar esse campo em branco para usar a seleção automática de dispositivos!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Instalar o driver de mouse virtual?\",\n    \"vmouse_confirm_uninstall\": \"Desinstalar o driver de mouse virtual?\",\n    \"vmouse_install\": \"Instalar driver\",\n    \"vmouse_installing\": \"Instalando...\",\n    \"vmouse_note\": \"O driver de mouse virtual requer instalação separada. Use o painel de controle do Sunshine para instalar ou gerenciar o driver.\",\n    \"vmouse_refresh\": \"Atualizar status\",\n    \"vmouse_status_installed\": \"Instalado (não ativo)\",\n    \"vmouse_status_not_installed\": \"Não instalado\",\n    \"vmouse_status_running\": \"Em execução\",\n    \"vmouse_uninstall\": \"Desinstalar driver\",\n    \"vmouse_uninstalling\": \"Desinstalando...\",\n    \"vt_coder\": \"Codificador do VideoToolbox\",\n    \"vt_realtime\": \"Codificação em tempo real do VideoToolbox\",\n    \"vt_software\": \"Codificação do software VideoToolbox\",\n    \"vt_software_allowed\": \"Permitido\",\n    \"vt_software_forced\": \"Forçado\",\n    \"wan_encryption_mode\": \"Modo de criptografia WAN\",\n    \"wan_encryption_mode_1\": \"Ativado para clientes compatíveis (padrão)\",\n    \"wan_encryption_mode_2\": \"Necessário para todos os clientes\",\n    \"wan_encryption_mode_desc\": \"Isso determina quando a criptografia será usada durante a transmissão pela Internet. A criptografia pode reduzir o desempenho da transmissão, principalmente em hosts e clientes menos potentes.\",\n    \"webhook_curl_command\": \"Comando\",\n    \"webhook_curl_command_desc\": \"Copie o seguinte comando para o seu terminal para testar se o webhook está funcionando corretamente:\",\n    \"webhook_curl_copy_failed\": \"Falha ao copiar, por favor selecione e copie manualmente\",\n    \"webhook_enabled\": \"Notificações Webhook\",\n    \"webhook_enabled_desc\": \"Quando habilitado, o Sunshine enviará notificações de eventos para a URL Webhook especificada\",\n    \"webhook_group\": \"Configurações de notificação Webhook\",\n    \"webhook_skip_ssl_verify\": \"Pular verificação de certificado SSL\",\n    \"webhook_skip_ssl_verify_desc\": \"Pular verificação de certificado SSL para conexões HTTPS, apenas para testes ou certificados auto-assinados\",\n    \"webhook_test\": \"Testar\",\n    \"webhook_test_failed\": \"Teste Webhook falhou\",\n    \"webhook_test_failed_note\": \"Nota: Por favor, verifique se a URL está correta ou verifique o console do navegador para mais informações.\",\n    \"webhook_test_success\": \"Teste Webhook bem-sucedido!\",\n    \"webhook_test_success_cors_note\": \"Nota: Devido às restrições CORS, o status de resposta do servidor não pode ser confirmado.\\nA solicitação foi enviada. Se o webhook estiver configurado corretamente, a mensagem deve ter sido entregue.\\n\\nSugestão: Verifique a guia Rede nas ferramentas de desenvolvedor do seu navegador para obter detalhes da solicitação.\",\n    \"webhook_test_url_required\": \"Por favor, insira primeiro a URL Webhook\",\n    \"webhook_timeout\": \"Tempo limite da solicitação\",\n    \"webhook_timeout_desc\": \"Tempo limite para solicitações Webhook em milissegundos, intervalo 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"A URL para receber notificações de eventos, suporta protocolos HTTP/HTTPS\",\n    \"wgc_checking_mode\": \"Verificando modo...\",\n    \"wgc_checking_running_mode\": \"Verificando modo de execução...\",\n    \"wgc_control_panel_only\": \"Este recurso está disponível apenas no Painel de Controle Sunshine\",\n    \"wgc_mode_switch_failed\": \"Falha ao alternar modo\",\n    \"wgc_mode_switch_started\": \"Alternância de modo iniciada. Se um prompt do UAC aparecer, clique em 'Sim' para confirmar.\",\n    \"wgc_service_mode_warning\": \"A captura WGC requer execução no modo de usuário. Se estiver executando no modo de serviço, clique no botão acima para alternar para o modo de usuário.\",\n    \"wgc_switch_to_service_mode\": \"Alternar para Modo de Serviço\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Atualmente em execução no modo de usuário. Clique para alternar para o modo de serviço.\",\n    \"wgc_switch_to_user_mode\": \"Alternar para Modo de Usuário\",\n    \"wgc_switch_to_user_mode_tooltip\": \"A captura WGC requer execução no modo de usuário. Clique neste botão para alternar para o modo de usuário.\",\n    \"wgc_user_mode_available\": \"Atualmente em execução no modo de usuário. A captura WGC está disponível.\",\n    \"window_title\": \"Window Title\",\n    \"window_title_desc\": \"The title of the window to capture (partial match, case-insensitive). If left empty, the current running application name will be used automatically.\",\n    \"window_title_placeholder\": \"e.g., Application Name\"\n  },\n  \"index\": {\n    \"description\": \"O Sunshine é um host de fluxo de jogos auto-hospedado para o Moonlight.\",\n    \"download\": \"Baixar\",\n    \"installed_version_not_stable\": \"Você está executando uma versão de pré-lançamento do Sunshine. É possível que você tenha bugs ou outros problemas. Informe todos os problemas que encontrar. Obrigado por ajudar a tornar o Sunshine um software melhor!\",\n    \"loading_latest\": \"Carregando a versão mais recente...\",\n    \"new_pre_release\": \"Uma nova versão de pré-lançamento está disponível!\",\n    \"new_stable\": \"Uma nova versão estável está disponível!\",\n    \"startup_errors\": \"<b>Atenção!</b> A Sunshine detectou esses erros durante a inicialização. <b>RECOMENDAMOS FORTEMENTE</b> corrigi-los antes da transmissão.\",\n    \"update_download_confirm\": \"Você está prestes a abrir a página de download de atualizações no seu navegador. Continuar?\",\n    \"version_dirty\": \"Obrigado por ajudar a tornar o Sunshine um software melhor!\",\n    \"version_latest\": \"Você está executando a versão mais recente do Sunshine\",\n    \"view_logs\": \"Ver logs\",\n    \"welcome\": \"Olá, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Aplicativos\",\n    \"configuration\": \"Configuração\",\n    \"home\": \"Início\",\n    \"password\": \"Alterar senha\",\n    \"pin\": \"Pino\",\n    \"theme_auto\": \"Automotivo\",\n    \"theme_dark\": \"Escuro\",\n    \"theme_light\": \"Luz\",\n    \"toggle_theme\": \"Tema\",\n    \"troubleshoot\": \"Solução de problemas\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Confirmar senha\",\n    \"current_creds\": \"Credenciais atuais\",\n    \"new_creds\": \"Novas credenciais\",\n    \"new_username_desc\": \"Se não for especificado, o nome de usuário não será alterado\",\n    \"password_change\": \"Alteração de senha\",\n    \"success_msg\": \"A senha foi alterada com sucesso! Esta página será recarregada em breve e seu navegador solicitará as novas credenciais.\"\n  },\n  \"pin\": {\n    \"actions\": \"Ações\",\n    \"cancel_editing\": \"Cancelar edição\",\n    \"client_name\": \"Nome\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Confirmar exclusão\",\n    \"delete_client\": \"Excluir cliente\",\n    \"delete_confirm_message\": \"Tem certeza de que deseja excluir <strong>{name}</strong>?\",\n    \"delete_warning\": \"Esta ação não pode ser desfeita.\",\n    \"device_name\": \"Nome do dispositivo\",\n    \"device_size\": \"Tamanho do dispositivo\",\n    \"device_size_info\": \"<strong>Tamanho do Dispositivo</strong>: Defina o tipo de tamanho de tela do dispositivo cliente (Pequeno - Telefone, Médio - Tablet, Grande - TV) para otimizar a experiência de transmissão e as operações de toque.\",\n    \"device_size_large\": \"Grande - TV\",\n    \"device_size_medium\": \"Médio - Tablet\",\n    \"device_size_small\": \"Pequeno - Telefone\",\n    \"edit_client_settings\": \"Editar configurações do cliente\",\n    \"hdr_profile\": \"Perfil HDR\",\n    \"hdr_profile_info\": \"<strong>Perfil HDR</strong>: Selecione o perfil de cores HDR (arquivo ICC) usado para este cliente para garantir que o conteúdo HDR seja exibido corretamente no dispositivo. Se estiver usando o cliente mais recente, que suporta sincronização automática de informações de brilho com a tela virtual do host, deixe este campo em branco para ativar a sincronização automática.\",\n    \"loading\": \"Carregando...\",\n    \"loading_clients\": \"Carregando clientes...\",\n    \"modify_in_gui\": \"Por favor, modifique na interface gráfica\",\n    \"none\": \"-- Nenhum --\",\n    \"or_manual_pin\": \"ou inserir PIN manualmente\",\n    \"pair_failure\": \"Falha no emparelhamento: Verifique se o PIN foi digitado corretamente\",\n    \"pair_success\": \"Sucesso! Por favor, verifique o Moonlight para continuar\",\n    \"pin_pairing\": \"Emparelhamento de PIN\",\n    \"qr_expires_in\": \"Expira em\",\n    \"qr_generate\": \"Gerar código QR\",\n    \"qr_paired_success\": \"Pareado com sucesso!\",\n    \"qr_pairing\": \"Pareamento por QR Code\",\n    \"qr_pairing_desc\": \"Gere um código QR para pareamento rápido. Escaneie com o cliente Moonlight para parear automaticamente.\",\n    \"qr_pairing_warning\": \"Recurso experimental. Se o pareamento falhar, use o pareamento manual por PIN abaixo. Nota: Este recurso só funciona em rede local.\",\n    \"qr_refresh\": \"Atualizar código QR\",\n    \"remove_paired_devices_desc\": \"Remova seus dispositivos emparelhados.\",\n    \"save_changes\": \"Salvar alterações\",\n    \"save_failed\": \"Falha ao salvar as configurações do cliente. Por favor, tente novamente.\",\n    \"save_or_cancel_first\": \"Por favor, salve ou cancele a edição primeiro\",\n    \"send\": \"Enviar\",\n    \"unknown_client\": \"Cliente desconhecido\",\n    \"unpair_all_confirm\": \"Tem certeza de que deseja desemparelhar todos os clientes? Esta ação não pode ser desfeita.\",\n    \"unsaved_changes\": \"Alterações não salvas\",\n    \"warning_msg\": \"Certifique-se de ter acesso ao cliente com o qual está fazendo o emparelhamento. Esse software pode dar controle total ao seu computador, portanto, tenha cuidado!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android recomendado\",\n    \"client_downloads\": \"Downloads de clientes\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"Discussões no GitHub\",\n    \"gpl_license_text_1\": \"Este software é licenciado sob a GPL-3.0. Você é livre para usá-lo, modificá-lo e distribuí-lo.\",\n    \"gpl_license_text_2\": \"Para proteger o ecossistema de código aberto, evite usar software que viole a licença GPL-3.0.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Participar da comunidade\",\n    \"join_group_desc\": \"Obter ajuda e compartilhar experiências\",\n    \"legal\": \"Legal\",\n    \"legal_desc\": \"Ao continuar a usar este software, você concorda com os termos e condições dos documentos a seguir.\",\n    \"license\": \"Licença\",\n    \"lizardbyte_website\": \"Site da LizardByte\",\n    \"official_website\": \"Site oficial\",\n    \"official_website_title\": \"AlkaidLab - Site oficial\",\n    \"open_source\": \"Código aberto\",\n    \"open_source_desc\": \"Star & Fork para apoiar o projeto\",\n    \"quick_start\": \"Início rápido\",\n    \"resources\": \"Recursos\",\n    \"resources_desc\": \"Recursos para o Sunshine!\",\n    \"third_party_desc\": \"Avisos de componentes de terceiros\",\n    \"third_party_moonlight\": \"Links amigos\",\n    \"third_party_notice\": \"Aviso de terceiros\",\n    \"tutorial\": \"Tutorial\",\n    \"tutorial_desc\": \"Guia detalhado de configuração e uso\",\n    \"view_license\": \"Ver licença completa\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Resumo da Configuração\",\n    \"android_client\": \"Cliente Android\",\n    \"base_display_title\": \"Display virtual\",\n    \"choose_adapter\": \"Automático\",\n    \"config_saved\": \"A configuração foi salva com sucesso.\",\n    \"description\": \"Vamos começar com uma configuração rápida\",\n    \"device_id\": \"ID do Dispositivo\",\n    \"device_state\": \"Estado\",\n    \"download_clients\": \"Baixar Clientes\",\n    \"finish\": \"Concluir Configuração\",\n    \"go_to_apps\": \"Configurar Aplicativos\",\n    \"harmony_goto_repo\": \"Ir para o repositório\",\n    \"harmony_modal_desc\": \"Para HarmonyOS NEXT Moonlight, pesquise Moonlight V+ na loja HarmonyOS\",\n    \"harmony_modal_link_notice\": \"Este link redirecionará para o repositório do projeto\",\n    \"ios_client\": \"Cliente iOS\",\n    \"load_error\": \"Falha ao carregar a configuração\",\n    \"next\": \"Próximo\",\n    \"physical_display\": \"Tela Física/Emulador EDID\",\n    \"physical_display_desc\": \"Transmita seus monitores físicos reais\",\n    \"previous\": \"Anterior\",\n    \"restart_countdown_unit\": \"segundos\",\n    \"restart_desc\": \"Configuração salva. O Sunshine está reiniciando para aplicar as configurações de exibição.\",\n    \"restart_go_now\": \"Ir agora\",\n    \"restart_title\": \"Reiniciando Sunshine\",\n    \"save_error\": \"Falha ao salvar a configuração\",\n    \"select_adapter\": \"Adaptador Gráfico\",\n    \"selected_adapter\": \"Adaptador Selecionado\",\n    \"selected_display\": \"Tela Selecionada\",\n    \"setup_complete\": \"Configuração Concluída!\",\n    \"setup_complete_desc\": \"As configurações básicas estão ativas. Você pode começar a transmitir com um cliente Moonlight imediatamente!\",\n    \"skip\": \"Pular Assistente de Configuração\",\n    \"skip_confirm\": \"Tem certeza de que deseja pular o assistente de configuração? Você pode configurar essas opções mais tarde na página de configurações.\",\n    \"skip_confirm_title\": \"Pular Assistente de Configuração\",\n    \"skip_error\": \"Falha ao pular\",\n    \"state_active\": \"Ativo\",\n    \"state_inactive\": \"Inativo\",\n    \"state_primary\": \"Primário\",\n    \"state_unknown\": \"Desconhecido\",\n    \"step0_description\": \"Escolha o idioma da interface\",\n    \"step0_title\": \"Idioma\",\n    \"step1_description\": \"Escolha a tela para transmitir\",\n    \"step1_title\": \"Seleção de Tela\",\n    \"step1_vdd_intro\": \"A tela base (VDD) é a tela virtual inteligente integrada do Sunshine Foundation, que suporta qualquer resolução, taxa de quadros e otimização HDR. É a escolha preferida para streaming com tela desligada e streaming de tela estendida.\",\n    \"step2_description\": \"Escolha seu adaptador gráfico\",\n    \"step2_title\": \"Selecionar Adaptador\",\n    \"step3_description\": \"Escolha a estratégia de preparação do dispositivo de exibição\",\n    \"step3_ensure_active\": \"Garantir ativação\",\n    \"step3_ensure_active_desc\": \"Ativa o monitor se ele não estiver ativo\",\n    \"step3_ensure_only_display\": \"Garantir monitor único\",\n    \"step3_ensure_only_display_desc\": \"Desativa todos os outros monitores e ativa apenas o monitor especificado (recomendado)\",\n    \"step3_ensure_primary\": \"Garantir monitor principal\",\n    \"step3_ensure_primary_desc\": \"Ativa o monitor e o define como monitor principal\",\n    \"step3_ensure_secondary\": \"Streaming secundário\",\n    \"step3_ensure_secondary_desc\": \"Usa apenas o monitor virtual para streaming secundário estendido\",\n    \"step3_no_operation\": \"Sem operação\",\n    \"step3_no_operation_desc\": \"Sem alterações no estado do monitor; o usuário deve garantir que o monitor esteja pronto\",\n    \"step3_title\": \"Estratégia de Exibição\",\n    \"step4_title\": \"Concluir\",\n    \"stream_mode\": \"Modo de Transmissão\",\n    \"unknown_display\": \"Tela Desconhecida\",\n    \"virtual_display\": \"Tela Virtual (ZakoHDR)\",\n    \"virtual_display_desc\": \"Transmitir usando um dispositivo de exibição virtual (requer instalação do driver ZakoVDD)\",\n    \"welcome\": \"Bem-vindo à Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Avançado\",\n    \"amd\": \"Codificador AMD AMF\",\n    \"av\": \"Áudio/Vídeo\",\n    \"encoders\": \"Codificadores\",\n    \"files\": \"Arquivos de Configuração\",\n    \"general\": \"Geral\",\n    \"input\": \"Entrada\",\n    \"network\": \"Rede\",\n    \"nv\": \"Codificador NVIDIA NVENC\",\n    \"qsv\": \"Codificador Intel QuickSync\",\n    \"sw\": \"Codificador de Software\",\n    \"vaapi\": \"Codificador VAAPI\",\n    \"vt\": \"Codificador VideoToolbox\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analisando...\",\n    \"ai_analyzing_logs\": \"Analisando logs, por favor aguarde...\",\n    \"ai_config\": \"Configuração IA\",\n    \"ai_copy_result\": \"Copiar\",\n    \"ai_diagnosis\": \"Diagnóstico IA\",\n    \"ai_diagnosis_title\": \"Diagnóstico IA dos logs\",\n    \"ai_error\": \"Análise falhou\",\n    \"ai_key_local\": \"A chave API é armazenada apenas localmente e nunca enviada\",\n    \"ai_model\": \"Modelo\",\n    \"ai_provider\": \"Provedor\",\n    \"ai_reanalyze\": \"Reanalisar\",\n    \"ai_result\": \"Resultado do diagnóstico\",\n    \"ai_retry\": \"Tentar novamente\",\n    \"ai_start_diagnosis\": \"Iniciar diagnóstico\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Se você precisar desligar o Sunshine imediatamente, pode usar esta função. Observe que será necessário iniciá-lo manualmente após o desligamento.\",\n    \"boom_sunshine_success\": \"Sunshine foi desligado\",\n    \"confirm_boom\": \"Realmente quer sair?\",\n    \"confirm_boom_desc\": \"Então você realmente quer sair? Bem, não posso impedi-lo, vá em frente e clique novamente\",\n    \"confirm_logout\": \"Confirmar saída?\",\n    \"confirm_logout_desc\": \"Será necessário digitar sua senha novamente para acessar a interface Web.\",\n    \"copy_config\": \"Copiar configuração\",\n    \"copy_config_error\": \"Falha ao copiar configuração\",\n    \"copy_config_success\": \"Configuração copiada para a área de transferência!\",\n    \"copy_logs\": \"Copiar logs\",\n    \"download_logs\": \"Baixar logs\",\n    \"force_close\": \"Forçar fechamento\",\n    \"force_close_desc\": \"Se o Moonlight reclamar de um aplicativo em execução, forçar o fechamento do aplicativo deve corrigir o problema.\",\n    \"force_close_error\": \"Erro ao fechar o aplicativo\",\n    \"force_close_success\": \"Aplicativo encerrado com sucesso!\",\n    \"ignore_case\": \"Ignorar maiúsculas e minúsculas\",\n    \"logout\": \"Sair\",\n    \"logout_desc\": \"Sair. Pode ser necessário fazer login novamente.\",\n    \"logout_localhost_tip\": \"O ambiente atual não exige login; sair não irá solicitar credenciais.\",\n    \"logs\": \"Registros\",\n    \"logs_desc\": \"Veja os registros carregados por Sunshine\",\n    \"logs_find\": \"Encontre...\",\n    \"match_contains\": \"Contém\",\n    \"match_exact\": \"Exato\",\n    \"match_regex\": \"Expressão regular\",\n    \"reopen_setup_wizard\": \"Reabrir assistente de configuração\",\n    \"reopen_setup_wizard_desc\": \"Reabrir a página do assistente de configuração para reconfigurar as configurações iniciais.\",\n    \"reopen_setup_wizard_error\": \"Erro ao reabrir o assistente de configuração\",\n    \"reset_display_device_desc_windows\": \"Se o Sunshine travar ao tentar restaurar as configurações alteradas do dispositivo de exibição, você pode redefinir as configurações e prosseguir para restaurar o estado de exibição manualmente.\\nIsso pode acontecer por vários motivos: o dispositivo não está mais disponível, foi conectado a uma porta diferente e assim por diante.\",\n    \"reset_display_device_error_windows\": \"Erro ao resetar persistência!\",\n    \"reset_display_device_success_windows\": \"Reset de persistência bem-sucedido!\",\n    \"reset_display_device_windows\": \"Resetar memória de exibição\",\n    \"restart_sunshine\": \"Reiniciar o Sunshine\",\n    \"restart_sunshine_desc\": \"Se o Sunshine não estiver funcionando corretamente, você pode tentar reiniciá-lo. Isso encerrará todas as sessões em execução.\",\n    \"restart_sunshine_success\": \"A luz do sol está reiniciando\",\n    \"troubleshooting\": \"Solução de problemas\",\n    \"unpair_all\": \"Desemparelhar tudo\",\n    \"unpair_all_error\": \"Erro ao desemparelhar\",\n    \"unpair_all_success\": \"Todos os dispositivos não estão emparelhados.\",\n    \"unpair_desc\": \"Remova seus dispositivos emparelhados. Os dispositivos não emparelhados individualmente com uma sessão ativa permanecerão conectados, mas não poderão iniciar ou retomar uma sessão.\",\n    \"unpair_single_no_devices\": \"Não há dispositivos emparelhados.\",\n    \"unpair_single_success\": \"No entanto, o(s) dispositivo(s) ainda pode(m) estar em uma sessão ativa. Use o botão \\\"Forçar fechamento\\\" acima para encerrar todas as sessões abertas.\",\n    \"unpair_single_unknown\": \"Cliente desconhecido\",\n    \"unpair_title\": \"Desemparelhar dispositivos\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Confirmar senha\",\n    \"create_creds\": \"Antes de começar, precisamos que você crie um novo nome de usuário e senha para acessar a interface do usuário da Web.\",\n    \"create_creds_alert\": \"As credenciais abaixo são necessárias para acessar a interface de usuário da Web do Sunshine. Mantenha-as em segurança, pois você nunca mais as verá!\",\n    \"creds_local_only\": \"Suas credenciais são armazenadas localmente offline e nunca serão enviadas para nenhum servidor.\",\n    \"error\": \"Erro!\",\n    \"greeting\": \"Bem-vindo ao Sunshine Foundation!\",\n    \"hide_password\": \"Ocultar senha\",\n    \"login\": \"Login\",\n    \"network_error\": \"Erro de rede, verifique sua conexão\",\n    \"password\": \"Senha\",\n    \"password_match\": \"Senhas coincidem\",\n    \"password_mismatch\": \"As senhas não coincidem\",\n    \"server_error\": \"Erro do servidor\",\n    \"show_password\": \"Mostrar senha\",\n    \"success\": \"Sucesso!\",\n    \"username\": \"Nome de usuário\",\n    \"welcome_success\": \"Esta página será recarregada em breve e seu navegador solicitará as novas credenciais\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/ru.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Применить\",\n    \"auto\": \"Автоматически\",\n    \"autodetect\": \"Автоопределение (рекомендуется)\",\n    \"beta\": \"(бета)\",\n    \"cancel\": \"Отмена\",\n    \"close\": \"Закрыть\",\n    \"copied\": \"Скопировано в буфер обмена\",\n    \"copy\": \"Копировать\",\n    \"delete\": \"Удалить\",\n    \"description\": \"Описание\",\n    \"disabled\": \"Отключено\",\n    \"disabled_def\": \"Отключено (по умолчанию)\",\n    \"dismiss\": \"Отклонить\",\n    \"do_cmd\": \"Выполнить команду\",\n    \"download\": \"Скачать\",\n    \"edit\": \"Изменить\",\n    \"elevated\": \"Требуются\",\n    \"enabled\": \"Включено\",\n    \"enabled_def\": \"Включено (по умолчанию)\",\n    \"error\": \"Ошибка!\",\n    \"no_changes\": \"Нет изменений\",\n    \"note\": \"Примечание:\",\n    \"password\": \"Пароль\",\n    \"remove\": \"Удалить\",\n    \"run_as\": \"Права администратора\",\n    \"save\": \"Сохранить\",\n    \"see_more\": \"Подробнее\",\n    \"success\": \"Успешно!\",\n    \"undo_cmd\": \"Команда закрытия\",\n    \"username\": \"Имя пользователя\",\n    \"warning\": \"Предупреждение!\"\n  },\n  \"apps\": {\n    \"actions\": \"Действия\",\n    \"add_cmds\": \"Добавить команды\",\n    \"add_new\": \"Добавить новое\",\n    \"advanced_options\": \"Дополнительные параметры\",\n    \"app_name\": \"Название приложения\",\n    \"app_name_desc\": \"Имя приложения, для показа в Moonlight\",\n    \"applications_desc\": \"Приложения обновятся только после перезапуска клиента\",\n    \"applications_title\": \"Приложения\",\n    \"auto_detach\": \"Продолжить трансляцию, если приложение завершит работу быстро\",\n    \"auto_detach_desc\": \"Пытаться автоматически обнаружить приложения-лаунчеры, которые быстро закрываются после запуска другой программы или собственной копии. Когда такое приложение обнаружено, оно рассматривается как независимое.\",\n    \"basic_info\": \"Основная информация\",\n    \"cmd\": \"Команда\",\n    \"cmd_desc\": \"Основное приложение для запуска. Если пустое, то не будет запущено приложение.\",\n    \"cmd_examples_title\": \"Распространённые примеры:\",\n    \"cmd_note\": \"Если путь к исполняемому файлу содержит пробелы, следует заключить его в кавычки.\",\n    \"cmd_prep_desc\": \"Список команд, которые должны быть выполнены до/после этого приложения. Если одна из команд не выполнена, запуск приложения прерывается.\",\n    \"cmd_prep_name\": \"Команды подготовки\",\n    \"command_settings\": \"Настройки команд\",\n    \"covers_found\": \"Найденные обложки\",\n    \"delete\": \"Удалить\",\n    \"delete_confirm\": \"Вы уверены, что хотите удалить \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Независимые команды\",\n    \"detached_cmds_add\": \"Добавить независимую команду\",\n    \"detached_cmds_desc\": \"Список команд, работающих в фоновом режиме.\",\n    \"detached_cmds_note\": \"Если путь к исполняемому файлу содержит пробелы, следует заключить его в кавычки.\",\n    \"detached_cmds_remove\": \"Удалить отсоединенную команду\",\n    \"edit\": \"Изменить\",\n    \"env_app_id\": \"ID приложения\",\n    \"env_app_name\": \"Название приложения\",\n    \"env_client_audio_config\": \"Запрошенная клиентом конфигурация аудио (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"Клиент запросил оптимизацию настроек игры для потокового вещания (истина/ложь)\",\n    \"env_client_fps\": \"Частота кадров, запрошенная клиентом (целое)\",\n    \"env_client_gcmap\": \"Запрашиваемая маска контроллера, в формате bitset/bitfield (целое)\",\n    \"env_client_hdr\": \"HDR включен клиентом (истина/ложь)\",\n    \"env_client_height\": \"Высота, запрошенная клиентом (целое)\",\n    \"env_client_host_audio\": \"Клиент запросил звук с сервера (истина/ложь)\",\n    \"env_client_name\": \"Дружественное имя клиента (строка)\",\n    \"env_client_width\": \"Ширина, запрошенная клиентом (целое)\",\n    \"env_displayplacer_example\": \"Пример - displayplacer для автоматизации решения:\",\n    \"env_qres_example\": \"Пример: автопереключение разрешения через QRes:\",\n    \"env_qres_path\": \"путь qres\",\n    \"env_var_name\": \"Переменная окружения\",\n    \"env_vars_about\": \"О переменных среды\",\n    \"env_vars_desc\": \"Всем командам по умолчанию передаются эти переменные окружения:\",\n    \"env_xrandr_example\": \"Пример - Xrandr для автоматизации решения:\",\n    \"exit_timeout\": \"Ожидание завершения\",\n    \"exit_timeout_desc\": \"Сколько секунд ожидать корректного завершения всех процессов приложения при закрытии. Если не указано, то по умолчанию, ожидание длится 5 секунд. Если указан нуль или отрицательное значение, приложение будет прекращено незамедлительно.\",\n    \"file_selector_not_initialized\": \"Селектор файлов не инициализирован\",\n    \"find_cover\": \"Найти обложку\",\n    \"form_invalid\": \"Пожалуйста, проверьте обязательные поля\",\n    \"form_valid\": \"Действительное приложение\",\n    \"global_prep_desc\": \"Включить/отключить исполнение глобальных команд подготовки для этого приложения.\",\n    \"global_prep_name\": \"Глобальные команды\",\n    \"image\": \"Изображение\",\n    \"image_desc\": \"Путь к иконке/обложке/изображению, который будет отправлен клиенту. Изображение должно быть в формате PNG. Если не указано, Sunshine пошлёт обложку по умолчанию.\",\n    \"image_settings\": \"Настройки изображения\",\n    \"loading\": \"Загрузка...\",\n    \"menu_cmd_actions\": \"Действия\",\n    \"menu_cmd_add\": \"Добавить команду меню\",\n    \"menu_cmd_command\": \"Команда\",\n    \"menu_cmd_desc\": \"После настройки эти команды будут видны в возвратном меню клиента, позволяя быстро выполнять определенные операции без прерывания потока, например, запуск вспомогательных программ.\\nПример: Отображаемое имя - Закрыть компьютер; Команда - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Отображаемое имя\",\n    \"menu_cmd_drag_sort\": \"Перетащите для сортировки\",\n    \"menu_cmd_name\": \"Команды меню\",\n    \"menu_cmd_placeholder_command\": \"Команда\",\n    \"menu_cmd_placeholder_display_name\": \"Отображаемое имя\",\n    \"menu_cmd_placeholder_execute\": \"Выполнить команду\",\n    \"menu_cmd_placeholder_undo\": \"Отменить команду\",\n    \"menu_cmd_remove_menu\": \"Удалить команду меню\",\n    \"menu_cmd_remove_prep\": \"Удалить команду подготовки\",\n    \"mouse_mode\": \"Режим мыши\",\n    \"mouse_mode_auto\": \"Авто (Глобальная настройка)\",\n    \"mouse_mode_desc\": \"Выберите метод ввода мыши для этого приложения. Авто использует глобальную настройку, Виртуальная мышь использует HID-драйвер, SendInput использует Windows API.\",\n    \"mouse_mode_sendinput\": \"SendInput (Windows API)\",\n    \"mouse_mode_vmouse\": \"Виртуальная мышь\",\n    \"name\": \"Название\",\n    \"output_desc\": \"Файл, в котором сохраняется вывод команды, если он не указан, вывод игнорируется\",\n    \"output_name\": \"Вывод\",\n    \"run_as_desc\": \"Это может потребоваться для некоторых приложений, которым требуются права администратора для правильного запуска.\",\n    \"scan_result_add_all\": \"Добавить всё\",\n    \"scan_result_edit_title\": \"Добавить и редактировать\",\n    \"scan_result_filter_all\": \"Все\",\n    \"scan_result_filter_epic_title\": \"Игры Epic Games\",\n    \"scan_result_filter_executable\": \"Исполняемый\",\n    \"scan_result_filter_executable_title\": \"Исполняемый файл\",\n    \"scan_result_filter_gog_title\": \"Игры GOG Galaxy\",\n    \"scan_result_filter_script\": \"Скрипт\",\n    \"scan_result_filter_script_title\": \"Пакетный/командный скрипт\",\n    \"scan_result_filter_shortcut\": \"Ярлык\",\n    \"scan_result_filter_shortcut_title\": \"Ярлык\",\n    \"scan_result_filter_steam_title\": \"Игры Steam\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Игра\",\n    \"scan_result_games_only\": \"Только игры\",\n    \"scan_result_matched\": \"Совпадений: {count}\",\n    \"scan_result_no_apps\": \"Приложения для добавления не найдены\",\n    \"scan_result_no_matches\": \"Совпадающих приложений не найдено\",\n    \"scan_result_quick_add_title\": \"Быстрое добавление\",\n    \"scan_result_remove_title\": \"Удалить из списка\",\n    \"scan_result_search_placeholder\": \"Поиск названия приложения, команды или пути...\",\n    \"scan_result_show_all\": \"Показать всё\",\n    \"scan_result_title\": \"Результаты сканирования\",\n    \"scan_result_try_different_keywords\": \"Попробуйте использовать другие ключевые слова для поиска\",\n    \"scan_result_type_batch\": \"Пакетный\",\n    \"scan_result_type_command\": \"Командный скрипт\",\n    \"scan_result_type_executable\": \"Исполняемый файл\",\n    \"scan_result_type_shortcut\": \"Ярлык\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Поиск приложений...\",\n    \"select\": \"Выбрать\",\n    \"test_menu_cmd\": \"Тестировать команду\",\n    \"test_menu_cmd_empty\": \"Команда не может быть пустой\",\n    \"test_menu_cmd_executing\": \"Выполнение команды...\",\n    \"test_menu_cmd_failed\": \"Ошибка выполнения команды\",\n    \"test_menu_cmd_success\": \"Команда успешно выполнена!\",\n    \"use_desktop_image\": \"Использовать текущие обои рабочего стола\",\n    \"wait_all\": \"Продолжать вещание, пока не завершатся все процессы приложения\",\n    \"wait_all_desc\": \"Продолжать вещание, пока все процессы, запущенные приложением не будут завершены. Если не выбрано, вещание прекратится, по завершении начального процесса приложения, даже если запущены другие подпроцессы.\",\n    \"working_dir\": \"Рабочая папка\",\n    \"working_dir_desc\": \"Рабочий каталог, передаваемый процессу. К примеру, некоторые приложения используют рабочий каталог для поиска конфигурационных файлов. Если не указан, Sunshine по умолчанию использует вышестоящий каталог команды\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Имя адаптера\",\n    \"adapter_name_desc_linux_1\": \"Вручную укажите GPU для захвата.\",\n    \"adapter_name_desc_linux_2\": \"найти все устройства, поддерживающие VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Замените ``renderD129`` устройством сверху, чтобы перечислить имя и возможности устройства. Чтобы быть поддержанным Sunshine, он должен иметь как минимум свое:\",\n    \"adapter_name_desc_windows\": \"Вручную укажите GPU для захвата. Если не задано, GPU выбирается автоматически. Примечание: к этому GPU должен быть подключен и включен дисплей. Если ваш ноутбук не может включить прямой вывод GPU, установите автоматический режим.\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Если установлена последняя версия виртуального дисплея, она может автоматически связаться с привязкой GPU\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Добавить\",\n    \"address_family\": \"Семейство адресов\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Установить семейство адресов, используемых Sunshine\",\n    \"address_family_ipv4\": \"Только IPv4\",\n    \"always_send_scancodes\": \"Всегда посылать коды клавиш\",\n    \"always_send_scancodes_desc\": \"Передача кодов клавиш улучшает совместимость с играми и приложениями, но может привести к неправильному вводу с клавиатуры, если клиент используют раскладку, отличную от английской США. Включите, если в каких-то приложениях ввод с клавиатуры не работает вовсе. Отключите, если клавиши клиента передают на ввод не те клавиши сервер.\",\n    \"amd_coder\": \"Кодировщик AMF (H264)\",\n    \"amd_coder_desc\": \"Позволяет выбрать энтропическую кодировку для приоритизации скорости или качества кодирования. Работает только с H.264.\",\n    \"amd_enforce_hrd\": \"AMF Hypothetical Reference Decoder (HRD) Enforcement\",\n    \"amd_enforce_hrd_desc\": \"Увеличивает ограничения на контроль за скоростью для удовлетворения требований модели HRD. Это значительно снижает переполнение битрейта, но может вызвать кодировку артефактов или уменьшить качество на некоторых картах.\",\n    \"amd_preanalysis\": \"Предварительный анализ AMF\",\n    \"amd_preanalysis_desc\": \"Это позволяет проводить предварительный анализ скорости управления, который может повысить качество за счет увеличения задержки кодирования.\",\n    \"amd_quality\": \"Качество AMF\",\n    \"amd_quality_balanced\": \"balanced -- сбалансированный (по умолчанию)\",\n    \"amd_quality_desc\": \"Задаёт соотношение между скоростью и качеством кодирования.\",\n    \"amd_quality_group\": \"Настройки качества AMF\",\n    \"amd_quality_quality\": \"quality -- упор на качество\",\n    \"amd_quality_speed\": \"speed -- упор на скорость\",\n    \"amd_qvbr_quality\": \"Уровень качества AMF QVBR\",\n    \"amd_qvbr_quality_desc\": \"Уровень качества для режима управления битрейтом QVBR. Диапазон: 1-51 (ниже = лучше качество). По умолчанию: 23. Применяется только когда управление битрейтом установлено на 'qvbr'.\",\n    \"amd_rc\": \"Контроль скорости AMF\",\n    \"amd_rc_cbr\": \"cbr -- постоянный битрейт (рекомендуется если HRD включен)\",\n    \"amd_rc_cqp\": \"cqp -- постоянный битрейт с сжатием уровня qp\",\n    \"amd_rc_desc\": \"Задает способ контроля тарифа, чтобы убедиться, что мы не превысили указанный клиентом битрейт. 'cqp' не подходит для контроля битрейта, а другие параметры помимо 'vbr_latency' зависят от соблюдения правил HRD для ограничения переполнения битрейта.\",\n    \"amd_rc_group\": \"Настройки контроля скорости AMF\",\n    \"amd_rc_hqcbr\": \"hqcbr -- высококачественный постоянный битрейт\",\n    \"amd_rc_hqvbr\": \"hqvbr -- высококачественный переменный битрейт\",\n    \"amd_rc_qvbr\": \"qvbr -- переменный битрейт качества (использует уровень качества QVBR)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- битрейт зависит от задержки до клиента (рекомендуется если HRD выключен; по умолчанию)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- битрейт зависит от максимального возможного для клиента\",\n    \"amd_usage\": \"Использование AMF\",\n    \"amd_usage_desc\": \"Определяет базовую кодировку профиля. Все параметры, представленные ниже, переопределят поднабор пользовательского профиля, но есть дополнительные скрытые настройки, которые не могут быть настроены в другом месте.\",\n    \"amd_usage_lowlatency\": \"lowlatency - низкая задержка (очень быстрый)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - низкая задержка, высокое качество (оптимальный)\",\n    \"amd_usage_transcoding\": \"transcoding -- перекодирование (самый медленный)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency - очень низкая задержка (самый быстрый; по умолчанию)\",\n    \"amd_usage_webcam\": \"webcam -- веб-камера (медленный)\",\n    \"amd_vbaq\": \"Адаптивное квантование на основе отклонений AMF (VBAQ)\",\n    \"amd_vbaq_desc\": \"Как правило, визуальная система человека менее чувствительна к артефактам в особо текстурированных районах. В режиме VBAQ отклонение пикселей используется для обозначения сложности пространственной текстуры, что позволяет кодировщику выделять больше битов для более плавности зон. Включение этой функции приводит к улучшению субъективного качества изображения с некоторым содержимым.\",\n    \"amf_draw_mouse_cursor\": \"Рисовать простой курсор при использовании метода захвата AMF\",\n    \"amf_draw_mouse_cursor_desc\": \"В некоторых случаях использование захвата AMF не отображает указатель мыши. Включение этой опции нарисует простой указатель мыши на экране. Примечание: Позиция указателя мыши будет обновляться только при обновлении экрана содержимого, поэтому в сценариях вне игры, например на рабочем столе, вы можете наблюдать медленное движение указателя мыши.\",\n    \"apply_note\": \"Нажмите 'Применить', чтобы перезапустить Sunshine и применить изменения. Все запущенные сессии будут завершены.\",\n    \"audio_sink\": \"Физическое аудиоустройство\",\n    \"audio_sink_desc_linux\": \"Название звукового приёмника, используемого для обратной ретрансляции. Если эта переменная не указана, pulseaudio выберет устройство по умолчанию. Определить название звукового приёмника можно либо командой:\",\n    \"audio_sink_desc_macos\": \"Название звуковой раковины, используемой для аудиоциклов. Sunshine может получить доступ только к микрофонам в macOS из-за ограничений системы. Для трансляции системного аудио с помощью Soundflower или BlackHole.\",\n    \"audio_sink_desc_windows\": \"Укажите вручную определённое аудиоустройство для захвата звука. Если не указано, то устройство выбирается автоматически. Крайне рекомендуем оставить это поле пустым! Если у вас несколько аудио устройств с одинаковыми именами, вы можете получить ID устройства, используя следующую команду:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Динамики (High Definition Audio Device)\",\n    \"av1_mode\": \"Поддержка AV1\",\n    \"av1_mode_0\": \"Sunshine будет уведомлять клиентов о поддержке AV1 на основе возможностей кодировщика (рекомендуется)\",\n    \"av1_mode_1\": \"Sunshine не будет уведомлять клиентов о поддержке AV1\",\n    \"av1_mode_2\": \"Sunshine будет уведомлять клиентов о поддержке AV1 Main 8-bit профиля\",\n    \"av1_mode_3\": \"Sunshine будет уведомлять клиентов о поддержке профилей AV1 Main 8-bit и 10-bit (HDR)\",\n    \"av1_mode_desc\": \"Позволяет клиенту запрашивать AV1 Main 8-бит или 10-битные видео потоки. AV1 будет потреблять больше ресурсов процессора для кодирования потока, поэтому включение этой опции может снизить производительность при использовании программного обеспечения.\",\n    \"back_button_timeout\": \"Время удержания для эмуляции кнопки Домой\",\n    \"back_button_timeout_desc\": \"Если кнопка \\\"Назад\\\"/\\\"Выбор\\\" удерживается вниз заданное количество миллисекунд, то вместо нажатой кнопки будет эмулирована кнопка \\\"Домой\\\". Если установлено значение < 0 (по умолчанию), удержание кнопки \\\"Назад\\\"/\\\"Выбор\\\" не будет эмулировать кнопку \\\"Домой\\\".\",\n    \"bind_address\": \"Адрес привязки (тестовая функция)\",\n    \"bind_address_desc\": \"Установите конкретный IP-адрес, к которому будет привязан Sunshine. Если оставить пустым, Sunshine будет привязан ко всем доступным адресам.\",\n    \"capture\": \"Принудительный метод захвата\",\n    \"capture_desc\": \"В автоматическом режиме Sunshine будет использовать первый работающий драйвер NvFBC.\",\n    \"capture_target\": \"Цель захвата\",\n    \"capture_target_desc\": \"Выберите тип цели для захвата. Выбрав 'Окно', вы можете захватить конкретное окно приложения (например, программу для AI интерполяции кадров) вместо всего дисплея.\",\n    \"capture_target_display\": \"Дисплей\",\n    \"capture_target_window\": \"Окно\",\n    \"cert\": \"Сертификат\",\n    \"cert_desc\": \"Сертификат, используемый для веб-интерфейса и привязки клиентов Moonlight. Для совместимости форма открытого ключа должен иметь RSA-2048.\",\n    \"channels\": \"Максимальное число подключенных клиентов\",\n    \"channels_desc_1\": \"Sunshine позволяет одновременное совместное использование одного сеанса потокового вещания.\",\n    \"channels_desc_2\": \"Некоторые аппаратные кодировщики могут иметь ограничения, уменьшающие производительность с несколькими потоками.\",\n    \"close_verify_safe\": \"Безопасная проверка совместима с старыми клиентами\",\n    \"close_verify_safe_desc\": \"Старые клиенты могут не подключаться к Sunshine, пожалуйста, отключите этот параметр или обновите клиента\",\n    \"coder_cabac\": \"cabac -- контекстная адаптивная арифметическая кодировка - более высокое качество\",\n    \"coder_cavlc\": \"cavlc -- контекстное адаптивное кодирование переменной длины - ускорение декодирования\",\n    \"configuration\": \"Конфигурация\",\n    \"controller\": \"Включить ввод с контроллера\",\n    \"controller_desc\": \"Позволяет гостям контролировать хост-систему с помощью геймпада / контроллера\",\n    \"credentials_file\": \"Файл учётных данных\",\n    \"credentials_file_desc\": \"Храните имя пользователя/пароль отдельно от файла состояния Sunshine.\",\n    \"display_device_options_note_desc_windows\": \"Windows сохраняет различные настройки отображения для каждой комбинации активных дисплеев.\\nSunshine затем применяет изменения к дисплею(-ям), принадлежащему к такой комбинации дисплеев.\\nЕсли вы отключите устройство, которое было активным, когда Sunshine применял настройки, изменения не могут быть\\nотменены, если только комбинацию не удастся активировать снова к моменту, когда Sunshine попытается отменить изменения!\",\n    \"display_device_options_note_windows\": \"Примечание о том, как применяются настройки\",\n    \"display_device_options_windows\": \"Настройки устройства отображения\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Активирует дисплей, если он ещё не активен\",\n    \"display_device_prep_ensure_active_windows\": \"Автоматически активировать дисплей\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Отключает все другие дисплеи и включает только указанный\",\n    \"display_device_prep_ensure_only_display_windows\": \"Деактивировать другие дисплеи и активировать только указанный дисплей\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Активирует дисплей и устанавливает его как основной\",\n    \"display_device_prep_ensure_primary_windows\": \"Автоматически активировать дисплей и сделать его основным\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Использует только виртуальный дисплей для вторичного расширенного стриминга\",\n    \"display_device_prep_ensure_secondary_windows\": \"Стриминг вторичного дисплея (только виртуальный дисплей)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Без изменений состояния дисплея; пользователь должен сам убедиться, что дисплей готов\",\n    \"display_device_prep_no_operation_windows\": \"Отключено\",\n    \"display_device_prep_windows\": \"Подготовка дисплея\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Должно быть указано хотя бы одно \\\"полученное\\\" и одно \\\"конечное\\\" значение.\\nПустое поле в разделе \\\"полученные\\\" означает \\\"соответствовать любому значению\\\". Пустое поле в разделе \\\"конечные\\\" означает \\\"сохранить полученное значение\\\".\\nПри желании вы можете сопоставить определённое значение FPS с определённым разрешением...\\n\\nПримечание: если в клиенте Moonlight не включена опция \\\"Оптимизировать настройки игры\\\", строки, содержащие значения разрешения, игнорируются.\",\n    \"display_mode_remapping_desc_windows\": \"Укажите, как определённое разрешение и/или частота обновления должны быть переназначены на другие значения.\\nВы можете стримить с более низким разрешением, при этом рендеринг на хосте выполняется с более высоким разрешением для эффекта суперсэмплинга.\\nИли вы можете стримить с более высоким FPS, ограничивая частоту обновления хоста более низким значением.\\nСопоставление выполняется сверху вниз. Как только запись совпадает, остальные больше не проверяются, но по-прежнему валидируются.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Конечная частота обновления\",\n    \"display_mode_remapping_final_resolution_windows\": \"Конечное разрешение\",\n    \"display_mode_remapping_optional\": \"необязательно\",\n    \"display_mode_remapping_received_fps_windows\": \"Полученный FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"Полученное разрешение\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Примечание: если в клиенте Moonlight не включена опция \\\"Оптимизировать настройки игры\\\", переназначение отключено.\",\n    \"display_mode_remapping_windows\": \"Переназначить режимы дисплея\",\n    \"display_modes\": \"Режимы отображения\",\n    \"ds4_back_as_touchpad_click\": \"Назад/Выберете для нажатия сенсорной панели\",\n    \"ds4_back_as_touchpad_click_desc\": \"При принудительной эмуляции DS4, нажмите на карточку Назад/Выделение для сенсорной панели\",\n    \"dsu_server_port\": \"DSU Server Port\",\n    \"dsu_server_port_desc\": \"DSU server listening port (default 26760). Sunshine will act as a DSU server to receive client connections and send motion data. Enable DSU server in your client(Yuzu,Ryujinx etc.) and set DSU server address(127.0.0.1) and port(26760)\",\n    \"enable_dsu_server\": \"Включить сервер DSU\",\n    \"enable_dsu_server_desc\": \"Enable DSU server to receive client connections and send motion data\",\n    \"encoder\": \"Принудительный кодировщик\",\n    \"encoder_desc\": \"Принудительно использовать конкретный кодировщик, иначе Sunshine выберет наилучший доступный вариант. Примечание: Если указать аппаратный кодировщик в Windows, тот должен соответствовать графическому ускорителю, к которому подключён экран.\",\n    \"encoder_software\": \"Программный\",\n    \"experimental\": \"Экспериментальный\",\n    \"experimental_features\": \"Экспериментальные функции\",\n    \"external_ip\": \"Внешний IP\",\n    \"external_ip_desc\": \"Если внешний IP адрес не указан, Sunshine будет автоматически определять внешний IP\",\n    \"fec_percentage\": \"Процент FEC\",\n    \"fec_percentage_desc\": \"Процент погрешности исправления пакетов по каждому пакету данных в каждом видеокадре. Более высокие значения могут корректно повлиять на потерю сетевых пакетов, но за счет увеличения пропускной способности.\",\n    \"ffmpeg_auto\": \"auto -- пусть ffmpeg решает (по умолчанию)\",\n    \"file_apps\": \"Файл приложений\",\n    \"file_apps_desc\": \"Файл, в котором хранятся текущие приложения Sunshine.\",\n    \"file_state\": \"Файл состояния\",\n    \"file_state_desc\": \"Файл, в котором хранится текущее состояние Sunshine\",\n    \"fps\": \"Объявленный FPS\",\n    \"gamepad\": \"Тип эмулируемого контроллера\",\n    \"gamepad_auto\": \"Настройка автоматического выбора\",\n    \"gamepad_desc\": \"Выберите тип контроллера для эмулирования на хосте\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 Manual Options\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Ручные настройки DS4\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Команды подготовки\",\n    \"global_prep_cmd_desc\": \"Настроить список команд, которые будут выполнены до или после запуска любого приложения. Если какая-либо из указанных команд подготовки не сработает должным образом, то весь процесс запуска приложения будет прерван.\",\n    \"hdr_luminance_analysis\": \"Динамические метаданные HDR (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Включает покадровый анализ яркости GPU и внедряет динамические метаданные HDR10+ (ST 2094-40) и HDR Vivid (CUVA) в кодированный поток. Обеспечивает покадровые подсказки тональной компрессии для поддерживаемых дисплеев. Добавляет небольшую нагрузку GPU (~0,5-1,5мс/кадр при высоких разрешениях). Отключите при падении частоты кадров с HDR.\",\n    \"hdr_prep_automatic_windows\": \"Switch on/off the HDR mode as requested by the client\",\n    \"hdr_prep_no_operation_windows\": \"Disabled\",\n    \"hdr_prep_windows\": \"HDR state change\",\n    \"hevc_mode\": \"Поддержка HEVC\",\n    \"hevc_mode_0\": \"Sunshine будет уведомлять клиентов о поддержке HEVC на основе возможностей кодировщика (рекомендуется)\",\n    \"hevc_mode_1\": \"Sunshine не будет уведомлять клиентов о поддержке HEVC\",\n    \"hevc_mode_2\": \"Sunshine будет уведомлять клиентов о поддержке HEVC Main\",\n    \"hevc_mode_3\": \"Sunshine будет уведомлять клиентов о поддержке профилей HEVC Main и Main10 (HDR)\",\n    \"hevc_mode_desc\": \"Позволяет клиенту запрашивать HEVC Main или HEVC Main 10-битное видео потоки. HEVC будет потреблять больше ресурсов процессора для кодирования потока, поэтому включение этой опции может снизить производительность при использовании программного обеспечения.\",\n    \"high_resolution_scrolling\": \"Поддержка прокрутки высокого разрешения\",\n    \"high_resolution_scrolling_desc\": \"Когда включено, Sunshine будет посылать события прокручивания колесика мыши с высоким разрешением от клиентов Moonlight. Отключение может быть полезно для старых приложений, которые слишком быстро прокручиваю при получении событий высокого разрешения.\",\n    \"install_steam_audio_drivers\": \"Установить Steam Audio Drivers\",\n    \"install_steam_audio_drivers_desc\": \"Если Steam установлен, он автоматически установит драйвер Steam Streaming Speakers для поддержки объёмного звука 5.1/7.1 и заглушения звука на сервере.\",\n    \"key_repeat_delay\": \"Задержка повтора нажатий\",\n    \"key_repeat_delay_desc\": \"Задает начальную задержку в миллисекундах до повторных нажатий.\",\n    \"key_repeat_frequency\": \"Частота повторения нажатий\",\n    \"key_repeat_frequency_desc\": \"Как часто нажатия повторяются за секунду. Эта настройка поддерживает десятичные дроби.\",\n    \"key_rightalt_to_key_win\": \"Назначить правый Alt на клавишу Windows\",\n    \"key_rightalt_to_key_win_desc\": \"Возможно, вы не можете послать нажатие кнопки Windows непосредственно из Moonlight. В таком случае, полезно чтобы Sunshine думал, что клавиша правый Alt является клавишей Windows\",\n    \"key_rightalt_to_key_windows\": \"Переопределить клавишу правый Alt как клавишу Windows\",\n    \"keyboard\": \"Включить ввод с клавиатуры\",\n    \"keyboard_desc\": \"Позволяет гостям управлять системой хоста с помощью клавиатуры\",\n    \"lan_encryption_mode\": \"Режим шифрования LAN\",\n    \"lan_encryption_mode_1\": \"Включено для поддерживаемых клиентов\",\n    \"lan_encryption_mode_2\": \"Требуется для всех клиентов\",\n    \"lan_encryption_mode_desc\": \"Определяет, когда шифрование будет использоваться при вещании в локальной сети. Шифрование может снизить качество вещания, особенно на более слабых серверах и клиентах.\",\n    \"locale\": \"Язык\",\n    \"locale_desc\": \"Локализация, используемая для пользовательского интерфейса Sunshine.\",\n    \"log_level\": \"Уровень журналирования\",\n    \"log_level_0\": \"Подробные\",\n    \"log_level_1\": \"Отладочные\",\n    \"log_level_2\": \"Информация\",\n    \"log_level_3\": \"Предупреждения\",\n    \"log_level_4\": \"Ошибки\",\n    \"log_level_5\": \"Критические\",\n    \"log_level_6\": \"Нет\",\n    \"log_level_desc\": \"Минимальный уровень отладочной информации, который будет записан в журнал\",\n    \"log_path\": \"Путь к файлу журнала\",\n    \"log_path_desc\": \"Файл, в котором хранятся текущие журналы Sunshine.\",\n    \"max_bitrate\": \"Максимальный битрейт\",\n    \"max_bitrate_desc\": \"Максимальный битрейт (в Кбит/с), которым Sunshine кодирует поток. Если установлено значение 0, он всегда будет использовать битрейт, запрошенный Moonlight.\",\n    \"max_fps_reached\": \"Достигнуты максимальные значения FPS\",\n    \"max_resolutions_reached\": \"Достигнуто максимальное количество разрешений\",\n    \"mdns_broadcast\": \"Автоматическое обнаружение компьютера в локальной сети\",\n    \"mdns_broadcast_desc\": \"Включение этой опции позволит автоматически обнаруживать компьютер в локальной сети, если Moonlight настроен на автоматическое обнаружение компьютера в локальной сети\",\n    \"min_threads\": \"Минимальное количество потоков ЦП\",\n    \"min_threads_desc\": \"Увеличение значения немного снижает эффективность кодирования, но полученный результат обычно стоит того, так как позволяет использовать больше ядер процессора для кодирования. Идеальное значение - это наименьшее значение, которое может надежно кодировать поток при желаемых настройках на вашем оборудовании.\",\n    \"minimum_fps_target\": \"Минимальная цель FPS\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Прочие параметры\",\n    \"motion_as_ds4\": \"Эмулировать контроллер DS4 если контроллер клиента сообщает о наличии датчиков движения\",\n    \"motion_as_ds4_desc\": \"Если отключено, датчики движения не будут учитываться при выборе типа контроллера.\",\n    \"mouse\": \"Включить ввод мыши\",\n    \"mouse_desc\": \"Позволяет гостям контролировать хост-систему мышкой\",\n    \"native_pen_touch\": \"Родная поддержка пера/сенсорного экрана\",\n    \"native_pen_touch_desc\": \"Если включено, Sunshine будет перенаправлять родные события пера/сенсорного экрана от клиентов Moonlight. Это может быть полезно для более старых приложений без поддержки пера/сенсорного экрана.\",\n    \"no_fps\": \"Значения FPS не добавлены\",\n    \"no_resolutions\": \"Разрешения не добавлены\",\n    \"notify_pre_releases\": \"Уведомлять о предварительных версиях\",\n    \"notify_pre_releases_desc\": \"Уведомлять ли о новых предварительных версиях Sunshine\",\n    \"nvenc_h264_cavlc\": \"Предпочитайте CAVLC поверх CABAC в H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Более простая форма кодирования энтропии. CAVLC требует на 10% больше битрейта для достижения того же качества. Только для очень старых декодирующих устройств.\",\n    \"nvenc_latency_over_power\": \"Предпочитайте более низкую задержку кодирования по сравнению с экономией энергии\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine будет запрашивать графический ускоритель работать на максимальной тактовой частоте во время трансляции, чтобы уменьшить задержку кодирования. Отключение этого параметра не рекомендуется, так как это может привести к значительному увеличению задержки кодирования.\",\n    \"nvenc_lookahead_depth\": \"Глубина Lookahead\",\n    \"nvenc_lookahead_depth_desc\": \"Количество кадров для предварительного анализа при кодировании (0-32). Lookahead улучшает качество кодирования, особенно в сложных сценах, обеспечивая лучшую оценку движения и распределение битрейта. Более высокие значения улучшают качество, но увеличивают задержку кодирования. Установите 0 для отключения. Требуется NVENC SDK 13.0 (1202) или новее.\",\n    \"nvenc_lookahead_level\": \"Уровень Lookahead\",\n    \"nvenc_lookahead_level_0\": \"Уровень 0 (низкое качество, быстрее)\",\n    \"nvenc_lookahead_level_1\": \"Уровень 1\",\n    \"nvenc_lookahead_level_2\": \"Уровень 2\",\n    \"nvenc_lookahead_level_3\": \"Уровень 3 (высокое качество, медленнее)\",\n    \"nvenc_lookahead_level_autoselect\": \"Автовыбор (драйвер выберет оптимальный уровень)\",\n    \"nvenc_lookahead_level_desc\": \"Уровень качества Lookahead. Более высокие уровни улучшают качество за счет производительности. Эта опция работает только если lookahead_depth больше 0. Требуется NVENC SDK 13.0 (1202) или новее.\",\n    \"nvenc_lookahead_level_disabled\": \"Отключено (то же, что и уровень 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Использовать OpenGL/Vulkan поверх DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine не может захватывать полноэкранные программы OpenGL и Vulkan с полной частотой кадров, если они не используются поверх DXGI. Это общесистемная настройка, которая будет сброшена к изначальной после выхода из Sunshine.\",\n    \"nvenc_preset\": \"Преднастройка производительности\",\n    \"nvenc_preset_1\": \"(быстрый, по умолчанию)\",\n    \"nvenc_preset_7\": \"(медленно)\",\n    \"nvenc_preset_desc\": \"Более высокие значения улучшают сжатие (качество на заданном битрейте) за счет увеличения задержки кодирования. Рекомендуется изменять только когда достигнуто ограничение сети или декодера, в противном случае подобный эффект может быть достигнут путем увеличения битрейта.\",\n    \"nvenc_rate_control\": \"Режим управления битрейтом\",\n    \"nvenc_rate_control_cbr\": \"CBR (Постоянный битрейт) - Низкая задержка\",\n    \"nvenc_rate_control_desc\": \"Выберите режим управления битрейтом. CBR обеспечивает фиксированный битрейт для стриминга с низкой задержкой. VBR позволяет битрейту меняться в зависимости от сложности сцены.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Переменный битрейт) - Лучшее качество\",\n    \"nvenc_realtime_hags\": \"Использовать приоритет реального времени в аппаратном ускоренном планировании gpu\",\n    \"nvenc_realtime_hags_desc\": \"В настоящее время драйвера NVIDIA могут зависнуть в кодировщике при включенном HAGS когда используется приоритет реального времени и использование видеопамяти графического ускорителя близко к максимуму. Отключение этой опции снижает приоритет на высокий что позволяет избежать зависание, но может привести к пониженной производительности захвата экрана при высокой нагрузке графического ускорителя.\",\n    \"nvenc_spatial_aq\": \"Spatial AQ\",\n    \"nvenc_spatial_aq_desc\": \"Назначает более высокие значения QP для плоских участков потока. Рекомендуется включить при потоке на более низких битрейтах.\",\n    \"nvenc_spatial_aq_disabled\": \"Disabled (faster, default)\",\n    \"nvenc_spatial_aq_enabled\": \"Enabled (slower)\",\n    \"nvenc_split_encode\": \"Разделённое кодирование кадров\",\n    \"nvenc_split_encode_desc\": \"Split the encoding of each video frame over multiple NVENC hardware units. Significantly reduces encoding latency with a marginal compression efficiency penalty. This option is ignored if your GPU has a singular NVENC unit.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Driver decides (default)\",\n    \"nvenc_split_encode_four_strips\": \"Принудительно 4 полосы (требуется 4+ движка NVENC)\",\n    \"nvenc_split_encode_three_strips\": \"Принудительно 3 полосы (требуется 3+ движка NVENC)\",\n    \"nvenc_split_encode_two_strips\": \"Принудительно 2 полосы (требуется 2+ движка NVENC)\",\n    \"nvenc_target_quality\": \"Целевое качество (режим VBR)\",\n    \"nvenc_target_quality_desc\": \"Целевой уровень качества для режима VBR (0-51 для H.264/HEVC, 0-63 для AV1). Меньшие значения = лучшее качество. Установите 0 для автовыбора.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Режим двухпрохождения\",\n    \"nvenc_twopass_desc\": \"Добавляет предварительную кодировку. Это позволяет обнаружить больше векторов движений, лучше распределять битрейт по всему кадру и более строго придерживаться лимитов битрейта. Отключение не рекомендуется, так как это может привести к возможной перегрузке битрейта и последующей потере пакетов.\",\n    \"nvenc_twopass_disabled\": \"Отключено (быстрый, не рекомендуется)\",\n    \"nvenc_twopass_full_res\": \"Полное разрешение (медленнее)\",\n    \"nvenc_twopass_quarter_res\": \"Квартальное разрешение (по умолчанию)\",\n    \"nvenc_vbv_increase\": \"Однокадровое увеличение VBV/HRD\",\n    \"nvenc_vbv_increase_desc\": \"По умолчанию Sunshine использует однокадровый VBV/HRD, что означает, что любой кодируемый размер кадра не должен превышать запрашиваемый клиентом битрейт, поделенный на частоту кадров. Увеличение этого значение может быть полезным и выступать в качестве переменного битрейта с низкой задержкой, но может также привести к потере пакетов, если в сети нет буфера для обработки резкого увеличения битрейта. Максимально допустимое значение - 400, что соответствует 5-кратному увеличенному пределу кадра в кодировке.\",\n    \"origin_web_ui_allowed\": \"Использование веб-интерфейса разрешено...\",\n    \"origin_web_ui_allowed_desc\": \"Источник адреса удаленной конечной точки, которой не запрещён доступ к веб-интерфейсу\",\n    \"origin_web_ui_allowed_lan\": \"Только ПК с локальном сети могут получить доступ к веб-интерфейсу\",\n    \"origin_web_ui_allowed_pc\": \"Только хост-система имеет доступ к веб-интерфейсу\",\n    \"origin_web_ui_allowed_wan\": \"Любой желающий имеет доступ к веб-интерфейсу\",\n    \"output_name_desc_unix\": \"Во время запуска Sunshine вы увидите список обнаруженных экранов. Примечание: используйте ID значения в скобках. Пример ниже; нужный экран можно обнаружить на вкладке Устранение проблем.\",\n    \"output_name_desc_windows\": \"Вручную укажите экран для захвата. Если не указано, то будет произведён захват основного экрана. Примечание: Если вы ранее указали графический ускоритель, этот экран должен быть подключен к тому графическому ускорителю. Подходящие значения определяются с помощью следующей команды:\",\n    \"output_name_unix\": \"Номер экрана\",\n    \"output_name_windows\": \"ID устройства вывода изображения\",\n    \"ping_timeout\": \"Время ожидания ответа\",\n    \"ping_timeout_desc\": \"Время ожидания данных от Moonlight до завершения вещания, в миллисекундах\",\n    \"pkey\": \"Закрытый ключ\",\n    \"pkey_desc\": \"Закрытый ключ, используемый для веб-интерфейса и привязки клиентов Moonlight. Для совместимости формат закрытого ключа должен быть RSA-2048.\",\n    \"port\": \"Порт\",\n    \"port_alert_1\": \"Sunshine не может использовать порты ниже 1024!\",\n    \"port_alert_2\": \"Порты выше 65535 недоступны!\",\n    \"port_desc\": \"Установить семейство портов, используемых Sunshine\",\n    \"port_http_port_note\": \"Используйте этот порт для подключения при помощи Moonlight.\",\n    \"port_note\": \"Примечание\",\n    \"port_port\": \"Порт\",\n    \"port_protocol\": \"Протокол\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Размещение веб-интерфейса для всех желающих может поставить Вашу хост-систему под угрозу! Продолжайте на свой страх и риск!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Параметр квантования\",\n    \"qp_desc\": \"Некоторые устройства могут не поддерживать постоянный битрейт. Для таких устройств используется параметр квантования. Более высокое значение означает большее сжатие, но меньшее качество.\",\n    \"qsv_coder\": \"Кодировщик QuickSync (H264)\",\n    \"qsv_preset\": \"Предустановка QuickSync\",\n    \"qsv_preset_fast\": \"fast (низкое качество)\",\n    \"qsv_preset_faster\": \"faster (худшее качество)\",\n    \"qsv_preset_medium\": \"medium (по умолчанию)\",\n    \"qsv_preset_slow\": \"slow (хорошее качество)\",\n    \"qsv_preset_slower\": \"slower (отличное качество)\",\n    \"qsv_preset_slowest\": \"slowest (лучшее качество)\",\n    \"qsv_preset_veryfast\": \"fastest (низкое качество)\",\n    \"qsv_slow_hevc\": \"Разрешить медленное HEVC кодирование\",\n    \"qsv_slow_hevc_desc\": \"Это позволяет включить HEVC кодирование на старых процессорах Intel за счет более высокого использования графического ускорителя и более низкой производительности.\",\n    \"refresh_rate_change_automatic_windows\": \"Use FPS value provided by the client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Enter the refresh rate to be used\",\n    \"refresh_rate_change_manual_windows\": \"Use manually entered refresh rate\",\n    \"refresh_rate_change_no_operation_windows\": \"Disabled\",\n    \"refresh_rate_change_windows\": \"FPS change\",\n    \"res_fps_desc\": \"Режимы отображения, объявленные Sunshine. Некоторые версии Moonlight, такие как Moonlight-nx (Switch), полагаются на эти списки, чтобы убедиться, что запрошенные разрешения и fps поддерживаются. Эта настройка не изменяет способ отправки потока экрана в Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Use resolution provided by the client\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_manual_windows\": \"Use manually entered resolution\",\n    \"resolution_change_no_operation_windows\": \"Disabled\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_windows\": \"Resolution change\",\n    \"resolutions\": \"Объявленные разрешения\",\n    \"restart_note\": \"Sunshine перезапускается, чтобы применить изменения.\",\n    \"sleep_mode\": \"Режим сна\",\n    \"sleep_mode_away\": \"Режим отсутствия (Экран выкл., мгновенное пробуждение)\",\n    \"sleep_mode_desc\": \"Управляет поведением при отправке клиентом команды сна. Ждущий режим (S3): традиционный сон, низкое энергопотребление, но требует WOL для пробуждения. Гибернация (S4): сохранение на диск, очень низкое энергопотребление. Режим отсутствия: экран выключается, но система продолжает работать для мгновенного пробуждения — идеально для серверов игрового стриминга.\",\n    \"sleep_mode_hibernate\": \"Гибернация (S4)\",\n    \"sleep_mode_suspend\": \"Ждущий режим (S3)\",\n    \"stream_audio\": \"Включить трансляцию аудио\",\n    \"stream_audio_desc\": \"Отключите эту опцию, чтобы остановить трансляцию аудио.\",\n    \"stream_mic\": \"Включить трансляцию микрофона\",\n    \"stream_mic_desc\": \"Отключите эту опцию, чтобы остановить трансляцию микрофона.\",\n    \"stream_mic_download_btn\": \"Скачать виртуальный микрофон\",\n    \"stream_mic_download_confirm\": \"Вы будете перенаправлены на страницу загрузки виртуального микрофона. Продолжить?\",\n    \"stream_mic_note\": \"Для использования этой функции требуется установка виртуального микрофона\",\n    \"sunshine_name\": \"Название сервера Sunshine\",\n    \"sunshine_name_desc\": \"Имя сервера, отображаемое в Moonlight. Если не указано, используется имя ПК\",\n    \"sw_preset\": \"Предустановки программного кодирования\",\n    \"sw_preset_desc\": \"Оптимизировать компромисс между скоростью кодирования (кодированные кадры в секунду) и эффективностью сжатия (качество за бит в потоке). По умолчанию superfast.\",\n    \"sw_preset_fast\": \"fast\",\n    \"sw_preset_faster\": \"faster\",\n    \"sw_preset_medium\": \"medium\",\n    \"sw_preset_slow\": \"slow\",\n    \"sw_preset_slower\": \"slower\",\n    \"sw_preset_superfast\": \"superfast (по умолчанию)\",\n    \"sw_preset_ultrafast\": \"ultrafast\",\n    \"sw_preset_veryfast\": \"veryfast\",\n    \"sw_preset_veryslow\": \"veryslow\",\n    \"sw_tune\": \"Дополнительные параметры программного кодирования\",\n    \"sw_tune_animation\": \"animation -- подходит для мультфильмов, использует агрессивное подавление блочности и больше опирается на изначальные кадры\",\n    \"sw_tune_desc\": \"Дополнительные параметры, применяемые после предустановки. По умолчанию zerolatency.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- позволяет ускорить декодирование за счет отключения некоторых фильтров\",\n    \"sw_tune_film\": \"film -- используется для высококачественного кино; использует простой метод подавления блочности\",\n    \"sw_tune_grain\": \"grain -- предаёт зернистость, как на старой фотоплёнке\",\n    \"sw_tune_stillimage\": \"stillimage -- хорош для малоподвижных изображений\",\n    \"sw_tune_zerolatency\": \"zerolatency -- хорош для быстрого кодирования и вещания с низкой задержкой (по умолчанию)\",\n    \"system_tray\": \"Включить системный трей\",\n    \"system_tray_desc\": \"Включить ли системный трей. Если включено, Sunshine будет отображать значок в системном трее и им можно управлять из системного трея.\",\n    \"touchpad_as_ds4\": \"Эмулировать контроллер DS4 если контроллер клиента сообщает о наличии сенсорной панели\",\n    \"touchpad_as_ds4_desc\": \"Если отключено, присутствие сенсорной панели не будет учитываться при выборе типа геймпада.\",\n    \"unsaved_changes_tooltip\": \"У вас есть несохраненные изменения. Нажмите, чтобы сохранить.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Автоматически настраивать переадресацию портов для вещания через Интернет\",\n    \"variable_refresh_rate\": \"Переменная частота обновления (VRR)\",\n    \"variable_refresh_rate_desc\": \"Разрешить частоте кадров видеопотока соответствовать частоте кадров рендеринга для поддержки VRR. При включении кодирование происходит только когда доступны новые кадры, позволяя потоку следовать фактической частоте кадров рендеринга.\",\n    \"vdd_reuse_desc_windows\": \"При включении все клиенты будут использовать один и тот же VDD (Virtual Display Device). При отключении (по умолчанию) каждый клиент получает свой собственный VDD. Включите это для более быстрого переключения клиентов, но учтите, что все клиенты будут использовать одинаковые настройки дисплея.\",\n    \"vdd_reuse_windows\": \"Использовать один VDD для всех клиентов\",\n    \"virtual_display\": \"Виртуальный дисплей\",\n    \"virtual_mouse\": \"Драйвер виртуальной мыши\",\n    \"virtual_mouse_desc\": \"При включении Sunshine будет использовать драйвер Zako Virtual Mouse (если установлен) для имитации ввода мыши на уровне HID. Это позволяет играм с Raw Input получать события мыши. При отключении или отсутствии драйвера используется SendInput.\",\n    \"virtual_sink\": \"Виртуальное аудиоустройство\",\n    \"virtual_sink_desc\": \"Вручную укажите виртуальное аудиоустройство. Если не указано, то устройство будет выбрано автоматически. Крайне рекомендуем оставить это поле пустым!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Установить драйвер виртуальной мыши?\",\n    \"vmouse_confirm_uninstall\": \"Удалить драйвер виртуальной мыши?\",\n    \"vmouse_install\": \"Установить драйвер\",\n    \"vmouse_installing\": \"Установка...\",\n    \"vmouse_note\": \"Драйвер виртуальной мыши требует отдельной установки. Используйте панель управления Sunshine для установки или управления драйвером.\",\n    \"vmouse_refresh\": \"Обновить статус\",\n    \"vmouse_status_installed\": \"Установлен (не активен)\",\n    \"vmouse_status_not_installed\": \"Не установлен\",\n    \"vmouse_status_running\": \"Работает\",\n    \"vmouse_uninstall\": \"Удалить драйвер\",\n    \"vmouse_uninstalling\": \"Удаление...\",\n    \"vt_coder\": \"Кодировщик VideoToolbox\",\n    \"vt_realtime\": \"Кодирование в реальном времени через VideoToolbox\",\n    \"vt_software\": \"Программное кодирование через VideoToolbox\",\n    \"vt_software_allowed\": \"Разрешено\",\n    \"vt_software_forced\": \"Принудительно\",\n    \"wan_encryption_mode\": \"Режим шифрования WAN\",\n    \"wan_encryption_mode_1\": \"Включено для поддерживаемых клиентов (по умолчанию)\",\n    \"wan_encryption_mode_2\": \"Требуется для всех клиентов\",\n    \"wan_encryption_mode_desc\": \"Определяет, когда будет использоваться шифрование при вещании через Интернет. Шифрование может снизить качество вещания, особенно на более слабых серверах и клиентах.\",\n    \"webhook_curl_command\": \"Команда\",\n    \"webhook_curl_command_desc\": \"Скопируйте следующую команду в ваш терминал, чтобы проверить, правильно ли работает webhook:\",\n    \"webhook_curl_copy_failed\": \"Ошибка копирования, пожалуйста, выберите и скопируйте вручную\",\n    \"webhook_enabled\": \"Уведомления Webhook\",\n    \"webhook_enabled_desc\": \"При включении Sunshine будет отправлять уведомления о событиях на указанный URL Webhook\",\n    \"webhook_group\": \"Настройки уведомлений Webhook\",\n    \"webhook_skip_ssl_verify\": \"Пропустить проверку SSL-сертификата\",\n    \"webhook_skip_ssl_verify_desc\": \"Пропустить проверку SSL-сертификата для HTTPS-соединений, только для тестирования или самоподписанных сертификатов\",\n    \"webhook_test\": \"Тест\",\n    \"webhook_test_failed\": \"Тест Webhook не удался\",\n    \"webhook_test_failed_note\": \"Примечание: Пожалуйста, проверьте, правильный ли URL, или проверьте консоль браузера для получения дополнительной информации.\",\n    \"webhook_test_success\": \"Тест Webhook успешен!\",\n    \"webhook_test_success_cors_note\": \"Примечание: Из-за ограничений CORS статус ответа сервера не может быть подтвержден.\\nЗапрос был отправлен. Если webhook настроен правильно, сообщение должно было быть доставлено.\\n\\nРекомендация: Проверьте вкладку Сеть в инструментах разработчика вашего браузера для получения подробностей о запросе.\",\n    \"webhook_test_url_required\": \"Пожалуйста, сначала введите URL Webhook\",\n    \"webhook_timeout\": \"Таймаут запроса\",\n    \"webhook_timeout_desc\": \"Таймаут для запросов Webhook в миллисекундах, диапазон 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"URL для получения уведомлений о событиях, поддерживает протоколы HTTP/HTTPS\",\n    \"wgc_checking_mode\": \"Проверка режима...\",\n    \"wgc_checking_running_mode\": \"Проверка режима работы...\",\n    \"wgc_control_panel_only\": \"Эта функция доступна только в панели управления Sunshine\",\n    \"wgc_mode_switch_failed\": \"Не удалось переключить режим\",\n    \"wgc_mode_switch_started\": \"Инициировано переключение режима. Если появится запрос UAC, нажмите 'Да' для подтверждения.\",\n    \"wgc_service_mode_warning\": \"Захват WGC требует запуска в режиме пользователя. Если в настоящее время запущен режим службы, нажмите кнопку выше, чтобы переключиться в режим пользователя.\",\n    \"wgc_switch_to_service_mode\": \"Переключиться в режим службы\",\n    \"wgc_switch_to_service_mode_tooltip\": \"В настоящее время работает в режиме пользователя. Нажмите, чтобы переключиться в режим службы.\",\n    \"wgc_switch_to_user_mode\": \"Переключиться в режим пользователя\",\n    \"wgc_switch_to_user_mode_tooltip\": \"Захват WGC требует запуска в режиме пользователя. Нажмите эту кнопку, чтобы переключиться в режим пользователя.\",\n    \"wgc_user_mode_available\": \"В настоящее время работает в режиме пользователя. Захват WGC доступен.\",\n    \"window_title\": \"Заголовок окна\",\n    \"window_title_desc\": \"Заголовок окна для захвата (частичное совпадение, без учета регистра). Если оставить пустым, будет использовано имя текущего запущенного приложения.\",\n    \"window_title_placeholder\": \"например, Имя Приложения\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine - это ваш собственный сервер вещания игр для Moonlight.\",\n    \"download\": \"Скачать\",\n    \"installed_version_not_stable\": \"Вы используете предварительную версию Sunshine. Вы можете столкнуться с ошибками или другими проблемами. Пожалуйста, сообщайте о проблемах, с которыми вы столкнётесь. Благодарим за помощь по улучшению Sunshine!\",\n    \"loading_latest\": \"Загрузка свежей версии...\",\n    \"new_pre_release\": \"Доступна новая предварительная версия!\",\n    \"new_stable\": \"Доступна новая стабильная версия!\",\n    \"startup_errors\": \"<b>Внимание!</b> Sunshine обнаружил эти ошибки во время запуска. Мы <b>НАСТОЯТЕЛЬНО РЕКОМЕНДУЕМ</b> исправить их перед запуском вещания.\",\n    \"update_download_confirm\": \"Вы собираетесь открыть страницу загрузки обновлений в браузере. Продолжить?\",\n    \"version_dirty\": \"Спасибо за помощь по улучшению Sunshine!\",\n    \"version_latest\": \"Вы используете свежую версию Sunshine\",\n    \"view_logs\": \"Просмотр журналов\",\n    \"welcome\": \"Привет, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Приложения\",\n    \"configuration\": \"Настройки\",\n    \"home\": \"Главная\",\n    \"password\": \"Изменить пароль\",\n    \"pin\": \"Pin\",\n    \"theme_auto\": \"Автоматически\",\n    \"theme_dark\": \"Тёмное\",\n    \"theme_light\": \"Светлое\",\n    \"toggle_theme\": \"Оформление\",\n    \"troubleshoot\": \"Устранение проблем\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Подтвердите пароль\",\n    \"current_creds\": \"Текущие учетные данные\",\n    \"new_creds\": \"Новые учетные данные\",\n    \"new_username_desc\": \"Если не указано, имя пользователя не изменится\",\n    \"password_change\": \"Смена пароля\",\n    \"success_msg\": \"Пароль успешно изменен! Эта страница скоро перезагрузится и ваш браузер запросит новые учетные данные.\"\n  },\n  \"pin\": {\n    \"actions\": \"Действия\",\n    \"cancel_editing\": \"Отменить редактирование\",\n    \"client_name\": \"Имя\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Подтвердить удаление\",\n    \"delete_client\": \"Удалить клиента\",\n    \"delete_confirm_message\": \"Вы уверены, что хотите удалить <strong>{name}</strong>?\",\n    \"delete_warning\": \"Это действие нельзя отменить.\",\n    \"device_name\": \"Имя устройства\",\n    \"device_size\": \"Размер устройства\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Большой - TV\",\n    \"device_size_medium\": \"Средний - Планшет\",\n    \"device_size_small\": \"Маленький - Телефон\",\n    \"edit_client_settings\": \"Редактировать настройки клиента\",\n    \"hdr_profile\": \"Профиль HDR\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Загрузка...\",\n    \"loading_clients\": \"Загрузка клиентов...\",\n    \"modify_in_gui\": \"Пожалуйста, измените в графическом интерфейсе\",\n    \"none\": \"-- Нет --\",\n    \"or_manual_pin\": \"или введите PIN вручную\",\n    \"pair_failure\": \"Не удалось привязать: проверьте правильность PIN-кода\",\n    \"pair_success\": \"Успешно! Перейдите в Moonlight для продолжения\",\n    \"pin_pairing\": \"PIN привязки\",\n    \"qr_expires_in\": \"Истекает через\",\n    \"qr_generate\": \"Сгенерировать QR-код\",\n    \"qr_paired_success\": \"Сопряжение выполнено!\",\n    \"qr_pairing\": \"Сопряжение по QR-коду\",\n    \"qr_pairing_desc\": \"Сгенерируйте QR-код для быстрого сопряжения. Отсканируйте его клиентом Moonlight для автоматического сопряжения.\",\n    \"qr_pairing_warning\": \"Экспериментальная функция. Если сопряжение не удастся, используйте ручной ввод PIN ниже. Примечание: Эта функция работает только в локальной сети.\",\n    \"qr_refresh\": \"Обновить QR-код\",\n    \"remove_paired_devices_desc\": \"Удалите ваши сопряжённые устройства.\",\n    \"save_changes\": \"Сохранить изменения\",\n    \"save_failed\": \"Не удалось сохранить настройки клиента. Пожалуйста, попробуйте снова.\",\n    \"save_or_cancel_first\": \"Пожалуйста, сначала сохраните или отмените редактирование\",\n    \"send\": \"Отправить\",\n    \"unknown_client\": \"Неизвестный клиент\",\n    \"unpair_all_confirm\": \"Вы уверены, что хотите отвязать всех клиентов? Это действие нельзя отменить.\",\n    \"unsaved_changes\": \"Несохранённые изменения\",\n    \"warning_msg\": \"Убедитесь, что у вас есть физический доступ к клиенту, который вы привязывайте. Данное ПО может передать полный контроль над вашим компьютером, так что будьте осторожны!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android рекомендуется\",\n    \"client_downloads\": \"Загрузки клиентов\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"GitHub Discussions\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Присоединиться к сообществу\",\n    \"join_group_desc\": \"Получить помощь и поделиться опытом\",\n    \"legal\": \"Юридическая информация\",\n    \"legal_desc\": \"Используя данное ПО, вы соглашаетесь с условиями, изложенными в следующих документах.\",\n    \"license\": \"Лицензия\",\n    \"lizardbyte_website\": \"Сайт LizardByte\",\n    \"official_website\": \"Официальный сайт\",\n    \"official_website_title\": \"AlkaidLab - Официальный сайт\",\n    \"open_source\": \"Открытый исходный код\",\n    \"open_source_desc\": \"Star & Fork для поддержки проекта\",\n    \"quick_start\": \"Быстрый старт\",\n    \"resources\": \"Полезные источники\",\n    \"resources_desc\": \"Полезные ресурсы, посвящённые Sunshine!\",\n    \"third_party_desc\": \"Уведомления о сторонних компонентах\",\n    \"third_party_moonlight\": \"Дружеские ссылки\",\n    \"third_party_notice\": \"Уведомление о третьих сторонах\",\n    \"tutorial\": \"Руководство\",\n    \"tutorial_desc\": \"Подробное руководство по настройке и использованию\",\n    \"view_license\": \"Просмотр полной лицензии\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Виртуальный дисплей\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"Перейти в репозиторий\",\n    \"harmony_modal_desc\": \"Для HarmonyOS NEXT Moonlight найдите Moonlight V+ в магазине HarmonyOS\",\n    \"harmony_modal_link_notice\": \"Эта ссылка перенаправит в репозиторий проекта\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"секунд\",\n    \"restart_desc\": \"Конфигурация сохранена. Sunshine перезапускается для применения настроек дисплея.\",\n    \"restart_go_now\": \"Перейти сейчас\",\n    \"restart_title\": \"Перезапуск Sunshine\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"Базовые настройки активированы. Теперь вы можете сразу начать стриминг с помощью клиента Moonlight!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"Базовый дисплей (VDD) — встроенный интеллектуальный виртуальный дисплей Sunshine Foundation, поддерживающий любое разрешение, частоту кадров и оптимизацию HDR. Идеальный выбор для стриминга с выключенным экраном и стриминга на расширенный дисплей.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Обеспечить активацию\",\n    \"step3_ensure_active_desc\": \"Активирует дисплей, если он ещё не активен\",\n    \"step3_ensure_only_display\": \"Обеспечить единственный дисплей\",\n    \"step3_ensure_only_display_desc\": \"Отключает все другие дисплеи и включает только указанный (рекомендуется)\",\n    \"step3_ensure_primary\": \"Обеспечить основной дисплей\",\n    \"step3_ensure_primary_desc\": \"Активирует дисплей и устанавливает его как основной\",\n    \"step3_ensure_secondary\": \"Вторичный стриминг\",\n    \"step3_ensure_secondary_desc\": \"Использует только виртуальный дисплей для вторичного расширенного стриминга\",\n    \"step3_no_operation\": \"Без действий\",\n    \"step3_no_operation_desc\": \"Без изменений состояния дисплея; пользователь должен сам убедиться, что дисплей готов\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Advanced\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Config Files\",\n    \"general\": \"General\",\n    \"input\": \"Input\",\n    \"network\": \"Network\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Анализ...\",\n    \"ai_analyzing_logs\": \"Анализ журналов, пожалуйста подождите...\",\n    \"ai_config\": \"Конфигурация ИИ\",\n    \"ai_copy_result\": \"Копировать\",\n    \"ai_diagnosis\": \"ИИ-диагностика\",\n    \"ai_diagnosis_title\": \"ИИ-диагностика журналов\",\n    \"ai_error\": \"Анализ не удался\",\n    \"ai_key_local\": \"API-ключ хранится только локально и никогда не загружается\",\n    \"ai_model\": \"Модель\",\n    \"ai_provider\": \"Провайдер\",\n    \"ai_reanalyze\": \"Повторный анализ\",\n    \"ai_result\": \"Результат диагностики\",\n    \"ai_retry\": \"Повторить\",\n    \"ai_start_diagnosis\": \"Начать диагностику\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Если вам нужно немедленно выключить Sunshine, вы можете использовать эту функцию. Обратите внимание, что вам нужно будет вручную запустить его снова после выключения.\",\n    \"boom_sunshine_success\": \"Sunshine выключен\",\n    \"confirm_boom\": \"Действительно хотите выйти?\",\n    \"confirm_boom_desc\": \"Так вы действительно хотите выйти? Ну, я не могу вас остановить, продолжайте и нажмите снова\",\n    \"confirm_logout\": \"Подтвердить выход?\",\n    \"confirm_logout_desc\": \"Для доступа к веб-интерфейсу потребуется снова ввести пароль.\",\n    \"copy_config\": \"Копировать конфигурацию\",\n    \"copy_config_error\": \"Не удалось скопировать конфигурацию\",\n    \"copy_config_success\": \"Конфигурация скопирована в буфер обмена!\",\n    \"copy_logs\": \"Копировать журналы\",\n    \"download_logs\": \"Скачать журналы\",\n    \"force_close\": \"Принудительное закрытие\",\n    \"force_close_desc\": \"Если Moonlight жалуется на запущенное приложение, принудительное закрытие приложения должно помочь.\",\n    \"force_close_error\": \"Ошибка при закрытии приложения\",\n    \"force_close_success\": \"Приложение успешно закрыто!\",\n    \"ignore_case\": \"Игнорировать регистр\",\n    \"logout\": \"Выйти\",\n    \"logout_desc\": \"Выйти. Может потребоваться повторный вход.\",\n    \"logout_localhost_tip\": \"Текущая среда не требует входа; выход не вызовет запрос пароля.\",\n    \"logs\": \"Журналы\",\n    \"logs_desc\": \"Смотреть журналы, выгруженные Sunshine\",\n    \"logs_find\": \"Найти...\",\n    \"match_contains\": \"Содержит\",\n    \"match_exact\": \"Точно\",\n    \"match_regex\": \"Регулярное выражение\",\n    \"reopen_setup_wizard\": \"Повторно открыть мастер настройки\",\n    \"reopen_setup_wizard_desc\": \"Повторно открыть страницу мастера настройки для перенастройки начальных параметров.\",\n    \"reopen_setup_wizard_error\": \"Ошибка при повторном открытии мастера настройки\",\n    \"reset_display_device_desc_windows\": \"Если Sunshine застрял при попытке восстановить изменённые настройки устройства отображения, вы можете сбросить настройки и вручную восстановить состояние дисплея.\\nЭто может произойти по разным причинам: устройство больше не доступно, было подключено к другому порту и т.д.\",\n    \"reset_display_device_error_windows\": \"Ошибка при сбросе persistencji!\",\n    \"reset_display_device_success_windows\": \"Сброс persistencji успешно завершен!\",\n    \"reset_display_device_windows\": \"Сброс памяти дисплея\",\n    \"restart_sunshine\": \"Перезапустить Sunshine\",\n    \"restart_sunshine_desc\": \"Если Sunshine работает некорректно, вы можете попробовать перезапустить его. Это прекратит работу всех запущенных сеансов.\",\n    \"restart_sunshine_success\": \"Sunshine перезапускается\",\n    \"troubleshooting\": \"Устранение проблем\",\n    \"unpair_all\": \"Отвязать все\",\n    \"unpair_all_error\": \"Ошибка при отвязывании\",\n    \"unpair_all_success\": \"Все устройства отвязаны.\",\n    \"unpair_desc\": \"Удалите свои привязанные устройства. Устройства с активным сеансом, отвязанные по одному, останутся подключенными, но не смогут начать или возобновить сеанс.\",\n    \"unpair_single_no_devices\": \"Нет привязанных устройств.\",\n    \"unpair_single_success\": \"Однако, устройство (устройства) может находиться в имеющемся сеансе. Воспользуйтесь кнопкой «Принудительное закрытие» выше для завершения всех сеансов.\",\n    \"unpair_single_unknown\": \"Неизвестный клиент\",\n    \"unpair_title\": \"Отвязать устройства\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Подтвердите пароль\",\n    \"create_creds\": \"Перед началом работы нам нужно создать новые логин и пароль для доступа к веб-интерфейсу.\",\n    \"create_creds_alert\": \"Учетные данные, указанные ниже, необходимы для доступа к веб-интерфейсу Sunshine. Сохраните их в надёжном месте, так как больше вы их не увидите!\",\n    \"creds_local_only\": \"Ваши учётные данные хранятся локально в автономном режиме и никогда не загружаются на сервер.\",\n    \"error\": \"Ошибка!\",\n    \"greeting\": \"Добро пожаловать в Sunshine Foundation!\",\n    \"hide_password\": \"Скрыть пароль\",\n    \"login\": \"Вход\",\n    \"network_error\": \"Ошибка сети, пожалуйста проверьте соединение\",\n    \"password\": \"Пароль\",\n    \"password_match\": \"Пароли совпадают\",\n    \"password_mismatch\": \"Пароли не совпадают\",\n    \"server_error\": \"Ошибка сервера\",\n    \"show_password\": \"Показать пароль\",\n    \"success\": \"Успешно!\",\n    \"username\": \"Имя пользователя\",\n    \"welcome_success\": \"Эта страница скоро перезагрузится и ваш браузер запросит новые учетные данные\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/sv.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Tillämpa\",\n    \"auto\": \"Automatisk\",\n    \"autodetect\": \"Autodetektera (rekommenderas)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"Avbryt\",\n    \"close\": \"Stäng\",\n    \"copied\": \"Kopierat till urklipp\",\n    \"copy\": \"Kopiera\",\n    \"delete\": \"Radera\",\n    \"description\": \"Beskrivning\",\n    \"disabled\": \"Inaktiverad\",\n    \"disabled_def\": \"Inaktiverad (standard)\",\n    \"dismiss\": \"Avfärda\",\n    \"do_cmd\": \"Gör kommando\",\n    \"download\": \"Ladda ner\",\n    \"edit\": \"Redigera\",\n    \"elevated\": \"Förhöjd\",\n    \"enabled\": \"Aktiverad\",\n    \"enabled_def\": \"Aktiverad (standard)\",\n    \"error\": \"Fel!\",\n    \"no_changes\": \"Inga ändringar\",\n    \"note\": \"Notera:\",\n    \"password\": \"Lösenord\",\n    \"remove\": \"Ta bort\",\n    \"run_as\": \"Kör som administratör\",\n    \"save\": \"Spara\",\n    \"see_more\": \"Se mer\",\n    \"success\": \"Klart!\",\n    \"undo_cmd\": \"Ångra kommando\",\n    \"username\": \"Användarnamn\",\n    \"warning\": \"Varning!\"\n  },\n  \"apps\": {\n    \"actions\": \"Åtgärder\",\n    \"add_cmds\": \"Lägg till kommandon\",\n    \"add_new\": \"Lägg till ny\",\n    \"advanced_options\": \"Avancerade alternativ\",\n    \"app_name\": \"Applikationens namn\",\n    \"app_name_desc\": \"Applikationsnamn, som visas på Moonlight\",\n    \"applications_desc\": \"Applikationer uppdateras endast när klienten startas om\",\n    \"applications_title\": \"Applikationer\",\n    \"auto_detach\": \"Fortsätt strömma om programmet avslutas snabbt\",\n    \"auto_detach_desc\": \"Detta kommer att försöka att automatiskt upptäcka launcher-typ appar som stänger snabbt efter att ha startat ett annat program eller instans av sig själva. När en app med launcher-typ upptäcks behandlas den som en fristående app.\",\n    \"basic_info\": \"Grundläggande information\",\n    \"cmd\": \"Kommando\",\n    \"cmd_desc\": \"Huvudansökan att starta. Om den är tom, kommer ingen ansökan att startas.\",\n    \"cmd_examples_title\": \"Vanliga exempel:\",\n    \"cmd_note\": \"Om sökvägen till kommandot exekverbart innehåller mellanslag, måste du bifoga det i citattecken.\",\n    \"cmd_prep_desc\": \"En lista över kommandon som ska köras före/efter detta program. Om något av prep-kommandona misslyckas, starta programmet avbryts.\",\n    \"cmd_prep_name\": \"Kommando förberedelser\",\n    \"command_settings\": \"Kommandoinställningar\",\n    \"covers_found\": \"Hittade omslag\",\n    \"delete\": \"Radera\",\n    \"delete_confirm\": \"Är du säker på att du vill radera \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Fristående kommandon\",\n    \"detached_cmds_add\": \"Lägg till fristående kommando\",\n    \"detached_cmds_desc\": \"En lista över kommandon som ska köras i bakgrunden.\",\n    \"detached_cmds_note\": \"Om sökvägen till kommandot exekverbart innehåller mellanslag, måste du bifoga det i citattecken.\",\n    \"detached_cmds_remove\": \"Ta bort fristående kommando\",\n    \"edit\": \"Redigera\",\n    \"env_app_id\": \"App ID\",\n    \"env_app_name\": \"Appens namn\",\n    \"env_client_audio_config\": \"Ljudkonfigurationen begärd av klienten (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"Klienten har begärt alternativet att optimera spelet för optimal streaming (true/false)\",\n    \"env_client_fps\": \"FPS begärd av klienten (int)\",\n    \"env_client_gcmap\": \"Den begärda gamepad masken, i bitset/bitfield format (int)\",\n    \"env_client_hdr\": \"HDR är aktiverat av klienten (true/false)\",\n    \"env_client_height\": \"Höjd som begärts av kunden (int)\",\n    \"env_client_host_audio\": \"Kunden har begärt värdljud (true/false)\",\n    \"env_client_name\": \"Klientens vänliga namn (sträng)\",\n    \"env_client_width\": \"Bredden begärd av klienten (int)\",\n    \"env_displayplacer_example\": \"Exempel - displayplacer för upplösning Automation:\",\n    \"env_qres_example\": \"Exempel - QRes för upplösning Automation:\",\n    \"env_qres_path\": \"qres sökväg\",\n    \"env_var_name\": \"Var namn\",\n    \"env_vars_about\": \"Om miljövariabler\",\n    \"env_vars_desc\": \"Alla kommandon får dessa miljövariabler som standard:\",\n    \"env_xrandr_example\": \"Exempel - Xrandr för upplösning Automation:\",\n    \"exit_timeout\": \"Avsluta Timeout\",\n    \"exit_timeout_desc\": \"Antal sekunder att vänta på att alla app-processer ska avslutas graciöst när det krävs för att avsluta. Om du inte har angett detta är standardvärdet att vänta upp till 5 sekunder. Om satt till noll eller ett negativt värde kommer appen att avslutas omedelbart.\",\n    \"file_selector_not_initialized\": \"Filväljare inte initialiserad\",\n    \"find_cover\": \"Hitta omslag\",\n    \"form_invalid\": \"Vänligen kontrollera obligatoriska fält\",\n    \"form_valid\": \"Giltig applikation\",\n    \"global_prep_desc\": \"Aktivera/Inaktivera exekvering av globala prep kommandon för denna applikation.\",\n    \"global_prep_name\": \"Globala prep kommandon\",\n    \"image\": \"Bild\",\n    \"image_desc\": \"Applikations ikon/bild/sökväg som kommer att skickas till klienten. Bilden måste vara en PNG-fil. Om den inte är inställd, kommer Sunshine att skicka standardrutans bild.\",\n    \"image_settings\": \"Bildinställningar\",\n    \"loading\": \"Laddar...\",\n    \"menu_cmd_actions\": \"Åtgärder\",\n    \"menu_cmd_add\": \"Lägg till menykommando\",\n    \"menu_cmd_command\": \"Kommando\",\n    \"menu_cmd_desc\": \"Efter konfiguration kommer dessa kommandon att vara synliga i klientens returmeny, vilket möjliggör snabb körning av specifika operationer utan att avbryta strömmen, till exempel att starta hjälpprogram.\\nExempel: Visningsnamn - Stäng av din dator; Kommando - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Visningsnamn\",\n    \"menu_cmd_drag_sort\": \"Dra för att sortera\",\n    \"menu_cmd_name\": \"Menykommandon\",\n    \"menu_cmd_placeholder_command\": \"Kommando\",\n    \"menu_cmd_placeholder_display_name\": \"Visningsnamn\",\n    \"menu_cmd_placeholder_execute\": \"Kör kommando\",\n    \"menu_cmd_placeholder_undo\": \"Ångra kommando\",\n    \"menu_cmd_remove_menu\": \"Ta bort menykommando\",\n    \"menu_cmd_remove_prep\": \"Ta bort förberedelseskommando\",\n    \"mouse_mode\": \"Musläge\",\n    \"mouse_mode_auto\": \"Auto (Global inställning)\",\n    \"mouse_mode_desc\": \"Välj musinmatningsmetod för denna applikation. Auto använder den globala inställningen, Virtuell mus använder HID-drivrutinen, SendInput använder Windows API.\",\n    \"mouse_mode_sendinput\": \"SendInput (Windows API)\",\n    \"mouse_mode_vmouse\": \"Virtuell mus\",\n    \"name\": \"Namn\",\n    \"output_desc\": \"Filen där kommandots utdata lagras, om den inte är angiven, så ignoreras utdata\",\n    \"output_name\": \"Utdata\",\n    \"run_as_desc\": \"Detta kan vara nödvändigt för vissa program som kräver administratörsbehörighet för att köras korrekt.\",\n    \"scan_result_add_all\": \"Lägg till alla\",\n    \"scan_result_edit_title\": \"Lägg till och redigera\",\n    \"scan_result_filter_all\": \"Alla\",\n    \"scan_result_filter_epic_title\": \"Epic Games-spel\",\n    \"scan_result_filter_executable\": \"Körbar\",\n    \"scan_result_filter_executable_title\": \"Körbar fil\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxy-spel\",\n    \"scan_result_filter_script\": \"Skript\",\n    \"scan_result_filter_script_title\": \"Batch/Kommandoskript\",\n    \"scan_result_filter_shortcut\": \"Genväg\",\n    \"scan_result_filter_shortcut_title\": \"Genväg\",\n    \"scan_result_filter_steam_title\": \"Steam-spel\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Spel\",\n    \"scan_result_games_only\": \"Endast spel\",\n    \"scan_result_matched\": \"Matchade: {count}\",\n    \"scan_result_no_apps\": \"Inga applikationer hittades att lägga till\",\n    \"scan_result_no_matches\": \"Inga matchande applikationer hittades\",\n    \"scan_result_quick_add_title\": \"Snabb tillägg\",\n    \"scan_result_remove_title\": \"Ta bort från listan\",\n    \"scan_result_search_placeholder\": \"Sök applikationsnamn, kommando eller sökväg...\",\n    \"scan_result_show_all\": \"Visa alla\",\n    \"scan_result_title\": \"Skanresultat\",\n    \"scan_result_try_different_keywords\": \"Försök använda olika söknyckelord\",\n    \"scan_result_type_batch\": \"Batch\",\n    \"scan_result_type_command\": \"Kommandoskript\",\n    \"scan_result_type_executable\": \"Körbar fil\",\n    \"scan_result_type_shortcut\": \"Genväg\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Sök applikationer...\",\n    \"select\": \"Välj\",\n    \"test_menu_cmd\": \"Testa kommando\",\n    \"test_menu_cmd_empty\": \"Kommandot kan inte vara tomt\",\n    \"test_menu_cmd_executing\": \"Kör kommando...\",\n    \"test_menu_cmd_failed\": \"Kommandokörning misslyckades\",\n    \"test_menu_cmd_success\": \"Kommandot kördes framgångsrikt!\",\n    \"use_desktop_image\": \"Använd aktuell skrivbordsbakgrund\",\n    \"wait_all\": \"Fortsätt strömma tills alla appprocesser avslutas\",\n    \"wait_all_desc\": \"Detta fortsätter strömningen tills alla processer som startats av appen har avslutats. När den avmarkeras kommer strömningen att sluta när den initiala appprocessen avslutas, även om andra appprocesser fortfarande är igång.\",\n    \"working_dir\": \"Arbetar katalog\",\n    \"working_dir_desc\": \"Den arbetskatalog som ska skickas till processen. Till exempel använder vissa program arbetskatalogen för att söka efter konfigurationsfiler. Om inte anges, kommer Sunshine standard till den överordnade katalogen i kommandot\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Adapter namn\",\n    \"adapter_name_desc_linux_1\": \"Ange manuellt en GPU som ska användas för att fånga.\",\n    \"adapter_name_desc_linux_2\": \"att hitta alla enheter som kan VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Ersätt ``renderD129`` med enheten ovanifrån för att lista enhetens namn och egenskaper. För att få stöd av Sunshine, måste det ha på minimum:\",\n    \"adapter_name_desc_windows\": \"Ange manuellt en GPU som ska användas för att fånga. Om du vill avbryta, väljs GPU automatiskt. Vi rekommenderar starkt att du lämnar det här fältet tomt för att använda automatiskt GPU-val! Obs: Denna GPU måste ha en display ansluten och påslagen. Du hittar lämpliga värden med hjälp av följande kommando:\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Om den senaste versionen av den virtuella bildskärmen är installerad kan den automatiskt associeras med GPU-bindningen\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Lägg till\",\n    \"address_family\": \"Adress Familj\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Ställ in adressfamiljen som används av Sunshine\",\n    \"address_family_ipv4\": \"Endast IPv4\",\n    \"always_send_scancodes\": \"Skicka alltid sökkoder\",\n    \"always_send_scancodes_desc\": \"Att skicka skanningskoder förbättrar kompatibiliteten med spel och appar men kan resultera i felaktig tangentbordsinmatning från vissa klienter som inte använder en amerikansk engelsk tangentbordslayout. Aktivera om tangentbordsinmatningen inte fungerar alls i vissa program. Inaktivera om nycklar på klienten genererar fel indata på värden.\",\n    \"amd_coder\": \"AMF-kod (H264)\",\n    \"amd_coder_desc\": \"Låter dig välja entropi-kodning för att prioritera kvalitet eller kodningshastighet. H.264 endast.\",\n    \"amd_enforce_hrd\": \"AMF Hypotetisk referensavkodare (HRD) verkställighet\",\n    \"amd_enforce_hrd_desc\": \"Ökar begränsningarna för hastighetskontroll för att uppfylla kraven i HRD-modellen. Detta minskar kraftigt bithastighetsöverflöden, men kan orsaka kodning artefakter eller minskad kvalitet på vissa kort.\",\n    \"amd_preanalysis\": \"AMF Föranalys\",\n    \"amd_preanalysis_desc\": \"Detta möjliggör föranalys av hastighetskontroll, vilket kan öka kvaliteten på bekostnad av ökad kodningstid.\",\n    \"amd_quality\": \"AMF Kvalitet\",\n    \"amd_quality_balanced\": \"balanced -- balanserad (standard)\",\n    \"amd_quality_desc\": \"Detta styr balansen mellan kodningshastighet och kvalitet.\",\n    \"amd_quality_group\": \"AMF Kvalitetsinställningar\",\n    \"amd_quality_quality\": \"kvalitet – föredra kvalitet\",\n    \"amd_quality_speed\": \"speed -- föredra hastighet\",\n    \"amd_qvbr_quality\": \"AMF QVBR-kvalitetsnivå\",\n    \"amd_qvbr_quality_desc\": \"Kvalitetsnivå för QVBR-hastighetskontrollläge. Intervall: 1-51 (lägre = bättre kvalitet). Standard: 23. Gäller bara när hastighetskontrollen är inställd på 'qvbr'.\",\n    \"amd_rc\": \"AMF Rate kontroll\",\n    \"amd_rc_cbr\": \"cbr – konstant bithastighet\",\n    \"amd_rc_cqp\": \"cqp – konstant qp-läge\",\n    \"amd_rc_desc\": \"Detta styr metoden för att säkerställa att vi inte överskrider klientens bithastighetsmål. 'cqp' är inte lämplig för bitrate targeting, och andra alternativ förutom 'vbr_latency' beror på HRD Enforcement för att begränsa bitrate overflows.\",\n    \"amd_rc_group\": \"Inställningar för AMF Rate\",\n    \"amd_rc_hqcbr\": \"hqcbr -- högkvalitativ konstant bitrate\",\n    \"amd_rc_hqvbr\": \"hqvbr -- högkvalitativ variabel bitrate\",\n    \"amd_rc_qvbr\": \"qvbr -- kvalitetsvariabel bitrate (använder QVBR-kvalitetsnivå)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency – fördröjningsbegränsad variabelbithastighet (standard)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak – peak constrained variabelbithastighet\",\n    \"amd_usage\": \"AMF användning\",\n    \"amd_usage_desc\": \"Detta ställer in grundkodningsprofilen. Alla alternativ som presenteras nedan kommer att åsidosätta en delmängd av användarprofilen, men det finns ytterligare dolda inställningar som inte kan konfigureras någon annanstans.\",\n    \"amd_usage_lowlatency\": \"låg latens - låg latens (snabb)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - låg latens, hög kvalitet (snabb)\",\n    \"amd_usage_transcoding\": \"transcoding – Omkodning (långsammare)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatens - extremt låg latens (snabbast)\",\n    \"amd_usage_webcam\": \"webcam – webbkamera (långsam)\",\n    \"amd_vbaq\": \"AMF Variansbaserad adaptiv kvantisering (VBAQ)\",\n    \"amd_vbaq_desc\": \"Det mänskliga visuella systemet är typiskt mindre känsligt för artefakter i mycket texturerade områden. I VBAQ läge används pixelvarians för att indikera komplexiteten i rumsliga texturer, vilket gör att kodaren kan allokera fler bitar till jämnare områden. Att aktivera denna funktion leder till förbättringar i subjektiv visuell kvalitet med lite innehåll.\",\n    \"amf_draw_mouse_cursor\": \"Rita en enkel markör vid användning av AMF-fångstmetoden\",\n    \"amf_draw_mouse_cursor_desc\": \"I vissa fall visar inte AMF-fångst muspekaren. Att aktivera detta alternativ ritar en enkel muspekare på skärmen. Obs: Muspekarpositionen uppdateras endast när innehållsskärmen uppdateras, så i icke-spel-scenarier som på skrivbordet kan du observera långsam muspekarrörelse.\",\n    \"apply_note\": \"Klicka på \\\"Tillämpa\\\" för att starta om solsken och tillämpa ändringar. Detta kommer att avsluta alla pågående sessioner.\",\n    \"audio_sink\": \"Ljud Sink\",\n    \"audio_sink_desc_linux\": \"Namnet på ljuddiskbänken som används för Audio Loopback. Om du inte anger denna variabel, kommer pulseaudio att välja standardövervakningsenheten. Du kan hitta namnet på audiosänkan med hjälp av antingen kommandot:\",\n    \"audio_sink_desc_macos\": \"Namnet på ljuddiskbänken som används för Audio Loopback. Solsken kan bara komma åt mikrofoner på macOS på grund av systembegränsningar. För att strömma systemljud med Soundflower eller BlackHole.\",\n    \"audio_sink_desc_windows\": \"Ange manuellt en specifik ljudenhet som ska fångas upp. Om enheten avaktiveras väljs enheten automatiskt. Vi rekommenderar starkt att lämna detta fält tomt för att använda automatiskt val av enhet! Om du har flera ljudenheter med identiska namn, kan du få enhets-ID med följande kommando:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Högtalare (High Definition Audio Device)\",\n    \"av1_mode\": \"AV1 Support\",\n    \"av1_mode_0\": \"Sunshine kommer att annonsera stöd för AV1 baserat på kodarfunktioner (rekommenderas)\",\n    \"av1_mode_1\": \"Solsken kommer inte att annonsera stöd för AV1\",\n    \"av1_mode_2\": \"Sunshine kommer att annonsera stöd för AV1 Main 8-bitars profil\",\n    \"av1_mode_3\": \"Sunshine kommer att annonsera stöd för AV1 Main 8-bitars och 10-bitars (HDR) profiler\",\n    \"av1_mode_desc\": \"Tillåter klienten att begära AV1 Main 8-bitars eller 10-bitars videoströmmar. AV1 är mer CPU-intensiv att koda, så att detta kan minska prestandan vid användning av programkodning.\",\n    \"back_button_timeout\": \"Hem/Guide Knapp Emulation Timeout\",\n    \"back_button_timeout_desc\": \"Om knappen Bakåt/Select hålls ned för det angivna antalet millisekunder, emuleras en Hem/Guide knapptryckning. Om satt till ett värde < 0 (standard) kommer inte knappen Hem/Guide att efterliknas knappen.\",\n    \"bind_address\": \"Bindningsadress (testfunktion)\",\n    \"bind_address_desc\": \"Ange den specifika IP-adressen som Sunshine ska binda till. Om tomt, binder Sunshine till alla tillgängliga adresser.\",\n    \"capture\": \"Tvinga specifik fångstmetod\",\n    \"capture_desc\": \"I autoläge använder Sunshine det första fungerande alternativet. NvFBC kräver patchade NVIDIA-drivrutiner.\",\n    \"capture_target\": \"Fångstmål\",\n    \"capture_target_desc\": \"Välj vilken typ av mål som ska fångas. När du väljer 'Fönster' kan du fångar ett specifikt programfönster (t.ex. AI-bildinterpoleringsprogram) istället för hela skärmen.\",\n    \"capture_target_display\": \"Skärm\",\n    \"capture_target_window\": \"Fönster\",\n    \"cert\": \"Certifikat\",\n    \"cert_desc\": \"Certifikatet används för webb UI och Moonlight klient parning. För bästa kompatibilitet bör detta ha en RSA-2048 publik nyckel.\",\n    \"channels\": \"Maximalt antal anslutna klienter\",\n    \"channels_desc_1\": \"Sunshine kan tillåta en enda streaming-session att delas med flera klienter samtidigt.\",\n    \"channels_desc_2\": \"Vissa hårdvarukodare kan ha begränsningar som minskar prestandan med flera strömmar.\",\n    \"close_verify_safe\": \"Säker verifiering kompatibel med gamla klienter\",\n    \"close_verify_safe_desc\": \"Gamla klienter kan inte ansluta till Sunshine, vänligen stäng av detta alternativ eller uppdatera klienten\",\n    \"coder_cabac\": \"cabac – kontext adaptiv binär aritmetisk kodning - högre kvalitet\",\n    \"coder_cavlc\": \"cavlc – sammanhangsberoende kodning med variabellängd - snabbare avkodning\",\n    \"configuration\": \"Konfiguration\",\n    \"controller\": \"Enable Gamepad Input\",\n    \"controller_desc\": \"Tillåter gästerna att styra värdsystemet med en gamepad / controller\",\n    \"credentials_file\": \"Filen för inloggningsuppgifter\",\n    \"credentials_file_desc\": \"Lagra användarnamn/lösenord separat från Sunshines statusfil.\",\n    \"display_device_options_note_desc_windows\": \"Windows sparar olika skärminställningar för varje kombination av för närvarande aktiva skärmar.\\nSunshine tillämpar sedan ändringar på en skärm (eller skärmar) som tillhör en sådan skärmkombination.\\nOm du kopplar bort en enhet som var aktiv när Sunshine tillämpade inställningarna, kan ändringarna inte återställas om inte kombinationen kan aktiveras igen när Sunshine försöker återställa ändringarna!\",\n    \"display_device_options_note_windows\": \"Kommentar om hur inställningar tillämpas\",\n    \"display_device_options_windows\": \"Alternativ för bildskärmsenhet\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Aktiverar skärmen om den inte redan är aktiv\",\n    \"display_device_prep_ensure_active_windows\": \"Aktivera skärmen automatiskt\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Inaktiverar alla andra skärmar och aktiverar bara den angivna skärmen\",\n    \"display_device_prep_ensure_only_display_windows\": \"Inaktivera andra skärmar och aktivera endast den angivna skärmen\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Aktiverar skärmen och ställer in den som primär skärm\",\n    \"display_device_prep_ensure_primary_windows\": \"Aktivera skärmen automatiskt och gör den till en primär skärm\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Använder bara den virtuella skärmen för sekundär utökad streaming\",\n    \"display_device_prep_ensure_secondary_windows\": \"Sekundär skärmstreaming (endast virtuell skärm)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Inga ändringar av skärmstatus; användaren måste själv se till att skärmen är redo\",\n    \"display_device_prep_no_operation_windows\": \"Inaktiverad\",\n    \"display_device_prep_windows\": \"Skärmförberedelse\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Minst ett \\\"mottaget\\\" och ett \\\"slutligt\\\" värde måste anges.\\nTomt fält i avsnittet \\\"mottaget\\\" betyder \\\"matcha alla\\\". Tomt fält i avsnittet \\\"slutligt\\\" betyder \\\"behåll mottaget värde\\\".\\nDu kan matcha specifikt FPS-värde till specifik upplösning om du vill...\\n\\nObs: om alternativet \\\"Optimera spelinställningar\\\" inte är aktiverat på Moonlight-klienten ignoreras raderna som innehåller upplösningsvärde(n).\",\n    \"display_mode_remapping_desc_windows\": \"Ange hur en specifik upplösning och/eller uppdateringsfrekvens ska mappas om till andra värden.\\nDu kan strömma med lägre upplösning medan du renderar med högre upplösning på värden för en översamplingseffekt.\\nEller så kan du strömma med högre FPS medan du begränsar värden till den lägre uppdateringsfrekvensen.\\nMatchning utförs uppifrån och ner. När posten matchas kontrolleras inte andra längre, men valideras fortfarande.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Slutlig uppdateringsfrekvens\",\n    \"display_mode_remapping_final_resolution_windows\": \"Slutlig upplösning\",\n    \"display_mode_remapping_optional\": \"valfritt\",\n    \"display_mode_remapping_received_fps_windows\": \"Mottagen FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"Mottagen upplösning\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Obs: om alternativet \\\"Optimera spelinställningar\\\" inte är aktiverat på Moonlight-klienten är ommappningen inaktiverad.\",\n    \"display_mode_remapping_windows\": \"Mappa om skärmlägen\",\n    \"display_modes\": \"Skärmlägen\",\n    \"ds4_back_as_touchpad_click\": \"Karta bakåt/välj att Touchpad Klicka\",\n    \"ds4_back_as_touchpad_click_desc\": \"När DS4-emulering tvingas, kartlägg Tillbaka/välj att Touchpad Klicka\",\n    \"dsu_server_port\": \"DSU-serverport\",\n    \"dsu_server_port_desc\": \"DSU-serverns lyssningsport (standard 26760). Sunshine kommer att fungera som en DSU-server för att ta emot klientanslutningar och skicka rörelsedata. Aktivera DSU-servern i din klient (Yuzu, Ryujinx, etc.) och ställ in DSU-serveradressen (127.0.0.1) och porten (26760).\",\n    \"enable_dsu_server\": \"Aktivera DSU-server\",\n    \"enable_dsu_server_desc\": \"Aktivera DSU-server för att ta emot klientanslutningar och skicka rörelsedata\",\n    \"encoder\": \"Tvinga en specifik kodare\",\n    \"encoder_desc\": \"Tvinga en specifik kodare, annars kommer Sunshine att välja det bästa tillgängliga alternativet. Obs: Om du anger en hårdvarukodare i Windows, måste den matcha GPU där skärmen är ansluten.\",\n    \"encoder_software\": \"Programvara\",\n    \"experimental\": \"Experimentell\",\n    \"experimental_features\": \"Experimentella funktioner\",\n    \"external_ip\": \"Extern IP\",\n    \"external_ip_desc\": \"Om ingen extern IP-adress anges, kommer Sunshine automatiskt upptäcka extern IP\",\n    \"fec_percentage\": \"FEC Procent\",\n    \"fec_percentage_desc\": \"Procent av felkorrigering av paket per datapaket i varje videoram. Högre värden kan korrigera för mer förlust av nätverkspaket, men på bekostnad av ökad bandbreddsanvändning.\",\n    \"ffmpeg_auto\": \"auto – låt ffmpeg bestämma (standard)\",\n    \"file_apps\": \"Appar Fil\",\n    \"file_apps_desc\": \"Filen där aktuella appar från Sunshine lagras.\",\n    \"file_state\": \"Status fil\",\n    \"file_state_desc\": \"Filen där nuvarande tillstånd av solsken lagras\",\n    \"fps\": \"Annonserad FPS\",\n    \"gamepad\": \"Emulerad speltyp\",\n    \"gamepad_auto\": \"Automatiska val\",\n    \"gamepad_desc\": \"Välj vilken typ av gamepad som ska emuleras på värden\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 Manual Options\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Manuella DS4-alternativ\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Kommando förberedelser\",\n    \"global_prep_cmd_desc\": \"Konfigurera en lista över kommandon som ska köras före eller efter att du kört något program. Om något av de angivna förberedelsekommandona misslyckas, kommer programstartprocessen att avbrytas.\",\n    \"hdr_luminance_analysis\": \"Dynamiska HDR-metadata (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Aktiverar per-frame GPU-luminansanalys och injicerar HDR10+ (ST 2094-40) och HDR Vivid (CUVA) dynamiska metadata i den kodade bitströmmen. Ger per-frame tonmappningsledtrådar för stödda skärmar. Lägger till liten GPU-overhead (~0,5-1,5ms/frame vid höga upplösningar). Inaktivera vid FPS-fall med HDR aktiverat.\",\n    \"hdr_prep_automatic_windows\": \"Slå på/av HDR-läge som begärts av klienten\",\n    \"hdr_prep_no_operation_windows\": \"Inaktiverad\",\n    \"hdr_prep_windows\": \"HDR-tillståndsändring\",\n    \"hevc_mode\": \"Stöd för HEVC\",\n    \"hevc_mode_0\": \"Sunshine kommer att annonsera stöd för HEVC baserat på kodarfunktioner (rekommenderas)\",\n    \"hevc_mode_1\": \"Solsken kommer inte att annonsera stöd för HEVC\",\n    \"hevc_mode_2\": \"Sunshine kommer att annonsera stöd för HEVC Main profil\",\n    \"hevc_mode_3\": \"Sunshine kommer att annonsera stöd för HEVC Main och Main10 (HDR) profiler\",\n    \"hevc_mode_desc\": \"Tillåter kunden att begära HEVC Main eller HEVC Main10 videoströmmar. HEVC är mer CPU-intensiv att koda, så att detta kan minska prestandan vid användning av programkodning.\",\n    \"high_resolution_scrolling\": \"Stöd för högupplöst rullning\",\n    \"high_resolution_scrolling_desc\": \"När den är aktiverad kommer Sunshine att passera genom högupplösta scroll-händelser från Moonlight klienter. Detta kan vara användbart för att inaktivera för äldre program som bläddrar för snabbt med högupplösta scroll-händelser.\",\n    \"install_steam_audio_drivers\": \"Installera Steam Audio-drivrutiner\",\n    \"install_steam_audio_drivers_desc\": \"Om Steam är installerat kommer detta automatiskt installera drivrutinen Steam Streaming Speakers för att stödja 5.1/7.1 surroundljud och ljuddämpning av värdljud.\",\n    \"key_repeat_delay\": \"Nyckel upprepas fördröjning\",\n    \"key_repeat_delay_desc\": \"Kontrollera hur snabba tangenter kommer att upprepa sig. Den initiala fördröjningen i millisekunder innan du upprepar nycklar.\",\n    \"key_repeat_frequency\": \"Nyckel Upprepa Frekvens\",\n    \"key_repeat_frequency_desc\": \"Hur ofta nycklar upprepa varje sekund. Detta konfigurerbara alternativ stöder decimaler.\",\n    \"key_rightalt_to_key_win\": \"Mappa höger Alt-tangent till Windows-tangent\",\n    \"key_rightalt_to_key_win_desc\": \"Det kan vara möjligt att du inte kan skicka Windows-tangenten från Moonlight direkt. I dessa fall kan det vara användbart att göra Sunshine tror att rätt Alt nyckel är Windows-tangenten\",\n    \"key_rightalt_to_key_windows\": \"Bind höger Alt tangent till Windows-tangenten\",\n    \"keyboard\": \"Aktivera tangentbordsinmatning\",\n    \"keyboard_desc\": \"Tillåter gäster att styra värdsystemet med tangentbordet\",\n    \"lan_encryption_mode\": \"LAN-krypteringsläge\",\n    \"lan_encryption_mode_1\": \"Aktiverad för stödda klienter\",\n    \"lan_encryption_mode_2\": \"Krävs för alla kunder\",\n    \"lan_encryption_mode_desc\": \"Detta avgör när kryptering kommer att användas vid strömning över ditt lokala nätverk. Kryptering kan minska strömningsprestanda, särskilt på mindre kraftfulla värdar och klienter.\",\n    \"locale\": \"Lokalt\",\n    \"locale_desc\": \"Lokalen som används för Sunshines användargränssnitt.\",\n    \"log_level\": \"Loggnivå\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Debug\",\n    \"log_level_2\": \"Information\",\n    \"log_level_3\": \"Varning\",\n    \"log_level_4\": \"Fel\",\n    \"log_level_5\": \"Fatal\",\n    \"log_level_6\": \"Ingen\",\n    \"log_level_desc\": \"Minsta loggnivå utskriven till standard ut\",\n    \"log_path\": \"Sökväg till loggfil\",\n    \"log_path_desc\": \"Filen där de aktuella loggarna av Sunshine lagras.\",\n    \"max_bitrate\": \"Maximal bithastighet\",\n    \"max_bitrate_desc\": \"Maximal bithastighet (i Kbps) som Sunshine kommer att koda strömmen på. Om satt till 0, kommer den alltid att använda den bithastighet som Moonlight begärt.\",\n    \"max_fps_reached\": \"Maximala FPS-värden nådda\",\n    \"max_resolutions_reached\": \"Maximalt antal upplösningar nått\",\n    \"mdns_broadcast\": \"Hitta denna dator i lokala nätverk\",\n    \"mdns_broadcast_desc\": \"Om detta alternativ är aktiverat kommer Sunshine att tillåta andra enheter att hitta denna dator automatiskt. Moonlight måste också vara inställd på att hitta denna dator automatiskt i lokala nätverk.\",\n    \"min_threads\": \"Minsta antal CPU-trådar\",\n    \"min_threads_desc\": \"Öka värdet något minskar kodningseffektivitet, men avvägningen är oftast värt det för att få användning av fler CPU-kärnor för kodning. Det ideala värdet är det lägsta värdet som tillförlitligt kan koda på dina önskade strömningsinställningar på din hårdvara.\",\n    \"minimum_fps_target\": \"Minsta FPS-mål\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Diverse alternativ\",\n    \"motion_as_ds4\": \"Emulera en DS4 gamepad om klienten gamepad rapporterar rörelsesensorer finns\",\n    \"motion_as_ds4_desc\": \"Om inaktiverad kommer rörelsesensorer inte att beaktas under val av speltyp.\",\n    \"mouse\": \"Aktivera musinmatning\",\n    \"mouse_desc\": \"Tillåter gäster att styra värdsystemet med musen\",\n    \"native_pen_touch\": \"Stöd för infödda pen/tryck\",\n    \"native_pen_touch_desc\": \"När aktiverad, kommer Sunshine att passera genom infödda penn/touch händelser från Moonlight klienter. Detta kan vara användbart för att inaktivera för äldre applikationer utan inbyggt penn/touch stöd.\",\n    \"no_fps\": \"Inga FPS-värden tillagda\",\n    \"no_resolutions\": \"Inga upplösningar tillagda\",\n    \"notify_pre_releases\": \"PreRelease Notiser\",\n    \"notify_pre_releases_desc\": \"Huruvida meddelas om nya förhandsversioner av Sunshine\",\n    \"nvenc_h264_cavlc\": \"Föredrar CAVLC över CABAC i H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Enklare form av entropi-kodning. CAVLC behöver cirka 10% mer bithastighet för samma kvalitet. Endast relevant för riktigt gamla avkodningsenheter.\",\n    \"nvenc_latency_over_power\": \"Föredrar lägre kodningsfördröjning över energibesparing\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine begär maximal GPU klockfrekvens medan strömning för att minska kodning latens. Inaktivera det rekommenderas inte eftersom detta kan leda till avsevärt ökad kodning latens.\",\n    \"nvenc_lookahead_depth\": \"Lookahead-djup\",\n    \"nvenc_lookahead_depth_desc\": \"Antal bildrutor att titta framåt under kodning (0-32). Lookahead förbättrar kodningskvaliteten, särskilt i komplexa scener, genom att ge bättre rörelseuppskattning och bitrate-fördelning. Högre värden förbättrar kvaliteten men ökar kodningsfördröjningen. Ställ in på 0 för att inaktivera lookahead. Kräver NVENC SDK 13.0 (1202) eller nyare.\",\n    \"nvenc_lookahead_level\": \"Lookahead-nivå\",\n    \"nvenc_lookahead_level_0\": \"Nivå 0 (lägsta kvalitet, snabbast)\",\n    \"nvenc_lookahead_level_1\": \"Nivå 1\",\n    \"nvenc_lookahead_level_2\": \"Nivå 2\",\n    \"nvenc_lookahead_level_3\": \"Nivå 3 (högsta kvalitet, långsammast)\",\n    \"nvenc_lookahead_level_autoselect\": \"Välj automatiskt (låt drivrutinen välja optimal nivå)\",\n    \"nvenc_lookahead_level_desc\": \"Kvalitetsnivå för lookahead. Högre nivåer förbättrar kvaliteten på bekostnad av prestanda. Detta alternativ träder endast i kraft när lookahead_depth är större än 0. Kräver NVENC SDK 13.0 (1202) eller nyare.\",\n    \"nvenc_lookahead_level_disabled\": \"Inaktiverad (samma som nivå 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Presentera OpenGL/Vulkan ovanpå DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine kan inte fånga fullskärm OpenGL och Vulkan program med full bildhastighet såvida de inte presenterar ovanpå DXGI. Detta är hela systemet inställning som återförs på solsken program utgången.\",\n    \"nvenc_preset\": \"Prestanda förinställning\",\n    \"nvenc_preset_1\": \"(snabb, standard)\",\n    \"nvenc_preset_7\": \"(långsammare)\",\n    \"nvenc_preset_desc\": \"Högre tal förbättrar kompression (kvalitet vid given bitrate) på bekostnad av ökad kodning latens. Rekommenderas att ändras endast när begränsad av nätverk eller avkodare, annars liknande effekt kan åstadkommas genom att öka bithastigheten.\",\n    \"nvenc_rate_control\": \"Kontrolläge för bithastighet\",\n    \"nvenc_rate_control_cbr\": \"CBR (Konstant bithastighet) - Låg latens\",\n    \"nvenc_rate_control_desc\": \"Välj läge för bithastighetskontroll. CBR (Konstant bithastighet) ger fast bithastighet för lågfördröjningsströmning. VBR (Variabel bithastighet) tillåter bithastigheten att variera baserat på scenkomplexitet, vilket ger bättre kvalitet för komplexa scener på bekostnad av variabel bithastighet.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Variabel bithastighet) - Bättre kvalitet\",\n    \"nvenc_realtime_hags\": \"Använd realtid prioritet i hårdvaruaccelererad gpu-schemaläggning\",\n    \"nvenc_realtime_hags_desc\": \"För närvarande NVIDIA-drivrutiner kan frysa i kodare när HAGS är aktiverad, realtid prioritet används och VRAM-användning är nära maximal. Inaktivering av det här alternativet sänker prioriteringen till höga, sidostäder frysningen på bekostnad av reducerad fångstprestanda när GPU är kraftigt belastad.\",\n    \"nvenc_spatial_aq\": \"Spatial AQ\",\n    \"nvenc_spatial_aq_desc\": \"Tilldela högre QP-värden till platta regioner i videon. Rekommenderas att aktivera vid strömning med lägre bithastigheter.\",\n    \"nvenc_spatial_aq_disabled\": \"Inaktiverad (snabbare, standard)\",\n    \"nvenc_spatial_aq_enabled\": \"Aktiverad (långsammare)\",\n    \"nvenc_split_encode\": \"Delad bildrutekodning\",\n    \"nvenc_split_encode_desc\": \"Dela kodningen av varje videoram över flera NVENC-hårdvaruenheter. Minskar kodningsfördröjningen avsevärt med en marginell komprimeringseffektivitetsstraff. Detta alternativ ignoreras om din GPU har en enda NVENC-enhet.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Drivrutinen bestämmer (standard)\",\n    \"nvenc_split_encode_four_strips\": \"Tvinga 4-remsor uppdelning (kräver 4+ NVENC-motorer)\",\n    \"nvenc_split_encode_three_strips\": \"Tvinga 3-remsor uppdelning (kräver 3+ NVENC-motorer)\",\n    \"nvenc_split_encode_two_strips\": \"Tvinga 2-remsor uppdelning (kräver 2+ NVENC-motorer)\",\n    \"nvenc_target_quality\": \"Målkvalitet (VBR-läge)\",\n    \"nvenc_target_quality_desc\": \"Målkvalitetsnivå för VBR-läge (0-51 för H.264/HEVC, 0-63 för AV1). Lägre värden = högre kvalitet. Ställ in på 0 för automatiskt kvalitetsval. Används endast när kontrolläget för bithastighet är VBR.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptiv kvantisering\",\n    \"nvenc_temporal_aq_desc\": \"Aktivera tidsanpassad kvantisering. Temporal AQ optimerar kvantisering över tid, vilket ger bättre bithastighetsfördelning och förbättrad kvalitet i rörelsescener. Den här funktionen fungerar tillsammans med spatial AQ och kräver lookahead för att vara aktiverat (lookahead_depth > 0). Kräver NVENC SDK 13.0 (1202) eller nyare.\",\n    \"nvenc_temporal_filter\": \"Tidsfilter\",\n    \"nvenc_temporal_filter_4\": \"Nivå 4 (maximal styrka)\",\n    \"nvenc_temporal_filter_desc\": \"Styrkan på tidsfiltreringen som tillämpas före kodning. Tidsfilter minskar brus och förbättrar komprimeringseffektiviteten, särskilt för naturligt innehåll. Högre nivåer ger bättre brusreducering men kan ge en liten suddighet. Kräver NVENC SDK 13.0 (1202) eller nyare. Obs: Kräver frameIntervalP >= 5, ej kompatibel med zeroReorderDelay eller stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Inaktiverad (ingen tidsfiltrering)\",\n    \"nvenc_twopass\": \"Tvåpass-läge\",\n    \"nvenc_twopass_desc\": \"Lägger till preliminär kodning pass. Detta gör det möjligt att upptäcka fler rörelsevektorer, bättre distribuera bithastighet över ramen och mer strikt följa bitrate gränser. Inaktivera det rekommenderas inte eftersom detta kan leda till enstaka bithastighet overshoot och efterföljande paketförlust.\",\n    \"nvenc_twopass_disabled\": \"Inaktiverad (snabbast rekommenderas inte)\",\n    \"nvenc_twopass_full_res\": \"Full upplösning (långsammare)\",\n    \"nvenc_twopass_quarter_res\": \"Kvartalsupplösning (snabbare, standard)\",\n    \"nvenc_vbv_increase\": \"Ensidig VBV/HRD-procentökning\",\n    \"nvenc_vbv_increase_desc\": \"Som standard solsken använder en enda ram VBV/HRD, vilket innebär att alla kodade video ramstorlek inte förväntas överstiga begärda bithastighet dividerat med begärda bildhastighet. Avslappning av denna begränsning kan vara fördelaktigt och fungera som låg latens variabel bitrate, men kan också leda till paketförlust om nätverket inte har buffertheadroom för att hantera bitrate spikar. Maximalt accepterat värde är 400, vilket motsvarar 5x ökad kodad video ram övre storleksgräns.\",\n    \"origin_web_ui_allowed\": \"Ursprung webbgränssnitt tillåtet\",\n    \"origin_web_ui_allowed_desc\": \"Ursprunget till fjärradressen som inte nekas åtkomst till Web UI\",\n    \"origin_web_ui_allowed_lan\": \"Endast de i LAN kan komma åt Web UI\",\n    \"origin_web_ui_allowed_pc\": \"Endast localhost kan komma åt webbgränssnitt\",\n    \"origin_web_ui_allowed_wan\": \"Vem som helst kan komma åt webbgränssnitt\",\n    \"output_name_desc_unix\": \"Under Sunshine start, bör du se listan över upptäckta visningar. Obs: Du måste använda id-värdet inuti parentesen.\",\n    \"output_name_desc_windows\": \"Ange manuellt en display som ska användas för att fånga. Om den avaktiveras fångas den primära displayen. Obs: Om du angav en GPU ovan måste denna display vara ansluten till den GPU. De lämpliga värdena kan hittas med följande kommando:\",\n    \"output_name_unix\": \"Visa nummer\",\n    \"output_name_windows\": \"Utdatanamn\",\n    \"ping_timeout\": \"Ping Timeout\",\n    \"ping_timeout_desc\": \"Hur lång tid att vänta i millisekunder för data från månsken innan du stänger av strömmen\",\n    \"pkey\": \"Privat nyckel\",\n    \"pkey_desc\": \"Den privata nyckeln som används för webb UI och Moonlight klient parning. För bästa kompatibilitet bör detta vara en RSA-2048 privat nyckel.\",\n    \"port\": \"Port\",\n    \"port_alert_1\": \"Solsken kan inte använda portar under 1024!\",\n    \"port_alert_2\": \"Hamnar över 65535 är inte tillgängliga!\",\n    \"port_desc\": \"Ställ in familjen av hamnar som används av Sunshine\",\n    \"port_http_port_note\": \"Använd denna port för att ansluta med Moonlight.\",\n    \"port_note\": \"Anteckning\",\n    \"port_port\": \"Port\",\n    \"port_protocol\": \"Protocol\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Exponera Web UI till internet är en säkerhetsrisk! Fortsätt på egen risk!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Kvantifieringsparameter\",\n    \"qp_desc\": \"Vissa enheter kanske inte stöder konstant Bit Rate. För dessa enheter, QP används istället. Högre värde innebär mer komprimering, men mindre kvalitet.\",\n    \"qsv_coder\": \"QuickSync kodare (H264)\",\n    \"qsv_preset\": \"QuickSync Preset\",\n    \"qsv_preset_fast\": \"snabbare (lägre kvalitet)\",\n    \"qsv_preset_faster\": \"snabbaste (lägsta kvalitet)\",\n    \"qsv_preset_medium\": \"medium (standard)\",\n    \"qsv_preset_slow\": \"långsam (god kvalitet)\",\n    \"qsv_preset_slower\": \"långsammare (bättre kvalitet)\",\n    \"qsv_preset_slowest\": \"långsammaste (bästa kvalitet)\",\n    \"qsv_preset_veryfast\": \"snabbaste (lägsta kvalitet)\",\n    \"qsv_slow_hevc\": \"Tillåt långsam HEVC-kodning\",\n    \"qsv_slow_hevc_desc\": \"Detta kan aktivera HEVC-kodning på äldre Intel GPU:er, på bekostnad av högre GPU-användning och sämre prestanda.\",\n    \"refresh_rate_change_automatic_windows\": \"Använd FPS-värde som tillhandahålls av klienten\",\n    \"refresh_rate_change_manual_desc_windows\": \"Ange uppdateringsfrekvensen som ska användas\",\n    \"refresh_rate_change_manual_windows\": \"Använd manuellt angiven uppdateringsfrekvens\",\n    \"refresh_rate_change_no_operation_windows\": \"Inaktiverad\",\n    \"refresh_rate_change_windows\": \"FPS-ändring\",\n    \"res_fps_desc\": \"Skärmlägen annonserade av Sunshine. Vissa versioner av Moonlight, som Moonlight-nx (Switch), förlitar sig på dessa listor för att säkerställa att de begärda upplösningarna och fps stöds. Denna inställning ändrar inte hur skärmströmmen skickas till Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Använd upplösning som tillhandahålls av klienten\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimera spelinställningar\\\"-alternativet måste vara aktiverat på Moonlight-klienten för att detta ska fungera.\",\n    \"resolution_change_manual_windows\": \"Använd manuellt angiven upplösning\",\n    \"resolution_change_no_operation_windows\": \"Inaktiverad\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimera spelinställningar\\\"-alternativet måste vara aktiverat på Moonlight-klienten för att detta ska fungera.\",\n    \"resolution_change_windows\": \"Upplösningsändring\",\n    \"resolutions\": \"Annonserade Upplösningar\",\n    \"restart_note\": \"Solsken börjar om för att tillämpa förändringar.\",\n    \"sleep_mode\": \"Viloläge\",\n    \"sleep_mode_away\": \"Frånvaroläge (Skärm av, omedelbar väckning)\",\n    \"sleep_mode_desc\": \"Styr vad som händer när klienten skickar ett vilokommando. Strömsparläge (S3): traditionellt viloläge, låg strömförbrukning men kräver WOL för väckning. Viloläge (S4): sparar till disk, mycket låg strömförbrukning. Frånvaroläge: skärmen stängs av men systemet fortsätter köra för omedelbar väckning - perfekt för spelströmningsservrar.\",\n    \"sleep_mode_hibernate\": \"Viloläge (S4)\",\n    \"sleep_mode_suspend\": \"Strömsparläge (S3)\",\n    \"stream_audio\": \"Aktivera ljudströmning\",\n    \"stream_audio_desc\": \"Inaktivera detta alternativ för att stoppa ljudströmningen.\",\n    \"stream_mic\": \"Aktivera mikrofonströmning\",\n    \"stream_mic_desc\": \"Inaktivera detta alternativ för att stoppa mikrofonströmningen.\",\n    \"stream_mic_download_btn\": \"Ladda ner virtuell mikrofon\",\n    \"stream_mic_download_confirm\": \"Du kommer att omdirigeras till nedladdningssidan för virtuell mikrofon. Fortsätta?\",\n    \"stream_mic_note\": \"Denna funktion kräver installation av en virtuell mikrofon\",\n    \"sunshine_name\": \"Solsken Namn\",\n    \"sunshine_name_desc\": \"Namnet som visas av Moonlight. Om det inte anges används datorns värdnamn\",\n    \"sw_preset\": \"SW förinställningar\",\n    \"sw_preset_desc\": \"Optimera avvägningen mellan kodningshastighet (kodade bildrutor per sekund) och komprimeringseffektivitet (kvalitet per bit i bitström). Standard är superfast.\",\n    \"sw_preset_fast\": \"snabb\",\n    \"sw_preset_faster\": \"snabbare\",\n    \"sw_preset_medium\": \"medium\",\n    \"sw_preset_slow\": \"långsam\",\n    \"sw_preset_slower\": \"långsammare\",\n    \"sw_preset_superfast\": \"superfast (standard)\",\n    \"sw_preset_ultrafast\": \"ultrasnabb\",\n    \"sw_preset_veryfast\": \"veryfast\",\n    \"sw_preset_veryslow\": \"veryslow\",\n    \"sw_tune\": \"SW Tune\",\n    \"sw_tune_animation\": \"animation – bra för karikatyrer; använder högre deblockering och fler referensramar\",\n    \"sw_tune_desc\": \"Tuning alternativ, som tillämpas efter förinställningen. Standardvärdet är noll.\",\n    \"sw_tune_fastdecode\": \"fastdecode – tillåter snabbare avkodning genom att inaktivera vissa filter\",\n    \"sw_tune_film\": \"film – användning för högkvalitativt filminnehåll; minskar avblockering\",\n    \"sw_tune_grain\": \"korn - bevarar kornstrukturen i gammalt, kornigt filmmaterial\",\n    \"sw_tune_stillimage\": \"stillimage – bra för bildspel liknande innehåll\",\n    \"sw_tune_zerolatency\": \"zerolatency – bra för snabb kodning och strömning med låg fördröjning (standard)\",\n    \"system_tray\": \"Aktivera systemfält\",\n    \"system_tray_desc\": \"Om systemfältet ska aktiveras. Om aktiverat kommer Sunshine att visa en ikon i systemfältet och kan styras från systemfältet.\",\n    \"touchpad_as_ds4\": \"Emulera en DS4 gamepad om klienten gamepad rapporterar en pekplatta är närvarande\",\n    \"touchpad_as_ds4_desc\": \"Om inaktiverad, kommer närvaro av pekplatta inte att beaktas under val av speltyp.\",\n    \"unsaved_changes_tooltip\": \"Du har osparade ändringar. Klicka för att spara.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Konfigurera portvidarebefordran automatiskt för streaming över Internet\",\n    \"variable_refresh_rate\": \"Variabel uppdateringsfrekvens (VRR)\",\n    \"variable_refresh_rate_desc\": \"Tillåt videoströmmens bildfrekvens att matcha renderingsbildfrekvensen för VRR-stöd. När aktiverat sker kodning endast när nya bildrutor är tillgängliga, vilket gör att strömmen kan följa den faktiska renderingsbildfrekvensen.\",\n    \"vdd_reuse_desc_windows\": \"När aktiverat delar alla klienter samma VDD (Virtual Display Device). När inaktiverat (standard) får varje klient sin egen VDD. Aktivera detta för snabbare klientbyte, men observera att alla klienter delar samma skärminställningar.\",\n    \"vdd_reuse_windows\": \"Återanvänd samma VDD för alla klienter\",\n    \"virtual_display\": \"Virtuell display\",\n    \"virtual_mouse\": \"Virtuell musdrivrutin\",\n    \"virtual_mouse_desc\": \"När aktiverad använder Sunshine Zako Virtual Mouse-drivrutinen (om installerad) för att simulera musinmatning på HID-nivå. Tillåter spel med Raw Input att ta emot mushändelser. När inaktiverad eller drivrutin ej installerad, återgår till SendInput.\",\n    \"virtual_sink\": \"Virtuell sink\",\n    \"virtual_sink_desc\": \"Ange manuellt en virtuell ljudenhet som ska användas. Om enheten inte är inställd väljs enheten automatiskt. Vi rekommenderar starkt att lämna detta fält tomt för att använda automatiskt val av enhet!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Installera den virtuella musdrivrutinen?\",\n    \"vmouse_confirm_uninstall\": \"Avinstallera den virtuella musdrivrutinen?\",\n    \"vmouse_install\": \"Installera drivrutin\",\n    \"vmouse_installing\": \"Installerar...\",\n    \"vmouse_note\": \"Den virtuella musdrivrutinen kräver separat installation. Använd Sunshine-kontrollpanelen för att installera eller hantera drivrutinen.\",\n    \"vmouse_refresh\": \"Uppdatera status\",\n    \"vmouse_status_installed\": \"Installerad (ej aktiv)\",\n    \"vmouse_status_not_installed\": \"Ej installerad\",\n    \"vmouse_status_running\": \"Körs\",\n    \"vmouse_uninstall\": \"Avinstallera drivrutin\",\n    \"vmouse_uninstalling\": \"Avinstallerar...\",\n    \"vt_coder\": \"VideoToolbox Coder\",\n    \"vt_realtime\": \"VideoToolbox Kodning i realtid\",\n    \"vt_software\": \"VideoToolbox programvara kodning\",\n    \"vt_software_allowed\": \"Tillåten\",\n    \"vt_software_forced\": \"Tvingad\",\n    \"wan_encryption_mode\": \"WAN krypteringsläge\",\n    \"wan_encryption_mode_1\": \"Aktiverad för stödda klienter (standard)\",\n    \"wan_encryption_mode_2\": \"Krävs för alla kunder\",\n    \"wan_encryption_mode_desc\": \"Detta avgör när kryptering kommer att användas vid strömning över Internet. Kryptering kan minska strömningsprestanda, särskilt på mindre kraftfulla värdar och klienter.\",\n    \"webhook_curl_command\": \"Kommando\",\n    \"webhook_curl_command_desc\": \"Kopiera följande kommando till din terminal för att testa om webhooken fungerar korrekt:\",\n    \"webhook_curl_copy_failed\": \"Kopiering misslyckades, välj och kopiera manuellt\",\n    \"webhook_enabled\": \"Webhook-meddelanden\",\n    \"webhook_enabled_desc\": \"När aktiverat kommer Sunshine att skicka händelsemeddelanden till den angivna Webhook-URL:en\",\n    \"webhook_group\": \"Webhook-meddelandeinställningar\",\n    \"webhook_skip_ssl_verify\": \"Hoppa över SSL-certifikatverifiering\",\n    \"webhook_skip_ssl_verify_desc\": \"Hoppa över SSL-certifikatverifiering för HTTPS-anslutningar, endast för testning eller självsignerade certifikat\",\n    \"webhook_test\": \"Testa\",\n    \"webhook_test_failed\": \"Webhook-test misslyckades\",\n    \"webhook_test_failed_note\": \"Notera: Kontrollera om URL:en är korrekt eller kontrollera webbläsarkonsolen för mer information.\",\n    \"webhook_test_success\": \"Webhook-test lyckades!\",\n    \"webhook_test_success_cors_note\": \"Notera: På grund av CORS-begränsningar kan serverresponsstatusen inte bekräftas.\\nBegäran har skickats. Om webhooken är korrekt konfigurerad bör meddelandet ha levererats.\\n\\nFörslag: Kontrollera fliken Nätverk i webbläsarens utvecklarverktyg för begärandedetaljer.\",\n    \"webhook_test_url_required\": \"Vänligen ange Webhook-URL först\",\n    \"webhook_timeout\": \"Förfrågan Timeout\",\n    \"webhook_timeout_desc\": \"Timeout för webhook-förfrågningar i millisekunder, intervall 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"URL:en för att ta emot händelsemeddelanden, stöder HTTP/HTTPS-protokoll\",\n    \"wgc_checking_mode\": \"Kontrollerar läge...\",\n    \"wgc_checking_running_mode\": \"Kontrollerar körläge...\",\n    \"wgc_control_panel_only\": \"Denna funktion är endast tillgänglig i Sunshine Kontrollpanel\",\n    \"wgc_mode_switch_failed\": \"Misslyckades med att byta läge\",\n    \"wgc_mode_switch_started\": \"Lägesbyte initierat. Om en UAC-prompt visas, klicka på 'Ja' för att bekräfta.\",\n    \"wgc_service_mode_warning\": \"WGC-fångst kräver körning i användarläge. Om du för närvarande kör i serviceläge, klicka på knappen ovan för att byta till användarläge.\",\n    \"wgc_switch_to_service_mode\": \"Byt till Serviceläge\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Kör för närvarande i användarläge. Klicka för att byta till serviceläge.\",\n    \"wgc_switch_to_user_mode\": \"Byt till Användarläge\",\n    \"wgc_switch_to_user_mode_tooltip\": \"WGC-fångst kräver körning i användarläge. Klicka på denna knapp för att byta till användarläge.\",\n    \"wgc_user_mode_available\": \"Kör för närvarande i användarläge. WGC-fångst är tillgängligt.\",\n    \"window_title\": \"Fönstertitel\",\n    \"window_title_desc\": \"Titeln på fönstret som ska fångas (partiell matchning, skiftlägesokänslig). Om tomt, kommer det nuvarande körande applikationsnamnet att användas automatiskt.\",\n    \"window_title_placeholder\": \"t.ex. Applikationsnamn\"\n  },\n  \"index\": {\n    \"description\": \"Solsken är en själv-värd spel ström värd för Moonlight.\",\n    \"download\": \"Hämta\",\n    \"installed_version_not_stable\": \"Du kör en förhandsversion av Sunshine. Du kan uppleva buggar eller andra problem. Vänligen rapportera eventuella problem du stöter. Tack för att du hjälper till att göra Sunshine till en bättre programvara!\",\n    \"loading_latest\": \"Laddar senaste utgåvan...\",\n    \"new_pre_release\": \"En ny version av förhandsversionen är tillgänglig!\",\n    \"new_stable\": \"En ny stabil version är tillgänglig!\",\n    \"startup_errors\": \"<b>Uppmärksamhet!</b> Solsken upptäckte dessa fel vid uppstart. Vi <b>STARKLIG KUNSKAP</b> fixar dem innan vi streamar.\",\n    \"update_download_confirm\": \"Du håller på att öppna uppdateringsnedladdningssidan i webbläsaren. Fortsätta?\",\n    \"version_dirty\": \"Tack för att du hjälper till att göra Sunshine till en bättre programvara!\",\n    \"version_latest\": \"Du kör den senaste versionen av Sunshine\",\n    \"view_logs\": \"Visa loggar\",\n    \"welcome\": \"Hej, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Applikationer\",\n    \"configuration\": \"Konfiguration\",\n    \"home\": \"Hem\",\n    \"password\": \"Ändra lösenord\",\n    \"pin\": \"Fäst\",\n    \"theme_auto\": \"Automatiskt\",\n    \"theme_dark\": \"Mörk\",\n    \"theme_light\": \"Ljus\",\n    \"toggle_theme\": \"Tema\",\n    \"troubleshoot\": \"Felsökning\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Bekräfta lösenord\",\n    \"current_creds\": \"Nuvarande uppgifter\",\n    \"new_creds\": \"Nya inloggningsuppgifter\",\n    \"new_username_desc\": \"Om det inte anges kommer användarnamnet inte att ändras\",\n    \"password_change\": \"Ändra lösenord\",\n    \"success_msg\": \"Lösenordet har ändrats! Den här sidan kommer att laddas om snart, din webbläsare kommer att be dig om de nya uppgifterna.\"\n  },\n  \"pin\": {\n    \"actions\": \"Åtgärder\",\n    \"cancel_editing\": \"Avbryt redigering\",\n    \"client_name\": \"Namn\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Bekräfta radering\",\n    \"delete_client\": \"Radera klient\",\n    \"delete_confirm_message\": \"Är du säker på att du vill radera <strong>{name}</strong>?\",\n    \"delete_warning\": \"Denna åtgärd kan inte ångras.\",\n    \"device_name\": \"Enhetens namn\",\n    \"device_size\": \"Enhetsstorlek\",\n    \"device_size_info\": \"<strong>Enhetsstorlek</strong>: Ställ in skärmstorlekstypen för klientenheten (Liten - Telefon, Medium - Surfplatta, Stor - TV) för att optimera strömningsupplevelsen och pekoperationer.\",\n    \"device_size_large\": \"Stor - TV\",\n    \"device_size_medium\": \"Medium - Surfplatta\",\n    \"device_size_small\": \"Liten - Telefon\",\n    \"edit_client_settings\": \"Redigera klientinställningar\",\n    \"hdr_profile\": \"HDR-profil\",\n    \"hdr_profile_info\": \"<strong>HDR-profil</strong>: Välj HDR-färgprofilen (ICC-fil) som används för denna klient för att säkerställa att HDR-innehåll visas korrekt på enheten. Om du använder den senaste klienten, som stöder automatisk synkronisering av ljusstyrkainformation till värdens virtuella skärm, lämna detta fält tomt för att aktivera automatisk synkronisering.\",\n    \"loading\": \"Laddar...\",\n    \"loading_clients\": \"Laddar klienter...\",\n    \"modify_in_gui\": \"Vänligen ändra i grafiska gränssnittet\",\n    \"none\": \"-- Ingen --\",\n    \"or_manual_pin\": \"eller ange PIN manuellt\",\n    \"pair_failure\": \"Parkoppling misslyckades: Kontrollera om PIN-koden är korrekt skriven\",\n    \"pair_success\": \"Klart! Kontrollera Moonlight för att fortsätta\",\n    \"pin_pairing\": \"PIN Pairing\",\n    \"qr_expires_in\": \"Går ut om\",\n    \"qr_generate\": \"Generera QR-kod\",\n    \"qr_paired_success\": \"Parkoppling lyckades!\",\n    \"qr_pairing\": \"QR-kodsparkoppling\",\n    \"qr_pairing_desc\": \"Generera en QR-kod för snabb parkoppling. Skanna den med Moonlight-klienten för att parkoppla automatiskt.\",\n    \"qr_pairing_warning\": \"Experimentell funktion. Om parkopplingen misslyckas, använd manuell PIN-parkoppling nedan. Obs: Denna funktion fungerar bara på LAN.\",\n    \"qr_refresh\": \"Uppdatera QR-kod\",\n    \"remove_paired_devices_desc\": \"Ta bort dina parade enheter.\",\n    \"save_changes\": \"Spara ändringar\",\n    \"save_failed\": \"Kunde inte spara klientinställningar. Vänligen försök igen.\",\n    \"save_or_cancel_first\": \"Vänligen spara eller avbryt redigeringen först\",\n    \"send\": \"Skicka\",\n    \"unknown_client\": \"Okänd klient\",\n    \"unpair_all_confirm\": \"Är du säker på att du vill koppla bort alla klienter? Denna åtgärd kan inte ångras.\",\n    \"unsaved_changes\": \"Osparade ändringar\",\n    \"warning_msg\": \"Se till att du har tillgång till klienten du parar ihop med. Denna programvara kan ge total kontroll till din dator, så var försiktig!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android rekommenderas\",\n    \"client_downloads\": \"Klientnedladdningar\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"GitHub Discussions\",\n    \"gpl_license_text_1\": \"Denna programvara är licensierad under GPL-3.0. Du är fri att använda, ändra och distribuera den.\",\n    \"gpl_license_text_2\": \"För att skydda ekosystemet med öppen källkod, undvik att använda programvara som bryter mot GPL-3.0-licensen.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Gå med i gemenskapen\",\n    \"join_group_desc\": \"Få hjälp och dela erfarenheter\",\n    \"legal\": \"Juridisk\",\n    \"legal_desc\": \"Genom att fortsätta använda denna programvara godkänner du villkoren i följande dokument.\",\n    \"license\": \"Licens\",\n    \"lizardbyte_website\": \"Webbplats LizardByte\",\n    \"official_website\": \"Officiell webbplats\",\n    \"official_website_title\": \"AlkaidLab - Officiell webbplats\",\n    \"open_source\": \"Öppen källkod\",\n    \"open_source_desc\": \"Star & Fork för att stödja projektet\",\n    \"quick_start\": \"Snabbstart\",\n    \"resources\": \"Resurser\",\n    \"resources_desc\": \"Resurser för Sunshine!\",\n    \"third_party_desc\": \"Tredjepartskomponentmeddelanden\",\n    \"third_party_moonlight\": \"Vänliga länkar\",\n    \"third_party_notice\": \"Meddelande från tredje part\",\n    \"tutorial\": \"Handledning\",\n    \"tutorial_desc\": \"Detaljerad konfigurations- och användarguide\",\n    \"view_license\": \"Visa fullständig licens\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Konfigurationssammanfattning\",\n    \"android_client\": \"Android-klient\",\n    \"base_display_title\": \"Virtuell skärm\",\n    \"choose_adapter\": \"Automatisk\",\n    \"config_saved\": \"Konfigurationen har sparats framgångsrikt.\",\n    \"description\": \"Låt oss komma igång med en snabb installation\",\n    \"device_id\": \"Enhets-ID\",\n    \"device_state\": \"Tillstånd\",\n    \"download_clients\": \"Ladda ner klienter\",\n    \"finish\": \"Slutför installationen\",\n    \"go_to_apps\": \"Konfigurera applikationer\",\n    \"harmony_goto_repo\": \"Gå till arkivet\",\n    \"harmony_modal_desc\": \"För HarmonyOS NEXT Moonlight, sök efter Moonlight V+ i HarmonyOS App Store\",\n    \"harmony_modal_link_notice\": \"Denna länk omdirigerar till projektarkivet\",\n    \"ios_client\": \"iOS-klient\",\n    \"load_error\": \"Det gick inte att ladda konfigurationen\",\n    \"next\": \"Nästa\",\n    \"physical_display\": \"Fysisk bildskärm/EDID-emulator\",\n    \"physical_display_desc\": \"Strömma dina faktiska fysiska bildskärmar\",\n    \"previous\": \"Föregående\",\n    \"restart_countdown_unit\": \"sekunder\",\n    \"restart_desc\": \"Konfigurationen sparad. Sunshine startar om för att tillämpa visningsinställningarna.\",\n    \"restart_go_now\": \"Gå nu\",\n    \"restart_title\": \"Startar om Sunshine\",\n    \"save_error\": \"Det gick inte att spara konfigurationen\",\n    \"select_adapter\": \"Grafikadapter\",\n    \"selected_adapter\": \"Vald adapter\",\n    \"selected_display\": \"Vald skärm\",\n    \"setup_complete\": \"Installation slutförd!\",\n    \"setup_complete_desc\": \"Grundinställningarna är nu aktiva. Du kan börja strömma med en Moonlight-klient direkt!\",\n    \"skip\": \"Hoppa över installationsguiden\",\n    \"skip_confirm\": \"Är du säker på att du vill hoppa över installationsguiden? Du kan konfigurera dessa alternativ senare på inställningssidan.\",\n    \"skip_confirm_title\": \"Hoppa över installationsguiden\",\n    \"skip_error\": \"Det gick inte att hoppa över\",\n    \"state_active\": \"Aktiv\",\n    \"state_inactive\": \"Inaktiv\",\n    \"state_primary\": \"Primär\",\n    \"state_unknown\": \"Okänd\",\n    \"step0_description\": \"Välj ditt gränssnittsspråk\",\n    \"step0_title\": \"Språk\",\n    \"step1_description\": \"Välj skärmen att strömma\",\n    \"step1_title\": \"Skärmval\",\n    \"step1_vdd_intro\": \"Basdisplayen (VDD) är Sunshine Foundations inbyggda smarta virtuella display som stöder valfri upplösning, bildfrekvens och HDR-optimering. Det är det föredragna valet för streaming med avstängd skärm och utökad display-streaming.\",\n    \"step2_description\": \"Välj din grafikadapter\",\n    \"step2_title\": \"Välj adapter\",\n    \"step3_description\": \"Välj förberedelsestrategi för bildskärmsenhet\",\n    \"step3_ensure_active\": \"Säkerställ aktivering\",\n    \"step3_ensure_active_desc\": \"Aktiverar skärmen om den inte redan är aktiv\",\n    \"step3_ensure_only_display\": \"Säkerställ enda skärm\",\n    \"step3_ensure_only_display_desc\": \"Inaktiverar alla andra skärmar och aktiverar bara den angivna skärmen (rekommenderas)\",\n    \"step3_ensure_primary\": \"Säkerställ primär skärm\",\n    \"step3_ensure_primary_desc\": \"Aktiverar skärmen och ställer in den som primär skärm\",\n    \"step3_ensure_secondary\": \"Sekundär streaming\",\n    \"step3_ensure_secondary_desc\": \"Använder bara den virtuella skärmen för sekundär utökad streaming\",\n    \"step3_no_operation\": \"Ingen åtgärd\",\n    \"step3_no_operation_desc\": \"Inga ändringar av skärmstatus; användaren måste själv se till att skärmen är redo\",\n    \"step3_title\": \"Skärmstrategi\",\n    \"step4_title\": \"Slutför\",\n    \"stream_mode\": \"Strömningsläge\",\n    \"unknown_display\": \"Okänd skärm\",\n    \"virtual_display\": \"Virtuell skärm (ZakoHDR)\",\n    \"virtual_display_desc\": \"Strömma med en virtuell bildskärmsenhet (kräver installation av ZakoVDD-drivrutin)\",\n    \"welcome\": \"Välkommen till Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Avancerat\",\n    \"amd\": \"AMD AMF-kodare\",\n    \"av\": \"Ljud/Video\",\n    \"encoders\": \"Kodare\",\n    \"files\": \"Konfigurationsfiler\",\n    \"general\": \"Allmänt\",\n    \"input\": \"Ingång\",\n    \"network\": \"Nätverk\",\n    \"nv\": \"NVIDIA NVENC-kodare\",\n    \"qsv\": \"Intel QuickSync-kodare\",\n    \"sw\": \"Programvarukodare\",\n    \"vaapi\": \"VAAPI-kodare\",\n    \"vt\": \"VideoToolbox-kodare\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analyserar...\",\n    \"ai_analyzing_logs\": \"Analyserar loggar, vänta...\",\n    \"ai_config\": \"AI-konfiguration\",\n    \"ai_copy_result\": \"Kopiera\",\n    \"ai_diagnosis\": \"AI-diagnos\",\n    \"ai_diagnosis_title\": \"AI-loggdiagnos\",\n    \"ai_error\": \"Analys misslyckades\",\n    \"ai_key_local\": \"API-nyckeln lagras endast lokalt och laddas aldrig upp\",\n    \"ai_model\": \"Modell\",\n    \"ai_provider\": \"Leverantör\",\n    \"ai_reanalyze\": \"Analysera igen\",\n    \"ai_result\": \"Diagnosresultat\",\n    \"ai_retry\": \"Försök igen\",\n    \"ai_start_diagnosis\": \"Starta diagnos\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Om du behöver stänga av Sunshine omedelbart kan du använda denna funktion. Observera att du behöver starta om det manuellt efter avstängning.\",\n    \"boom_sunshine_success\": \"Sunshine har stängts av\",\n    \"confirm_boom\": \"Verkligen vill avsluta?\",\n    \"confirm_boom_desc\": \"Så du vill verkligen avsluta? Nåväl, jag kan inte stoppa dig, fortsätt och klicka igen\",\n    \"confirm_logout\": \"Bekräfta utloggning?\",\n    \"confirm_logout_desc\": \"Du måste ange ditt lösenord igen för att få åtkomst till webbgränssnittet.\",\n    \"copy_config\": \"Kopiera konfiguration\",\n    \"copy_config_error\": \"Kunde inte kopiera konfiguration\",\n    \"copy_config_success\": \"Konfiguration kopierad till urklipp!\",\n    \"copy_logs\": \"Kopiera loggar\",\n    \"download_logs\": \"Ladda ner loggar\",\n    \"force_close\": \"Tvinga stängning\",\n    \"force_close_desc\": \"Om Moonlight klagar på att en app för närvarande är igång måste appen stängas åtgärdas.\",\n    \"force_close_error\": \"Fel vid stängning av program\",\n    \"force_close_success\": \"Ansökan Stängt Framgång!\",\n    \"ignore_case\": \"Ignorera skiftläge\",\n    \"logout\": \"Logga ut\",\n    \"logout_desc\": \"Logga ut. Du kan behöva logga in igen.\",\n    \"logout_localhost_tip\": \"Aktuell miljö kräver ingen inloggning; utloggning utlöser inte lösenordsfrågan.\",\n    \"logs\": \"Loggar\",\n    \"logs_desc\": \"Se stockarna som laddats upp av Sunshine\",\n    \"logs_find\": \"Sök...\",\n    \"match_contains\": \"Innehåller\",\n    \"match_exact\": \"Exakt\",\n    \"match_regex\": \"Regex\",\n    \"reopen_setup_wizard\": \"Öppna installationsguiden igen\",\n    \"reopen_setup_wizard_desc\": \"Öppna installationsguidens sida igen för att konfigurera om de initiala inställningarna.\",\n    \"reopen_setup_wizard_error\": \"Fel vid återöppning av installationsguiden\",\n    \"reset_display_device_desc_windows\": \"Om Sunshine fastnar när den försöker återställa de ändrade bildskärmsinställningarna kan du återställa inställningarna och fortsätta att återställa skärmläget manuellt.\\nDetta kan hända av olika anledningar: enheten är inte längre tillgänglig, har anslutits till en annan port osv.\",\n    \"reset_display_device_error_windows\": \"Fel vid återställning av persistencji!\",\n    \"reset_display_device_success_windows\": \"Återställning av persistencji lyckades!\",\n    \"reset_display_device_windows\": \"Återställ displayminne\",\n    \"restart_sunshine\": \"Starta om solsken\",\n    \"restart_sunshine_desc\": \"Om solsken inte fungerar som den ska, kan du prova att starta om den. Detta avslutar alla pågående sessioner.\",\n    \"restart_sunshine_success\": \"Solsken startar om\",\n    \"troubleshooting\": \"Felsökning\",\n    \"unpair_all\": \"Ta bort alla\",\n    \"unpair_all_error\": \"Fel vid avkoppling\",\n    \"unpair_all_success\": \"Opara ihop framgångsrikt!\",\n    \"unpair_desc\": \"Ta bort dina parkopplade enheter. Enskilda okopplade enheter med en aktiv session förblir anslutna, men kan inte starta eller återuppta en session.\",\n    \"unpair_single_no_devices\": \"Det finns inga parkopplade enheter.\",\n    \"unpair_single_success\": \"Enheten (er) kan dock fortfarande vara i en aktiv session. Använd knappen \\\"Tvinga nära\\\" ovan för att avsluta alla öppna sessioner.\",\n    \"unpair_single_unknown\": \"Okänd klient\",\n    \"unpair_title\": \"Ta bort enheter\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Bekräfta lösenord\",\n    \"create_creds\": \"Innan du kommer igång behöver du göra ett nytt användarnamn och lösenord för att komma åt webbgränssnittet.\",\n    \"create_creds_alert\": \"Användaruppgifterna nedan behövs för att komma åt Sunshines webbgränssnitt. Håll dem säkra, eftersom du aldrig kommer att se dem igen!\",\n    \"creds_local_only\": \"Dina autentiseringsuppgifter lagras lokalt offline och laddas aldrig upp till någon server.\",\n    \"error\": \"Fel!\",\n    \"greeting\": \"Välkommen till Sunshine Foundation!\",\n    \"hide_password\": \"Dölj lösenord\",\n    \"login\": \"Inloggning\",\n    \"network_error\": \"Nätverksfel, kontrollera din anslutning\",\n    \"password\": \"Lösenord\",\n    \"password_match\": \"Lösenorden matchar\",\n    \"password_mismatch\": \"Lösenorden matchar inte\",\n    \"server_error\": \"Serverfel\",\n    \"show_password\": \"Visa lösenord\",\n    \"success\": \"Klart!\",\n    \"username\": \"Användarnamn\",\n    \"welcome_success\": \"Denna sida kommer att laddas om snart, din webbläsare kommer att be dig om nya uppgifter\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/tr.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Uygula\",\n    \"auto\": \"Otomatik\",\n    \"autodetect\": \"Otomatik Algıla (önerilir)\",\n    \"beta\": \"(beta)\",\n    \"cancel\": \"İptal\",\n    \"close\": \"Kapat\",\n    \"copied\": \"Panoya kopyalandı\",\n    \"copy\": \"Kopyala\",\n    \"delete\": \"Sil\",\n    \"description\": \"Açıklama\",\n    \"disabled\": \"Devre Dışı\",\n    \"disabled_def\": \"Devre Dışı (varsayılan)\",\n    \"dismiss\": \"Yoksay\",\n    \"do_cmd\": \"Komut Yap\",\n    \"download\": \"İndir\",\n    \"edit\": \"Düzenle\",\n    \"elevated\": \"Yükseltilmiş\",\n    \"enabled\": \"Etkin\",\n    \"enabled_def\": \"Etkin (varsayılan)\",\n    \"error\": \"Hata!\",\n    \"no_changes\": \"Değişiklik yok\",\n    \"note\": \"Not:\",\n    \"password\": \"Şifre\",\n    \"remove\": \"Kaldır\",\n    \"run_as\": \"Yönetici olarak çalıştır\",\n    \"save\": \"Kaydet\",\n    \"see_more\": \"Daha Fazla Gör\",\n    \"success\": \"Başarılı!\",\n    \"undo_cmd\": \"Geri Al Komutu\",\n    \"username\": \"Kullanıcı Adı\",\n    \"warning\": \"Uyarı!\"\n  },\n  \"apps\": {\n    \"actions\": \"Eylemler\",\n    \"add_cmds\": \"Komutlar Ekle\",\n    \"add_new\": \"Yeni Ekle\",\n    \"advanced_options\": \"Gelişmiş Seçenekler\",\n    \"app_name\": \"Uygulama Adı\",\n    \"app_name_desc\": \"Uygulama Adı, Moonlight'ta gösterildiği gibi\",\n    \"applications_desc\": \"Uygulamalar yalnızca İstemci yeniden başlatıldığında yenilenir\",\n    \"applications_title\": \"Uygulamalar\",\n    \"auto_detach\": \"Uygulama hızlı bir şekilde çıkarsa akışa devam edin\",\n    \"auto_detach_desc\": \"Bu, başka bir programı veya kendi örneğini başlattıktan sonra hızla kapanan başlatıcı tipi uygulamaları otomatik olarak tespit etmeye çalışacaktır. Başlatıcı tipi bir uygulama tespit edildiğinde, bu uygulama ayrılmış bir uygulama olarak değerlendirilir.\",\n    \"basic_info\": \"Temel Bilgiler\",\n    \"cmd\": \"Komut\",\n    \"cmd_desc\": \"Başlatılacak ana uygulama. Boşsa, hiçbir uygulama başlatılmayacaktır.\",\n    \"cmd_examples_title\": \"Yaygın örnekler:\",\n    \"cmd_note\": \"Komut çalıştırılabilir dosyasının yolu boşluk içeriyorsa, tırnak içine almanız gerekir.\",\n    \"cmd_prep_desc\": \"Bu uygulamadan önce/sonra çalıştırılacak komutların bir listesi. Hazırlık komutlarından herhangi biri başarısız olursa, uygulamanın başlatılması iptal edilir.\",\n    \"cmd_prep_name\": \"Komut Hazırlıkları\",\n    \"command_settings\": \"Komut Ayarları\",\n    \"covers_found\": \"Kapaklar Bulundu\",\n    \"delete\": \"Sil\",\n    \"delete_confirm\": \"\\\"{name}\\\" uygulamasını silmek istediğinizden emin misiniz?\",\n    \"detached_cmds\": \"Müstakil Komutlar\",\n    \"detached_cmds_add\": \"Müstakil Komut Ekleme\",\n    \"detached_cmds_desc\": \"Arka planda çalıştırılacak komutların bir listesi.\",\n    \"detached_cmds_note\": \"Komut çalıştırılabilir dosyasının yolu boşluk içeriyorsa, tırnak içine almanız gerekir.\",\n    \"detached_cmds_remove\": \"Müstakil Komutu Kaldır\",\n    \"edit\": \"Düzenle\",\n    \"env_app_id\": \"Uygulama Kimliği\",\n    \"env_app_name\": \"Uygulama Adı\",\n    \"env_client_audio_config\": \"İstemci tarafından talep edilen Ses Yapılandırması (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"İstemci, oyunu optimum akış için optimize etme seçeneğini talep etti (doğru/yanlış)\",\n    \"env_client_fps\": \"İstemci tarafından talep edilen FPS (int)\",\n    \"env_client_gcmap\": \"İstenen kontrolcü maskesi, bit kümesi/bit alanı biçiminde (int)\",\n    \"env_client_hdr\": \"HDR istemci tarafından etkinleştirildi (true/false)\",\n    \"env_client_height\": \"İstemci tarafından talep edilen Yükseklik (int)\",\n    \"env_client_host_audio\": \"İstemci ana bilgisayar sesi talep etti (true/false)\",\n    \"env_client_name\": \"İstemci dostu adı (dize)\",\n    \"env_client_width\": \"İstemci tarafından talep edilen Genişlik (int)\",\n    \"env_displayplacer_example\": \"Örnek - Resolution Automation için displayplacer:\",\n    \"env_qres_example\": \"Örnek - Çözünürlük Otomasyonu için QRes:\",\n    \"env_qres_path\": \"qres yolu\",\n    \"env_var_name\": \"Var Adı\",\n    \"env_vars_about\": \"Ortam Değişkenleri Hakkında\",\n    \"env_vars_desc\": \"Tüm komutlar varsayılan olarak bu ortam değişkenlerini alır:\",\n    \"env_xrandr_example\": \"Örnek - Çözüm Otomasyonu için Xrandr:\",\n    \"exit_timeout\": \"Çıkış Zaman Aşımı\",\n    \"exit_timeout_desc\": \"Uygulama işlemlerinin kapatılma isteği gönderildiğinde zarif bir şekilde çıkmaları için beklenilecek saniye sayısı. Ayarlanmadığı takdirde, varsayılan olarak 5 saniyeye kadar beklenir. Sıfır veya negatif bir değer ayarlandığında, uygulama anında sonlandırılacaktır.\",\n    \"file_selector_not_initialized\": \"Dosya seçici başlatılmadı\",\n    \"find_cover\": \"Kapak Resmi Bul\",\n    \"form_invalid\": \"Lütfen gerekli alanları kontrol edin\",\n    \"form_valid\": \"Geçerli uygulama\",\n    \"global_prep_desc\": \"Bu uygulama için Global Hazırlık Komutlarının yürütülmesini etkinleştirin/devre dışı bırakın.\",\n    \"global_prep_name\": \"Global Hazırlık Komutları\",\n    \"image\": \"Resim\",\n    \"image_desc\": \"İstemciye gönderilecek uygulama simgesi/resmi/görüntü yolu. Görüntü bir PNG dosyası olmalıdır. Ayarlanmazsa, Sunshine varsayılan kutu görüntüsünü gönderir.\",\n    \"image_settings\": \"Görüntü Ayarları\",\n    \"loading\": \"Yükleniyor...\",\n    \"menu_cmd_actions\": \"İşlemler\",\n    \"menu_cmd_add\": \"Menü komutu ekle\",\n    \"menu_cmd_command\": \"Komut\",\n    \"menu_cmd_desc\": \"Yapılandırmadan sonra, bu komutlar istemcinin dönüş menüsünde görünür olacak ve akışı kesmeden belirli işlemlerin hızlı bir şekilde yürütülmesine izin verecektir, örneğin yardımcı programları başlatma.\\nÖrnek: Görünen Ad - Bilgisayarınızı kapatın; Komut - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Görünen Ad\",\n    \"menu_cmd_drag_sort\": \"Sıralamak için sürükleyin\",\n    \"menu_cmd_name\": \"Menü Komutları\",\n    \"menu_cmd_placeholder_command\": \"Komut\",\n    \"menu_cmd_placeholder_display_name\": \"Görünen ad\",\n    \"menu_cmd_placeholder_execute\": \"Komutu çalıştır\",\n    \"menu_cmd_placeholder_undo\": \"Komutu geri al\",\n    \"menu_cmd_remove_menu\": \"Menü komutunu kaldır\",\n    \"menu_cmd_remove_prep\": \"Hazırlık komutunu kaldır\",\n    \"mouse_mode\": \"Fare Modu\",\n    \"mouse_mode_auto\": \"Otomatik (Genel Ayar)\",\n    \"mouse_mode_desc\": \"Bu uygulama için fare giriş yöntemini seçin. Otomatik genel ayarı kullanır, Sanal Fare HID sürücüsünü kullanır, SendInput Windows API kullanır.\",\n    \"mouse_mode_sendinput\": \"SendInput (Windows API)\",\n    \"mouse_mode_vmouse\": \"Sanal Fare\",\n    \"name\": \"Ad\",\n    \"output_desc\": \"Komutun çıktısının saklanacağı dosya, belirtilmezse çıktı yok sayılır\",\n    \"output_name\": \"Çıktı\",\n    \"run_as_desc\": \"Bu, düzgün çalışması için yönetici izinleri gerektiren bazı uygulamalar için gerekli olabilir.\",\n    \"scan_result_add_all\": \"Tümünü Ekle\",\n    \"scan_result_edit_title\": \"Ekle ve düzenle\",\n    \"scan_result_filter_all\": \"Tümü\",\n    \"scan_result_filter_epic_title\": \"Epic Games oyunları\",\n    \"scan_result_filter_executable\": \"Çalıştırılabilir\",\n    \"scan_result_filter_executable_title\": \"Çalıştırılabilir dosya\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxy oyunları\",\n    \"scan_result_filter_script\": \"Betik\",\n    \"scan_result_filter_script_title\": \"Toplu/Komut betiği\",\n    \"scan_result_filter_shortcut\": \"Kısayol\",\n    \"scan_result_filter_shortcut_title\": \"Kısayol\",\n    \"scan_result_filter_steam_title\": \"Steam oyunları\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Oyun\",\n    \"scan_result_games_only\": \"Sadece Oyunlar\",\n    \"scan_result_matched\": \"Eşleşen: {count}\",\n    \"scan_result_no_apps\": \"Eklenecek uygulama bulunamadı\",\n    \"scan_result_no_matches\": \"Eşleşen uygulama bulunamadı\",\n    \"scan_result_quick_add_title\": \"Hızlı ekle\",\n    \"scan_result_remove_title\": \"Listeden kaldır\",\n    \"scan_result_search_placeholder\": \"Uygulama adı, komut veya yol ara...\",\n    \"scan_result_show_all\": \"Tümünü Göster\",\n    \"scan_result_title\": \"Tarama Sonuçları\",\n    \"scan_result_try_different_keywords\": \"Farklı arama anahtar kelimeleri kullanmayı deneyin\",\n    \"scan_result_type_batch\": \"Toplu\",\n    \"scan_result_type_command\": \"Komut betiği\",\n    \"scan_result_type_executable\": \"Çalıştırılabilir dosya\",\n    \"scan_result_type_shortcut\": \"Kısayol\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Uygulamaları ara...\",\n    \"select\": \"Seç\",\n    \"test_menu_cmd\": \"Komutu test et\",\n    \"test_menu_cmd_empty\": \"Komut boş olamaz\",\n    \"test_menu_cmd_executing\": \"Komut çalıştırılıyor...\",\n    \"test_menu_cmd_failed\": \"Komut çalıştırma başarısız\",\n    \"test_menu_cmd_success\": \"Komut başarıyla çalıştırıldı!\",\n    \"use_desktop_image\": \"Mevcut masaüstü duvar kağıdını kullan\",\n    \"wait_all\": \"Tüm uygulama süreçleri çıkana kadar akışa devam edin\",\n    \"wait_all_desc\": \"Bu, uygulama tarafından başlatılan tüm işlemler sonlandırılana kadar akışa devam edecektir. İşaretlenmediğinde, diğer uygulama süreçleri hala çalışıyor olsa bile ilk uygulama süreci çıktığında akış duracaktır.\",\n    \"working_dir\": \"Çalışma Dizini\",\n    \"working_dir_desc\": \"Sürece aktarılması gereken çalışma dizini. Örneğin, bazı uygulamalar yapılandırma dosyalarını aramak için çalışma dizinini kullanır. Ayarlanmazsa, Sunshine varsayılan olarak komutun üst dizinini kullanır\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Adaptör Adı\",\n    \"adapter_name_desc_linux_1\": \"Yakalama için kullanılacak GPU'yu manuel olarak belirleyin.\",\n    \"adapter_name_desc_linux_2\": \"VAAPI kullanabilen tüm cihazları bulmak için\",\n    \"adapter_name_desc_linux_3\": \"Cihazın adını ve özelliklerini listelemek için ``renderD129`` yerine yukarıdaki cihazı yazın. Sunshine tarafından desteklenebilmesi için en azından şu özelliklere sahip olması gerekir:\",\n    \"adapter_name_desc_windows\": \"Yakalama için kullanılacak GPU'yu manuel olarak belirleyin. Ayarlanmamışsa, GPU otomatik olarak seçilir. Otomatik GPU seçimini kullanmak için bu alanı boş bırakmanızı şiddetle tavsiye ederiz! Not: Bu GPU'nun bağlı ve açık bir ekranı olmalıdır. Uygun değerler aşağıdaki komut kullanılarak bulunabilir:\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Sanal ekranın en son sürümü yüklüyse, GPU bağlamasıyla otomatik olarak ilişkilendirilebilir\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Ekle\",\n    \"address_family\": \"Adres Ailesi\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Sunshine tarafından kullanılan adres ailesini ayarlama\",\n    \"address_family_ipv4\": \"Yalnızca IPv4\",\n    \"always_send_scancodes\": \"Her Zaman Scancode Gönder\",\n    \"always_send_scancodes_desc\": \"Scancode göndermek oyun ve uygulamalarla uyumluluğu artırır, ancak ABD İngilizcesi klavye düzeni kullanmayan bazı istemcilerden yanlış klavye girişine neden olabilir. Belirli uygulamalarda klavye girişi hiç çalışmıyorsa etkinleştirin. İstemcideki tuşlar ana bilgisayarda yanlış girdi oluşturuyorsa devre dışı bırakın.\",\n    \"amd_coder\": \"AMF Kodlayıcı (H264)\",\n    \"amd_coder_desc\": \"Kaliteye veya kodlama hızına öncelik vermek için entropi kodlamasını seçmenizi sağlar. Yalnızca H.264.\",\n    \"amd_enforce_hrd\": \"AMF Varsayımsal Referans Kod Çözücü (HRD) Uygulaması\",\n    \"amd_enforce_hrd_desc\": \"HRD modeli gereksinimlerini karşılamak için hız kontrolü üzerindeki kısıtlamaları artırır. Bu, bit hızı taşmalarını büyük ölçüde azaltır, ancak bazı kartlarda kodlama artefaktlarına veya düşük kaliteye neden olabilir.\",\n    \"amd_preanalysis\": \"AMF Ön Analizi\",\n    \"amd_preanalysis_desc\": \"Bu, artan kodlama gecikmesi pahasına kaliteyi artırabilecek hız kontrolü ön analizini mümkün kılar.\",\n    \"amd_quality\": \"AMF Kalitesi\",\n    \"amd_quality_balanced\": \"dengeli -- dengeli (varsayılan)\",\n    \"amd_quality_desc\": \"Bu, kodlama hızı ve kalitesi arasındaki dengeyi kontrol eder.\",\n    \"amd_quality_group\": \"AMF Kalite Ayarları\",\n    \"amd_quality_quality\": \"kalite -- kaliteyi tercih et\",\n    \"amd_quality_speed\": \"hız -- hızı tercih et\",\n    \"amd_qvbr_quality\": \"AMF QVBR Kalite Seviyesi\",\n    \"amd_qvbr_quality_desc\": \"QVBR hız kontrolü modu için kalite seviyesi. Aralık: 1-51 (düşük = daha iyi kalite). Varsayılan: 23. Yalnızca hız kontrolü 'qvbr' olarak ayarlandığında geçerlidir.\",\n    \"amd_rc\": \"AMF Oran Kontrolü\",\n    \"amd_rc_cbr\": \"cbr -- sabit bit hızı (HRD etkinleştirilmişse önerilir)\",\n    \"amd_rc_cqp\": \"cqp -- sabit qp modu\",\n    \"amd_rc_desc\": \"Bu, istemci bit hızı hedefini aşmadığımızdan emin olmak için hız kontrol yöntemini kontrol eder. 'cqp' bit hızı hedeflemesi için uygun değildir ve 'vbr_latency' dışındaki diğer seçenekler bit hızı taşmalarını kısıtlamaya yardımcı olmak için HRD Enforcement'a bağlıdır.\",\n    \"amd_rc_group\": \"AMF Hız Kontrol Ayarları\",\n    \"amd_rc_hqcbr\": \"hqcbr -- yüksek kaliteli sabit bit hızı\",\n    \"amd_rc_hqvbr\": \"hqvbr -- yüksek kaliteli değişken bit hızı\",\n    \"amd_rc_qvbr\": \"qvbr -- kaliteli değişken bit hızı (QVBR kalite seviyesi kullanır)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- gecikme kısıtlı değişken bit hızı (HRD devre dışı bırakılmışsa önerilir; varsayılan)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- tepe noktası kısıtlı değişken bit hızı\",\n    \"amd_usage\": \"AMF Kullanımı\",\n    \"amd_usage_desc\": \"Bu, temel kodlama profilini ayarlar. Aşağıda sunulan tüm seçenekler kullanım profilinin bir alt kümesini geçersiz kılar, ancak başka bir yerde yapılandırılamayan ek gizli ayarlar uygulanır.\",\n    \"amd_usage_lowlatency\": \"lowlatency - düşük gecikme süresi (en hızlı)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - düşük gecikme süresi, yüksek kalite (hızlı)\",\n    \"amd_usage_transcoding\": \"kod dönüştürme -- kod dönüştürme (en yavaş)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency - ultra düşük gecikme süresi (en hızlı; varsayılan)\",\n    \"amd_usage_webcam\": \"web kamerası -- web kamerası (yavaş)\",\n    \"amd_vbaq\": \"AMF Varyans Tabanlı Uyarlanabilir Niceleme (VBAQ)\",\n    \"amd_vbaq_desc\": \"İnsan görsel sistemi genellikle yüksek dokulu alanlardaki yapaylıklara karşı daha az hassastır. VBAQ modunda, piksel varyansı uzamsal dokuların karmaşıklığını belirtmek için kullanılır ve kodlayıcının daha pürüzsüz alanlara daha fazla bit ayırmasına olanak tanır. Bu özelliğin etkinleştirilmesi, bazı içeriklerde öznel görsel kalitede iyileşmelere yol açar.\",\n    \"amf_draw_mouse_cursor\": \"AMF yakalama yöntemi kullanılırken basit bir imleç çiz\",\n    \"amf_draw_mouse_cursor_desc\": \"Bazı durumlarda, AMF yakalama kullanmak fare işaretçisini görüntülemez. Bu seçeneği etkinleştirmek ekranda basit bir fare işaretçisi çizer. Not: Fare işaretçisi konumu yalnızca içerik ekranında güncelleme olduğunda güncelleneceğinden, masaüstü gibi oyun dışı senaryolarda fare işaretçisi hareketinin yavaş olduğunu gözlemleyebilirsiniz.\",\n    \"apply_note\": \"Sunshine'ı yeniden başlatmak ve değişiklikleri uygulamak için 'Uygula'ya tıklayın. Bu, çalışan tüm oturumları sonlandıracaktır.\",\n    \"audio_sink\": \"Ses Hedefi\",\n    \"audio_sink_desc_linux\": \"Ses Döngü Yönlendirmesi için kullanılan ses çıkışının adı. Bu değişkeni belirtmezseniz, pulseaudio varsayılan monitor aygıtını seçecektir. Ses çıkışının adını şu komutlardan biriyle bulabilirsiniz:\",\n    \"audio_sink_desc_macos\": \"Ses Döngü Yönlendirmesi için kullanılan ses çıkışının adı. Sunshine, sistem kısıtlamaları nedeniyle macOS'te yalnızca mikrofonlara erişebilir. Sistem sesini Soundflower veya BlackHole kullanarak yayınlamak mümkündür.\",\n    \"audio_sink_desc_windows\": \"Yakalanacak belirli bir ses cihazını manuel olarak belirleyin. Ayarlanmamışsa, cihaz otomatik olarak seçilir. Otomatik cihaz seçimini kullanmak için bu alanı boş bırakmanızı şiddetle tavsiye ederiz! Aynı ada sahip birden fazla ses cihazınız varsa, aşağıdaki komutu kullanarak Cihaz Kimliğini alabilirsiniz:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Hoparlörler (Yüksek Çözünürlüklü Ses Cihazı)\",\n    \"av1_mode\": \"AV1 Desteği\",\n    \"av1_mode_0\": \"Sunshine, kodlayıcı özelliklerine göre AV1 desteğini duyuracak (önerilir)\",\n    \"av1_mode_1\": \"Sunshine AV1 için desteği duyurmayacak\",\n    \"av1_mode_2\": \"Sunshine AV1 Ana 8-bit profil desteğinin reklamını yapacak\",\n    \"av1_mode_3\": \"Sunshine, AV1 Ana 8-bit ve 10-bit (HDR) profilleri desteğini duyuracak\",\n    \"av1_mode_desc\": \"İstemcinin AV1 Ana 8 bit veya 10 bit video akışları talep etmesini sağlar. AV1'in kodlanması daha yoğun CPU gerektirir, bu nedenle bunun etkinleştirilmesi yazılım kodlaması kullanılırken performansı düşürebilir.\",\n    \"back_button_timeout\": \"Ana/Ev Tuşu Emülasyon Zaman Aşımı\",\n    \"back_button_timeout_desc\": \"Geri/Seçim düğmesi belirtilen milisaniye sayısı kadar basılı tutulursa, Ana Sayfa/Güdüm düğmesine basılması taklit edilir. <0 (varsayılan) değerine ayarlanırsa, Geri/Seç düğmesinin basılı tutulması Ana Sayfa/Güdüm düğmesini taklit etmeyecektir.\",\n    \"bind_address\": \"Bağlama adresi (test özelliği)\",\n    \"bind_address_desc\": \"Sunshine'ın bağlanacağı belirli IP adresini ayarlayın. Boş bırakılırsa, Sunshine tüm uygun adreslere bağlanacaktır.\",\n    \"capture\": \"Belirli Bir Yakalama Yöntemini Zorlama\",\n    \"capture_desc\": \"Otomatik modda Sunshine çalışan ilk sürücüyü kullanacaktır. NvFBC yamalanmış nvidia sürücüleri gerektirir.\",\n    \"capture_target\": \"Yakalama Hedefi\",\n    \"capture_target_desc\": \"Yakalanacak hedef türünü seçin. 'Pencere' seçildiğinde, tüm ekran yerine belirli bir uygulama penceresini (örneğin AI kare enterpolasyon yazılımı) yakalayabilirsiniz.\",\n    \"capture_target_display\": \"Ekran\",\n    \"capture_target_window\": \"Pencere\",\n    \"cert\": \"Sertifika\",\n    \"cert_desc\": \"Web kullanıcı arayüzü ve Moonlight istemci eşleştirmesi için kullanılan sertifika. En iyi uyumluluk için, bunun bir RSA-2048 ortak anahtarı olmalıdır.\",\n    \"channels\": \"Maksimum Bağlı İstemci\",\n    \"channels_desc_1\": \"Sunshine, tek bir akış oturumunun aynı anda birden fazla istemci ile paylaşılmasına izin verebilir.\",\n    \"channels_desc_2\": \"Bazı donanım kodlayıcıları, birden fazla akışla performansı düşüren sınırlamalara sahip olabilir.\",\n    \"close_verify_safe\": \"Güvenli Doğrulama Eski İstemcilerle Uyumlu\",\n    \"close_verify_safe_desc\": \"Eski istemciler Sunshine'a bağlanamayabilir, lütfen bu seçeneği kapatın veya istemciyi güncelleyin\",\n    \"coder_cabac\": \"cabac -- bağlama uyarlanabi̇li̇r i̇ki̇li̇ ari̇tmeti̇k kodlama - daha yüksek kali̇te\",\n    \"coder_cavlc\": \"cavlc -- bağlam uyarlamalı değişken uzunluklu kodlama - daha hızlı kod çözme\",\n    \"configuration\": \"Konfigürasyon\",\n    \"controller\": \"Kontrolcü Girişini Etkinleştir\",\n    \"controller_desc\": \"Konukların ana sistemi bir gamepad / kontrol cihazı ile kontrol etmesine olanak tanır\",\n    \"credentials_file\": \"Kimlik Bilgileri Dosyası\",\n    \"credentials_file_desc\": \"Kullanıcı Adı/Şifre'yi Sunshine'ın durum dosyasından ayrı olarak saklayın.\",\n    \"display_device_options_note_desc_windows\": \"Windows, şu anda aktif olan ekranların her kombinasyonu için çeşitli ekran ayarlarını kaydeder.\\nSunshine daha sonra değişiklikleri bu tür bir ekran kombinasyonuna ait bir ekrana (veya ekranlara) uygular.\\nSunshine ayarları uyguladığında aktif olan bir cihazın bağlantısını keserseniz, Sunshine değişiklikleri geri almaya çalışana kadar kombinasyon tekrar etkinleştirilmediği sürece değişiklikler geri alınamaz!\",\n    \"display_device_options_note_windows\": \"Ayarların nasıl uygulandığına dair not\",\n    \"display_device_options_windows\": \"Ekran cihazı seçenekleri\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Ekran etkin değilse etkinleştirir\",\n    \"display_device_prep_ensure_active_windows\": \"Ekranı otomatik olarak etkinleştir\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Diğer tüm ekranları devre dışı bırakır ve yalnızca belirtilen ekranı etkinleştirir\",\n    \"display_device_prep_ensure_only_display_windows\": \"Diğer ekranları devre dışı bırak ve yalnızca belirtilen ekranı etkinleştir\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Ekranı etkinleştirir ve birincil ekran olarak ayarlar\",\n    \"display_device_prep_ensure_primary_windows\": \"Ekranı otomatik olarak etkinleştir ve birincil ekran yap\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Yalnızca sanal ekranı ikincil genişletilmiş yayın için kullanır\",\n    \"display_device_prep_ensure_secondary_windows\": \"İkincil ekran yayını (yalnızca sanal ekran)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Ekran durumunda değişiklik yapılmaz; kullanıcı ekranın hazır olduğundan emin olmalıdır\",\n    \"display_device_prep_no_operation_windows\": \"Devre Dışı\",\n    \"display_device_prep_windows\": \"Ekran hazırlığı\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"En az bir \\\"alınan\\\" ve bir \\\"son\\\" değer belirtilmelidir.\\n\\\"alınan\\\" bölümündeki boş alan \\\"herhangi biriyle eşleş\\\" anlamına gelir. \\\"son\\\" bölümündeki boş alan \\\"alınan değeri koru\\\" anlamına gelir.\\nİsterseniz belirli FPS değerini belirli çözünürlükle eşleştirebilirsiniz...\\n\\nNot: Moonlight istemcisinde \\\"Oyun ayarlarını optimize et\\\" seçeneği etkin değilse, çözünürlük değeri (veya değerleri) içeren satırlar yoksayılır.\",\n    \"display_mode_remapping_desc_windows\": \"Belirli bir çözünürlüğün ve/veya yenileme hızının diğer değerlere nasıl yeniden eşleneceğini belirtin.\\nDaha düşük çözünürlükte akış yaparken, ana bilgisayarda daha yüksek çözünürlükte işleyerek süper örnekleme efekti elde edebilirsiniz.\\nVeya daha yüksek FPS'de akış yaparken ana bilgisayarı daha düşük yenileme hızına sınırlayabilirsiniz.\\nEşleştirme yukarıdan aşağıya doğru yapılır. Giriş eşleştiğinde, diğerleri artık kontrol edilmez ancak yine de doğrulanır.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Son yenileme hızı\",\n    \"display_mode_remapping_final_resolution_windows\": \"Son çözünürlük\",\n    \"display_mode_remapping_optional\": \"isteğe bağlı\",\n    \"display_mode_remapping_received_fps_windows\": \"Alınan FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"Alınan çözünürlük\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Not: Moonlight istemcisinde \\\"Oyun ayarlarını optimize et\\\" seçeneği etkin değilse, yeniden eşleme devre dışı bırakılır.\",\n    \"display_mode_remapping_windows\": \"Ekran modlarını yeniden eşle\",\n    \"display_modes\": \"Ekran Modları\",\n    \"ds4_back_as_touchpad_click\": \"Geri/Seçimi Dokunmatik Yüzeye Eşle Tıklama\",\n    \"ds4_back_as_touchpad_click_desc\": \"DS4 emülasyonunu zorlarken, Geri/Seç'i Dokunmatik Yüzey Tıklaması ile eşleyin\",\n    \"dsu_server_port\": \"DSU Sunucu Portu\",\n    \"dsu_server_port_desc\": \"DSU sunucu dinleme portu (varsayılan 26760). Sunshine, istemci bağlantılarını almak ve hareket verilerini göndermek için bir DSU sunucusu olarak hareket edecektir. İstemcinizde (Yuzu, Ryujinx vb.) DSU sunucusunu etkinleştirin ve DSU sunucu adresini (127.0.0.1) ve portunu (26760) ayarlayın\",\n    \"enable_dsu_server\": \"DSU Sunucusunu Etkinleştir\",\n    \"enable_dsu_server_desc\": \"İstemci bağlantılarını almak ve hareket verilerini göndermek için DSU sunucusunu etkinleştirin\",\n    \"encoder\": \"Belirli Bir Kodlayıcıyı Zorla\",\n    \"encoder_desc\": \"Belirli bir kodlayıcıyı zorlayın, aksi takdirde Sunshine mevcut en iyi seçeneği seçecektir. Not: Windows'ta bir donanım kodlayıcı belirtirseniz, ekranın bağlı olduğu GPU ile eşleşmelidir.\",\n    \"encoder_software\": \"Yazılım\",\n    \"experimental\": \"Deneysel\",\n    \"experimental_features\": \"Deneysel Özellikler\",\n    \"external_ip\": \"Harici IP\",\n    \"external_ip_desc\": \"Harici IP adresi verilmezse, Sunshine harici IP'yi otomatik olarak algılar\",\n    \"fec_percentage\": \"FEC Yüzdesi\",\n    \"fec_percentage_desc\": \"Her video karesindeki veri paketi başına hata düzeltme paketlerinin yüzdesi. Daha yüksek değerler daha fazla ağ paketi kaybını düzeltebilir, ancak bant genişliği kullanımını artırma pahasına.\",\n    \"ffmpeg_auto\": \"auto -- ffmpeg'in karar vermesine izin ver (varsayılan)\",\n    \"file_apps\": \"Uygulamalar Dosyası\",\n    \"file_apps_desc\": \"Sunshine'ın mevcut uygulamalarının depolandığı dosya.\",\n    \"file_state\": \"Durum Dosyası\",\n    \"file_state_desc\": \"Sunshine'ın mevcut durumunun depolandığı dosya\",\n    \"fps\": \"Duyurulan FPS\",\n    \"gamepad\": \"Emüle Edilmiş Oyun Kumandası Türü\",\n    \"gamepad_auto\": \"Otomatik seçim seçenekleri\",\n    \"gamepad_desc\": \"Ana bilgisayarda hangi tür gamepad'in taklit edileceğini seçin\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 Manual Options\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Manuel DS4 seçenekleri\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Komut Hazırlıkları\",\n    \"global_prep_cmd_desc\": \"Herhangi bir uygulamayı çalıştırmadan önce veya sonra yürütülecek komutların bir listesini yapılandırın. Belirtilen hazırlık komutlarından herhangi biri başarısız olursa, uygulama başlatma işlemi iptal edilir.\",\n    \"hdr_luminance_analysis\": \"HDR Dinamik Meta Verileri (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Kare başına GPU parlaklık analizini etkinleştirir ve kodlanmış bit akışına HDR10+ (ST 2094-40) ve HDR Vivid (CUVA) dinamik meta verilerini enjekte eder. Desteklenen ekranlar için kare başına ton eşleme ipuçları sağlar. Yüksek çözünürlüklerde küçük GPU yükü ekler (~0,5-1,5ms/kare). HDR ile kare hızı düşüşleri yaşarsanız devre dışı bırakın.\",\n    \"hdr_prep_automatic_windows\": \"İstemci tarafından talep edildiği gibi HDR modunu aç/kapat\",\n    \"hdr_prep_no_operation_windows\": \"Devre Dışı\",\n    \"hdr_prep_windows\": \"HDR durum değişikliği\",\n    \"hevc_mode\": \"HEVC Desteği\",\n    \"hevc_mode_0\": \"Sunshine, kodlayıcı yeteneklerine göre HEVC desteğini yayınlayacak (önerilir)\",\n    \"hevc_mode_1\": \"Sunshine HEVC desteğinin reklamını yapmayacak\",\n    \"hevc_mode_2\": \"Sunshine, HEVC Ana profil desteğinin reklamını yapacak\",\n    \"hevc_mode_3\": \"Sunshine, HEVC Main ve Main10 (HDR) profillerine yönelik desteğin reklamını yapacak\",\n    \"hevc_mode_desc\": \"İstemcinin HEVC Main veya HEVC Main10 video akışlarını talep etmesini sağlar. HEVC'nin kodlanması daha yoğun CPU gerektirir, bu nedenle bunun etkinleştirilmesi yazılım kodlaması kullanılırken performansı düşürebilir.\",\n    \"high_resolution_scrolling\": \"Yüksek Çözünürlüklü Kaydırma Desteği\",\n    \"high_resolution_scrolling_desc\": \"Etkinleştirildiğinde Sunshine, Moonlight istemcilerinden gelen yüksek çözünürlüklü kaydırma olaylarını geçirir. Bu, yüksek çözünürlüklü kaydırma olaylarıyla çok hızlı kaydırma yapan eski uygulamalar için devre dışı bırakmak için yararlı olabilir.\",\n    \"install_steam_audio_drivers\": \"Steam Ses Sürücülerini Yükleyin\",\n    \"install_steam_audio_drivers_desc\": \"Steam yüklüyse, 5.1/7.1 surround sesi desteklemek ve ana bilgisayar sesini kapatmak için Steam Streaming Speakers sürücüsünü otomatik olarak yükleyecektir.\",\n    \"key_repeat_delay\": \"Tuş Tekrarlama Gecikmesi\",\n    \"key_repeat_delay_desc\": \"Tuşların kendilerini ne kadar hızlı tekrarlayacaklarını kontrol edin. Tuşları tekrarlamadan önce milisaniye cinsinden ilk gecikme.\",\n    \"key_repeat_frequency\": \"Anahtar Tekrarlama Sıklığı\",\n    \"key_repeat_frequency_desc\": \"Tuşların her saniye ne sıklıkta tekrarlanacağı. Bu yapılandırılabilir seçenek ondalık sayıları destekler.\",\n    \"key_rightalt_to_key_win\": \"Sağ Alt tuşunu Windows tuşuna eşle\",\n    \"key_rightalt_to_key_win_desc\": \"Windows Tuşunu Moonlight'tan doğrudan gönderemiyor olabilirsiniz. Bu gibi durumlarda Sunshine'ın Sağ Alt tuşunun Windows tuşu olduğunu düşünmesini sağlamak yararlı olabilir\",\n    \"key_rightalt_to_key_windows\": \"Sağ Alt tuşunu Windows tuşuyla eşleştirme\",\n    \"keyboard\": \"Klavye Girişini Etkinleştir\",\n    \"keyboard_desc\": \"Konukların ana sistemi klavye ile kontrol etmesini sağlar\",\n    \"lan_encryption_mode\": \"LAN Şifreleme Modu\",\n    \"lan_encryption_mode_1\": \"Desteklenen istemciler için etkinleştirildi\",\n    \"lan_encryption_mode_2\": \"Tüm müşteriler için gereklidir\",\n    \"lan_encryption_mode_desc\": \"Bu, yerel ağınız üzerinden akış yaparken şifrelemenin ne zaman kullanılacağını belirler. Şifreleme, özellikle daha az güçlü ana bilgisayarlarda ve istemcilerde akış performansını düşürebilir.\",\n    \"locale\": \"Yerel\",\n    \"locale_desc\": \"Sunshine'ın kullanıcı arayüzü için kullanılan yerel ayar.\",\n    \"log_level\": \"Günlük Seviyesi\",\n    \"log_level_0\": \"Verbose\",\n    \"log_level_1\": \"Hata Ayıklama\",\n    \"log_level_2\": \"Bilgi\",\n    \"log_level_3\": \"Uyarı\",\n    \"log_level_4\": \"Hata\",\n    \"log_level_5\": \"Kritik\",\n    \"log_level_6\": \"Hiçbiri\",\n    \"log_level_desc\": \"Standart çıkışa yazdırılan minimum günlük düzeyi\",\n    \"log_path\": \"Günlük Dosyası Yolu\",\n    \"log_path_desc\": \"Sunshine'ın geçerli günlüklerinin depolandığı dosya.\",\n    \"max_bitrate\": \"Maksimum Bit Hızı\",\n    \"max_bitrate_desc\": \"Sunshine'ın akışı kodlayacağı maksimum bit hızı (Kbps cinsinden). 0 olarak ayarlanırsa, her zaman Moonlight tarafından istenen bit hızını kullanır.\",\n    \"max_fps_reached\": \"Maksimum FPS değerlerine ulaşıldı\",\n    \"max_resolutions_reached\": \"Maksimum çözünürlük sayısına ulaşıldı\",\n    \"mdns_broadcast\": \"Yerel Ağda Bu Bilgisayarı Bul\",\n    \"mdns_broadcast_desc\": \"Bu seçeneğin etkinleştirilmesi, bağlı olmayan cihazların bu bilgisayarı otomatik olarak bulmasını sağlar. Moonlight'ın yerel ağda bilgisayarı otomatik olarak bulması gerekir.\",\n    \"min_threads\": \"Minimum CPU İplik Sayısı\",\n    \"min_threads_desc\": \"Değerin artırılması kodlama verimliliğini biraz azaltır, ancak kodlama için daha fazla CPU çekirdeği kullanımı elde etmek için genellikle buna değer. İdeal değer, donanımınızda istediğiniz akış ayarlarında güvenilir bir şekilde kodlama yapabilen en düşük değerdir.\",\n    \"minimum_fps_target\": \"Minimum FPS hedefi\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Çeşitli seçenekler\",\n    \"motion_as_ds4\": \"İstemci gamepad hareket sensörlerinin mevcut olduğunu bildirirse bir DS4 gamepad taklit edin\",\n    \"motion_as_ds4_desc\": \"Devre dışı bırakılırsa, gamepad tipi seçimi sırasında hareket sensörleri dikkate alınmaz.\",\n    \"mouse\": \"Fare Girişini Etkinleştir\",\n    \"mouse_desc\": \"Konukların ana sistemi fare ile kontrol etmesini sağlar\",\n    \"native_pen_touch\": \"Kalem/Dokunma Desteği\",\n    \"native_pen_touch_desc\": \"Etkinleştirildiğinde Sunshine, Moonlight istemcilerinden gelen yerel kalem/dokunma olaylarını aktarır. Bu, yerel kalem/dokunmatik desteği olmayan eski uygulamalar için devre dışı bırakmak için yararlı olabilir.\",\n    \"no_fps\": \"FPS değeri eklenmedi\",\n    \"no_resolutions\": \"Çözünürlük eklenmedi\",\n    \"notify_pre_releases\": \"Yayın Öncesi Bildirimler\",\n    \"notify_pre_releases_desc\": \"Sunshine'ın yeni yayın öncesi sürümlerinden haberdar edilip edilmeme\",\n    \"nvenc_h264_cavlc\": \"H.264'te CAVLC'yi CABAC'a tercih edin\",\n    \"nvenc_h264_cavlc_desc\": \"Entropi kodlamanın daha basit biçimi. CAVLC aynı kalite için yaklaşık %10 daha fazla bit hızına ihtiyaç duyar. Yalnızca gerçekten eski kod çözme cihazları için geçerlidir.\",\n    \"nvenc_latency_over_power\": \"Güç tasarrufu yerine daha düşük kodlama gecikmesini tercih edin\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine, kodlama gecikmesini azaltmak için akış sırasında maksimum GPU saat hızı ister. Kodlama gecikmesinin önemli ölçüde artmasına neden olabileceğinden devre dışı bırakılması önerilmez.\",\n    \"nvenc_lookahead_depth\": \"İleriye bakma derinliği\",\n    \"nvenc_lookahead_depth_desc\": \"Kodlama sırasında ileriye bakılacak kare sayısı (0-32). İleriye bakma, daha iyi hareket tahmini ve bit hızı dağılımı sağlayarak, özellikle karmaşık sahnelerde kodlama kalitesini artırır. Daha yüksek değerler kaliteyi artırır ancak kodlama gecikmesini artırır. İleriye bakmayı devre dışı bırakmak için 0 olarak ayarlayın. NVENC SDK 13.0 (1202) veya daha yenisini gerektirir.\",\n    \"nvenc_lookahead_level\": \"İleriye bakma seviyesi\",\n    \"nvenc_lookahead_level_0\": \"Seviye 0 (en düşük kalite, en hızlı)\",\n    \"nvenc_lookahead_level_1\": \"Seviye 1\",\n    \"nvenc_lookahead_level_2\": \"Seviye 2\",\n    \"nvenc_lookahead_level_3\": \"Seviye 3 (en yüksek kalite, en yavaş)\",\n    \"nvenc_lookahead_level_autoselect\": \"Otomatik seçim (sürücünün en uygun seviyeyi seçmesine izin ver)\",\n    \"nvenc_lookahead_level_desc\": \"İleriye bakma kalite seviyesi. Daha yüksek seviyeler, performans pahasına kaliteyi artırır. Bu seçenek yalnızca nvenc_lookahead_depth 0'dan büyük olduğunda etkili olur. NVENC SDK 13.0 (1202) veya daha yenisini gerektirir.\",\n    \"nvenc_lookahead_level_disabled\": \"Devre dışı (seviye 0 ile aynı)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"OpenGL/Vulkan'ı DXGI üzerinde sunma\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine, DXGI'nin üstünde bulunmadıkları sürece tam ekran OpenGL ve Vulkan programlarını tam kare hızında yakalayamaz. Bu, sunshine programından çıkıldığında geri döndürülen sistem genelinde bir ayardır.\",\n    \"nvenc_preset\": \"Performans ön ayarı\",\n    \"nvenc_preset_1\": \"(en hızlı, varsayılan)\",\n    \"nvenc_preset_7\": \"(en yavaş)\",\n    \"nvenc_preset_desc\": \"Daha yüksek sayılar, artan kodlama gecikmesi pahasına sıkıştırmayı (verilen bit hızında kalite) iyileştirir. Yalnızca ağ veya kod çözücü tarafından sınırlandırıldığında değiştirilmesi önerilir, aksi takdirde benzer etki bit hızını artırarak elde edilebilir.\",\n    \"nvenc_rate_control\": \"Oran kontrol modu\",\n    \"nvenc_rate_control_cbr\": \"CBR (Sabit Bit Hızı) - Düşük gecikme\",\n    \"nvenc_rate_control_desc\": \"Oran kontrol modunu seçin. CBR (Sabit Bit Hızı), düşük gecikmeli akış için sabit bit hızı sağlar. VBR (Değişken Bit Hızı), bit hızının sahne karmaşıklığına göre değişmesine izin vererek, değişken bit hızı pahasına karmaşık sahneler için daha iyi kalite sağlar.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Değişken Bit Hızı) - Daha iyi kalite\",\n    \"nvenc_realtime_hags\": \"Donanım hızlandırmalı gpu zamanlamasında gerçek zamanlı önceliği kullanma\",\n    \"nvenc_realtime_hags_desc\": \"Şu anda NVIDIA sürücüleri, HAGS etkinleştirildiğinde, gerçek zamanlı öncelik kullanıldığında ve VRAM kullanımı maksimuma yakın olduğunda kodlayıcıda donabilir. Bu seçeneğin devre dışı bırakılması, önceliği yüksek seviyeye düşürerek GPU'ya çok fazla yük bindiğinde yakalama performansının düşmesi pahasına donmayı önler.\",\n    \"nvenc_spatial_aq\": \"Uzamsal AQ\",\n    \"nvenc_spatial_aq_desc\": \"Videonun düz bölgelerine daha yüksek QP değerleri atayın. Düşük bit hızlarında akış yaparken etkinleştirilmesi önerilir.\",\n    \"nvenc_spatial_aq_disabled\": \"Devre dışı (daha hızlı, varsayılan)\",\n    \"nvenc_spatial_aq_enabled\": \"Etkin (daha yavaş)\",\n    \"nvenc_split_encode\": \"Bölünmüş kare kodlama\",\n    \"nvenc_split_encode_desc\": \"Her video karesinin kodlamasını birden fazla NVENC donanım birimine bölün. Marjinal bir sıkıştırma verimliliği cezası ile kodlama gecikmesini önemli ölçüde azaltır. GPU'nuz tek bir NVENC birimine sahipse bu seçenek yoksayılır.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Sürücü karar verir (varsayılan)\",\n    \"nvenc_split_encode_four_strips\": \"4 şeritli bölünmeyi zorla (4+ NVENC motoru gerektirir)\",\n    \"nvenc_split_encode_three_strips\": \"3 şeritli bölünmeyi zorla (3+ NVENC motoru gerektirir)\",\n    \"nvenc_split_encode_two_strips\": \"2 şeritli bölünmeyi zorla (2+ NVENC motoru gerektirir)\",\n    \"nvenc_target_quality\": \"Hedef kalite (VBR modu)\",\n    \"nvenc_target_quality_desc\": \"VBR modu için hedef kalite seviyesi (H.264/HEVC için 0-51, AV1 için 0-63). Düşük değerler = daha yüksek kalite. Otomatik kalite seçimi için 0 olarak ayarlayın. Yalnızca oran kontrol modu VBR olduğunda kullanılır.\",\n    \"nvenc_temporal_aq\": \"Zamansal Uyarlanabilir Niceleme\",\n    \"nvenc_temporal_aq_desc\": \"Zamansal uyarlanabilir nicelemeyi etkinleştirin. Zamansal AQ, nicelemeyi zaman içinde optimize ederek, hareketli sahnelerde daha iyi bit hızı dağılımı ve gelişmiş kalite sağlar. Bu özellik zamansal AQ ile birlikte çalışır ve ileriye bakmanın (nvenc_lookahead_depth > 0) etkinleştirilmesini gerektirir. NVENC SDK 13.0 (1202) veya daha yenisini gerektirir.\",\n    \"nvenc_temporal_filter\": \"Zamansal Filtre\",\n    \"nvenc_temporal_filter_4\": \"Seviye 4 (maksimum güç)\",\n    \"nvenc_temporal_filter_desc\": \"Kodlamadan önce uygulanan zamansal filtreleme gücü. Zamansal filtre, özellikle doğal içerik için gürültüyü azaltır ve sıkıştırma verimliliğini artırır. Daha yüksek seviyeler daha iyi gürültü azaltma sağlar ancak hafif bulanıklığa neden olabilir. NVENC SDK 13.0 (1202) veya daha yenisini gerektirir. Not: frameIntervalP >= 5 gerektirir, zeroReorderDelay veya stereo MVC ile uyumlu değildir.\",\n    \"nvenc_temporal_filter_disabled\": \"Devre Dışı (zamansal filtreleme yok)\",\n    \"nvenc_twopass\": \"İki geçişli mod\",\n    \"nvenc_twopass_desc\": \"Ön kodlama geçişi ekler. Bu, daha fazla hareket vektörünün algılanmasını, bit hızının kareye daha iyi dağıtılmasını ve bit hızı sınırlarına daha sıkı uyulmasını sağlar. Zaman zaman bit hızının aşılmasına ve ardından paket kaybına neden olabileceğinden devre dışı bırakılması önerilmez.\",\n    \"nvenc_twopass_disabled\": \"Devre dışı (en hızlı, önerilmez)\",\n    \"nvenc_twopass_full_res\": \"Tam çözünürlük (daha yavaş)\",\n    \"nvenc_twopass_quarter_res\": \"Çeyrek çözünürlük (daha hızlı, varsayılan)\",\n    \"nvenc_vbv_increase\": \"Tek kare VBV/HRD yüzde artışı\",\n    \"nvenc_vbv_increase_desc\": \"Varsayılan olarak sunshine tek kare VBV/HRD kullanır, yani kodlanmış herhangi bir video karesi boyutunun istenen bit hızının istenen kare hızına bölünmesini aşması beklenmez. Bu kısıtlamanın gevşetilmesi faydalı olabilir ve düşük gecikmeli değişken bit hızı olarak işlev görebilir, ancak ağın bit hızı artışlarını idare edecek tampon boşluğu yoksa paket kaybına da yol açabilir. Kabul edilen maksimum değer 400'dür, bu da 5 kat artırılmış kodlanmış video karesi üst boyut sınırına karşılık gelir.\",\n    \"origin_web_ui_allowed\": \"Origin Web Kullanıcı Arayüzüne İzin Verildi\",\n    \"origin_web_ui_allowed_desc\": \"Web UI'ye erişimi reddedilmeyen uzak uç nokta adresinin kaynağı\",\n    \"origin_web_ui_allowed_lan\": \"Yalnızca LAN'da bulunanlar Web UI'ye erişebilir\",\n    \"origin_web_ui_allowed_pc\": \"Web kullanıcı arayüzüne yalnızca localhost erişebilir\",\n    \"origin_web_ui_allowed_wan\": \"Web UI'ye herkes erişebilir\",\n    \"output_name_desc_unix\": \"Sunshine başlangıcı sırasında, algılanan ekranların listesini görmelisiniz. Not: Parantez içindeki id değerini kullanmanız gerekir.\",\n    \"output_name_desc_windows\": \"Yakalama için kullanılacak bir ekranı manuel olarak belirleyin. Ayarlanmamışsa, birincil ekran yakalanır. Not: Yukarıda bir GPU belirttiyseniz, bu ekranın o GPU'ya bağlı olması gerekir. Uygun değerler aşağıdaki komut kullanılarak bulunabilir:\",\n    \"output_name_unix\": \"Ekran numarası\",\n    \"output_name_windows\": \"Çıktı Adı\",\n    \"ping_timeout\": \"Ping Zaman Aşımı\",\n    \"ping_timeout_desc\": \"Akışı kapatmadan önce ay ışığından gelen veriler için milisaniye cinsinden ne kadar süre bekleneceği\",\n    \"pkey\": \"Özel Anahtar\",\n    \"pkey_desc\": \"Web UI ve Moonlight istemci eşleştirmesi için kullanılan özel anahtar. En iyi uyumluluk için bu bir RSA-2048 özel anahtarı olmalıdır.\",\n    \"port\": \"Port\",\n    \"port_alert_1\": \"Sunshine, 1024 altındaki portları kullanamaz!\",\n    \"port_alert_2\": \"65535'in üzerindeki bağlantı noktaları kullanılamaz!\",\n    \"port_desc\": \"Sunshine tarafından kullanılan bağlantı noktaları ailesini ayarlayın\",\n    \"port_http_port_note\": \"Moonlight ile bağlantı kurmak için bu bağlantı noktasını kullanın.\",\n    \"port_note\": \"Not\",\n    \"port_port\": \"Port\",\n    \"port_protocol\": \"Protokol\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Web kullanıcı arayüzünü internete açmak bir güvenlik riskidir! Kendi sorumluluğunuzda devam edin!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Kuantizasyon Parametresi\",\n    \"qp_desc\": \"Bazı cihazlar Sabit Bit Hızını desteklemeyebilir. Bu cihazlar için bunun yerine QP kullanılır. Daha yüksek değer daha fazla sıkıştırma, ancak daha az kalite anlamına gelir.\",\n    \"qsv_coder\": \"QuickSync Kodlayıcı (H264)\",\n    \"qsv_preset\": \"QuickSync Ön Ayarı\",\n    \"qsv_preset_fast\": \"hızlı (düşük kalite)\",\n    \"qsv_preset_faster\": \"daha hızlı (daha düşük kalite)\",\n    \"qsv_preset_medium\": \"orta (varsayılan)\",\n    \"qsv_preset_slow\": \"yavaş (iyi kalite)\",\n    \"qsv_preset_slower\": \"daha yavaş (daha iyi kalite)\",\n    \"qsv_preset_slowest\": \"en yavaş (en iyi kalite)\",\n    \"qsv_preset_veryfast\": \"en hızlı (en düşük kalite)\",\n    \"qsv_slow_hevc\": \"Yavaş HEVC Kodlamasına İzin Ver\",\n    \"qsv_slow_hevc_desc\": \"Bu, daha yüksek GPU kullanımı ve daha kötü performans pahasına eski Intel GPU'larda HEVC kodlamasını etkinleştirebilir.\",\n    \"refresh_rate_change_automatic_windows\": \"İstemci tarafından sağlanan FPS değerini kullan\",\n    \"refresh_rate_change_manual_desc_windows\": \"Kullanılacak yenileme hızını girin\",\n    \"refresh_rate_change_manual_windows\": \"Manuel olarak girilen yenileme hızını kullan\",\n    \"refresh_rate_change_no_operation_windows\": \"Devre Dışı\",\n    \"refresh_rate_change_windows\": \"FPS değişikliği\",\n    \"res_fps_desc\": \"Sunshine tarafından duyurulan ekran modları. Moonlight-nx (Switch) gibi Moonlight'ın bazı sürümleri, istenen çözünürlüklerin ve fps'nin desteklendiğinden emin olmak için bu listelere güvenir. Bu ayar, ekran akışının Moonlight'a nasıl gönderildiğini değiştirmez.\",\n    \"resolution_change_automatic_windows\": \"İstemci tarafından sağlanan çözünürlüğü kullan\",\n    \"resolution_change_manual_desc_windows\": \"Bunun çalışması için Moonlight istemcisinde \\\"Oyun ayarlarını optimize et\\\" seçeneği etkinleştirilmelidir.\",\n    \"resolution_change_manual_windows\": \"Manuel olarak girilen çözünürlüğü kullan\",\n    \"resolution_change_no_operation_windows\": \"Devre Dışı\",\n    \"resolution_change_ogs_desc_windows\": \"Bunun çalışması için Moonlight istemcisinde \\\"Oyun ayarlarını optimize et\\\" seçeneği etkinleştirilmelidir.\",\n    \"resolution_change_windows\": \"Çözünürlük değişikliği\",\n    \"resolutions\": \"Duyurulan Çözünürlükler\",\n    \"restart_note\": \"Sunshine değişikleri uygulamak için yeniden başlatılıyor.\",\n    \"sleep_mode\": \"Uyku Modu\",\n    \"sleep_mode_away\": \"Uzakta Modu (Ekran Kapalı, Anında Uyanma)\",\n    \"sleep_mode_desc\": \"İstemci bir uyku komutu gönderdiğinde ne olacağını kontrol eder. Askıya Al (S3): geleneksel uyku, düşük güç tüketimi ancak uyanmak için WOL gerektirir. Hazırda Beklet (S4): diske kaydeder, çok düşük güç tüketimi. Uzakta Modu: ekran kapanır ancak sistem anında uyanma için çalışmaya devam eder - oyun akış sunucuları için idealdir.\",\n    \"sleep_mode_hibernate\": \"Hazırda Beklet (S4)\",\n    \"sleep_mode_suspend\": \"Askıya Al (S3 Uyku)\",\n    \"stream_audio\": \"Ses akışını etkinleştir\",\n    \"stream_audio_desc\": \"Ses akışını durdurmak için bu seçeneği devre dışı bırakın.\",\n    \"stream_mic\": \"Mikrofon akışını etkinleştir\",\n    \"stream_mic_desc\": \"Mikrofon akışını durdurmak için bu seçeneği devre dışı bırakın.\",\n    \"stream_mic_download_btn\": \"Sanal Mikrofon İndir\",\n    \"stream_mic_download_confirm\": \"Sanal mikrofon indirme sayfasına yönlendirileceksiniz. Devam edilsin mi?\",\n    \"stream_mic_note\": \"Bu özellik sanal bir mikrofon kurulumu gerektirir\",\n    \"sunshine_name\": \"Günışığı Adı\",\n    \"sunshine_name_desc\": \"Moonlight tarafından görüntülenen ad. Belirtilmezse, bilgisayarın ana bilgisayar adı kullanılır\",\n    \"sw_preset\": \"SW Ön Ayarları\",\n    \"sw_preset_desc\": \"Kodlama hızı (saniye başına kodlanan kare sayısı) ile sıkıştırma verimliliği (bit akışındaki bit başına kalite) arasındaki dengeyi optimize edin. Varsayılan değer süper hızlıdır.\",\n    \"sw_preset_fast\": \"hızlı\",\n    \"sw_preset_faster\": \"daha hızlı\",\n    \"sw_preset_medium\": \"orta\",\n    \"sw_preset_slow\": \"yavaş\",\n    \"sw_preset_slower\": \"daha yavaş\",\n    \"sw_preset_superfast\": \"süper hızlı (varsayılan)\",\n    \"sw_preset_ultrafast\": \"ultra hızlı\",\n    \"sw_preset_veryfast\": \"çok hızlı\",\n    \"sw_preset_veryslow\": \"çok yavaş\",\n    \"sw_tune\": \"SW Tune\",\n    \"sw_tune_animation\": \"animasyon -- çizgi filmler için iyi; daha yüksek deblocking ve daha fazla referans karesi kullanır\",\n    \"sw_tune_desc\": \"Ön ayardan sonra uygulanan ayarlama seçenekleri. Varsayılan değer sıfır gecikmedir.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- belirli filtreleri devre dışı bırakarak daha hızlı kod çözme sağlar\",\n    \"sw_tune_film\": \"film -- yüksek kaliteli film içeriği için kullanın; deblocking'i azaltır\",\n    \"sw_tune_grain\": \"gren -- eski, grenli film malzemesinde gren yapısını korur\",\n    \"sw_tune_stillimage\": \"stillimage -- slayt gösterisi benzeri içerik için iyi\",\n    \"sw_tune_zerolatency\": \"zerolatency -- hızlı kodlama ve düşük gecikmeli akış için iyidir (varsayılan)\",\n    \"system_tray\": \"Sistem tepsisini etkinleştir\",\n    \"system_tray_desc\": \"Sistem tepsisinin etkinleştirilip etkinleştirilmeyeceği. Etkinleştirilirse, Sunshine sistem tepsisinde bir simge görüntüler ve sistem tepsisinden kontrol edilebilir.\",\n    \"touchpad_as_ds4\": \"İstemci oyun kumandası bir dokunmatik yüzey olduğunu bildirirse bir DS4 oyun kumandasını taklit edin\",\n    \"touchpad_as_ds4_desc\": \"Devre dışı bırakılırsa, oyun kumandası türü seçimi sırasında dokunmatik yüzey varlığı dikkate alınmaz.\",\n    \"unsaved_changes_tooltip\": \"Kaydedilmemiş değişiklikleriniz var. Kaydetmek için tıklayın.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"İnternet üzerinden akış için port yönlendirmeyi otomatik olarak yapılandırın\",\n    \"variable_refresh_rate\": \"Değişken Yenileme Hızı (VRR)\",\n    \"variable_refresh_rate_desc\": \"VRR desteği için video akışı kare hızının render kare hızıyla eşleşmesine izin ver. Etkinleştirildiğinde, kodlama yalnızca yeni kareler mevcut olduğunda gerçekleşir ve akışın gerçek render kare hızını takip etmesine olanak tanır.\",\n    \"vdd_reuse_desc_windows\": \"Etkinleştirildiğinde, tüm istemciler aynı VDD'yi (Sanal Görüntü Cihazı) paylaşır. Devre dışı bırakıldığında (varsayılan), her istemci kendi VDD'sini alır. Daha hızlı istemci değiştirme için bunu etkinleştirin, ancak tüm istemcilerin aynı ekran ayarlarını paylaşacağını unutmayın.\",\n    \"vdd_reuse_windows\": \"Tüm İstemciler İçin Aynı VDD'yi Yeniden Kullan\",\n    \"virtual_display\": \"Sanal Ekran\",\n    \"virtual_mouse\": \"Sanal Fare Sürücüsü\",\n    \"virtual_mouse_desc\": \"Etkinleştirildiğinde, Sunshine Zako Virtual Mouse sürücüsünü (kuruluysa) HID düzeyinde fare girişini simüle etmek için kullanır. Raw Input kullanan oyunların fare olaylarını almasını sağlar. Devre dışı bırakıldığında veya sürücü kurulu değilse SendInput kullanılır.\",\n    \"virtual_sink\": \"Sanal Lavabo\",\n    \"virtual_sink_desc\": \"Kullanılacak sanal ses cihazını manuel olarak belirleyin. Ayarlanmamışsa, cihaz otomatik olarak seçilir. Otomatik cihaz seçimini kullanmak için bu alanı boş bırakmanızı şiddetle tavsiye ederiz!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Sanal fare sürücüsü kurulsun mu?\",\n    \"vmouse_confirm_uninstall\": \"Sanal fare sürücüsü kaldırılsın mı?\",\n    \"vmouse_install\": \"Sürücü Kur\",\n    \"vmouse_installing\": \"Kuruluyor...\",\n    \"vmouse_note\": \"Sanal fare sürücüsü ayrı kurulum gerektirir. Sürücüyü kurmak veya yönetmek için Sunshine Kontrol Panelini kullanın.\",\n    \"vmouse_refresh\": \"Durumu Yenile\",\n    \"vmouse_status_installed\": \"Kurulu (aktif değil)\",\n    \"vmouse_status_not_installed\": \"Kurulu değil\",\n    \"vmouse_status_running\": \"Çalışıyor\",\n    \"vmouse_uninstall\": \"Sürücü Kaldır\",\n    \"vmouse_uninstalling\": \"Kaldırılıyor...\",\n    \"vt_coder\": \"VideoToolbox Kodlayıcı\",\n    \"vt_realtime\": \"VideoToolbox Gerçek Zamanlı Kodlama\",\n    \"vt_software\": \"VideoToolbox Yazılım Kodlaması\",\n    \"vt_software_allowed\": \"İzin verildi\",\n    \"vt_software_forced\": \"Zorla\",\n    \"wan_encryption_mode\": \"WAN Şifreleme Modu\",\n    \"wan_encryption_mode_1\": \"Desteklenen istemciler için etkin (varsayılan)\",\n    \"wan_encryption_mode_2\": \"Tüm müşteriler için gereklidir\",\n    \"wan_encryption_mode_desc\": \"Bu, İnternet üzerinden akış yaparken şifrelemenin ne zaman kullanılacağını belirler. Şifreleme, özellikle daha az güçlü ana bilgisayarlarda ve istemcilerde akış performansını düşürebilir.\",\n    \"webhook_curl_command\": \"Komut\",\n    \"webhook_curl_command_desc\": \"Webhook'un düzgün çalışıp çalışmadığını test etmek için aşağıdaki komutu terminalinize kopyalayın:\",\n    \"webhook_curl_copy_failed\": \"Kopyalama başarısız, lütfen manuel olarak seçin ve kopyalayın\",\n    \"webhook_enabled\": \"Webhook Bildirimleri\",\n    \"webhook_enabled_desc\": \"Etkinleştirildiğinde, Sunshine belirtilen Webhook URL'sine olay bildirimleri gönderir\",\n    \"webhook_group\": \"Webhook Bildirim Ayarları\",\n    \"webhook_skip_ssl_verify\": \"SSL Sertifikası Doğrulamasını Atla\",\n    \"webhook_skip_ssl_verify_desc\": \"HTTPS bağlantıları için SSL sertifikası doğrulamasını atla, yalnızca test veya kendi imzalanmış sertifikalar için\",\n    \"webhook_test\": \"Test\",\n    \"webhook_test_failed\": \"Webhook testi başarısız\",\n    \"webhook_test_failed_note\": \"Not: Lütfen URL'nin doğru olup olmadığını kontrol edin veya daha fazla bilgi için tarayıcı konsolunu kontrol edin.\",\n    \"webhook_test_success\": \"Webhook testi başarılı!\",\n    \"webhook_test_success_cors_note\": \"Not: CORS kısıtlamaları nedeniyle, sunucu yanıt durumu doğrulanamaz.\\nİstek gönderildi. Webhook doğru yapılandırılmışsa, mesaj teslim edilmiş olmalıdır.\\n\\nÖneri: İstek ayrıntıları için tarayıcınızın geliştirici araçlarındaki Ağ sekmesini kontrol edin.\",\n    \"webhook_test_url_required\": \"Lütfen önce Webhook URL'sini girin\",\n    \"webhook_timeout\": \"İstek Zaman Aşımı\",\n    \"webhook_timeout_desc\": \"Webhook isteklerinin zaman aşımı süresi (milisaniye), aralık 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"Olay bildirimlerini almak için URL, HTTP/HTTPS protokollerini destekler\",\n    \"wgc_checking_mode\": \"Mod kontrol ediliyor...\",\n    \"wgc_checking_running_mode\": \"Çalışma modu kontrol ediliyor...\",\n    \"wgc_control_panel_only\": \"Bu özellik yalnızca Sunshine Kontrol Panelinde mevcuttur\",\n    \"wgc_mode_switch_failed\": \"Mod değiştirilemedi\",\n    \"wgc_mode_switch_started\": \"Mod değişimi başlatıldı. Bir UAC istemi görünürse, onaylamak için lütfen 'Evet'e tıklayın.\",\n    \"wgc_service_mode_warning\": \"WGC yakalama, kullanıcı modunda çalışmayı gerektirir. Şu anda hizmet modunda çalışıyorsa, kullanıcı moduna geçmek için lütfen yukarıdaki düğmeye tıklayın.\",\n    \"wgc_switch_to_service_mode\": \"Hizmet Moduna Geç\",\n    \"wgc_switch_to_service_mode_tooltip\": \"Şu anda kullanıcı modunda çalışıyor. Hizmet moduna geçmek için tıklayın.\",\n    \"wgc_switch_to_user_mode\": \"Kullanıcı Moduna Geç\",\n    \"wgc_switch_to_user_mode_tooltip\": \"WGC yakalama, kullanıcı modunda çalışmayı gerektirir. Kullanıcı moduna geçmek için bu düğmeye tıklayın.\",\n    \"wgc_user_mode_available\": \"Şu anda kullanıcı modunda çalışıyor. WGC yakalama mevcut.\",\n    \"window_title\": \"Pencere Başlığı\",\n    \"window_title_desc\": \"Yakalanacak pencerenin başlığı (kısmi eşleşme, büyük/küçük harf duyarsız). Boş bırakılırsa, mevcut çalışan uygulama adı otomatik olarak kullanılacaktır.\",\n    \"window_title_placeholder\": \"örneğin, Uygulama Adı\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine, Moonlight için kendi kendine barındırılan bir oyun akışı sunucusudur.\",\n    \"download\": \"İndir\",\n    \"installed_version_not_stable\": \"Sunshine'ın yayın öncesi bir sürümünü çalıştırıyorsunuz. Hatalar veya başka sorunlar yaşayabilirsiniz. Lütfen karşılaştığınız sorunları bildirin. Sunshine'ın daha iyi bir yazılım olmasına yardımcı olduğunuz için teşekkür ederiz!\",\n    \"loading_latest\": \"Son sürüm yükleniyor...\",\n    \"new_pre_release\": \"Yeni Bir Yayın Öncesi Sürüm Mevcut!\",\n    \"new_stable\": \"Yeni bir Kararlı Sürüm Kullanılabilir!\",\n    \"startup_errors\": \"<b>Dikkat!</b> Sunshine başlatma sırasında bu hataları tespit etti. Yayınlamadan önce bunları düzeltmenizi <b>ŞİDDETLE TAVSİYE</b> EDERİZ.\",\n    \"update_download_confirm\": \"Güncelleme indirme sayfası tarayıcınızda açılacak. Devam edilsin mi?\",\n    \"version_dirty\": \"Sunshine'ı daha iyi bir yazılım haline getirmeye yardımcı olduğunuz için teşekkür ederiz!\",\n    \"version_latest\": \"Sunshine'ın en son sürümünü çalıştırıyorsunuz\",\n    \"view_logs\": \"Günlükleri görüntüle\",\n    \"welcome\": \"Merhaba, Günışığı!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Uygulamalar\",\n    \"configuration\": \"Konfigürasyon\",\n    \"home\": \"Ana Sayfa\",\n    \"password\": \"Şifre Değiştir\",\n    \"pin\": \"Pin\",\n    \"theme_auto\": \"Otomatik\",\n    \"theme_dark\": \"Karanlık\",\n    \"theme_light\": \"Açık\",\n    \"toggle_theme\": \"Tema\",\n    \"troubleshoot\": \"Sorun Giderme\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Şifreyi Onayla\",\n    \"current_creds\": \"Güncel Kimlik Bilgileri\",\n    \"new_creds\": \"Yeni Kimlik Bilgileri\",\n    \"new_username_desc\": \"Belirtilmezse, kullanıcı adı değişmez\",\n    \"password_change\": \"Şifre Değişikliği\",\n    \"success_msg\": \"Şifre başarıyla değiştirildi! Bu sayfa yakında yeniden yüklenecek, tarayıcınız sizden yeni kimlik bilgilerinizi isteyecektir.\"\n  },\n  \"pin\": {\n    \"actions\": \"İşlemler\",\n    \"cancel_editing\": \"Düzenlemeyi iptal et\",\n    \"client_name\": \"Ad\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Silme işlemini onayla\",\n    \"delete_client\": \"İstemciyi sil\",\n    \"delete_confirm_message\": \"<strong>{name}</strong> öğesini silmek istediğinizden emin misiniz?\",\n    \"delete_warning\": \"Bu işlem geri alınamaz.\",\n    \"device_name\": \"Cihaz Adı\",\n    \"device_size\": \"Cihaz boyutu\",\n    \"device_size_info\": \"<strong>Cihaz Boyutu</strong>: Akış deneyimini ve dokunmatik işlemleri optimize etmek için istemci cihazının ekran boyutu türünü (Küçük - Telefon, Orta - Tablet, Büyük - TV) ayarlayın.\",\n    \"device_size_large\": \"Büyük - TV\",\n    \"device_size_medium\": \"Orta - Tablet\",\n    \"device_size_small\": \"Küçük - Telefon\",\n    \"edit_client_settings\": \"İstemci ayarlarını düzenle\",\n    \"hdr_profile\": \"HDR Profili\",\n    \"hdr_profile_info\": \"<strong>HDR Profili</strong>: HDR içeriğin cihazda doğru görüntülenmesini sağlamak için bu istemci için kullanılacak HDR renk profilini (ICC dosyası) seçin. En son istemciyi kullanıyorsanız, parlaklık bilgilerinin ana bilgisayar sanal ekranına otomatik senkronizasyonunu desteklemek için bu alanı boş bırakarak otomatik senkronizasyonu etkinleştirin.\",\n    \"loading\": \"Yükleniyor...\",\n    \"loading_clients\": \"İstemciler yükleniyor...\",\n    \"modify_in_gui\": \"Lütfen grafik arayüzde değiştirin\",\n    \"none\": \"-- Yok --\",\n    \"or_manual_pin\": \"veya PIN kodunu manuel girin\",\n    \"pair_failure\": \"Eşleştirme Başarısız: PIN'in doğru yazılıp yazılmadığını kontrol edin\",\n    \"pair_success\": \"Başarılı! Devam etmek için lütfen Moonlight'ı kontrol ediniz\",\n    \"pin_pairing\": \"PIN Eşleştirme\",\n    \"qr_expires_in\": \"Süre dolmak üzere\",\n    \"qr_generate\": \"QR Kodu Oluştur\",\n    \"qr_paired_success\": \"Eşleştirme başarılı!\",\n    \"qr_pairing\": \"QR Kodu ile Eşleştirme\",\n    \"qr_pairing_desc\": \"Hızlı eşleştirme için bir QR kodu oluşturun. Otomatik eşleştirme için Moonlight istemcisiyle tarayın.\",\n    \"qr_pairing_warning\": \"Deneysel özellik. Eşleştirme başarısız olursa, lütfen aşağıdaki manuel PIN eşleştirmesini kullanın. Not: Bu özellik yalnızca LAN üzerinde çalışır.\",\n    \"qr_refresh\": \"QR Kodunu Yenile\",\n    \"remove_paired_devices_desc\": \"Eşleştirilmiş cihazlarınızı kaldırın.\",\n    \"save_changes\": \"Değişiklikleri kaydet\",\n    \"save_failed\": \"İstemci ayarları kaydedilemedi. Lütfen tekrar deneyin.\",\n    \"save_or_cancel_first\": \"Lütfen önce düzenlemeyi kaydedin veya iptal edin\",\n    \"send\": \"Gönder\",\n    \"unknown_client\": \"Bilinmeyen istemci\",\n    \"unpair_all_confirm\": \"Tüm istemcilerin eşleştirmesini kaldırmak istediğinizden emin misiniz? Bu işlem geri alınamaz.\",\n    \"unsaved_changes\": \"Kaydedilmemiş değişiklikler\",\n    \"warning_msg\": \"Eşleştirdiğiniz istemciye erişiminiz olduğundan emin olun. Bu yazılım bilgisayarınıza tam kontrol sağlayabilir, bu yüzden dikkatli olun!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android önerilir\",\n    \"client_downloads\": \"İstemci İndirmeleri\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"GitHub Tartışmaları\",\n    \"gpl_license_text_1\": \"Bu yazılım GPL-3.0 altında lisanslanmıştır. Kullanmakta, değiştirmekte ve dağıtmakta özgürsünüz.\",\n    \"gpl_license_text_2\": \"Açık kaynak ekosistemini korumak için, lütfen GPL-3.0 lisansını ihlal eden yazılımları kullanmaktan kaçının.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Topluluğa katıl\",\n    \"join_group_desc\": \"Yardım alın ve deneyimlerinizi paylaşın\",\n    \"legal\": \"Yasal\",\n    \"legal_desc\": \"Bu yazılımı kullanmaya devam ederek aşağıdaki belgelerde yer alan hüküm ve koşulları kabul etmiş olursunuz.\",\n    \"license\": \"Lisans\",\n    \"lizardbyte_website\": \"LizardByte Web Sitesi\",\n    \"official_website\": \"Resmi web sitesi\",\n    \"official_website_title\": \"AlkaidLab - Resmi web sitesi\",\n    \"open_source\": \"Açık Kaynak\",\n    \"open_source_desc\": \"Projeyi desteklemek için Star & Fork yapın\",\n    \"quick_start\": \"Hızlı Başlangıç\",\n    \"resources\": \"Kaynaklar\",\n    \"resources_desc\": \"Günışığı için Kaynaklar!\",\n    \"third_party_desc\": \"Üçüncü taraf bileşen bildirimleri\",\n    \"third_party_moonlight\": \"Dostça Bağlantılar\",\n    \"third_party_notice\": \"Üçüncü Taraf Bildirimi\",\n    \"tutorial\": \"Öğretici\",\n    \"tutorial_desc\": \"Detaylı yapılandırma ve kullanım kılavuzu\",\n    \"view_license\": \"Tam lisansı görüntüle\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Yapılandırma Özeti\",\n    \"android_client\": \"Android İstemcisi\",\n    \"base_display_title\": \"Sanal Ekran\",\n    \"choose_adapter\": \"Otomatik\",\n    \"config_saved\": \"Yapılandırma başarıyla kaydedildi.\",\n    \"description\": \"Hızlı bir kurulumla başlayalım\",\n    \"device_id\": \"Cihaz Kimliği\",\n    \"device_state\": \"Durum\",\n    \"download_clients\": \"İstemcileri İndir\",\n    \"finish\": \"Kurulumu Bitir\",\n    \"go_to_apps\": \"Uygulamaları Yapılandır\",\n    \"harmony_goto_repo\": \"Depoya git\",\n    \"harmony_modal_desc\": \"HarmonyOS NEXT Moonlight için HarmonyOS mağazasında Moonlight V+ arayın\",\n    \"harmony_modal_link_notice\": \"Bu bağlantı proje deposuna yönlendirecektir\",\n    \"ios_client\": \"iOS İstemcisi\",\n    \"load_error\": \"Yapılandırma yüklenemedi\",\n    \"next\": \"Sonraki\",\n    \"physical_display\": \"Fiziksel Ekran/EDID Öykünücü\",\n    \"physical_display_desc\": \"Gerçek fiziksel monitörlerinizi yayınlayın\",\n    \"previous\": \"Önceki\",\n    \"restart_countdown_unit\": \"saniye\",\n    \"restart_desc\": \"Yapılandırma kaydedildi. Sunshine görüntü ayarlarını uygulamak için yeniden başlatılıyor.\",\n    \"restart_go_now\": \"Şimdi git\",\n    \"restart_title\": \"Sunshine yeniden başlatılıyor\",\n    \"save_error\": \"Yapılandırma kaydedilemedi\",\n    \"select_adapter\": \"Grafik Adaptörü\",\n    \"selected_adapter\": \"Seçilen Adaptör\",\n    \"selected_display\": \"Seçilen Ekran\",\n    \"setup_complete\": \"Kurulum Tamamlandı!\",\n    \"setup_complete_desc\": \"Temel ayarlar şimdi aktif. Hemen bir Moonlight istemcisi ile yayın yapmaya başlayabilirsiniz!\",\n    \"skip\": \"Kurulum Sihirbazını Atla\",\n    \"skip_confirm\": \"Kurulum sihirbazını atlamak istediğinizden emin misiniz? Bu seçenekleri daha sonra ayarlar sayfasından yapılandırabilirsiniz.\",\n    \"skip_confirm_title\": \"Kurulum Sihirbazını Atla\",\n    \"skip_error\": \"Atlama başarısız oldu\",\n    \"state_active\": \"Aktif\",\n    \"state_inactive\": \"Pasif\",\n    \"state_primary\": \"Birincil\",\n    \"state_unknown\": \"Bilinmiyor\",\n    \"step0_description\": \"Arayüz dilinizi seçin\",\n    \"step0_title\": \"Dil\",\n    \"step1_description\": \"Yayınlanacak ekranı seçin\",\n    \"step1_title\": \"Ekran Seçimi\",\n    \"step1_vdd_intro\": \"Temel Ekran (VDD), Sunshine Foundation'ın yerleşik akıllı sanal ekranıdır ve herhangi bir çözünürlük, kare hızı ve HDR optimizasyonunu destekler. Ekran kapalı akış ve genişletilmiş ekran akışı için tercih edilen seçenektir.\",\n    \"step2_description\": \"Grafik adaptörünüzü seçin\",\n    \"step2_title\": \"Adaptör Seç\",\n    \"step3_description\": \"Görüntü cihazı hazırlık stratejisini seçin\",\n    \"step3_ensure_active\": \"Etkinleştirmeyi sağla\",\n    \"step3_ensure_active_desc\": \"Ekran etkin değilse etkinleştirir\",\n    \"step3_ensure_only_display\": \"Tek ekranı sağla\",\n    \"step3_ensure_only_display_desc\": \"Diğer tüm ekranları devre dışı bırakır ve yalnızca belirtilen ekranı etkinleştirir (önerilir)\",\n    \"step3_ensure_primary\": \"Birincil ekranı sağla\",\n    \"step3_ensure_primary_desc\": \"Ekranı etkinleştirir ve birincil ekran olarak ayarlar\",\n    \"step3_ensure_secondary\": \"İkincil yayın\",\n    \"step3_ensure_secondary_desc\": \"Yalnızca sanal ekranı ikincil genişletilmiş yayın için kullanır\",\n    \"step3_no_operation\": \"İşlem yok\",\n    \"step3_no_operation_desc\": \"Ekran durumunda değişiklik yapılmaz; kullanıcı ekranın hazır olduğundan emin olmalıdır\",\n    \"step3_title\": \"Görüntü Stratejisi\",\n    \"step4_title\": \"Tamamlandı\",\n    \"stream_mode\": \"Yayın Modu\",\n    \"unknown_display\": \"Bilinmeyen Ekran\",\n    \"virtual_display\": \"Sanal Ekran (ZakoHDR)\",\n    \"virtual_display_desc\": \"Sanal bir görüntü cihazı kullanarak yayın yapın (ZakoVDD sürücüsü kurulumu gerektirir)\",\n    \"welcome\": \"Sunshine Foundation'a Hoş Geldiniz\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Gelişmiş\",\n    \"amd\": \"AMD AMF Kodlayıcı\",\n    \"av\": \"Ses/Video\",\n    \"encoders\": \"Kodlayıcılar\",\n    \"files\": \"Yapılandırma Dosyaları\",\n    \"general\": \"Genel\",\n    \"input\": \"Giriş\",\n    \"network\": \"Ağ\",\n    \"nv\": \"NVIDIA NVENC Kodlayıcı\",\n    \"qsv\": \"Intel QuickSync Kodlayıcı\",\n    \"sw\": \"Yazılım Kodlayıcı\",\n    \"vaapi\": \"VAAPI Kodlayıcı\",\n    \"vt\": \"VideoToolbox Kodlayıcı\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Analiz ediliyor...\",\n    \"ai_analyzing_logs\": \"Günlükler analiz ediliyor, lütfen bekleyin...\",\n    \"ai_config\": \"AI Yapılandırma\",\n    \"ai_copy_result\": \"Kopyala\",\n    \"ai_diagnosis\": \"AI Tanılama\",\n    \"ai_diagnosis_title\": \"AI Günlük Tanılama\",\n    \"ai_error\": \"Analiz başarısız\",\n    \"ai_key_local\": \"API anahtarı yalnızca yerel olarak saklanır ve asla yüklenmez\",\n    \"ai_model\": \"Model\",\n    \"ai_provider\": \"Sağlayıcı\",\n    \"ai_reanalyze\": \"Yeniden analiz et\",\n    \"ai_result\": \"Tanılama Sonucu\",\n    \"ai_retry\": \"Tekrar dene\",\n    \"ai_start_diagnosis\": \"Tanılamayı Başlat\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Sunshine'ı hemen kapatmanız gerekiyorsa, bu işlevi kullanabilirsiniz. Kapatıldıktan sonra manuel olarak yeniden başlatmanız gerekeceğini unutmayın.\",\n    \"boom_sunshine_success\": \"Sunshine kapatıldı\",\n    \"confirm_boom\": \"Gerçekten çıkmak istiyor musunuz?\",\n    \"confirm_boom_desc\": \"Yani gerçekten çıkmak istiyorsunuz? Peki, sizi durduramam, devam edin ve tekrar tıklayın\",\n    \"confirm_logout\": \"Çıkışı onaylıyor musunuz?\",\n    \"confirm_logout_desc\": \"Web arayüzüne erişmek için şifrenizi tekrar girmeniz gerekecektir.\",\n    \"copy_config\": \"Yapılandırmayı kopyala\",\n    \"copy_config_error\": \"Yapılandırma kopyalanamadı\",\n    \"copy_config_success\": \"Yapılandırma panoya kopyalandı!\",\n    \"copy_logs\": \"Günlükleri kopyala\",\n    \"download_logs\": \"Günlükleri indir\",\n    \"force_close\": \"Kapatmaya Zorla\",\n    \"force_close_desc\": \"Moonlight çalışmakta olan bir uygulama hakkında şikayet ederse, uygulamayı kapatmaya zorlamak sorunu çözecektir.\",\n    \"force_close_error\": \"Uygulama kapatılırken hata oluştu\",\n    \"force_close_success\": \"Başvuru Başarıyla Sonuçlandı!\",\n    \"ignore_case\": \"Büyük/küçük harf duyarlılığını yok say\",\n    \"logout\": \"Çıkış\",\n    \"logout_desc\": \"Çıkış. Yeniden giriş yapmayı gerektirebilir.\",\n    \"logout_localhost_tip\": \"Mevcut ortam giriş gerektirmez; çıkış yapmak parola penceresini açmaz.\",\n    \"logs\": \"Günlükler\",\n    \"logs_desc\": \"Sunshine tarafından yüklenen günlüklere bakın\",\n    \"logs_find\": \"Bul...\",\n    \"match_contains\": \"İçerir\",\n    \"match_exact\": \"Tam\",\n    \"match_regex\": \"Düzenli ifade\",\n    \"reopen_setup_wizard\": \"Kurulum sihirbazını yeniden aç\",\n    \"reopen_setup_wizard_desc\": \"Başlangıç ayarlarını yeniden yapılandırmak için kurulum sihirbazı sayfasını yeniden açın.\",\n    \"reopen_setup_wizard_error\": \"Kurulum sihirbazını yeniden açma hatası\",\n    \"reset_display_device_desc_windows\": \"Sunshine değiştirilen görüntü cihazı ayarlarını geri yüklerken takılırsa, ayarları sıfırlayabilir ve görüntü durumunu manuel olarak geri yüklemeye devam edebilirsiniz.\\nBu, çeşitli nedenlerle olabilir: cihaz artık mevcut değil, farklı bir porta takılmış vb.\",\n    \"reset_display_device_error_windows\": \"Persistencji sıfırlama hatası!\",\n    \"reset_display_device_success_windows\": \"Persistencji sıfırlama başarılı!\",\n    \"reset_display_device_windows\": \"Ekran belleğini sıfırla\",\n    \"restart_sunshine\": \"Günışığını Yeniden Başlat\",\n    \"restart_sunshine_desc\": \"Sunshine düzgün çalışmıyorsa, yeniden başlatmayı deneyebilirsiniz. Bu, çalışan tüm oturumları sonlandıracaktır.\",\n    \"restart_sunshine_success\": \"Günışığı yeniden başlıyor\",\n    \"troubleshooting\": \"Sorun Giderme\",\n    \"unpair_all\": \"Tümünü Eşleşmeleri Kaldır\",\n    \"unpair_all_error\": \"Eşleştirme kaldırılırken hata oluştu\",\n    \"unpair_all_success\": \"Tüm cihazlar eşleştirilmemiş.\",\n    \"unpair_desc\": \"Eşleştirilmiş cihazlarınızı kaldırın. Etkin bir oturumu olan eşleştirilmemiş cihazlar bağlı kalmaya devam eder ancak bir oturumu başlatamaz veya devam ettiremez.\",\n    \"unpair_single_no_devices\": \"Eşleştirilmiş cihaz yok.\",\n    \"unpair_single_success\": \"Ancak, cihaz(lar) hala aktif bir oturumda olabilir. Açık oturumları sonlandırmak için yukarıdaki 'Kapatmaya Zorla' düğmesini kullanın.\",\n    \"unpair_single_unknown\": \"Bilinmeyen Müşteri\",\n    \"unpair_title\": \"Cihazların Eşleşmesini Kaldırma\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Şifreyi onayla\",\n    \"create_creds\": \"Başlamadan önce, Web UI'ye erişmek için yeni bir kullanıcı adı ve şifre oluşturmanız gerekmektedir.\",\n    \"create_creds_alert\": \"Sunshine'ın Web kullanıcı arayüzüne erişmek için aşağıdaki kimlik bilgileri gereklidir. Onları güvende tutun, çünkü bir daha asla görmeyeceksiniz!\",\n    \"creds_local_only\": \"Kimlik bilgileriniz çevrimdışı olarak yerel olarak depolanır ve hiçbir zaman bir sunucuya yüklenmez.\",\n    \"error\": \"Hata!\",\n    \"greeting\": \"Sunshine Foundation'a Hoş Geldiniz!\",\n    \"hide_password\": \"Şifreyi gizle\",\n    \"login\": \"Giriş\",\n    \"network_error\": \"Ağ hatası, lütfen bağlantınızı kontrol edin\",\n    \"password\": \"Şifre\",\n    \"password_match\": \"Şifreler eşleşiyor\",\n    \"password_mismatch\": \"Şifreler eşleşmiyor\",\n    \"server_error\": \"Sunucu hatası\",\n    \"show_password\": \"Şifreyi göster\",\n    \"success\": \"Başarılı!\",\n    \"username\": \"Kullanıcı Adı\",\n    \"welcome_success\": \"Bu sayfa yakında yeniden yüklenecek, tarayıcınız sizden yeni kimlik bilgilerinizi isteyecek\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/uk.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"Застосувати\",\n    \"auto\": \"Автоматично\",\n    \"autodetect\": \"Автовизначення (рекомендовано)\",\n    \"beta\": \"(бета-версія)\",\n    \"cancel\": \"Скасувати\",\n    \"close\": \"Закрити\",\n    \"copied\": \"Скопійовано в буфер обміну\",\n    \"copy\": \"Копіювати\",\n    \"delete\": \"Видалити\",\n    \"description\": \"Опис\",\n    \"disabled\": \"Вимкнено\",\n    \"disabled_def\": \"Вимкнено (за замовчуванням)\",\n    \"dismiss\": \"Відхилити\",\n    \"do_cmd\": \"Виконати команду\",\n    \"download\": \"Завантажити\",\n    \"edit\": \"Редагувати\",\n    \"elevated\": \"Потребуються\",\n    \"enabled\": \"Увімкнено\",\n    \"enabled_def\": \"Увімкнено (за замовчуванням)\",\n    \"error\": \"Помилка!\",\n    \"no_changes\": \"Немає змін\",\n    \"note\": \"Примітка:\",\n    \"password\": \"Пароль\",\n    \"remove\": \"Видалити\",\n    \"run_as\": \"Запустити від імені адміністратора\",\n    \"save\": \"Зберегти\",\n    \"see_more\": \"Дивитися більше\",\n    \"success\": \"Успішно!\",\n    \"undo_cmd\": \"Скасувати команду\",\n    \"username\": \"Ім'я користувача\",\n    \"warning\": \"Попередження!\"\n  },\n  \"apps\": {\n    \"actions\": \"Дії\",\n    \"add_cmds\": \"Додати команди\",\n    \"add_new\": \"Додати новий\",\n    \"advanced_options\": \"Розширені параметри\",\n    \"app_name\": \"Назва програми\",\n    \"app_name_desc\": \"Назва програми, як показано в Moonlight\",\n    \"applications_desc\": \"Програми оновлюються лише після перезапуску Клієнта\",\n    \"applications_title\": \"Програми\",\n    \"auto_detach\": \"Продовжити стримінг, якщо програма швидко завершується\",\n    \"auto_detach_desc\": \"Ц зробить спробу автоматично виявити програми типу launcher, які швидко закриваються після запуску або запуску іншої програми. Якщо буде виявлено програму типу launcher, її буде розпізнано як окрему програму.\",\n    \"basic_info\": \"Основна інформація\",\n    \"cmd\": \"Команда\",\n    \"cmd_desc\": \"Основна програма для запуску. Якщо поле порожнє, жодна програма не буде запущена.\",\n    \"cmd_examples_title\": \"Загальні приклади:\",\n    \"cmd_note\": \"Якщо шлях до виконуваного файлу команди містить пробіли, ви повинні взяти його в лапки.\",\n    \"cmd_prep_desc\": \"Список команд, які потрібно запустити до/після цього додатка. Якщо будь-яка з підготовчих команд не спрацює, запуск програми буде перервано.\",\n    \"cmd_prep_name\": \"Підготовчі Команди\",\n    \"command_settings\": \"Налаштування команд\",\n    \"covers_found\": \"Обкладинки знайдено\",\n    \"delete\": \"Видалити\",\n    \"delete_confirm\": \"Are you sure you want to delete \\\"{name}\\\"?\",\n    \"detached_cmds\": \"Відокремлені команди\",\n    \"detached_cmds_add\": \"Додати окрему команду\",\n    \"detached_cmds_desc\": \"Список команд для запуску у фоновому режимі.\",\n    \"detached_cmds_note\": \"Якщо шлях до виконуваного файлу команди містить пробіли, ви повинні взяти його в лапки.\",\n    \"detached_cmds_remove\": \"Видалити відокремлену команду\",\n    \"edit\": \"Редагувати\",\n    \"env_app_id\": \"ID застосунку\",\n    \"env_app_name\": \"Назва програми\",\n    \"env_client_audio_config\": \"Конфігурація аудіо, яку запитує клієнт (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"Клієнт запросив опцію для оптимізації гри та оптимальної якості стримінгу (так/ні)\",\n    \"env_client_fps\": \"FPS, який запитує клієнт (ціле число)\",\n    \"env_client_gcmap\": \"Запитувана маска геймпада у форматі бітового набору/бітового поля (ціле число)\",\n    \"env_client_hdr\": \"HDR увімкнено клієнтом (так/ні)\",\n    \"env_client_height\": \"Висота, яку запитує клієнт (ціле число)\",\n    \"env_client_host_audio\": \"Клієнт запросив аудіо хоста (так/ні)\",\n    \"env_client_name\": \"Зручне ім'я клієнта (рядок)\",\n    \"env_client_width\": \"Ширина, яку запитує клієнт (int)\",\n    \"env_displayplacer_example\": \"Приклад - displayplacer для Автоматизації Роздільної Здатності:\",\n    \"env_qres_example\": \"Приклад - QR-коди для Автоматизації Роздільної Здатності:\",\n    \"env_qres_path\": \"qres шлях\",\n    \"env_var_name\": \"Назва Змінної Середовища\",\n    \"env_vars_about\": \"Про Змінні Середовища\",\n    \"env_vars_desc\": \"Всі команди отримують ці Змінні Середовища за замовчуванням:\",\n    \"env_xrandr_example\": \"Приклад - Xrandr для Автоматизації Роздільної Здатності:\",\n    \"exit_timeout\": \"Тайм-аут виходу\",\n    \"exit_timeout_desc\": \"Кількість секунд, протягом яких всі процеси програми будуть примусово завершені після запиту на вихід. Якщо значення не встановлено, за замовчуванням програма буде чекати до 5 секунд. Якщо встановлено на нуль або від'ємне значення, програму буде негайно завершено.\",\n    \"file_selector_not_initialized\": \"Селектор файлів не ініціалізовано\",\n    \"find_cover\": \"Знайти обкладинку\",\n    \"form_invalid\": \"Будь ласка, перевірте обов'язкові поля\",\n    \"form_valid\": \"Дійсна програма\",\n    \"global_prep_desc\": \"Ввімкнути/Вимкнути виконання глобальних команд Prep для цього застосунку.\",\n    \"global_prep_name\": \"Глобальні команди підготовки\",\n    \"image\": \"Зображення\",\n    \"image_desc\": \"Іконка програми/зображення/шлях до зображення, яке буде надіслано клієнту. Зображення має бути у форматі PNG. Якщо не вказано, Sunshine надішле зображення за замовчуванням.\",\n    \"image_settings\": \"Налаштування зображення\",\n    \"loading\": \"Завантаження...\",\n    \"menu_cmd_actions\": \"Дії\",\n    \"menu_cmd_add\": \"Додати команду меню\",\n    \"menu_cmd_command\": \"Команда\",\n    \"menu_cmd_desc\": \"Після налаштування ці команди будуть видимі в меню повернення клієнта, дозволяючи швидке виконання певних операцій без переривання потоку, наприклад, запуск допоміжних програм.\\nПриклад: Відображається ім'я - Закрити комп'ютер; Команда - shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"Відображається ім'я\",\n    \"menu_cmd_drag_sort\": \"Перетягніть для сортування\",\n    \"menu_cmd_name\": \"Команди меню\",\n    \"menu_cmd_placeholder_command\": \"Команда\",\n    \"menu_cmd_placeholder_display_name\": \"Відображається ім'я\",\n    \"menu_cmd_placeholder_execute\": \"Виконати команду\",\n    \"menu_cmd_placeholder_undo\": \"Скасувати команду\",\n    \"menu_cmd_remove_menu\": \"Видалити команду меню\",\n    \"menu_cmd_remove_prep\": \"Видалити команду підготовки\",\n    \"mouse_mode\": \"Режим миші\",\n    \"mouse_mode_auto\": \"Авто (Глобальне налаштування)\",\n    \"mouse_mode_desc\": \"Оберіть метод введення миші для цього додатку. Авто використовує глобальне налаштування, Віртуальна миша використовує HID-драйвер, SendInput використовує Windows API.\",\n    \"mouse_mode_sendinput\": \"SendInput (Windows API)\",\n    \"mouse_mode_vmouse\": \"Віртуальна миша\",\n    \"name\": \"Ім'я\",\n    \"output_desc\": \"Файл, в якому зберігається вивід команди, якщо його не вказано, то вивід ігнорується\",\n    \"output_name\": \"Виведення\",\n    \"run_as_desc\": \"Це може знадобитися для деяких програм, які потребують дозволів адміністратора для нормального функціонування.\",\n    \"scan_result_add_all\": \"Додати все\",\n    \"scan_result_edit_title\": \"Додати та редагувати\",\n    \"scan_result_filter_all\": \"Усе\",\n    \"scan_result_filter_epic_title\": \"Ігри Epic Games\",\n    \"scan_result_filter_executable\": \"Виконуваний\",\n    \"scan_result_filter_executable_title\": \"Виконуваний файл\",\n    \"scan_result_filter_gog_title\": \"Ігри GOG Galaxy\",\n    \"scan_result_filter_script\": \"Скрипт\",\n    \"scan_result_filter_script_title\": \"Пакетний/командний скрипт\",\n    \"scan_result_filter_shortcut\": \"Ярлик\",\n    \"scan_result_filter_shortcut_title\": \"Ярлик\",\n    \"scan_result_filter_steam_title\": \"Ігри Steam\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"Гра\",\n    \"scan_result_games_only\": \"Тільки ігри\",\n    \"scan_result_matched\": \"Збігів: {count}\",\n    \"scan_result_no_apps\": \"Програми для додавання не знайдено\",\n    \"scan_result_no_matches\": \"Відповідних програм не знайдено\",\n    \"scan_result_quick_add_title\": \"Швидке додавання\",\n    \"scan_result_remove_title\": \"Видалити зі списку\",\n    \"scan_result_search_placeholder\": \"Пошук назви програми, команди або шляху...\",\n    \"scan_result_show_all\": \"Показати все\",\n    \"scan_result_title\": \"Результати сканування\",\n    \"scan_result_try_different_keywords\": \"Спробуйте використати інші ключові слова для пошуку\",\n    \"scan_result_type_batch\": \"Пакетний\",\n    \"scan_result_type_command\": \"Командний скрипт\",\n    \"scan_result_type_executable\": \"Виконуваний файл\",\n    \"scan_result_type_shortcut\": \"Ярлик\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"Пошук програм...\",\n    \"select\": \"Вибрати\",\n    \"test_menu_cmd\": \"Тестувати команду\",\n    \"test_menu_cmd_empty\": \"Команда не може бути порожньою\",\n    \"test_menu_cmd_executing\": \"Виконання команди...\",\n    \"test_menu_cmd_failed\": \"Помилка виконання команди\",\n    \"test_menu_cmd_success\": \"Команда успішно виконана!\",\n    \"use_desktop_image\": \"Використовувати поточні шпалери робочого столу\",\n    \"wait_all\": \"Продовжуйте стримінг доти, доки всі процеси програми не завершаться\",\n    \"wait_all_desc\": \"Це продовжить стримінг доти, доки не завершаться всі процеси, запущені програмою. Якщо цей прапорець не ввімкнений, стримінг припиниться після закриття початкової програми, навіть якщо інші процеси програми все ще запущено.\",\n    \"working_dir\": \"Робочий каталог\",\n    \"working_dir_desc\": \"Робочий каталог, який переданий процесу. Наприклад, деякі програми використовують робочий каталог для пошуку файлів конфігурації. Якщо цей параметр не встановлено, Sunshine за замовчуванням буде використовувати батьківський каталог команди\"\n  },\n  \"config\": {\n    \"adapter_name\": \"Назва Адаптера\",\n    \"adapter_name_desc_linux_1\": \"Вкажіть вручну GPU для захоплення.\",\n    \"adapter_name_desc_linux_2\": \"знайти всі пристрої з підтримкою VAAPI\",\n    \"adapter_name_desc_linux_3\": \"Замініть ``renderD129`` на пристрій зверху, щоб вивести назву та можливості пристрою. Для підтримки Sunshine пристрій повинен мати як мінімум такі параметри:\",\n    \"adapter_name_desc_windows\": \"Вручну вкажіть GPU для захоплення. Якщо GPU не встановлений вручну, то його буде обрано автоматично. Ми наполегливо рекомендуємо залишити це поле порожнім, щоб використовувати автоматичний вибір GPU! Зауважимо, що цей GPU повинен бути ввімкнутим та під'\\nєднаним. Допустимі значення можна знайти за допомогою наступної команди:\",\n    \"adapter_name_desc_windows_vdd_hint\": \"Якщо встановлено останню версію віртуального дисплея, вона може автоматично асоціюватися з прив'язкою графічного процесора\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"Додати\",\n    \"address_family\": \"Сімейство Адрес\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"Встановити сімейство адрес, що використовується в Sunshine\",\n    \"address_family_ipv4\": \"Тільки IPv4\",\n    \"always_send_scancodes\": \"Завжди Надсилати Скан-коди\",\n    \"always_send_scancodes_desc\": \"Надсилання скан-кодів покращує сумісність з іграми та програмами, але може призвести до некоректного введення з клавіатури деякими клієнтами, які не використовують розкладку клавіатури США. Увімкніть, якщо введення з клавіатури взагалі не працює у певних програмах. Вимкніть, якщо клавіші на клієнті генерують неправильні клавіші на хості.\",\n    \"amd_coder\": \"AMF Coder (H264)\",\n    \"amd_coder_desc\": \"Дозволяє вибрати ентропійне кодування, щоб надати пріоритет якості або швидкості кодування. Тільки H.264.\",\n    \"amd_enforce_hrd\": \"Примусове застосування AMF Hypothetical Reference Decoder (HRD)\",\n    \"amd_enforce_hrd_desc\": \"Збільшує обмеження на керування швидкістю, щоб відповідати вимогам моделі HRD. Це значно зменшує переповнення бітрейту, але може спричинити артефакти кодування або зниження якості на певних GPU.\",\n    \"amd_preanalysis\": \"Попередній аналіз AMF\",\n    \"amd_preanalysis_desc\": \"Це вмикає попередній аналіз контролю швидкості, що може підвищити якість шляхом збільшення затримки кодування.\",\n    \"amd_quality\": \"Якість AMF\",\n    \"amd_quality_balanced\": \"balanced -- збалансований (за замовчуванням)\",\n    \"amd_quality_desc\": \"Це дозволяє контролювати баланс між швидкістю та якістю кодування.\",\n    \"amd_quality_group\": \"Налаштування якості AMF\",\n    \"amd_quality_quality\": \"якість - пріоритизувати якість\",\n    \"amd_quality_speed\": \"швидкість - пріоритизувати швидкість\",\n    \"amd_qvbr_quality\": \"Рівень якості AMF QVBR\",\n    \"amd_qvbr_quality_desc\": \"Рівень якості для режиму керування бітрейтом QVBR. Діапазон: 1-51 (нижче = краща якість). За замовчуванням: 23. Застосовується лише коли керування бітрейтом встановлено на 'qvbr'.\",\n    \"amd_rc\": \"Контроль Швидкості AMF\",\n    \"amd_rc_cbr\": \"cbr -- постійний бітрейт (рекомендується, якщо увімкнено HRD)\",\n    \"amd_rc_cqp\": \"cqp -- постійний режим qp\",\n    \"amd_rc_desc\": \"Цей параметр контролює метод керування швидкістю, щоб переконатися, що ми не перевищуємо цільовий бітрейт клієнта. 'cqp' не підходить для визначення бітрейту, а інші параметри, окрім 'vbr_latency', залежать від Примусового HRD, щоб допомогти уникнути перезаповнення бітрейту.\",\n    \"amd_rc_group\": \"Налаштування контролю швидкості AMF\",\n    \"amd_rc_hqcbr\": \"hqcbr -- високоякісний постійний бітрейт\",\n    \"amd_rc_hqvbr\": \"hqvbr -- високоякісний змінний бітрейт\",\n    \"amd_rc_qvbr\": \"qvbr -- змінний бітрейт якості (використовує рівень якості QVBR)\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- обмеженням затримки змінного бітрейту (рекомендується, якщо HRD вимкнено; за замовчуванням)\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- пікове обмеження змінного бітрейту\",\n    \"amd_usage\": \"Використання AMF\",\n    \"amd_usage_desc\": \"Тут встановлюється базовий профіль кодування. Усі параметри, наведені нижче, перевизначають підмножину профілю використання, але також застосовуються додаткові приховані налаштування, які не можна налаштувати деінде, окрім як тут.\",\n    \"amd_usage_lowlatency\": \"lowlatency - з низькою затримкою (найшвидший)\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality - низька затримка, висока якість (швидкий)\",\n    \"amd_usage_transcoding\": \"transcoding -- перекодування (найповільніше)\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency - наднизька затримка (найшвидша; за замовчуванням)\",\n    \"amd_usage_webcam\": \"веб-камера -- веб-камера (повільно)\",\n    \"amd_vbaq\": \"Адаптивне квантування на основі дисперсії AMF (VBAQ)\",\n    \"amd_vbaq_desc\": \"Людський зір зазвичай менш чутливий до артефактів у високотекстурованих областях. У режимі VBAQ дисперсія пікселів використовується для позначення складності просторових текстур, що дозволяє кодеру виділяти більше бітів для більш гладких ділянок. Увімкнення цієї функції призводить до покращення суб'єктивної візуальної якості певного контенту.\",\n    \"amf_draw_mouse_cursor\": \"Малювати простий курсор при використанні методу захоплення AMF\",\n    \"amf_draw_mouse_cursor_desc\": \"У деяких випадках використання захоплення AMF не відображатиме вказівник миші. Увімкнення цієї опції намалює простий вказівник миші на екрані. Примітка: Позиція вказівника миші оновлюватиметься лише при оновленні екрана вмісту, тому в сценаріях поза грою, наприклад на робочому столі, ви можете спостерігати повільний рух вказівника миші.\",\n    \"apply_note\": \"Натисніть \\\"Застосувати\\\", щоб перезапустити Sunshine і застосувати зміни. Це призведе до завершення всіх запущених сеансів.\",\n    \"audio_sink\": \"Пристрій виведення аудіо\",\n    \"audio_sink_desc_linux\": \"Назва пристрою аудіовиводу, що використовується для зациклення звуку. Якщо ви не вкажете цю змінну, pulseaudio вибере пристрій за замовчуванням. Ви можете дізнатися назву пристрою аудіовиводу за допомогою будь-якої з команд:\",\n    \"audio_sink_desc_macos\": \"Назва пристрою аудіовиводу, що використовується для зациклення звуку (Loopback). Sunshine може отримати доступ до мікрофонів лише на macOS через системні обмеження. Для трансляції системного аудіо за допомогою Soundflower або BlackHole.\",\n    \"audio_sink_desc_windows\": \"Вручну вкажіть конкретний аудіопристрій для захоплення. Якщо не вказано, пристрій буде обрано автоматично. Ми наполегливо рекомендуємо залишити це поле порожнім, щоб використовувати автоматичний вибір пристрою! Якщо у вас є кілька аудіопристроїв з однаковими іменами, ви можете отримати ідентифікатор пристрою за допомогою наступної команди:\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"Динаміки (High Definition аудіопристрої)\",\n    \"av1_mode\": \"Підтримка AV1\",\n    \"av1_mode_0\": \"Sunshine пропонуватиме підтримку AV1 на основі можливостей кодерів (рекомендовано)\",\n    \"av1_mode_1\": \"Sunshine не буде пропонувати підтримку AV1\",\n    \"av1_mode_2\": \"Sunshine пропонуватиме підтримку 8-бітового профілю AV1 Main\",\n    \"av1_mode_3\": \"Sunshine пропонуватиме підтримку профілів AV1 Main 8-біт і 10-біт (HDR)\",\n    \"av1_mode_desc\": \"Дозволяє клієнту запитувати основні 8-бітні або 10-бітні відеопотоки AV1. Кодування AV1 вимагає більше ресурсів CPU, тому увімкнення цієї опції може знизити продуктивність при використанні програмного кодування.\",\n    \"back_button_timeout\": \"Тайм-аут емуляції Home/Guide кнопок керування\",\n    \"back_button_timeout_desc\": \"Якщо утримувати кнопки Back/Select протягом вказаної кількості мілісекунд, імітується натискання кнопки Home/Guide. Якщо встановлено значення < 0 (за замовчуванням), утримання кнопки Back/Select не імітуватиме натискання кнопок Home/Guide.\",\n    \"bind_address\": \"Адреса прив'язки (тестова функція)\",\n    \"bind_address_desc\": \"Встановіть конкретну IP-адресу, до якої буде прив'язаний Sunshine. Якщо залишити пустим, Sunshine буде прив'язаний до всіх доступних адрес.\",\n    \"capture\": \"Примусове застосування конкретного методу захоплення (Capture)\",\n    \"capture_desc\": \"У автоматичному режимі Sunshine використовуватиме перший-ліпший драйвер. Для роботи NvFBC потрібні пропатчені драйвери nVidia.\",\n    \"capture_target\": \"Ціль захоплення\",\n    \"capture_target_desc\": \"Виберіть тип цілі для захоплення. Вибравши 'Вікно', ви можете захопити конкретне вікно програми (наприклад, програму для AI інтерполяції кадрів) замість всього дисплея.\",\n    \"capture_target_display\": \"Дисплей\",\n    \"capture_target_window\": \"Вікно\",\n    \"cert\": \"Сертифікат\",\n    \"cert_desc\": \"Сертифікат, який використовується для створення пари між веб UI й клієнтом Moonlight. Для найкращої сумісності він повинен мати відкритий ключ RSA-2048.\",\n    \"channels\": \"Максимальна кількість підключених клієнтів\",\n    \"channels_desc_1\": \"Sunshine може дозволити спільний доступ до однієї стримінгової сесії кільком клієнтам одночасно.\",\n    \"channels_desc_2\": \"Деякі апаратні кодери можуть мати обмеження, які знижують продуктивність при роботі з декількома потоками.\",\n    \"close_verify_safe\": \"Безпечний замість замкненого перевірки\",\n    \"close_verify_safe_desc\": \"Старий клієнт може не підтримувати Sunshine, будь ласка, вимкніть цей параметр або оновіть клієнт\",\n    \"coder_cabac\": \"cabac -- контекстно-адаптивне двійкове арифметичне кодування - вища якість\",\n    \"coder_cavlc\": \"cavlc -- контекстно-адаптивне кодування змінної довжини - швидке декодування\",\n    \"configuration\": \"Налаштування\",\n    \"controller\": \"Увімкнути введення з геймпада\",\n    \"controller_desc\": \"Дозволяє гостям керувати хост-системою за допомогою геймпада / контролера\",\n    \"credentials_file\": \"Файл облікових даних\",\n    \"credentials_file_desc\": \"Зберігайте ім'я користувача/пароль окремо від файлу стану Sunshine.\",\n    \"display_device_options_note_desc_windows\": \"Windows зберігає різні налаштування відображення для кожної комбінації наразі активних дисплеїв.\\nSunshine потім застосовує зміни до дисплея(-їв), що належить до такої комбінації дисплеїв.\\nЯкщо ви від'єднаєте пристрій, який був активним, коли Sunshine застосував налаштування, зміни не можуть бути\\nскасовані, поки комбінацію не вдасться активувати знову на момент, коли Sunshine спробує скасувати зміни!\",\n    \"display_device_options_note_windows\": \"Примітка про те, як застосовуються налаштування\",\n    \"display_device_options_windows\": \"Налаштування пристрою відображення\",\n    \"display_device_prep_ensure_active_desc_windows\": \"Активує дисплей, якщо він ще не активний\",\n    \"display_device_prep_ensure_active_windows\": \"Автоматично активувати дисплей\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"Вимикає всі інші дисплеї та вмикає лише вказаний\",\n    \"display_device_prep_ensure_only_display_windows\": \"Деактивувати інші дисплеї та активувати лише вказаний дисплей\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"Активує дисплей та встановлює його як основний\",\n    \"display_device_prep_ensure_primary_windows\": \"Автоматично активувати дисплей та зробити його основним\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"Використовує лише віртуальний дисплей для вторинного розширеного стримінгу\",\n    \"display_device_prep_ensure_secondary_windows\": \"Стримінг вторинного дисплея (лише віртуальний дисплей)\",\n    \"display_device_prep_no_operation_desc_windows\": \"Без змін стану дисплея; користувач повинен самостійно переконатися, що дисплей готовий\",\n    \"display_device_prep_no_operation_windows\": \"Вимкнено\",\n    \"display_device_prep_windows\": \"Підготовка дисплея\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"Необхідно вказати принаймні одне \\\"отримане\\\" та одне \\\"кінцеве\\\" значення.\\nПорожнє поле в розділі \\\"отримані\\\" означає \\\"відповідати будь-якому значенню\\\". Порожнє поле в розділі \\\"кінцеві\\\" означає \\\"зберегти отримане значення\\\".\\nЗа бажанням ви можете зіставити конкретне значення FPS з конкретною роздільною здатністю...\\n\\nПримітка: якщо в клієнті Moonlight не ввімкнено опцію \\\"Оптимізувати налаштування гри\\\", рядки, що містять значення роздільної здатності, ігноруються.\",\n    \"display_mode_remapping_desc_windows\": \"Вкажіть, як певна роздільна здатність та/або частота оновлення повинні бути перепризначені на інші значення.\\nВи можете стрімити з нижчою роздільною здатністю, при цьому рендеринг на хості виконується з вищою роздільною здатністю для ефекту суперсемплінгу.\\nАбо ви можете стрімити з вищим FPS, обмежуючи частоту оновлення хоста нижчим значенням.\\nЗіставлення виконується зверху вниз. Як тільки запис збігається, інші більше не перевіряються, але все одно валідуються.\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"Кінцева частота оновлення\",\n    \"display_mode_remapping_final_resolution_windows\": \"Кінцева роздільна здатність\",\n    \"display_mode_remapping_optional\": \"необов'язково\",\n    \"display_mode_remapping_received_fps_windows\": \"Отриманий FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"Отримана роздільна здатність\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"Примітка: якщо в клієнті Moonlight не ввімкнено опцію \\\"Оптимізувати налаштування гри\\\", перепризначення вимкнено.\",\n    \"display_mode_remapping_windows\": \"Перепризначити режими дисплея\",\n    \"display_modes\": \"Режими відображення\",\n    \"ds4_back_as_touchpad_click\": \"Призначити клавіші Back/Select на сенсорну клавіатуру\",\n    \"ds4_back_as_touchpad_click_desc\": \"При включеній примусовій емуляції DS4, налаштуйте Back/Select на клацання touchpad'а\",\n    \"dsu_server_port\": \"DSU Server Port\",\n    \"dsu_server_port_desc\": \"DSU server listening port (default 26760). Sunshine will act as a DSU server to receive client connections and send motion data. Enable DSU server in your client(Yuzu,Ryujinx etc.) and set DSU server address(127.0.0.1) and port(26760)\",\n    \"enable_dsu_server\": \"Увімкнути сервер DSU\",\n    \"enable_dsu_server_desc\": \"Enable DSU server to receive client connections and send motion data\",\n    \"encoder\": \"Примусове використання певного кодера\",\n    \"encoder_desc\": \"Примусово використовуйте конкретний кодер, інакше Sunshine обере найкращий з доступних варіантів. Примітка: Якщо ви вказуєте апаратний кодер у Windows, він має відповідати графічному процесору, до якого під'єднано монітор.\",\n    \"encoder_software\": \"Програмне забезпечення\",\n    \"experimental\": \"Експериментально\",\n    \"experimental_features\": \"Експериментальні функції\",\n    \"external_ip\": \"Зовнішня IP-адреса\",\n    \"external_ip_desc\": \"Якщо зовнішню IP-адресу не вказано, Sunshine автоматично визначить зовнішню IP-адресу\",\n    \"fec_percentage\": \"Відсоток FEC\",\n    \"fec_percentage_desc\": \"Відсоток пакетів виправлення помилок на кожен пакет даних у кожному відеокадрі. Вищі значення можуть викликати більшу втрату мережевих пакетів, але використовувати збільшену пропускну здатність.\",\n    \"ffmpeg_auto\": \"auto -- дозволити ffmpeg вирішувати (за замовчуванням)\",\n    \"file_apps\": \"Файли програми\",\n    \"file_apps_desc\": \"Файл, у якому зберігаються поточні програми Sunshine.\",\n    \"file_state\": \"Файл стану\",\n    \"file_state_desc\": \"Файл, у якому зберігається поточний стан Sunshine\",\n    \"fps\": \"Оголошені FPS\",\n    \"gamepad\": \"Тип емульованого геймпаду\",\n    \"gamepad_auto\": \"Параметри автоматичного вибору\",\n    \"gamepad_desc\": \"Виберіть тип геймпаду для емуляції на хості\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"Опції DS4\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"Налаштування DS4 вручну\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"Підготовчі Команди\",\n    \"global_prep_cmd_desc\": \"Налаштуйте список команд, які потрібно виконати до або після запуску будь-якої програми. Якщо будь-яка із зазначених команд підготовки не спрацює, процес запуску програми буде перервано.\",\n    \"hdr_luminance_analysis\": \"Динамічні метадані HDR (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"Вмикає покадровий аналіз яскравості GPU та вбудовує динамічні метадані HDR10+ (ST 2094-40) та HDR Vivid (CUVA) в кодований потік. Надає покадрові підказки тонального відображення для підтримуваних дисплеїв. Додає невелике навантаження GPU (~0,5-1,5мс/кадр при високих роздільностях). Вимкніть при падінні частоти кадрів з HDR.\",\n    \"hdr_prep_automatic_windows\": \"Switch on/off the HDR mode as requested by the client\",\n    \"hdr_prep_no_operation_windows\": \"Disabled\",\n    \"hdr_prep_windows\": \"HDR state change\",\n    \"hevc_mode\": \"Підтримка HEVC\",\n    \"hevc_mode_0\": \"Sunshine буде рекламувати підтримку HEVC на основі можливостей кодера (рекомендовано)\",\n    \"hevc_mode_1\": \"Sunshine не буде пропонувати підтримку HEVC\",\n    \"hevc_mode_2\": \"Sunshine пропонуватиме підтримку для Основного HEVC профілю\",\n    \"hevc_mode_3\": \"Sunshine пропонуватиме підтримку профілів HEVC Main та Main10 (HDR)\",\n    \"hevc_mode_desc\": \"Дозволяє клієнту запитувати відеопотоки HEVC Main або HEVC Main10. Кодування HEVC вимагає більше ресурсів CPU, тому увімкнення цієї опції може знизити продуктивність при використанні програмного кодування.\",\n    \"high_resolution_scrolling\": \"Підтримка прокрутки з високою роздільною здатністю\",\n    \"high_resolution_scrolling_desc\": \"Якщо увімкнено, Sunshine пропускатиме події прокрутки з високою роздільною здатністю від клієнтів Moonlight. Вимкнення цього може бути корисним для старих програм, які прокручують вміст занадто швидко за допомогою подій прокрутки у високій роздільній здатності.\",\n    \"install_steam_audio_drivers\": \"Встановити звукові Steam драйвери\",\n    \"install_steam_audio_drivers_desc\": \"Якщо Steam інстальовано, це автоматично інсталює драйвер Steam Streaming Speakers для підтримки об'ємного звуку 5.1/7.1 і вимкнення звуку хост-комп'ютера.\",\n    \"key_repeat_delay\": \"Затримка повтору клавіш\",\n    \"key_repeat_delay_desc\": \"Керування швидкістю повторення клавіш. Початкова затримка у мілісекундах перед повторенням натискання.\",\n    \"key_repeat_frequency\": \"Частота повторення клавіш\",\n    \"key_repeat_frequency_desc\": \"Як часто клавіші повторюються щосекунди. Цей параметр підтримує десяткові числа.\",\n    \"key_rightalt_to_key_win\": \"Прив'язати правий Alt до клавіші Windows\",\n    \"key_rightalt_to_key_win_desc\": \"Може статися так, що ви не зможете надіслати команду клавіші Windows з Moonshine напряму. У таких випадках може бути корисним змусити Sunshine вважати клавішу Alt праворуч клавішею Windows\",\n    \"key_rightalt_to_key_windows\": \"Map Right Alt key to Windows key\",\n    \"keyboard\": \"Увімкнути введення з клавіатури\",\n    \"keyboard_desc\": \"Дозволити гостям керувати хост-системою за допомогою клавіатури\",\n    \"lan_encryption_mode\": \"Режим шифрування LAN мережі\",\n    \"lan_encryption_mode_1\": \"Увімкнено для підтримуваних клієнтів\",\n    \"lan_encryption_mode_2\": \"Обов'язкове для всіх клієнтів\",\n    \"lan_encryption_mode_desc\": \"Цей параметр визначає, коли буде використовуватися шифрування під час потокового передавання через локальну мережу. Шифрування може знизити продуктивність потокового передавання, особливо на менш потужних хостах і клієнтах.\",\n    \"locale\": \"Мова\",\n    \"locale_desc\": \"Мова, що використовується для Sunshine UI.\",\n    \"log_level\": \"Рівень Логування\",\n    \"log_level_0\": \"Детально (Verbose)\",\n    \"log_level_1\": \"Режим налагодження (Debug)\",\n    \"log_level_2\": \"Інфо\",\n    \"log_level_3\": \"Попередження\",\n    \"log_level_4\": \"Помилка\",\n    \"log_level_5\": \"Критична помилка\",\n    \"log_level_6\": \"Нічого\",\n    \"log_level_desc\": \"Мінімальний стандартний рівень логування, що виводиться\",\n    \"log_path\": \"Шлях до лог-файлу\",\n    \"log_path_desc\": \"Файл, у якому зберігаються поточні логи Sunshine.\",\n    \"max_bitrate\": \"Максимальний бітрейт\",\n    \"max_bitrate_desc\": \"Максимальний бітрейт (в Kbp), який здійснює кодування Sunshine на нього. Якщо встановлено в 0, то він завжди буде використовувати бітрейт із проханням Місячного світла.\",\n    \"max_fps_reached\": \"Досягнуто максимальні значення FPS\",\n    \"max_resolutions_reached\": \"Досягнуто максимальну кількість роздільних здатностей\",\n    \"mdns_broadcast\": \"Автоматичне виявлення комп'ютера в локальній мережі\",\n    \"mdns_broadcast_desc\": \"Увімкнення цієї опції дозволяє автоматично виявляти комп'ютер в локальній мережі, якщо Moonlight налаштований на автоматичне виявлення комп'ютера в локальній мережі\",\n    \"min_threads\": \"Мінімальна кількість потоків CPU\",\n    \"min_threads_desc\": \"Збільшення значення дещо знижує ефективність кодування, але цей компроміс зазвичай вартий того, щоб отримати можливість використовувати більше ядер CPU для кодування. Ідеальне значення - це найменше значення, яке може надійно кодувати за бажаних налаштувань стримінгу на вашому обладнанні.\",\n    \"minimum_fps_target\": \"Мінімальна ціль FPS\",\n    \"minimum_fps_target_desc\": \"Minimum FPS to maintain when encoding (0 = auto, about half the stream FPS; 1-1000 = minimum FPS to maintain). When variable refresh rate is enabled, this setting is ignored if set to 0.\",\n    \"misc\": \"Інші параметри\",\n    \"motion_as_ds4\": \"Емулювати геймпад DS4, якщо клієнтський геймпад повідомляє про наявність motion датчиків\",\n    \"motion_as_ds4_desc\": \"Якщо вимкнено, motion датчики не враховуватимуться під час вибору типу геймпада.\",\n    \"mouse\": \"Увімкнути введення за допомогою миші\",\n    \"mouse_desc\": \"Дозволяє гостям керувати хост-системою за допомогою миші\",\n    \"native_pen_touch\": \"Вбудована підтримка пера/сенсорного вводу\",\n    \"native_pen_touch_desc\": \"Якщо увімкнено, Sunshine передаватиме події нативного пера/дотику від клієнтів Moonlight. Для старих програм без підтримки нативного пера/дотику може бути корисним вимкнення цього налаштування.\",\n    \"no_fps\": \"Значення FPS не додано\",\n    \"no_resolutions\": \"Роздільні здатності не додано\",\n    \"notify_pre_releases\": \"PreRelease Сповіщення\",\n    \"notify_pre_releases_desc\": \"Чи отримувати сповіщення про нові pre-release версії Sunshine\",\n    \"nvenc_h264_cavlc\": \"Надайте перевагу CAVLC над CABAC в H.264\",\n    \"nvenc_h264_cavlc_desc\": \"Простіша форма ентропійного кодування. CAVLC потребує приблизно на 10% більшого бітрейту для такої ж якості. Актуально лише для дуже старих декодерів.\",\n    \"nvenc_latency_over_power\": \"Надайте перевагу меншій затримці кодування над економією енергії\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine запитує максимальну тактову частоту CPU під час стримінгу, щоб зменшити затримку кодування. Вимкнення цього параметра не рекомендується, оскільки це може призвести до значного збільшення затримки кодування.\",\n    \"nvenc_lookahead_depth\": \"Глибина Lookahead\",\n    \"nvenc_lookahead_depth_desc\": \"Кількість кадрів для попереднього аналізу під час кодування (0-32). Lookahead покращує якість кодування, особливо в складних сценах, забезпечуючи кращу оцінку руху та розподіл бітрейту. Вищі значення покращують якість, але збільшують затримку кодування. Встановіть 0 для вимкнення. Потрібно NVENC SDK 13.0 (1202) або новіше.\",\n    \"nvenc_lookahead_level\": \"Рівень Lookahead\",\n    \"nvenc_lookahead_level_0\": \"Рівень 0 (найнижча якість, найшвидше)\",\n    \"nvenc_lookahead_level_1\": \"Рівень 1\",\n    \"nvenc_lookahead_level_2\": \"Рівень 2\",\n    \"nvenc_lookahead_level_3\": \"Рівень 3 (найвища якість, найповільніше)\",\n    \"nvenc_lookahead_level_autoselect\": \"Автовибір (дозволити драйверу вибрати оптимальний рівень)\",\n    \"nvenc_lookahead_level_desc\": \"Рівень якості Lookahead. Вищі рівні покращують якість за рахунок продуктивності. Ця опція працює лише якщо lookahead_depth більше 0. Потрібно NVENC SDK 13.0 (1202) або новіше.\",\n    \"nvenc_lookahead_level_disabled\": \"Вимкнено (те саме, що і рівень 0)\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"Відображати OpenGL/Vulkan поверх DXGI\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine не може захоплювати повноекранні програми OpenGL та Vulkan з повною частотою кадрів, якщо вони не присутні поверх DXGI. Це загальносистемне налаштування, яке повертається до початкового стану після завершення роботи програми.\",\n    \"nvenc_preset\": \"Пресет продуктивності\",\n    \"nvenc_preset_1\": \"(найшвидший, за замовчуванням)\",\n    \"nvenc_preset_7\": \"(найповільніше)\",\n    \"nvenc_preset_desc\": \"Більші значення покращують стиснення (якість при заданому бітрейті) ціною збільшення затримки кодування. Рекомендується змінювати тільки тоді, коли це обмежено мережею або декодером, інакше аналогічного ефекту можна досягти збільшенням бітрейту.\",\n    \"nvenc_rate_control\": \"Режим контролю бітрейту\",\n    \"nvenc_rate_control_cbr\": \"CBR (Постійний бітрейт) - Низька затримка\",\n    \"nvenc_rate_control_desc\": \"Виберіть режим контролю бітрейту. CBR (Постійний бітрейт) забезпечує фіксований бітрейт для стрімінгу з низькою затримкою. VBR (Змінний бітрейт) дозволяє змінювати бітрейт залежно від складності сцени, забезпечуючи кращу якість для складних сцен за рахунок змінного бітрейту.\",\n    \"nvenc_rate_control_vbr\": \"VBR (Змінний бітрейт) - Краща якість\",\n    \"nvenc_realtime_hags\": \"Використання пріоритету реального часу у плануванні GPU з апаратним прискоренням\",\n    \"nvenc_realtime_hags_desc\": \"Наразі драйвери NVIDIA можуть зависати в кодері, коли ввімкнено HAGS, використовується пріоритет реального часу та завантаження VRAM близьке до максимального. Вимкнення цієї опції знижує пріоритет до високого, що дозволяє уникнути зависання ціною зниження продуктивності захоплення при високому навантаженні GPU.\",\n    \"nvenc_spatial_aq\": \"Просторове AQ\",\n    \"nvenc_spatial_aq_desc\": \"Призначає вищі значення QP пласким ділянкам відео. Рекомендується вмикати під час стримінгу з низьким бітрейтом.\",\n    \"nvenc_spatial_aq_disabled\": \"Disabled (faster, default)\",\n    \"nvenc_spatial_aq_enabled\": \"Enabled (slower)\",\n    \"nvenc_split_encode\": \"Розділене кодування кадрів\",\n    \"nvenc_split_encode_desc\": \"Split the encoding of each video frame over multiple NVENC hardware units. Significantly reduces encoding latency with a marginal compression efficiency penalty. This option is ignored if your GPU has a singular NVENC unit.\",\n    \"nvenc_split_encode_driver_decides_def\": \"Driver decides (default)\",\n    \"nvenc_split_encode_four_strips\": \"Примусово 4 смуги (потрібно 4+ двигуни NVENC)\",\n    \"nvenc_split_encode_three_strips\": \"Примусово 3 смуги (потрібно 3+ двигуни NVENC)\",\n    \"nvenc_split_encode_two_strips\": \"Примусово 2 смуги (потрібно 2+ двигуни NVENC)\",\n    \"nvenc_target_quality\": \"Цільова якість (режим VBR)\",\n    \"nvenc_target_quality_desc\": \"Цільовий рівень якості для режиму VBR (0-51 для H.264/HEVC, 0-63 для AV1). Менші значення = вища якість. Встановіть 0 для автоматичного вибору якості. Використовується лише коли режим контролю бітрейту VBR.\",\n    \"nvenc_temporal_aq\": \"Temporal adaptive quantization\",\n    \"nvenc_temporal_aq_desc\": \"Enable temporal adaptive quantization. Temporal AQ optimizes quantization across time, providing better bitrate distribution and improved quality in motion scenes. This feature works in conjunction with spatial AQ and requires lookahead to be enabled (lookahead_depth > 0). Requires NVENC SDK 13.0 (1202) or newer.\",\n    \"nvenc_temporal_filter\": \"Temporal filter\",\n    \"nvenc_temporal_filter_4\": \"Level 4 (maximum strength)\",\n    \"nvenc_temporal_filter_desc\": \"Temporal filtering strength applied before encoding. Temporal filter reduces noise and improves compression efficiency, especially for natural content. Higher levels provide better noise reduction but may introduce slight blurring. Requires NVENC SDK 13.0 (1202) or newer. Note: Requires frameIntervalP >= 5, not compatible with zeroReorderDelay or stereo MVC.\",\n    \"nvenc_temporal_filter_disabled\": \"Disabled (no temporal filtering)\",\n    \"nvenc_twopass\": \"Режим двоетапної перевірки\",\n    \"nvenc_twopass_desc\": \"Додає попередній прохід кодування. Це дозволяє виявити більше векторів руху, краще розподілити бітрейт у кадрі та суворіше дотримуватися лімітів бітрейту. Вимикати його не рекомендується, оскільки це може призвести до випадкового перевищення бітрейту і подальшої втрати пакетів.\",\n    \"nvenc_twopass_disabled\": \"Вимкнено (найшвидше, не рекомендується)\",\n    \"nvenc_twopass_full_res\": \"Повна роздільна здатність (повільніше)\",\n    \"nvenc_twopass_quarter_res\": \"Чверть роздільної здатності (швидше, за замовчуванням)\",\n    \"nvenc_vbv_increase\": \"Збільшення однокадрового VBV/HRD у відсотках\",\n    \"nvenc_vbv_increase_desc\": \"За замовчуванням Sunshine використовує однокадровий VBV/HRD, що означає, що розмір будь-якого закодованого відеокадру не повинен перевищувати запитуваного бітрейту, поділеного на запитувану частоту кадрів. Послаблення цього обмеження може бути корисним і діяти як змінний бітрейт з низькою затримкою, але також може призвести до втрати пакетів, якщо мережа не має достатньої ємності буфера, щоб впоратися зі стрибками бітрейту. Максимально допустиме значення 400, що відповідає 5-кратному збільшенню верхньої межі розміру кодованого відеокадру.\",\n    \"origin_web_ui_allowed\": \"Origin Web UI Дозволено\",\n    \"origin_web_ui_allowed_desc\": \"Походження адреси endpoint'а, якій не заборонено доступ до Web UI\",\n    \"origin_web_ui_allowed_lan\": \"Доступ до Web UI мають лише ті, хто перебуває в LAN мережі\",\n    \"origin_web_ui_allowed_pc\": \"Доступ до Web UI може мати лише localhost\",\n    \"origin_web_ui_allowed_wan\": \"Будь-хто може отримати доступ до Web UI\",\n    \"output_name_desc_unix\": \"Під час запуску Sunshine ви повинні побачити список виявлених дисплеїв. Примітка: Ви маєте використовувати значення ідентифікатора у дужках. Нижче наведено приклад; фактичний вивід можна знайти на вкладці Виправлення неполадок.\",\n    \"output_name_desc_windows\": \"Вручну вкажіть ідентифікатор пристрою для захоплення. Якщо не вказано, буде захоплено основний екран. Примітка: Якщо вище ви вказали GPU, цей дисплей повинен бути підключений до цього ж GPU. Під час запуску Sunshine, ви повинні побачити список виявлених дисплеїв. Нижче наведено приклад, фактичний вивід зображення можна знайти на вкладці Виправлення неполадок.\",\n    \"output_name_unix\": \"Номер дисплея\",\n    \"output_name_windows\": \"Показувати ID пристрою\",\n    \"ping_timeout\": \"Тайм-аут пінгу\",\n    \"ping_timeout_desc\": \"Скільки часу в мілісекундах чекати на дані від Moonlight, перш ніж вимкнути стримінг\",\n    \"pkey\": \"Приватний ключ\",\n    \"pkey_desc\": \"Приватний ключ, який використовується для створення пари між Web UI й клієнтом Moonlight. Для найкращої сумісності це має бути приватний ключ формату RSA-2048.\",\n    \"port\": \"Порт\",\n    \"port_alert_1\": \"Sunshine не може використовувати порти нижче 1024!\",\n    \"port_alert_2\": \"Порти вище 65535 недоступні!\",\n    \"port_desc\": \"Встановити сімейство портів, що використовуються Sunshine\",\n    \"port_http_port_note\": \"Використовуйте цей порт для підключення до Moonlight.\",\n    \"port_note\": \"Нотатка\",\n    \"port_port\": \"Порт\",\n    \"port_protocol\": \"Протокол\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"Оприлюднення Web UI в Інтернеті є ризикованим щодо безпеки! Дійте на власний страх і ризик!\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"Параметр квантування\",\n    \"qp_desc\": \"Деякі пристрої можуть не підтримувати постійну швидкість передачі даних. На таких пристроях замість неї використовується QP. Вище значення означає більше стиснення, але нижчу якість.\",\n    \"qsv_coder\": \"QuickSync кодер (H264)\",\n    \"qsv_preset\": \"QuickSync Пресет\",\n    \"qsv_preset_fast\": \"швидко (низька якість)\",\n    \"qsv_preset_faster\": \"швидше (нижча якість)\",\n    \"qsv_preset_medium\": \"середнє (за замовчуванням)\",\n    \"qsv_preset_slow\": \"повільно (хороша якість)\",\n    \"qsv_preset_slower\": \"повільніше (краща якість)\",\n    \"qsv_preset_slowest\": \"найповільніше (найкраща якість)\",\n    \"qsv_preset_veryfast\": \"найшвидше (найнижча якість)\",\n    \"qsv_slow_hevc\": \"Дозволити повільне HEVC кодування\",\n    \"qsv_slow_hevc_desc\": \"Це може дозволити кодування HEVC на старих Intel GPU, але внаслідок більшого використання GPU та гіршої продуктивності.\",\n    \"refresh_rate_change_automatic_windows\": \"Use FPS value provided by the client\",\n    \"refresh_rate_change_manual_desc_windows\": \"Enter the refresh rate to be used\",\n    \"refresh_rate_change_manual_windows\": \"Use manually entered refresh rate\",\n    \"refresh_rate_change_no_operation_windows\": \"Disabled\",\n    \"refresh_rate_change_windows\": \"FPS change\",\n    \"res_fps_desc\": \"Режими відображення, оголошені Sunshine. Деякі версії Moonlight, такі як Moonlight-nx (Switch), покладаються на ці списки, щоб переконатися, що запитані роздільності та fps підтримуються. Це налаштування не змінює спосіб надсилання потоку екрана до Moonlight.\",\n    \"resolution_change_automatic_windows\": \"Use resolution provided by the client\",\n    \"resolution_change_manual_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_manual_windows\": \"Use manually entered resolution\",\n    \"resolution_change_no_operation_windows\": \"Disabled\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"Optimize game settings\\\" option must be enabled on the Moonlight client for this to work.\",\n    \"resolution_change_windows\": \"Resolution change\",\n    \"resolutions\": \"Оголошені роздільності\",\n    \"restart_note\": \"Sunshine перезапускається, щоб застосувати зміни.\",\n    \"sleep_mode\": \"Режим сну\",\n    \"sleep_mode_away\": \"Режим відсутності (Екран вимк., миттєве пробудження)\",\n    \"sleep_mode_desc\": \"Керує поведінкою при надсиланні клієнтом команди сну. Режим очікування (S3): традиційний сон, низьке енергоспоживання, але потребує WOL для пробудження. Гібернація (S4): збереження на диск, дуже низьке енергоспоживання. Режим відсутності: екран вимикається, але система продовжує працювати для миттєвого пробудження — ідеально для серверів ігрового стримінгу.\",\n    \"sleep_mode_hibernate\": \"Гібернація (S4)\",\n    \"sleep_mode_suspend\": \"Режим очікування (S3)\",\n    \"stream_audio\": \"Увімкнути трансляцію аудіо\",\n    \"stream_audio_desc\": \"Вимкніть цей параметр, щоб вимкнути трансляцію аудіо.\",\n    \"stream_mic\": \"Увімкнути трансляцію мікрофона\",\n    \"stream_mic_desc\": \"Вимкніть цей параметр, щоб вимкнути трансляцію мікрофона.\",\n    \"stream_mic_download_btn\": \"Завантажити віртуальний мікрофон\",\n    \"stream_mic_download_confirm\": \"Ви будете перенаправлені на сторінку завантаження віртуального мікрофона. Продовжити?\",\n    \"stream_mic_note\": \"Ця функція вимагає встановлення віртуального мікрофона\",\n    \"sunshine_name\": \"Sunshine ім'я\",\n    \"sunshine_name_desc\": \"Ім'я, яке відображається Moonlight. Якщо не вказано, використовується ім'я хоста комп'ютера\",\n    \"sw_preset\": \"Пресети SW\",\n    \"sw_preset_desc\": \"Оптимізація компромісу між швидкістю кодування (кількість закодованих кадрів за секунду) та ефективністю стиснення (якість на біт у бітовому потоці). За замовчуванням - супершвидко.\",\n    \"sw_preset_fast\": \"швидко\",\n    \"sw_preset_faster\": \"швидше\",\n    \"sw_preset_medium\": \"середнє\",\n    \"sw_preset_slow\": \"повільно\",\n    \"sw_preset_slower\": \"повільніше\",\n    \"sw_preset_superfast\": \"супершвидкий (за замовчуванням)\",\n    \"sw_preset_ultrafast\": \"ультрашвидкий\",\n    \"sw_preset_veryfast\": \"дуже швидкий\",\n    \"sw_preset_veryslow\": \"дуже повільний\",\n    \"sw_tune\": \"ПЗ налаштування\",\n    \"sw_tune_animation\": \"анімація - добре підходить для мультфільмів; використовує вищий рівень деблокування та більше кадрів порівняння\",\n    \"sw_tune_desc\": \"Параметри налаштування, які застосовуються після пресету. За замовчуванням - нульова латентність.\",\n    \"sw_tune_fastdecode\": \"fastdecode -- дозволяє пришвидшити декодування, вимкнувши певні фільтри\",\n    \"sw_tune_film\": \"фільм - використовується для високоякісного кіноконтенту; зменшує деблокування\",\n    \"sw_tune_grain\": \"зернистість - зберігає зернисту структуру в старих, зернистих плівкових матеріалах\",\n    \"sw_tune_stillimage\": \"стоп-кадр - добре підходить для контенту, схожого на слайд-шоу\",\n    \"sw_tune_zerolatency\": \"zerolatency - добре підходить для швидкого кодування та стримінгу з низькою затримкою (за замовчуванням)\",\n    \"system_tray\": \"Увімкнути системний лоток\",\n    \"system_tray_desc\": \"Чи вмикати системний трей. Якщо ввімкнено, Sunshine відображатиме іконку в системному треї і ним можна керувати з системного трея.\",\n    \"touchpad_as_ds4\": \"Емулювати геймпад DS4, якщо клієнтський геймпад повідомляє про наявність touchpad'а\",\n    \"touchpad_as_ds4_desc\": \"Якщо вимкнено, наявність touchpad не враховуватиметься під час вибору типу геймпада.\",\n    \"unsaved_changes_tooltip\": \"У вас є незбережені зміни. Натисніть, щоб зберегти.\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"Автоматичне налаштування переадресації портів для стримінгу через Інтернет\",\n    \"variable_refresh_rate\": \"Змінна частота оновлення (VRR)\",\n    \"variable_refresh_rate_desc\": \"Дозволити частоті кадрів відеопотоку відповідати частоті кадрів рендерінгу для підтримки VRR. Коли ввімкнено, кодування відбувається лише коли доступні нові кадри, дозволяючи потоку слідувати фактичній частоті кадрів рендерінгу.\",\n    \"vdd_reuse_desc_windows\": \"Коли увімкнено, всі клієнти будуть використовувати один і той самий VDD (Virtual Display Device). Коли вимкнено (за замовчуванням), кожен клієнт отримує власний VDD. Увімкніть це для швидшого переключення клієнтів, але зверніть увагу, що всі клієнти будуть використовувати однакові налаштування дисплея.\",\n    \"vdd_reuse_windows\": \"Використовувати один VDD для всіх клієнтів\",\n    \"virtual_display\": \"Віртуальний дисплей\",\n    \"virtual_mouse\": \"Драйвер віртуальної миші\",\n    \"virtual_mouse_desc\": \"При увімкненні Sunshine використовуватиме драйвер Zako Virtual Mouse (якщо встановлений) для імітації введення миші на рівні HID. Дозволяє іграм з Raw Input отримувати події миші. При вимкненні або відсутності драйвера використовується SendInput.\",\n    \"virtual_sink\": \"Віртуальний пристрій виведення аудіо\",\n    \"virtual_sink_desc\": \"Вручну вкажіть віртуальний аудіопристрій для використання. Якщо не вказано, пристрій буде обрано автоматично. Ми наполегливо рекомендуємо залишити це поле порожнім, щоб використовувати автоматичний вибір пристрою!\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"Встановити драйвер віртуальної миші?\",\n    \"vmouse_confirm_uninstall\": \"Видалити драйвер віртуальної миші?\",\n    \"vmouse_install\": \"Встановити драйвер\",\n    \"vmouse_installing\": \"Встановлення...\",\n    \"vmouse_note\": \"Драйвер віртуальної миші потребує окремої установки. Використовуйте панель керування Sunshine для установки або керування драйвером.\",\n    \"vmouse_refresh\": \"Оновити статус\",\n    \"vmouse_status_installed\": \"Встановлений (не активний)\",\n    \"vmouse_status_not_installed\": \"Не встановлений\",\n    \"vmouse_status_running\": \"Працює\",\n    \"vmouse_uninstall\": \"Видалити драйвер\",\n    \"vmouse_uninstalling\": \"Видалення...\",\n    \"vt_coder\": \"VideoToolbox Кодер\",\n    \"vt_realtime\": \"Кодування VideoToolbox у реальному часі\",\n    \"vt_software\": \"Кодування ПЗ VideoToolbox\",\n    \"vt_software_allowed\": \"Дозволено\",\n    \"vt_software_forced\": \"Примусово\",\n    \"wan_encryption_mode\": \"Режим шифрування WAN\",\n    \"wan_encryption_mode_1\": \"Увімкнено для підтримуваних клієнтів (за замовчуванням)\",\n    \"wan_encryption_mode_2\": \"Обов'язково для всіх клієнтів\",\n    \"wan_encryption_mode_desc\": \"Цей параметр визначає, коли буде використовуватися шифрування під час потокового передавання через Інтернет. Шифрування може знизити продуктивність потокового стримінгу, особливо на менш потужних хостах і клієнтах.\",\n    \"webhook_curl_command\": \"Команда\",\n    \"webhook_curl_command_desc\": \"Скопіюйте наступну команду у ваш термінал, щоб перевірити, чи правильно працює webhook:\",\n    \"webhook_curl_copy_failed\": \"Помилка копіювання, будь ласка, виберіть і скопіюйте вручну\",\n    \"webhook_enabled\": \"Сповіщення Webhook\",\n    \"webhook_enabled_desc\": \"Коли увімкнено, Sunshine буде надсилати сповіщення про події до вказаного URL Webhook\",\n    \"webhook_group\": \"Налаштування сповіщень Webhook\",\n    \"webhook_skip_ssl_verify\": \"Пропустити перевірку SSL-сертифікату\",\n    \"webhook_skip_ssl_verify_desc\": \"Пропустити перевірку SSL-сертифікату для HTTPS-з'єднань, лише для тестування або самопідписаних сертифікатів\",\n    \"webhook_test\": \"Тест\",\n    \"webhook_test_failed\": \"Тест Webhook невдалий\",\n    \"webhook_test_failed_note\": \"Примітка: Будь ласка, перевірте, чи правильний URL, або перевірте консоль браузера для отримання додаткової інформації.\",\n    \"webhook_test_success\": \"Тест Webhook успішний!\",\n    \"webhook_test_success_cors_note\": \"Примітка: Через обмеження CORS статус відповіді сервера не може бути підтверджений.\\nЗапит було відправлено. Якщо webhook налаштовано правильно, повідомлення має бути доставлено.\\n\\nПропозиція: Перевірте вкладку Мережа в інструментах розробника вашого браузера для отримання деталей запиту.\",\n    \"webhook_test_url_required\": \"Будь ласка, спочатку введіть URL Webhook\",\n    \"webhook_timeout\": \"Таймаут запиту\",\n    \"webhook_timeout_desc\": \"Таймаут для запитів Webhook в мілісекундах, діапазон 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"URL для отримання сповіщень про події, підтримує протоколи HTTP/HTTPS\",\n    \"wgc_checking_mode\": \"Перевірка режиму...\",\n    \"wgc_checking_running_mode\": \"Перевірка режиму роботи...\",\n    \"wgc_control_panel_only\": \"Ця функція доступна лише в панелі керування Sunshine\",\n    \"wgc_mode_switch_failed\": \"Не вдалося переключити режим\",\n    \"wgc_mode_switch_started\": \"Ініційовано перемикання режиму. Якщо з'явиться запит UAC, натисніть 'Так', щоб підтвердити.\",\n    \"wgc_service_mode_warning\": \"Захоплення WGC вимагає запуску в режимі користувача. Якщо зараз запущено режим служби, натисніть кнопку вище, щоб переключитися в режим користувача.\",\n    \"wgc_switch_to_service_mode\": \"Переключитися в режим служби\",\n    \"wgc_switch_to_service_mode_tooltip\": \"В даний час працює в режимі користувача. Натисніть, щоб переключитися в режим служби.\",\n    \"wgc_switch_to_user_mode\": \"Переключитися в режим користувача\",\n    \"wgc_switch_to_user_mode_tooltip\": \"Захоплення WGC вимагає запуску в режимі користувача. Натисніть цю кнопку, щоб переключитися в режим користувача.\",\n    \"wgc_user_mode_available\": \"В даний час працює в режимі користувача. Захоплення WGC доступне.\",\n    \"window_title\": \"Заголовок вікна\",\n    \"window_title_desc\": \"Заголовок вікна для захоплення (частковий збіг, без урахування регістру). Якщо залишити пустим, буде використано ім'я поточної запущеної програми автоматично.\",\n    \"window_title_placeholder\": \"наприклад, Ім'я Програми\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine - це самостійний ігровий стримінговий хостинг для Moonlight.\",\n    \"download\": \"Завантажити\",\n    \"installed_version_not_stable\": \"Ви використовуєте попередню версію Sunshine. Ви можете зіткнутися з помилками або іншими проблемами. Будь ласка, повідомляйте про будь-які проблеми, з якими ви зіткнулися. Дякуємо, що допомагаєте зробити Sunshine кращою програмою!\",\n    \"loading_latest\": \"Завантаження останньої версії...\",\n    \"new_pre_release\": \"Доступна нова Pre-Release версія!\",\n    \"new_stable\": \"Доступна нова стабільна версія!\",\n    \"startup_errors\": \"<b>Увага!</b> Sunshine виявив ці помилки під час запуску. Ми НАПОЛЕГЛИВО <b>РЕКОМЕНДУЄМО</b> виправити їх перед стримінгом.\",\n    \"update_download_confirm\": \"Ви збираєтеся відкрити сторінку завантаження оновлень у браузері. Продовжити?\",\n    \"version_dirty\": \"Дякуємо, що допомагаєте зробити Sunshine кращою програмою!\",\n    \"version_latest\": \"Ви використовуєте останню версію Sunshine\",\n    \"view_logs\": \"Переглянути журнали\",\n    \"welcome\": \"Привіт, Sunshine!\"\n  },\n  \"navbar\": {\n    \"applications\": \"Застосунки\",\n    \"configuration\": \"Конфігурація\",\n    \"home\": \"Головна\",\n    \"password\": \"Змінити Пароль\",\n    \"pin\": \"Закріпити\",\n    \"theme_auto\": \"Авто\",\n    \"theme_dark\": \"Темна\",\n    \"theme_light\": \"Світла\",\n    \"toggle_theme\": \"Тема\",\n    \"troubleshoot\": \"Усунення неполадок\"\n  },\n  \"password\": {\n    \"confirm_password\": \"Підтвердити пароль\",\n    \"current_creds\": \"Поточні облікові дані\",\n    \"new_creds\": \"Нові облікові дані\",\n    \"new_username_desc\": \"Якщо не вказано, ім'я користувача не зміниться\",\n    \"password_change\": \"Зміна пароля\",\n    \"success_msg\": \"Пароль успішно змінено! Ця сторінка незабаром перезавантажиться, ваш браузер запитає вас про нові облікові дані.\"\n  },\n  \"pin\": {\n    \"actions\": \"Дії\",\n    \"cancel_editing\": \"Скасувати редагування\",\n    \"client_name\": \"Ім'я\",\n    \"client_settings_info\": \"Tip:\",\n    \"confirm_delete\": \"Підтвердити видалення\",\n    \"delete_client\": \"Видалити клієнта\",\n    \"delete_confirm_message\": \"Ви впевнені, що хочете видалити <strong>{name}</strong>?\",\n    \"delete_warning\": \"Цю дію неможливо скасувати.\",\n    \"device_name\": \"Назва пристрою\",\n    \"device_size\": \"Розмір пристрою\",\n    \"device_size_info\": \"<strong>Device Size</strong>: Set the screen size type of the client device (Small - Phone, Medium - Tablet, Large - TV) to optimize streaming experience and touch operations.\",\n    \"device_size_large\": \"Великий - TV\",\n    \"device_size_medium\": \"Середній - Планшет\",\n    \"device_size_small\": \"Малий - Телефон\",\n    \"edit_client_settings\": \"Редагувати налаштування клієнта\",\n    \"hdr_profile\": \"Профіль HDR\",\n    \"hdr_profile_info\": \"<strong>HDR Profile</strong>: Select the HDR color profile (ICC file) used for this client to ensure HDR content is displayed correctly on the device. If using the latest client, support automatic synchronization of brightness information to the host virtual screen, leave this field blank to enable automatic synchronization.\",\n    \"loading\": \"Завантаження...\",\n    \"loading_clients\": \"Завантаження клієнтів...\",\n    \"modify_in_gui\": \"Будь ласка, змініть у графічному інтерфейсі\",\n    \"none\": \"-- Немає --\",\n    \"or_manual_pin\": \"або введіть PIN вручну\",\n    \"pair_failure\": \"Не вдалося створити пару: Перевірте правильність введення PIN-коду\",\n    \"pair_success\": \"Успішно! Будь ласка, перевірте Moonlight, щоб продовжити\",\n    \"pin_pairing\": \"Сполучення PIN-коду\",\n    \"qr_expires_in\": \"Закінчується через\",\n    \"qr_generate\": \"Згенерувати QR-код\",\n    \"qr_paired_success\": \"Сполучення виконано!\",\n    \"qr_pairing\": \"Сполучення через QR-код\",\n    \"qr_pairing_desc\": \"Згенеруйте QR-код для швидкого сполучення. Відскануйте його клієнтом Moonlight для автоматичного сполучення.\",\n    \"qr_pairing_warning\": \"Експериментальна функція. Якщо сполучення не вдасться, використовуйте ручне введення PIN нижче. Примітка: Ця функція працює лише в локальній мережі.\",\n    \"qr_refresh\": \"Оновити QR-код\",\n    \"remove_paired_devices_desc\": \"Видаліть ваші сполучені пристрої.\",\n    \"save_changes\": \"Зберегти зміни\",\n    \"save_failed\": \"Не вдалося зберегти налаштування клієнта. Будь ласка, спробуйте ще раз.\",\n    \"save_or_cancel_first\": \"Будь ласка, спочатку збережіть або скасуйте редагування\",\n    \"send\": \"Надіслати\",\n    \"unknown_client\": \"Невідомий клієнт\",\n    \"unpair_all_confirm\": \"Ви впевнені, що хочете відв'язати всіх клієнтів? Цю дію неможливо скасувати.\",\n    \"unsaved_changes\": \"Незбережені зміни\",\n    \"warning_msg\": \"Переконайтеся, що у вас є доступ до клієнта, з яким ви створюєте пару. Це програмне забезпечення може повністю контролювати ваш комп'ютер, тому будьте обережні!\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android рекомендовано\",\n    \"client_downloads\": \"Завантаження клієнтів\",\n    \"crown_edition\": \"Crown Edition\",\n    \"github_discussions\": \"Обговорення на GitHub\",\n    \"gpl_license_text_1\": \"This software is licensed under GPL-3.0. You are free to use, modify, and distribute it.\",\n    \"gpl_license_text_2\": \"To protect the open source ecosystem, please avoid using software that violates the GPL-3.0 license.\",\n    \"harmony_client\": \"HarmonyOS Moonlight V+\",\n    \"join_group\": \"Приєднатися до спільноти\",\n    \"join_group_desc\": \"Отримати допомогу та поділитися досвідом\",\n    \"legal\": \"Юридична інформація\",\n    \"legal_desc\": \"Продовжуючи використовувати це програмне забезпечення, ви погоджуєтеся з умовами та положеннями, викладеними в наступних документах.\",\n    \"license\": \"Ліцензія\",\n    \"lizardbyte_website\": \"Вебсайт LizardByte\",\n    \"official_website\": \"Official Website\",\n    \"official_website_title\": \"AlkaidLab - Офіційний сайт\",\n    \"open_source\": \"Відкритий код\",\n    \"open_source_desc\": \"Star & Fork для підтримки проєкту\",\n    \"quick_start\": \"Швидкий старт\",\n    \"resources\": \"Ресурси\",\n    \"resources_desc\": \"Ресурси для Sunshine!\",\n    \"third_party_desc\": \"Повідомлення про сторонні компоненти\",\n    \"third_party_moonlight\": \"Дружні посилання\",\n    \"third_party_notice\": \"Сповіщення третім особам\",\n    \"tutorial\": \"Посібник\",\n    \"tutorial_desc\": \"Докладний посібник з налаштування та використання\",\n    \"view_license\": \"Переглянути повну ліцензію\",\n    \"voidlink_title\": \"VoidLink\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"Configuration Summary\",\n    \"android_client\": \"Android Client\",\n    \"base_display_title\": \"Virtual Display\",\n    \"choose_adapter\": \"Auto\",\n    \"config_saved\": \"Configuration has been saved successfully.\",\n    \"description\": \"Let's get you started with a quick setup\",\n    \"device_id\": \"Device ID\",\n    \"device_state\": \"State\",\n    \"download_clients\": \"Download Clients\",\n    \"finish\": \"Finish Setup\",\n    \"go_to_apps\": \"Configure Applications\",\n    \"harmony_goto_repo\": \"Go to Repository\",\n    \"harmony_modal_desc\": \"For HarmonyOS NEXT Moonlight, please search for Moonlight V+ in the HarmonyOS App Store\",\n    \"harmony_modal_link_notice\": \"This link will redirect to the project repository\",\n    \"ios_client\": \"iOS Client\",\n    \"load_error\": \"Failed to load configuration\",\n    \"next\": \"Next\",\n    \"physical_display\": \"Physical Display/EDID Emulator\",\n    \"physical_display_desc\": \"Stream your actual physical monitors\",\n    \"previous\": \"Previous\",\n    \"restart_countdown_unit\": \"секунд\",\n    \"restart_desc\": \"Конфігурацію збережено. Sunshine перезапускається для застосування налаштувань дисплея.\",\n    \"restart_go_now\": \"Перейти зараз\",\n    \"restart_title\": \"Перезапуск Sunshine\",\n    \"save_error\": \"Failed to save configuration\",\n    \"select_adapter\": \"Graphics Adapter\",\n    \"selected_adapter\": \"Selected Adapter\",\n    \"selected_display\": \"Selected Display\",\n    \"setup_complete\": \"Setup Complete!\",\n    \"setup_complete_desc\": \"Базові налаштування активовано. Тепер ви можете одразу почати стрімінг за допомогою клієнта Moonlight!\",\n    \"skip\": \"Skip Setup Wizard\",\n    \"skip_confirm\": \"Are you sure you want to skip the setup wizard? You can configure these options later in the settings page.\",\n    \"skip_confirm_title\": \"Skip Setup Wizard\",\n    \"skip_error\": \"Failed to skip\",\n    \"state_active\": \"Active\",\n    \"state_inactive\": \"Inactive\",\n    \"state_primary\": \"Primary\",\n    \"state_unknown\": \"Unknown\",\n    \"step0_description\": \"Choose your interface language\",\n    \"step0_title\": \"Language\",\n    \"step1_description\": \"Choose the display to stream\",\n    \"step1_title\": \"Display Selection\",\n    \"step1_vdd_intro\": \"Базовий дисплей (VDD) — вбудований інтелектуальний віртуальний дисплей Sunshine Foundation, що підтримує будь-яку роздільну здатність, частоту кадрів та оптимізацію HDR. Ідеальний вибір для стримінгу з вимкненим екраном та стримінгу на розширений дисплей.\",\n    \"step2_description\": \"Choose your graphics adapter\",\n    \"step2_title\": \"Select Adapter\",\n    \"step3_description\": \"Choose display device preparation strategy\",\n    \"step3_ensure_active\": \"Забезпечити активацію\",\n    \"step3_ensure_active_desc\": \"Активує дисплей, якщо він ще не активний\",\n    \"step3_ensure_only_display\": \"Забезпечити єдиний дисплей\",\n    \"step3_ensure_only_display_desc\": \"Вимикає всі інші дисплеї та вмикає лише вказаний (рекомендовано)\",\n    \"step3_ensure_primary\": \"Забезпечити основний дисплей\",\n    \"step3_ensure_primary_desc\": \"Активує дисплей та встановлює його як основний\",\n    \"step3_ensure_secondary\": \"Вторинний стримінг\",\n    \"step3_ensure_secondary_desc\": \"Використовує лише віртуальний дисплей для вторинного розширеного стримінгу\",\n    \"step3_no_operation\": \"Без дій\",\n    \"step3_no_operation_desc\": \"Без змін стану дисплея; користувач повинен самостійно переконатися, що дисплей готовий\",\n    \"step3_title\": \"Display Strategy\",\n    \"step4_title\": \"Complete\",\n    \"stream_mode\": \"Stream Mode\",\n    \"unknown_display\": \"Unknown Display\",\n    \"virtual_display\": \"Virtual Display (ZakoHDR)\",\n    \"virtual_display_desc\": \"Stream using a virtual display device (requires ZakoVDD driver installation)\",\n    \"welcome\": \"Welcome to Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"Advanced\",\n    \"amd\": \"AMD AMF Encoder\",\n    \"av\": \"Audio/Video\",\n    \"encoders\": \"Encoders\",\n    \"files\": \"Config Files\",\n    \"general\": \"General\",\n    \"input\": \"Input\",\n    \"network\": \"Network\",\n    \"nv\": \"NVIDIA NVENC Encoder\",\n    \"qsv\": \"Intel QuickSync Encoder\",\n    \"sw\": \"Software Encoder\",\n    \"vaapi\": \"VAAPI Encoder\",\n    \"vt\": \"VideoToolbox Encoder\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"Аналіз...\",\n    \"ai_analyzing_logs\": \"Аналіз журналів, будь ласка зачекайте...\",\n    \"ai_config\": \"Конфігурація ШІ\",\n    \"ai_copy_result\": \"Копіювати\",\n    \"ai_diagnosis\": \"ШІ-діагностика\",\n    \"ai_diagnosis_title\": \"ШІ-діагностика журналів\",\n    \"ai_error\": \"Аналіз не вдався\",\n    \"ai_key_local\": \"API-ключ зберігається лише локально і ніколи не завантажується\",\n    \"ai_model\": \"Модель\",\n    \"ai_provider\": \"Постачальник\",\n    \"ai_reanalyze\": \"Повторний аналіз\",\n    \"ai_result\": \"Результат діагностики\",\n    \"ai_retry\": \"Повторити\",\n    \"ai_start_diagnosis\": \"Почати діагностику\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"Якщо вам потрібно негайно вимкнути Sunshine, ви можете використати цю функцію. Зверніть увагу, що вам потрібно буде вручну запустити його знову після вимкнення.\",\n    \"boom_sunshine_success\": \"Sunshine вимкнено\",\n    \"confirm_boom\": \"Дійсно хочете вийти?\",\n    \"confirm_boom_desc\": \"Отже, ви дійсно хочете вийти? Ну, я не можу вас зупинити, продовжуйте і натисніть знову\",\n    \"confirm_logout\": \"Підтвердити вихід?\",\n    \"confirm_logout_desc\": \"Потрібно буде знову ввести пароль для доступу до веб-інтерфейсу.\",\n    \"copy_config\": \"Копіювати конфігурацію\",\n    \"copy_config_error\": \"Не вдалося скопіювати конфігурацію\",\n    \"copy_config_success\": \"Конфігурацію скопійовано в буфер обміну!\",\n    \"copy_logs\": \"Копіювати журнали\",\n    \"download_logs\": \"Завантажити журнали\",\n    \"force_close\": \"Закрити примусово\",\n    \"force_close_desc\": \"Якщо Moonlight скаржиться на запущену програму, примусове закриття програми має вирішити проблему.\",\n    \"force_close_error\": \"Помилка під час закриття програми\",\n    \"force_close_success\": \"Застосунок успішно закрито!\",\n    \"ignore_case\": \"Ігнорувати регістр\",\n    \"logout\": \"Вийти\",\n    \"logout_desc\": \"Вийти. Може бути потрібно знову авторизуватися.\",\n    \"logout_localhost_tip\": \"Поточне середовище не вимагає входу; вихід не викличе запит пароля.\",\n    \"logs\": \"Логи\",\n    \"logs_desc\": \"Перегляньте логи, завантажені Sunshine\",\n    \"logs_find\": \"Пошук...\",\n    \"match_contains\": \"Містить\",\n    \"match_exact\": \"Точно\",\n    \"match_regex\": \"Regex\",\n    \"reopen_setup_wizard\": \"Повторно відкрити майстер налаштування\",\n    \"reopen_setup_wizard_desc\": \"Повторно відкрити сторінку майстра налаштування для переналаштування початкових параметрів.\",\n    \"reopen_setup_wizard_error\": \"Помилка при повторному відкритті майстра налаштування\",\n    \"reset_display_device_desc_windows\": \"Якщо Sunshine застряг при спробі відновити змінені налаштування пристрою відображення, ви можете скинути налаштування та вручну відновити стан дисплея.\\nЦе може статися з різних причин: пристрій більше не доступний, був підключений до іншого порту тощо.\",\n    \"reset_display_device_error_windows\": \"Помилка при скиданні persistencji!\",\n    \"reset_display_device_success_windows\": \"Скидання persistencji успішно завершено!\",\n    \"reset_display_device_windows\": \"Скинути пам'ять дисплея\",\n    \"restart_sunshine\": \"Перезапустити Sunshine\",\n    \"restart_sunshine_desc\": \"Якщо Sunshine не працює належним чином, ви можете спробувати перезапустити його. Це призведе до завершення усіх запущених сеансів.\",\n    \"restart_sunshine_success\": \"Sunshine перезапускається\",\n    \"troubleshooting\": \"Усунення неполадок\",\n    \"unpair_all\": \"Відв'язати всі пари\",\n    \"unpair_all_error\": \"Помилка під час від'єднання пари\",\n    \"unpair_all_success\": \"Усі пристрої не під'єднані.\",\n    \"unpair_desc\": \"Видаліть пов’язані пристрої. Вбудовані пристрої з активною сесією залишаться, але не зможуть почати або відновити сеанс.\",\n    \"unpair_single_no_devices\": \"Немає пов'язаних пристроїв.\",\n    \"unpair_single_success\": \"Однак пристрій(ої) все ще можуть бути в активному сеансі. Використовуйте кнопку \\\"Примусове закриття\\\" вище, щоб завершити будь-які відкриті сеанси.\",\n    \"unpair_single_unknown\": \"Невідомий клієнт\",\n    \"unpair_title\": \"Відв'язати пристрої\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"Підтвердити пароль\",\n    \"create_creds\": \"Перед початком роботи нам потрібно, щоб ви створили нове ім'я користувача та пароль для доступу до Web UI.\",\n    \"create_creds_alert\": \"Наведені нижче облікові дані необхідні для доступу до Sunshine's Web UI. Зберігайте їх у безпеці, оскільки ви більше ніколи їх не побачите!\",\n    \"creds_local_only\": \"Ваші облікові дані зберігаються локально в автономному режимі та ніколи не завантажуються на жоден сервер.\",\n    \"error\": \"Помилка!\",\n    \"greeting\": \"Ласкаво просимо до Sunshine Foundation!\",\n    \"hide_password\": \"Сховати пароль\",\n    \"login\": \"Авторизація\",\n    \"network_error\": \"Помилка мережі, перевірте з'єднання\",\n    \"password\": \"Пароль\",\n    \"password_match\": \"Паролі збігаються\",\n    \"password_mismatch\": \"Паролі не співпадають\",\n    \"server_error\": \"Помилка сервера\",\n    \"show_password\": \"Показати пароль\",\n    \"success\": \"Успішно!\",\n    \"username\": \"Ім'я користувача\",\n    \"welcome_success\": \"Ця сторінка незабаром перезавантажиться, ваш браузер попросить вас ввести нові облікові дані\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/zh.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"应用\",\n    \"auto\": \"自动\",\n    \"autodetect\": \"自动检测（推荐）\",\n    \"beta\": \"(测试版)\",\n    \"cancel\": \"取消\",\n    \"close\": \"关闭\",\n    \"copied\": \"已复制到剪贴板\",\n    \"copy\": \"复制\",\n    \"delete\": \"删除\",\n    \"description\": \"说明\",\n    \"disabled\": \"禁用\",\n    \"disabled_def\": \"禁用（默认）\",\n    \"dismiss\": \"忽略\",\n    \"do_cmd\": \"打开时执行命令\",\n    \"download\": \"下载\",\n    \"edit\": \"编辑\",\n    \"elevated\": \"提权运行\",\n    \"enabled\": \"启用\",\n    \"enabled_def\": \"启用（默认）\",\n    \"error\": \"错误！\",\n    \"no_changes\": \"没有变化\",\n    \"note\": \"注：\",\n    \"password\": \"密码\",\n    \"remove\": \"移除\",\n    \"run_as\": \"以管理员身份运行\",\n    \"save\": \"保存\",\n    \"see_more\": \"查看更多\",\n    \"success\": \"成功！\",\n    \"undo_cmd\": \"退出应用时要执行的命令\",\n    \"username\": \"用户名\",\n    \"warning\": \"警告！\"\n  },\n  \"apps\": {\n    \"actions\": \"Clients\",\n    \"add_cmds\": \"添加命令\",\n    \"add_new\": \"添加新应用\",\n    \"advanced_options\": \"高级选项\",\n    \"app_name\": \"应用名称\",\n    \"app_name_desc\": \"在 Moonlight 中显示的应用名称\",\n    \"applications_desc\": \"应用列表仅在客户端重启时刷新\",\n    \"applications_title\": \"应用\",\n    \"auto_detach\": \"启动串流后应用突然关闭时不退出串流\",\n    \"auto_detach_desc\": \"自动检测启动后很快关闭的启动类应用（如启动器）。检测到的应用将被视为独立应用处理。\",\n    \"basic_info\": \"基本信息\",\n    \"cmd\": \"主程序\",\n    \"cmd_desc\": \"要启动的主要应用程序。留空则不启动任何应用。\",\n    \"cmd_examples_title\": \"常用示例：\",\n    \"cmd_note\": \"如果可执行文件路径包含空格，必须用引号括起来。\",\n    \"cmd_prep_desc\": \"应用运行前后执行的命令列表。前置命令失败将中止应用启动。\",\n    \"cmd_prep_name\": \"命令准备\",\n    \"command_settings\": \"命令设置\",\n    \"covers_found\": \"找到的封面\",\n    \"delete\": \"删除\",\n    \"delete_confirm\": \"确定要删除应用 \\\"{name}\\\" 吗？\",\n    \"detached_cmds\": \"独立命令\",\n    \"detached_cmds_add\": \"添加独立命令\",\n    \"detached_cmds_desc\": \"在后台运行的命令列表\",\n    \"detached_cmds_note\": \"如果可执行文件路径包含空格，请用引号括起来。\",\n    \"detached_cmds_remove\": \"删除独立命令\",\n    \"edit\": \"编辑\",\n    \"env_app_id\": \"应用 ID\",\n    \"env_app_name\": \"应用名称\",\n    \"env_client_audio_config\": \"客户端请求的音频配置 (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"客户端请求自动优化游戏设置 (true/false)\",\n    \"env_client_fps\": \"客户端请求的帧率 (int)\",\n    \"env_client_gcmap\": \"客户端请求的游戏手柄掩码 (bitset/bitfield 格式, int)\",\n    \"env_client_hdr\": \"HDR 已启用 (true/false)\",\n    \"env_client_height\": \"客户端请求的分辨率高度 (int)\",\n    \"env_client_host_audio\": \"客户端请求在主机播放声音 (true/false)\",\n    \"env_client_name\": \"客户端名称 (string)\",\n    \"env_client_width\": \"客户端请求的分辨率宽度 (int)\",\n    \"env_displayplacer_example\": \"示例 - 使用 displayplacer 自动调整分辨率：\",\n    \"env_qres_example\": \"示例 - 使用 QRes 自动调整分辨率：\",\n    \"env_qres_path\": \"QRes 路径\",\n    \"env_var_name\": \"变量名称\",\n    \"env_vars_about\": \"关于环境变量\",\n    \"env_vars_desc\": \"默认情况下，所有命令都会获得以下环境变量：\",\n    \"env_xrandr_example\": \"示例 - 使用 Xrandr 自动调整分辨率：\",\n    \"exit_timeout\": \"退出超时\",\n    \"exit_timeout_desc\": \"请求退出时，等待所有应用进程正常关闭的秒数。未设置时默认等待5秒。设置为零或负值将立即终止应用。\",\n    \"mouse_mode\": \"鼠标模式\",\n    \"mouse_mode_desc\": \"选择此应用的鼠标输入方式。自动使用全局设置，虚拟鼠标使用 HID 驱动，SendInput 使用 Windows API。\",\n    \"mouse_mode_auto\": \"自动（使用全局设置）\",\n    \"mouse_mode_vmouse\": \"虚拟鼠标\",\n    \"mouse_mode_sendinput\": \"SendInput（Windows 输入 API）\",\n    \"file_selector_not_initialized\": \"文件选择器未初始化\",\n    \"find_cover\": \"查找封面\",\n    \"form_invalid\": \"请检查必填字段\",\n    \"form_valid\": \"应用合规\",\n    \"global_prep_desc\": \"启用或禁用此应用的全局预处理命令\",\n    \"global_prep_name\": \"全局预处理命令\",\n    \"image\": \"图片\",\n    \"image_desc\": \"发送到客户端的应用图标/图片路径, 未设置时使用默认图片。\",\n    \"image_settings\": \"图片设置\",\n    \"loading\": \"加载中...\",\n    \"menu_cmd_actions\": \"操作\",\n    \"menu_cmd_add\": \"添加菜单命令\",\n    \"menu_cmd_command\": \"指令\",\n    \"menu_cmd_desc\": \"配置后在客户端返回菜单中可见，用于在不打断串流的情况下快速执行特定操作，例如调出辅助程序。\\n示例：展示名称-关闭你的电脑；指令-shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"展示名称\",\n    \"menu_cmd_drag_sort\": \"拖拽排序\",\n    \"menu_cmd_name\": \"菜单命令\",\n    \"menu_cmd_placeholder_command\": \"命令\",\n    \"menu_cmd_placeholder_display_name\": \"显示名称\",\n    \"menu_cmd_placeholder_execute\": \"执行命令\",\n    \"menu_cmd_placeholder_undo\": \"撤销命令\",\n    \"menu_cmd_remove_menu\": \"删除菜单命令\",\n    \"menu_cmd_remove_prep\": \"删除准备命令\",\n    \"name\": \"名称\",\n    \"output_desc\": \"存储命令输出的文件路径。未指定时忽略输出。\",\n    \"output_name\": \"输出\",\n    \"run_as_desc\": \"某些需要管理员权限才能正常运行的应用可能需要此设置\",\n    \"scan_result_add_all\": \"全部添加\",\n    \"scan_result_edit_title\": \"添加并编辑\",\n    \"scan_result_filter_all\": \"全部\",\n    \"scan_result_filter_executable\": \"可执行\",\n    \"scan_result_filter_executable_title\": \"可执行文件\",\n    \"scan_result_filter_script\": \"脚本\",\n    \"scan_result_filter_script_title\": \"批处理/命令脚本\",\n    \"scan_result_filter_shortcut\": \"快捷方式\",\n    \"scan_result_filter_shortcut_title\": \"快捷方式\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_filter_steam_title\": \"Steam 游戏\",\n    \"scan_result_filter_epic_title\": \"Epic Games 游戏\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxy 游戏\",\n    \"scan_result_game\": \"游戏\",\n    \"scan_result_games_only\": \"仅游戏\",\n    \"scan_result_matched\": \"匹配: {count}\",\n    \"scan_result_no_apps\": \"未找到可添加的应用\",\n    \"scan_result_no_matches\": \"未找到匹配的应用\",\n    \"scan_result_quick_add_title\": \"直接添加\",\n    \"scan_result_remove_title\": \"从列表移除\",\n    \"scan_result_search_placeholder\": \"搜索应用名称、命令或路径...\",\n    \"scan_result_show_all\": \"显示全部\",\n    \"scan_result_title\": \"扫描结果\",\n    \"scan_result_try_different_keywords\": \"尝试使用不同的搜索关键词\",\n    \"scan_result_type_batch\": \"批处理\",\n    \"scan_result_type_command\": \"命令脚本\",\n    \"scan_result_type_executable\": \"可执行文件\",\n    \"scan_result_type_shortcut\": \"快捷方式\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"搜索应用...\",\n    \"select\": \"选择\",\n    \"test_menu_cmd\": \"测试命令\",\n    \"test_menu_cmd_empty\": \"命令不能为空\",\n    \"test_menu_cmd_executing\": \"正在执行命令...\",\n    \"test_menu_cmd_failed\": \"命令执行失败\",\n    \"test_menu_cmd_success\": \"命令执行成功！\",\n    \"use_desktop_image\": \"使用当前桌面壁纸\",\n    \"wait_all\": \"等待所有应用进程终止\",\n    \"wait_all_desc\": \"串流将持续到应用启动的所有进程终止。未选中时，串流将在初始应用进程终止时停止，即使其他进程仍在运行。\",\n    \"working_dir\": \"工作目录\",\n    \"working_dir_desc\": \"传递给进程的工作目录。某些应用使用工作目录搜索配置文件。未设置时默认使用命令的父目录。\"\n  },\n  \"config\": {\n    \"adapter_name\": \"适配器名称\",\n    \"adapter_name_desc_linux_1\": \"手动指定用于捕获的 GPU。\",\n    \"adapter_name_desc_linux_2\": \"找到所有能够使用 VAAPI 的设备\",\n    \"adapter_name_desc_linux_3\": \"用上面的设备替换``renderD129``，列出设备的名称和功能。要获得 Sunshine 的支持，设备至少需要具备以下功能：\",\n    \"adapter_name_desc_windows\": \"手动指定用于捕获的 GPU 。如果未设置，GPU 将被自动选择。注意：此GPU 必须连接并开启显示器。<br>如果笔记本无法启用独显直连，此处请设置为自动。\",\n    \"adapter_name_desc_windows_vdd_hint\": \"如有安装最新版基地显示器，可自动关联GPU绑定\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"添加\",\n    \"address_family\": \"IP 地址族\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"设置 Sunshine 使用的 IP 地址族\",\n    \"address_family_ipv4\": \"仅 IPv4\",\n    \"always_send_scancodes\": \"总是发送扫描码\",\n    \"always_send_scancodes_desc\": \"发送扫描码增强了与游戏和应用的兼容性，但可能会导致某些客户端输入不正确的键盘，而这些客户端不使用美国的英文键盘布局。 如果键盘输入在某些应用程序中根本无法工作，请启用。 如果客户端上的密钥在主机上生成错误的输入，请禁用。\",\n    \"amd_coder\": \"AMF 编码器 (H264)\",\n    \"amd_coder_desc\": \"允许您选择用于优先质量或编码速度的缠绕编码。 H.264。\",\n    \"amd_slices_per_frame\": \"AMF 每帧分片数\",\n    \"amd_slices_per_frame_auto\": \"自动（由客户端决定）\",\n    \"amd_slices_per_frame_desc\": \"将每帧分成多个分片/瓦片进行并行编码和解码，可降低延迟。对于 H.264/HEVC 设置每帧分片数；对于 AV1 设置每帧瓦片数。设为 0 由客户端决定。\",\n    \"amd_enforce_hrd\": \"AMF 推测参考解码器 (HRD)\",\n    \"amd_enforce_hrd_desc\": \"增强对码率控制的限制，以满足假想参考解码器（HRD）模型的要求。 这可以大大降低超过指定码率限制的可能，但可能导致编码伪影或降低在某些显卡上的编码质量。\",\n    \"amd_preanalysis\": \"AMF 预分析\",\n    \"amd_preanalysis_desc\": \"启用码率控制预分析，可能会以增加编码延迟为代价提高质量。\",\n    \"amd_quality\": \"AMF 质量\",\n    \"amd_quality_balanced\": \"balanced -- 平衡（默认）\",\n    \"amd_quality_desc\": \"这将控制编码速度和质量之间的平衡。\",\n    \"amd_quality_group\": \"AMF 质量设置\",\n    \"amd_quality_quality\": \"quality -- 偏好质量\",\n    \"amd_quality_speed\": \"speed -- 偏好速度\",\n    \"amd_qvbr_quality\": \"AMF QVBR 质量等级\",\n    \"amd_qvbr_quality_desc\": \"QVBR码率控制模式的质量等级。范围：1-51（越低越好）。默认：23。仅在码率控制设为 'qvbr' 时生效。\",\n    \"amd_rc\": \"AMF 码率控制\",\n    \"amd_rc_cbr\": \"cbr -- 恒定比特率（在启用HDR时推荐）\",\n    \"amd_rc_cqp\": \"cqp -- 恒定 QP 模式\",\n    \"amd_rc_desc\": \"这将控制码率控制方法，确保我们不超过客户端比特率限制。 “cqp”不适合于比特率限制，除“vbr_latency”之外的其他选项依赖于HRD（假想参考解码器）限制来防止比特率超过限制。\",\n    \"amd_rc_group\": \"AMF 码率控制设置\",\n    \"amd_rc_hqcbr\": \"hqcbr -- 高质量恒定比特率\",\n    \"amd_rc_hqvbr\": \"hqvbr -- 高质量可变比特率\",\n    \"amd_rc_qvbr\": \"qvbr -- 质量可变比特率（使用QVBR质量等级）\",\n    \"amd_rc_vbr_latency\": \"vbr_latency -- 考虑延迟的可变比特率（在禁用HDR时推荐使用；默认）\",\n    \"amd_rc_vbr_peak\": \"vbr_peak -- 受峰值限制的可变比特率\",\n    \"amd_usage\": \"AMF 工作模式\",\n    \"amd_usage_desc\": \"设置基本编码配置文件。 以下列出的所有选项将覆盖使用情况简介的子集，但是应用到了其他不可配置的隐藏设置。\",\n    \"amd_usage_lowlatency\": \"lowlatency - 低延迟（最快）\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality -- 低延迟、高质量（快速）\",\n    \"amd_usage_transcoding\": \"transcoding -- 转码（最慢）\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency -- 超低延迟（最快；默认）\",\n    \"amd_usage_webcam\": \"webcam -- 网络摄像头（慢）\",\n    \"amd_vbaq\": \"AMF 基于方差的自适应量化 (VBAQ)\",\n    \"amd_vbaq_desc\": \"人类的视觉系统通常对高度纹理化区域中的瑕疵不太敏感。在VBAQ模式下，像素方差被用来指示空间纹理的复杂性，这使得编码器可以将更多的比特分配给更平滑的区域。启用这个特性可以在某些内容上提升主观视觉质量。\",\n    \"amf_draw_mouse_cursor\": \"使用 AMF 捕获时绘制简易鼠标指针\",\n    \"amf_draw_mouse_cursor_desc\": \"某些情况下，使用 AMF 捕获不会显示鼠标指针，启用此选项会在屏幕上绘制简易鼠标指针。注意：鼠标指针位置仅在内容画面更新时更新，因此在桌面等非游戏场景下可能会观察到鼠标移动卡顿。\",\n    \"apply_note\": \"点击“应用”重启 Sunshine 并应用更改。这将终止任何正在运行的会话。\",\n    \"audio_sink\": \"音频输出设备\",\n    \"audio_sink_desc_linux\": \"手动指定需要抓取的音频输出设备。如果您没有指定此变量，PulseAudio 将选择默认监测设备。 您可以使用以下任何命令找到音频输出设备的名称：\",\n    \"audio_sink_desc_macos\": \"手动指定需要抓取的音频输出设备。由于系统限制，Sunshine 在 macOS 上只能访问麦克风。 使用 Soundflow 或 BlackHole 来串流系统音频。\",\n    \"audio_sink_desc_windows\": \"手动指定要抓取的特定音频设备。如果未设置，则自动选择该设备。 我们强烈建议将此字段留空以使用自动选择设备！ 如果您有多个具有相同名称的音频设备，您可以使用以下命令获取设备ID：\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"扬声器（High Definition Audio Device）\",\n    \"av1_mode\": \"AV1 支持\",\n    \"av1_mode_0\": \"根据编码器能力自动通告 AV1 支持（推荐）\",\n    \"av1_mode_1\": \"不通告 AV1 支持\",\n    \"av1_mode_2\": \"通告 AV1 Main 8-bit 配置支持\",\n    \"av1_mode_3\": \"通告 AV1 Main 8-bit 和 10-bit (HDR) 配置支持\",\n    \"av1_mode_desc\": \"允许客户端请求 AV1 Main 8-bit 或 10-bit 视频流。AV1 编码对 CPU 要求较高，使用软件编码时启用可能会影响性能。\",\n    \"back_button_timeout\": \"主页/导航按钮模拟超时\",\n    \"back_button_timeout_desc\": \"按住\\\"返回/选择\\\"按钮达到指定毫秒数后，将模拟按下\\\"主页/导航\\\"按钮。若设置值小于0（默认值），则不会触发此功能。\",\n    \"bind_address\": \"绑定地址（测试功能）\",\n    \"bind_address_desc\": \"设置 Sunshine 绑定的 IP 地址。如果留空，Sunshine 将绑定到所有可用接口（0.0.0.0 用于 IPv4 或 :: 用于 IPv6）。\",\n    \"capture\": \"强制指定捕获方法\",\n    \"capture_desc\": \"自动模式下，Sunshine将优先使用第一个可正常工作的捕获模式。NvFBC需要已打补丁的NVIDIA驱动程序。\",\n    \"amd_capture_no_virtual_display\": \"AMD Display Capture 不支持虚拟显示驱动（如 IddCx/VDD）。如果你正在使用虚拟显示器，请改用 WGC 或 Desktop Duplication API。\",\n    \"capture_target\": \"捕获目标\",\n    \"capture_target_desc\": \"选择要捕获的目标类型。选择\\\"窗口\\\"时，可以捕获特定应用程序窗口（如AI插帧软件），而不是整个显示器。\",\n    \"capture_target_display\": \"显示器\",\n    \"capture_target_window\": \"窗口\",\n    \"cert\": \"证书\",\n    \"cert_desc\": \"用于Web UI和Moonlight客户端配对的证书。为获得最佳兼容性，建议使用RSA-2048公钥。\",\n    \"channels\": \"最大同时连接客户端数\",\n    \"channels_desc_1\": \"Sunshine支持多个客户端同时共享同一串流会话。\",\n    \"channels_desc_2\": \"部分硬件编码器可能存在多流性能限制。\",\n    \"close_verify_safe\": \"安全验证兼容旧版客户端\",\n    \"close_verify_safe_desc\": \"旧版客户端可能无法连接到 Sunshine, 请关闭此选项或升级客户端\",\n    \"coder_cabac\": \"cabac -- 上下文自适应二进制算术编码（质量优先）\",\n    \"coder_cavlc\": \"cavlc -- 上下文自适应变长编码（解码速度优先）\",\n    \"configuration\": \"配置\",\n    \"controller\": \"启用游戏手柄输入\",\n    \"controller_desc\": \"允许客户端通过游戏手柄控制主机系统\",\n    \"credentials_file\": \"凭据文件\",\n    \"credentials_file_desc\": \"将用户名和密码与Sunshine的状态文件分开存储\",\n    \"display_device_options_note_desc_windows\": \"Windows会记忆当前活动显示器的各种显示设置组合。\\nSunshine会应用这些组合状态。\\n如果断开Sunshine应用设置时处于活动状态的设备，除非在Sunshine尝试恢复更改时能重新激活该组合，否则无法恢复预期的显示状态。\\n若因错误设置导致实体显示器无法显示，可在Sunshine故障排除页面重置显示设备记忆，或在非串流状态下按两次WIN+P键直到实体显示器恢复显示。\",\n    \"display_device_options_note_windows\": \"串流开始/结束时的显示器组合状态设置\",\n    \"display_device_options_windows\": \"显示设备设置\",\n    \"display_device_prep_ensure_active_desc_windows\": \"如果显示器未激活则激活它\",\n    \"display_device_prep_ensure_active_windows\": \"自动激活指定显示器串流\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"停用其他显示器，只开启指定的显示器\",\n    \"display_device_prep_ensure_only_display_windows\": \"仅启用指定显示器串流（禁用其他显示器）\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"确保显示器激活并设置为主显示器\",\n    \"display_device_prep_ensure_primary_windows\": \"主屏串流（自动激活指定显示器并设为主显示器）\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"仅使用基地显示器进行副屏扩展串流\",\n    \"display_device_prep_ensure_secondary_windows\": \"副屏串流（仅使用基地显示器支持）\",\n    \"display_device_prep_no_operation_desc_windows\": \"不对显示器进行任何操作，用户需自行确保显示器状态\",\n    \"display_device_prep_no_operation_windows\": \"无操作\",\n    \"display_device_prep_windows\": \"串流时显示器组合状态设置\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"必须至少指定一个\\\"接收\\\"值和一个\\\"最终\\\"值。\\n\\\"接收\\\"部分的空字段表示\\\"匹配任意值\\\"。\\\"最终\\\"部分的空字段表示\\\"保持接收值\\\"。\\n您可以根据需要将特定的FPS值与特定的分辨率进行匹配...\\n\\n注意：如果在Moonlight客户端上未启用\\\"优化游戏设置\\\"选项，则包含分辨率值的行将被忽略。\",\n    \"display_mode_remapping_desc_windows\": \"指定如何将特定的分辨率和/或刷新率重新映射到其他值。\\n您可以在较低分辨率下进行串流，同时在主机上以较高分辨率渲染以获得超采样效果。\\n或者您可以在较高FPS下进行串流，同时将主机限制在较低的刷新率。\\n匹配从上到下执行。一旦匹配到条目，其他条目将不再检查，但仍会进行验证。\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"最终刷新率\",\n    \"display_mode_remapping_final_resolution_windows\": \"最终分辨率\",\n    \"display_mode_remapping_optional\": \"可选\",\n    \"display_mode_remapping_received_fps_windows\": \"接收的FPS\",\n    \"display_mode_remapping_received_resolution_windows\": \"接收的分辨率\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"注意：如果在Moonlight客户端上未启用\\\"优化游戏设置\\\"选项，则重新映射功能将被禁用。\",\n    \"display_mode_remapping_windows\": \"重新映射显示模式\",\n    \"display_modes\": \"显示模式\",\n    \"ds4_back_as_touchpad_click\": \"映射回/选择触摸板点击\",\n    \"ds4_back_as_touchpad_click_desc\": \"强制使用 DS4 模拟时，将“返回”/“选择”映射到触摸板点击\",\n    \"dsu_server_port\": \"DSU服务器端口\",\n    \"dsu_server_port_desc\": \"DSU服务器的监听端口（默认26760）。Sunshine将作为DSU服务器接收客户端连接并发送运动数据。在你的客户端(Yuzu,Ryujinx等)中启用DSU服务器并设置DSU服务器地址(127.0.0.1)和端口(26760)\",\n    \"enable_dsu_server\": \"启用DSU服务器\",\n    \"enable_dsu_server_desc\": \"启用后，Sunshine将作为DSU服务器接收客户端连接并发送运动数据\",\n    \"encoder\": \"强制指定编码器\",\n    \"encoder_desc\": \"强制指定一个特定编码器，否则 Sunshine 将选择最佳可用选项。注意：如果您在 Windows 上指定了硬件编码器，它必须匹配连接显示器的 GPU。\",\n    \"encoder_software\": \"软件\",\n    \"experimental\": \"实验性\",\n    \"experimental_features\": \"实验性功能\",\n    \"external_ip\": \"外部 IP\",\n    \"external_ip_desc\": \"如果没有指定外部 IP 地址，Sunshine 将自动检测外部 IP\",\n    \"fec_percentage\": \"FEC (前向纠错) 参数\",\n    \"fec_percentage_desc\": \"每个视频帧中的错误纠正数据包百分比。较高的值可纠正更多的网络数据包丢失，但代价是增加带宽使用量。\",\n    \"ffmpeg_auto\": \"auto -- 由 ffmpeg 决定（默认）\",\n    \"file_apps\": \"应用程序配置文件\",\n    \"file_apps_desc\": \"Sunshine 保存应用程序配置的文件。\",\n    \"file_state\": \"实时状态文件\",\n    \"file_state_desc\": \"Sunshine 保存当前状态的文件\",\n    \"fps\": \"基地显示器支持的帧率\",\n    \"gamepad\": \"模拟游戏手柄类型\",\n    \"gamepad_auto\": \"自动选择选项\",\n    \"gamepad_desc\": \"选择要在主机上模拟的游戏手柄类型\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 选择选项\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"DS4 手柄手动配置选项\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"命令准备工作\",\n    \"global_prep_cmd_desc\": \"任何应用运行前/后要运行的命令列表。如果任何前置命令失败，应用的启动过程将被中止。\",\n    \"hdr_luminance_analysis\": \"HDR 动态元数据 (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"启用逐帧 GPU 亮度分析，并将 HDR10+ (ST 2094-40) 和 HDR Vivid (CUVA) 动态元数据注入编码比特流。可为支持动态 HDR 的终端（如华为 HDR Vivid 设备）提供逐帧色调映射参考。在高分辨率下会增加少量 GPU 开销（约 0.5-1.5ms/帧）。若开启 HDR 后出现帧率下降，可关闭此选项，串流将仅使用静态 HDR 元数据。\",\n    \"hdr_prep_automatic_windows\": \"根据客户端请求开启/关闭 HDR 模式\",\n    \"hdr_prep_no_operation_windows\": \"禁用\",\n    \"hdr_prep_windows\": \"HDR 状态更改\",\n    \"hevc_mode\": \"HEVC 支持\",\n    \"hevc_mode_0\": \"根据编码器能力自动通告 HEVC 支持（推荐）\",\n    \"hevc_mode_1\": \"不通告 HEVC 支持\",\n    \"hevc_mode_2\": \"通告 HEVC Main 配置支持\",\n    \"hevc_mode_3\": \"通告 HEVC Main 和 Main10 (HDR) 配置支持\",\n    \"hevc_mode_desc\": \"允许客户端请求 HEVC Main 或 HEVC Main10 视频流。HEVC 编码对性能要求更高，使用软件编码时可能会影响性能。\",\n    \"high_resolution_scrolling\": \"高分辨率滚动支持\",\n    \"high_resolution_scrolling_desc\": \"启用后，Sunshine 将透传来自 Moonlight 客户端的高分辨率滚动事件。对于那些使用高分辨率滚动事件时滚动速度过快的旧版应用程序来说，禁用此功能非常有用。\",\n    \"install_steam_audio_drivers\": \"安装 Steam 音频驱动程序\",\n    \"install_steam_audio_drivers_desc\": \"如果安装了 Steam，则会自动安装 Steam Streaming Speakers 驱动程序，以支持 5.1/7.1 环绕声和主机音频静音。\",\n    \"key_repeat_delay\": \"按键重复延迟\",\n    \"key_repeat_delay_desc\": \"控制按键重复的速度。重复按键前的初始延迟（毫秒）。\",\n    \"key_repeat_frequency\": \"按键重复频率\",\n    \"key_repeat_frequency_desc\": \"按键每秒重复多少次。此可配置的选项支持小数。\",\n    \"key_rightalt_to_key_win\": \"将右 Alt 键映射为 Windows 键\",\n    \"key_rightalt_to_key_win_desc\": \"您可能无法直接从 Moonlight 发送 Windows 键。在这种情况下，让 Sunshine 认为右 Alt 键是 Windows 键可能会很有用。\",\n    \"key_rightalt_to_key_windows\": \"将右 Alt 键映射为 Windows 键\",\n    \"keyboard\": \"启用键盘输入\",\n    \"keyboard_desc\": \"允许客户端使用键盘控制主机系统\",\n    \"lan_encryption_mode\": \"局域网加密模式\",\n    \"lan_encryption_mode_1\": \"为支持的客户端启用\",\n    \"lan_encryption_mode_2\": \"强制所有客户端使用\",\n    \"lan_encryption_mode_desc\": \"这将决定在本地网络上进行流媒体传输时何时使用加密。加密会降低流媒体性能，尤其是在功能较弱的主机和客户端上。\",\n    \"locale\": \"本地化\",\n    \"locale_desc\": \"用于 Sunshine 用户界面的本地化设置。\",\n    \"log_level\": \"日志级别\",\n    \"log_level_0\": \"详细 (Verbose)\",\n    \"log_level_1\": \"调试 (Debug)\",\n    \"log_level_2\": \"信息 (Info)\",\n    \"log_level_3\": \"警告 (Warning)\",\n    \"log_level_4\": \"错误 (Error)\",\n    \"log_level_5\": \"致命 (Fatal)\",\n    \"log_level_6\": \"无 (None)\",\n    \"log_level_desc\": \"打印到标准输出的最小日志级别\",\n    \"log_path\": \"日志文件路径\",\n    \"log_path_desc\": \"Sunshine 当前日志文件的存储路径。\",\n    \"max_bitrate\": \"最大比特率\",\n    \"max_bitrate_desc\": \"Sunshine 编码流的最大比特率（Kbps）。如果设置为 0，将始终使用 Moonlight 客户端请求的比特率。\",\n    \"max_fps_reached\": \"已达到最大帧率值\",\n    \"max_resolutions_reached\": \"已达到最大分辨率数量\",\n    \"mdns_broadcast\": \"本地网络发现此计算机\",\n    \"mdns_broadcast_desc\": \"开启后将允许未连接设备自动发现此计算机，需要 Moonlight开启自动在本地网络上查找计算机\",\n    \"min_threads\": \"最低 CPU 线程数\",\n    \"min_threads_desc\": \"提高此值会略微降低编码效率，但通常值得以获得更多 CPU 核心用于编码。理想值是在您的硬件配置上以所需串流设置进行可靠编码的最低线程数。\",\n    \"minimum_fps_target\": \"最小编码帧率\",\n    \"minimum_fps_target_desc\": \"编码时要保持的最小帧率（0 = 自动，约为流帧率的一半；1-1000 = 要保持的最小帧率）。启用可变刷新率时，如果设置为 0，则忽略此设置。\",\n    \"misc\": \"其他选项\",\n    \"motion_as_ds4\": \"若客户端报告游戏手柄存在陀螺仪，则模拟 DS4 游戏手柄\",\n    \"motion_as_ds4_desc\": \"若禁用，在选择游戏手柄类型时将不考虑陀螺仪的存在。\",\n    \"mouse\": \"启用鼠标输入\",\n    \"mouse_desc\": \"允许客户端使用鼠标控制主机系统\",\n    \"native_pen_touch\": \"原生笔/触摸支持\",\n    \"native_pen_touch_desc\": \"启用后，Sunshine 将透传来自 Moonlight 客户端的原生笔/触控事件。对于不支持原生笔/触控的旧版应用程序，禁用此功能可能更有用。\",\n    \"no_fps\": \"未添加帧率值\",\n    \"no_resolutions\": \"未添加分辨率\",\n    \"notify_pre_releases\": \"预发布版本通知\",\n    \"notify_pre_releases_desc\": \"是否接收 Sunshine 新预发布版本的通知\",\n    \"nvenc_h264_cavlc\": \"在 H.264 中优先使用 CAVLC 而非 CABAC\",\n    \"nvenc_h264_cavlc_desc\": \"一种更简单的熵编码形式。相同质量下，CAVLC 需要增加约 10% 的比特率。仅适用于非常老旧的解码设备。\",\n    \"nvenc_latency_over_power\": \"优先降低编码延迟而非省电\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine 在串流时请求最高 GPU 核心频率以降低编码延迟。不建议禁用，否则会显著增加编码延迟。\",\n    \"nvenc_lookahead_depth\": \"前瞻深度\",\n    \"nvenc_lookahead_depth_desc\": \"编码时前瞻的帧数（0-32）。前瞻功能可以提升编码质量，特别是在复杂场景中，通过提供更好的运动估计和码率分配。数值越高，质量越好，但会增加编码延迟。设置为 0 可禁用前瞻。需要 NVENC SDK 13.0 (1202) 或更新版本。\",\n    \"nvenc_lookahead_level\": \"前瞻级别\",\n    \"nvenc_lookahead_level_0\": \"级别 0（最低质量，最快）\",\n    \"nvenc_lookahead_level_1\": \"级别 1\",\n    \"nvenc_lookahead_level_2\": \"级别 2\",\n    \"nvenc_lookahead_level_3\": \"级别 3（最高质量，最慢）\",\n    \"nvenc_lookahead_level_autoselect\": \"自动选择（让驱动选择最优级别）\",\n    \"nvenc_lookahead_level_desc\": \"前瞻质量级别。更高级别会提升质量，但会降低性能。此选项仅在前瞻深度大于 0 时生效。需要 NVENC SDK 13.0 (1202) 或更新版本。\",\n    \"nvenc_lookahead_level_disabled\": \"禁用（等同于级别 0）\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"在 DXGI 基础上呈现 OpenGL/Vulkan\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine 无法以满帧率捕获不在 DXGI 顶部的全屏 OpenGL 和 Vulkan 程序。此为系统级设置，Sunshine 退出时会恢复。\",\n    \"nvenc_preset\": \"性能预设\",\n    \"nvenc_preset_1\": \"最快（默认）\",\n    \"nvenc_preset_7\": \"最慢\",\n    \"nvenc_preset_desc\": \"数值越大，压缩效果（相同比特率下的质量）越好，但编码延迟会增加。建议仅在受网络或解码器限制时更改，否则可通过提高比特率获得类似效果。\",\n    \"nvenc_rate_control\": \"码率控制模式\",\n    \"nvenc_rate_control_cbr\": \"CBR（恒定码率）- 低延迟\",\n    \"nvenc_rate_control_desc\": \"选择码率控制模式。CBR（恒定码率）提供固定码率，适合低延迟流媒体。VBR（可变码率）允许码率根据场景复杂度变化，在复杂场景中提供更好的质量，但码率会变化。\",\n    \"nvenc_rate_control_vbr\": \"VBR（可变码率）- 更高质量\",\n    \"nvenc_realtime_hags\": \"在硬件加速 GPU 调度中使用实时优先级\",\n    \"nvenc_realtime_hags_desc\": \"当前启用 HAGS、使用实时优先级且 VRAM 利用率接近最大值时，NVIDIA 驱动程序可能冻结编码器。禁用此选项可将优先级降至高，避免冻结，但 GPU 负载较高时捕捉性能会降低。\",\n    \"nvenc_spatial_aq\": \"空间自适应量化\",\n    \"nvenc_spatial_aq_desc\": \"将较高的 QP 值分配给视频的平坦区域。建议在较低比特率串流时启用。\",\n    \"nvenc_spatial_aq_disabled\": \"禁用（更快，默认）\",\n    \"nvenc_spatial_aq_enabled\": \"启用（较慢）\",\n    \"nvenc_split_encode\": \"分割帧编码\",\n    \"nvenc_split_encode_desc\": \"将每个视频帧的编码分配到多个 NVENC 硬件单元。显著降低编码延迟，压缩效率损失很小。如果您的 GPU 只有一个 NVENC 单元，此选项将被忽略。\",\n    \"nvenc_split_encode_driver_decides_def\": \"由驱动程序决定（默认）\",\n    \"nvenc_split_encode_four_strips\": \"强制 4 条带（需要 4+ NVENC 引擎）\",\n    \"nvenc_split_encode_three_strips\": \"强制 3 条带（需要 3+ NVENC 引擎）\",\n    \"nvenc_split_encode_two_strips\": \"强制 2 条带（需要 2+ NVENC 引擎）\",\n    \"nvenc_target_quality\": \"目标质量（VBR 模式）\",\n    \"nvenc_target_quality_desc\": \"VBR 模式的目标质量级别（H.264/HEVC 为 0-51，AV1 为 0-63）。值越低 = 质量越高。设为 0 表示自动质量选择。仅在码率控制模式为 VBR 时使用。\",\n    \"nvenc_temporal_aq\": \"时域自适应量化\",\n    \"nvenc_temporal_aq_desc\": \"启用时域自适应量化。时域 AQ 在时间维度上优化量化，提供更好的码率分配，并改善运动场景的质量。此功能与空间 AQ 配合使用，需要启用前瞻（前瞻深度 > 0）。需要 NVENC SDK 13.0 (1202) 或更新版本。\",\n    \"nvenc_temporal_filter\": \"时域滤波\",\n    \"nvenc_temporal_filter_4\": \"级别 4（最大强度）\",\n    \"nvenc_temporal_filter_desc\": \"编码前应用的时域滤波强度。时域滤波可以减少噪声并提高压缩效率，特别适合自然内容。更高级别提供更好的降噪效果，但可能会引入轻微的模糊。需要 NVENC SDK 13.0 (1202) 或更新版本。注意：需要 frameIntervalP >= 5，与 zeroReorderDelay 或立体声 MVC 不兼容。\",\n    \"nvenc_temporal_filter_disabled\": \"禁用（无时域滤波）\",\n    \"nvenc_twopass\": \"二次编码模式\",\n    \"nvenc_twopass_desc\": \"启用二次编码。可检测更多运动矢量，更好地分配帧比特率，并更严格地遵守比特率限制。不建议禁用，否则可能导致偶发比特率超限和后续丢包。\",\n    \"nvenc_twopass_disabled\": \"禁用（最快，不推荐）\",\n    \"nvenc_twopass_full_res\": \"全分辨率（较慢）\",\n    \"nvenc_twopass_quarter_res\": \"四分之一分辨率（快速，默认）\",\n    \"nvenc_vbv_increase\": \"单帧 VBV/HRD 百分比增加\",\n    \"nvenc_vbv_increase_desc\": \"默认 Sunshine 使用单帧 VBV/HRD，意味着任何编码视频帧大小预计不超过请求比特率除以帧率。放宽此限制可能有益，可作为低延迟可变比特率，但若网络无缓冲区处理比特率跳跃，也可能导致丢包。最大接受值为 400，相当于编码视频帧上限增加 5 倍。\",\n    \"origin_web_ui_allowed\": \"允许访问 Web UI 的来源\",\n    \"origin_web_ui_allowed_desc\": \"未被拒绝访问 Web UI 的远程地址来源\",\n    \"origin_web_ui_allowed_lan\": \"仅局域网设备可访问 Web UI\",\n    \"origin_web_ui_allowed_pc\": \"仅本地主机可访问 Web UI\",\n    \"origin_web_ui_allowed_wan\": \"任何人都可访问 Web UI\",\n    \"output_name_desc_unix\": \"Sunshine 启动时会显示检测到的显示器列表。注意：需使用括号内的 ID 值。\",\n    \"output_name_desc_windows\": \"指定用于捕获的显示器。若未设置，则捕获主显示器。注意：若已指定 GPU，此显示器必须连接到该 GPU。\",\n    \"output_name_unix\": \"显示器编号\",\n    \"output_name_windows\": \"输出显示器指定\",\n    \"ping_timeout\": \"Ping 超时\",\n    \"ping_timeout_desc\": \"关闭串流前等待 Moonlight 数据的时间（毫秒）\",\n    \"pkey\": \"私钥\",\n    \"pkey_desc\": \"用于 Web UI 和 Moonlilght 客户端配对的私钥。为了最佳兼容性，这应该是一个 RSA-2048 私钥。\",\n    \"port\": \"端口\",\n    \"port_alert_1\": \"Sunshine 不能使用低于1024 的端口！ \",\n    \"port_alert_2\": \"超过 65535 的端口不可用！\",\n    \"port_desc\": \"设置 Sunshine 使用的端口族\",\n    \"port_http_port_note\": \"使用此端口连接 Moonlight。\",\n    \"port_note\": \"说明\",\n    \"port_port\": \"端口\",\n    \"port_protocol\": \"协议\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"暴露 Web UI 到公网存在安全风险！请自行承担风险！\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"量化参数 (QP)\",\n    \"qp_desc\": \"某些设备可能不支持恒定码率。对于这些设备，则使用 QP 代替。数值越大，压缩率越高，但质量越差。\",\n    \"qsv_coder\": \"QuickSync 编码器 (H264)\",\n    \"qsv_preset\": \"QSV 编码器预设\",\n    \"qsv_preset_fast\": \"fast - 较快（较低质量）\",\n    \"qsv_preset_faster\": \"最快的 (最低质量)\",\n    \"qsv_preset_medium\": \"medium - 中等（默认）\",\n    \"qsv_preset_slow\": \"slow - 较慢（较高质量）\",\n    \"qsv_preset_slower\": \"slower - 更慢（更高质量）\",\n    \"qsv_preset_slowest\": \"slowest - 最慢（最高质量）\",\n    \"qsv_preset_veryfast\": \"最快的 (最低质量)\",\n    \"qsv_slow_hevc\": \"允许慢速 HEVC 编码\",\n    \"qsv_slow_hevc_desc\": \"这可以让英特尔旧的 GPU 上的 HEVC 编码，代价是GPU 使用率更高和性能更差。\",\n    \"refresh_rate_change_automatic_windows\": \"根据客户端提供的帧率值\",\n    \"refresh_rate_change_manual_desc_windows\": \"输入要使用的刷新率\",\n    \"refresh_rate_change_manual_windows\": \"使用手动输入的刷新率\",\n    \"refresh_rate_change_no_operation_windows\": \"禁用\",\n    \"refresh_rate_change_windows\": \"帧率更改\",\n    \"res_fps_desc\": \"为基地显示器设置的分辨率(不超过25组)、帧率(不超过5组)，预置组合已满足大部分常见串流客户端。\",\n    \"resolution_change_automatic_windows\": \"使用客户端要求的分辨率\",\n    \"resolution_change_manual_desc_windows\": \"\\\"优化游戏设置\\\" 必须在 Moonlight 客户端上启用选项才能生效。\",\n    \"resolution_change_manual_windows\": \"手动指定分辨率\",\n    \"resolution_change_no_operation_windows\": \"忽略客户端的分辨率要求\",\n    \"resolution_change_ogs_desc_windows\": \"\\\"优化游戏设置\\\" 必须在 Moonlight 客户端上启用选项才能生效。\",\n    \"resolution_change_windows\": \"分辨率调整\",\n    \"resolutions\": \"基地显示器支持的分辨率\",\n    \"restart_note\": \"正在重启 Sunshine 以应用更改。\",\n    \"sleep_mode\": \"睡眠模式\",\n    \"sleep_mode_away\": \"离开模式（关闭显示器，即时唤醒）\",\n    \"sleep_mode_desc\": \"控制客户端发送睡眠命令时的行为。挂起(S3)：传统睡眠，低功耗但需要 WOL 唤醒。休眠(S4)：保存到磁盘，极低功耗。离开模式：关闭显示器但系统保持运行，可即时唤醒 - 非常适合游戏串流服务器。\",\n    \"sleep_mode_hibernate\": \"休眠（S4）\",\n    \"sleep_mode_suspend\": \"挂起（S3 睡眠）\",\n    \"stream_audio\": \"启用串流音频\",\n    \"stream_audio_desc\": \"禁用此选项以停止串流音频。\",\n    \"stream_mic\": \"启用串流麦克风\",\n    \"stream_mic_desc\": \"禁用此选项以停止串流麦克风。\",\n    \"stream_mic_download_btn\": \"下载虚拟麦克风\",\n    \"stream_mic_download_confirm\": \"即将跳转到虚拟麦克风下载页面，是否继续？\",\n    \"stream_mic_note\": \"启用此功能需要安装虚拟麦克风\",\n    \"sunshine_name\": \"Sunshine 主机名称\",\n    \"sunshine_name_desc\": \"在 Moonlight 中显示的名称。如果未指定，则使用 PC 的主机名\",\n    \"sw_preset\": \"软件编码预设\",\n    \"sw_preset_desc\": \"在编码速度和压缩效率之间权衡。默认为 superfast - 超快。\",\n    \"sw_preset_fast\": \"fast - 快\",\n    \"sw_preset_faster\": \"faster - 更快\",\n    \"sw_preset_medium\": \"medium - 中等\",\n    \"sw_preset_slow\": \"slow - 慢\",\n    \"sw_preset_slower\": \"slower - 更慢\",\n    \"sw_preset_superfast\": \"superfast - 超快（默认）\",\n    \"sw_preset_ultrafast\": \"ultrafast - 极快\",\n    \"sw_preset_veryfast\": \"veryfast - 非常快\",\n    \"sw_preset_veryslow\": \"veryslow - 非常慢\",\n    \"sw_tune\": \"软件编码调校\",\n    \"sw_tune_animation\": \"animation -- 适合动画片；使用更高的去块和更多的参考帧\",\n    \"sw_tune_desc\": \"调校选项，在预设后应用。默认值为 zerolatency。\",\n    \"sw_tune_fastdecode\": \"fastdecode -- 通过禁用某些过滤器来加快解码速度\",\n    \"sw_tune_film\": \"film -- 用于高质量的电影内容；降低去块\",\n    \"sw_tune_grain\": \"grain -- 保留旧的颗粒胶片材料的颗粒结构\",\n    \"sw_tune_stillimage\": \"stillimage -- 适用于类似幻灯片的内容\",\n    \"sw_tune_zerolatency\": \"zerolatency -- 适用于快速编码和低延迟串流（默认值）\",\n    \"system_tray\": \"启用系统托盘\",\n    \"system_tray_desc\": \"是否启用系统托盘。启用后，Sunshine 将在系统托盘中显示图标，并可从系统托盘进行控制。\",\n    \"touchpad_as_ds4\": \"如果客户端报告游戏手柄存在触摸板，则模拟一个 DS4 游戏手柄\",\n    \"touchpad_as_ds4_desc\": \"如果禁用，则在选择游戏手柄类型时不会考虑触摸板的存在。\",\n    \"unsaved_changes_tooltip\": \"您有未保存的更改。点击保存。\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"为公网串流自动配置端口转发\",\n    \"variable_refresh_rate\": \"可变刷新率 (VRR)\",\n    \"variable_refresh_rate_desc\": \"允许视频流帧率与渲染帧率匹配以支持 VRR。启用后，仅在有新帧可用时进行编码，允许流跟随实际渲染帧率。\",\n    \"vdd_reuse_desc_windows\": \"不建议启用，启用此选项可加快客户端切换速度，但无法记忆屏幕缩放。\",\n    \"vdd_reuse_windows\": \"所有客户端复用同一基地显示器\",\n    \"virtual_display\": \"基地显示器\",\n    \"virtual_mouse\": \"虚拟鼠标驱动\",\n    \"virtual_mouse_desc\": \"启用后，Sunshine 将使用 Zako 虚拟鼠标驱动（需已安装）在 HID 层模拟鼠标输入，使 Raw Input 游戏能正常接收鼠标事件。禁用或未安装驱动时，回退至 SendInput。\",\n    \"virtual_sink\": \"虚拟音频输出设备\",\n    \"virtual_sink_desc\": \"手动指定要使用的虚拟音频设备。如果未设置，则会自动选择设备。我们强烈建议将此字段留空，以便使用自动设备选择！\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"安装虚拟鼠标驱动？\",\n    \"vmouse_confirm_uninstall\": \"卸载虚拟鼠标驱动？\",\n    \"vmouse_install\": \"安装驱动\",\n    \"vmouse_installing\": \"安装中...\",\n    \"vmouse_note\": \"虚拟鼠标驱动需要单独安装，请使用 Sunshine 控制面板来安装或管理驱动。\",\n    \"vmouse_refresh\": \"刷新状态\",\n    \"vmouse_status_installed\": \"已安装（未激活）\",\n    \"vmouse_status_not_installed\": \"未安装\",\n    \"vmouse_status_running\": \"运行中\",\n    \"vmouse_uninstall\": \"卸载驱动\",\n    \"vmouse_uninstalling\": \"卸载中...\",\n    \"vt_coder\": \"VideoToolbox 编码器\",\n    \"vt_realtime\": \"VideoToolbox 实时编码\",\n    \"vt_software\": \"VideoToolbox 软件编码\",\n    \"vt_software_allowed\": \"允许\",\n    \"vt_software_forced\": \"强制\",\n    \"wan_encryption_mode\": \"公网加密模式\",\n    \"wan_encryption_mode_1\": \"为支持的客户端启用（默认）\",\n    \"wan_encryption_mode_2\": \"强制所有客户端使用\",\n    \"wan_encryption_mode_desc\": \"这决定了在公网串流时是否加密。加密会降低串流性能，尤其是在性能较弱的主机和客户端上。\",\n    \"webhook_curl_command\": \"命令\",\n    \"webhook_curl_command_desc\": \"复制以下命令到终端中执行，可以测试 webhook 是否正常工作：\",\n    \"webhook_curl_copy_failed\": \"复制失败，请手动选择并复制\",\n    \"webhook_enabled\": \"Webhook 通知\",\n    \"webhook_enabled_desc\": \"启用后将向指定的 Webhook URL 发送事件通知\",\n    \"webhook_group\": \"Webhook 通知设置\",\n    \"webhook_skip_ssl_verify\": \"跳过 SSL 证书验证\",\n    \"webhook_skip_ssl_verify_desc\": \"跳过 HTTPS 连接的 SSL 证书验证，仅用于测试或自签名证书\",\n    \"webhook_test\": \"测试\",\n    \"webhook_test_failed\": \"Webhook 测试失败\",\n    \"webhook_test_failed_note\": \"提示：请检查 URL 是否正确，或查看浏览器控制台获取更多信息。\",\n    \"webhook_test_success\": \"Webhook 测试成功！\",\n    \"webhook_test_success_cors_note\": \"提示：由于 CORS 限制，无法确认服务器响应状态。\\n请求已发送，如果 webhook 配置正确，消息应该已送达。\\n\\n建议：在浏览器开发者工具的 Network 标签页中查看请求详情。\",\n    \"webhook_test_url_required\": \"请先输入 Webhook URL\",\n    \"webhook_timeout\": \"请求超时时间\",\n    \"webhook_timeout_desc\": \"Webhook 请求的超时时间（毫秒），范围 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"接收事件通知的 Webhook 地址，支持 HTTP/HTTPS 协议\",\n    \"wgc_checking_mode\": \"检查中...\",\n    \"wgc_checking_running_mode\": \"正在检查运行模式...\",\n    \"wgc_control_panel_only\": \"此功能仅在 Sunshine Control Panel 中可用\",\n    \"wgc_mode_switch_failed\": \"切换模式失败\",\n    \"wgc_mode_switch_started\": \"模式切换已启动，如果弹出 UAC 提示，请点击\\\"是\\\"以确认。\",\n    \"wgc_service_mode_warning\": \"WGC 捕获需要在用户模式下运行。如果当前以服务模式运行，请点击上方按钮切换到用户模式。\",\n    \"wgc_switch_to_service_mode\": \"切换到服务模式\",\n    \"wgc_switch_to_service_mode_tooltip\": \"当前为用户模式，点击切换到服务模式\",\n    \"wgc_switch_to_user_mode\": \"切换到用户模式\",\n    \"wgc_switch_to_user_mode_tooltip\": \"WGC 捕获需要在用户模式下运行。点击此按钮切换到用户模式。\",\n    \"wgc_user_mode_available\": \"当前以用户模式运行，WGC 捕获可用。\",\n    \"window_title\": \"窗口标题\",\n    \"window_title_desc\": \"要捕获的窗口标题（部分匹配，不区分大小写）。如果留空，将自动使用当前运行的应用名称。\",\n    \"wgc_disable_secure_desktop\": \"自动提升 UAC（WGC 模式）\",\n    \"wgc_disable_secure_desktop_desc\": \"使用 WGC 捕获时，临时禁用 UAC 提示，管理员权限请求将自动通过而不弹出确认对话框。这可以避免远程串流时 UAC 弹窗导致的中断。串流结束后自动恢复原始 UAC 设置。警告：这会降低系统安全性——串流期间任何程序请求管理员权限将被静默提升。\",\n    \"window_title_placeholder\": \"例如：yuanshen\"\n  },\n  \"index\": {\n    \"description\": \"桑帅 - 杂鱼们一起搞的串流服务\",\n    \"download\": \"下崽\",\n    \"installed_version_not_stable\": \"你正在用 Sunshine 的测试版呢！可能会遇到各种小问题，记得报告给咱哦～感谢帮忙让 Sunshine 变得更好！\",\n    \"loading_latest\": \"正在检查最新版本...\",\n    \"new_pre_release\": \"有新的不稳定版可以玩啦！\",\n    \"new_stable\": \"新的杂鱼版本来啦！\",\n    \"startup_errors\": \"<b>注意啦！</b>Sunshine 启动时发现了一些问题。杂鱼们<b>最好</b>在串流前修好它们哦～\",\n    \"update_download_confirm\": \"即将在浏览器中打开更新下载页面，是否继续？\",\n    \"version_dirty\": \"感谢杂鱼帮忙让 Sunshine 变得更好！\",\n    \"version_latest\": \"杂鱼用的是最新版的 Sunshine 呢～\",\n    \"view_logs\": \"查看日志\",\n    \"welcome\": \"你好，Sunshine Foundation！\"\n  },\n  \"navbar\": {\n    \"applications\": \"应用程序\",\n    \"configuration\": \"配置\",\n    \"home\": \"首页\",\n    \"password\": \"更改密码\",\n    \"pin\": \"PIN 码\",\n    \"theme_auto\": \"自动操作\",\n    \"theme_dark\": \"深色\",\n    \"theme_light\": \"浅色\",\n    \"toggle_theme\": \"主题\",\n    \"troubleshoot\": \"故障排除\"\n  },\n  \"password\": {\n    \"confirm_password\": \"确认密码\",\n    \"current_creds\": \"当前账户信息\",\n    \"new_creds\": \"新的账户信息\",\n    \"new_username_desc\": \"如果不输入新的用户名, 用户名将保持不变\",\n    \"password_change\": \"更改密码\",\n    \"success_msg\": \"密码已成功更改！此页面即将重新加载，您的浏览器将要求您输入新的账户信息。\"\n  },\n  \"pin\": {\n    \"actions\": \"操作\",\n    \"cancel_editing\": \"取消编辑\",\n    \"client_name\": \"名称\",\n    \"client_settings_info\": \"提示：\",\n    \"confirm_delete\": \"确认删除\",\n    \"delete_client\": \"删除客户端\",\n    \"delete_confirm_message\": \"您确定要删除 <strong>{name}</strong> 吗？\",\n    \"delete_warning\": \"此操作无法撤销。\",\n    \"device_name\": \"设备名称\",\n    \"device_size\": \"设备尺寸\",\n    \"device_size_info\": \"根据客户端设备的屏幕大小选择合适的类型（小-手机、中-平板、大-电视），以优化串流画质和触控交互体验。\",\n    \"device_size_large\": \"大 - 电视\",\n    \"device_size_medium\": \"中 - 平板\",\n    \"device_size_small\": \"小 - 手机\",\n    \"edit_client_settings\": \"编辑客户端设置\",\n    \"hdr_profile\": \"HDR 配置文件\",\n    \"hdr_profile_info\": \"为该客户端指定 HDR 色彩配置文件（ICC 文件），确保 HDR 内容正确显示。若使用最新版客户端，支持自动同步亮度信息至主机虚拟屏幕，将此项留空即可启用自动同步。\",\n    \"loading\": \"加载中...\",\n    \"loading_clients\": \"正在加载客户端...\",\n    \"modify_in_gui\": \"请在图形界面中修改\",\n    \"none\": \"-- 无 --\",\n    \"or_manual_pin\": \"或手动输入 PIN 码\",\n    \"pair_failure\": \"配对失败：请检查 PIN 码是否正确输入\",\n    \"pair_success\": \"成功！请检查 Moonlight 以继续\",\n    \"pin_pairing\": \"PIN 码配对\",\n    \"qr_expires_in\": \"{seconds} 秒后过期\",\n    \"qr_generate\": \"生成二维码\",\n    \"qr_paired_success\": \"配对成功！\",\n    \"qr_pairing\": \"二维码配对\",\n    \"qr_pairing_desc\": \"生成二维码快速配对。使用 Moonlight 客户端扫描即可自动配对。\",\n    \"qr_pairing_warning\": \"实验性测试功能，若无法配对成功，请使用下面的手动输入 PIN 码配对。注意！此功能只能在局域网内使用。\",\n    \"qr_refresh\": \"刷新\",\n    \"remove_paired_devices_desc\": \"移除您已配对的设备。\",\n    \"save_changes\": \"保存更改\",\n    \"save_failed\": \"保存客户端设置失败。请重试。\",\n    \"save_or_cancel_first\": \"请先保存或取消编辑\",\n    \"send\": \"发送\",\n    \"unknown_client\": \"未知客户端\",\n    \"unpair_all_confirm\": \"您确定要取消所有客户端的配对吗？此操作无法撤销。\",\n    \"unsaved_changes\": \"未保存的更改\",\n    \"warning_msg\": \"请确保您可以掌控您正在配对的客户端。该软件可以完全控制您的计算机，请务必小心！\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android 推荐\",\n    \"client_downloads\": \"客户端下载\",\n    \"crown_edition\": \"王冠版\",\n    \"crown_edition_desc\": \"by WACrown · Android\",\n    \"moonlight_macos_enhanced\": \"moonlight-macos-enhanced\",\n    \"moonlight_macos_enhanced_desc\": \"by skyhua0224 · macOS 原生增强版客户端\",\n    \"moonlight_ohos\": \"moonlight-ohos\",\n    \"moonlight_ohos_desc\": \"by smdsbz · OpenHarmony 鸿蒙版先行者\",\n    \"github_discussions\": \"Github 讨论区\",\n    \"gpl_license_text_1\": \"本软件遵循 GPL-3.0 开源协议，您可自由使用、修改及分发\",\n    \"gpl_license_text_2\": \"为保障开源生态健康发展，请避免使用违反 GPL-3.0 协议的软件\",\n    \"harmony_client\": \"鸿蒙Moonlight V+\",\n    \"join_group\": \"加入串流裙\",\n    \"join_group_desc\": \"获取帮助与交流经验\",\n    \"legal\": \"法律声明\",\n    \"legal_desc\": \"继续使用本软件即表示您同意以下文档中的条款和条件。\",\n    \"license\": \"许可协议\",\n    \"lizardbyte_website\": \"LizardByte 网站\",\n    \"official_website\": \"基地官网\",\n    \"official_website_title\": \"瑶光流梦 - 官方网站\",\n    \"open_source\": \"开源项目\",\n    \"open_source_desc\": \"Star & Fork 支持项目发展\",\n    \"quick_start\": \"快速入门\",\n    \"resources\": \"相关资源\",\n    \"resources_desc\": \"Sunshine 相关资源！\",\n    \"third_party_desc\": \"第三方组件声明\",\n    \"third_party_moonlight\": \"友情链接\",\n    \"third_party_notice\": \"第三方通知\",\n    \"tutorial\": \"使用教程\",\n    \"tutorial_desc\": \"详细的配置与使用指南\",\n    \"view_license\": \"查看完整许可证\",\n    \"voidlink_title\": \"虚空终端 (VoidLink)\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"配置摘要\",\n    \"android_client\": \"安卓客户端\",\n    \"base_display_title\": \"基地显示器\",\n    \"choose_adapter\": \"自动\",\n    \"config_saved\": \"配置已成功保存。\",\n    \"description\": \"让我们快速完成初始设置\",\n    \"device_id\": \"设备ID\",\n    \"device_state\": \"状态\",\n    \"download_clients\": \"下载客户端\",\n    \"finish\": \"完成设置\",\n    \"go_to_apps\": \"配置应用程序\",\n    \"restart_title\": \"正在重启 Sunshine\",\n    \"restart_desc\": \"已保存配置并触发重启，显示设置将在重启后生效。\",\n    \"restart_countdown_unit\": \"秒\",\n    \"restart_go_now\": \"立即跳转\",\n    \"harmony_goto_repo\": \"前往仓库\",\n    \"harmony_modal_desc\": \"鸿蒙Next Moonlight 请在鸿蒙商店内搜索 Moonlight V+\",\n    \"harmony_modal_link_notice\": \"此链接将跳转至项目仓库\",\n    \"ios_client\": \"iOS 客户端\",\n    \"load_error\": \"加载配置失败\",\n    \"next\": \"下一步\",\n    \"physical_display\": \"物理显示器/显卡欺骗器\",\n    \"physical_display_desc\": \"串流您的实际物理显示器\",\n    \"previous\": \"上一步\",\n    \"save_error\": \"保存配置失败\",\n    \"select_adapter\": \"显卡适配器\",\n    \"selected_adapter\": \"已选择的显卡\",\n    \"selected_display\": \"选择的显示器\",\n    \"setup_complete\": \"设置完成！\",\n    \"setup_complete_desc\": \"基本设置已生效，您现在可以直接使用 Moonlight 客户端开始串流了！\",\n    \"skip\": \"跳过新手引导\",\n    \"skip_confirm\": \"确定要跳过新手引导吗？稍后可以在设置页面中配置这些选项。\",\n    \"skip_confirm_title\": \"跳过新手引导\",\n    \"skip_error\": \"跳过失败\",\n    \"state_active\": \"激活\",\n    \"state_inactive\": \"未激活\",\n    \"state_primary\": \"主显示器\",\n    \"state_unknown\": \"未知\",\n    \"step0_description\": \"请选择您的界面语言\",\n    \"step0_title\": \"选择语言\",\n    \"step1_description\": \"选择要串流的显示器\",\n    \"step1_title\": \"串流显示器\",\n    \"step1_vdd_intro\": \"基地显示器为Sunshine基地版内置的智慧显示屏，支持任意分辨率、帧率和HDR优化，是息屏串流、副屏串流的首选。\",\n    \"step2_description\": \"选择您的显卡适配器\",\n    \"step2_title\": \"选择显卡\",\n    \"step3_description\": \"选择显示器组合策略\",\n    \"step3_ensure_active\": \"确保激活\",\n    \"step3_ensure_active_desc\": \"如果显示器未激活则激活它\",\n    \"step3_ensure_only_display\": \"确保唯一显示器\",\n    \"step3_ensure_only_display_desc\": \"停用其他显示器，只开启指定的显示器（推荐）\",\n    \"step3_ensure_primary\": \"确保主显示器\",\n    \"step3_ensure_primary_desc\": \"确保显示器激活并设置为主显示器\",\n    \"step3_ensure_secondary\": \"副屏串流\",\n    \"step3_ensure_secondary_desc\": \"仅使用基地显示器进行副屏扩展串流\",\n    \"step3_no_operation\": \"无操作\",\n    \"step3_no_operation_desc\": \"不对显示器进行任何操作，用户需自行确保显示器状态\",\n    \"step3_title\": \"显示器策略\",\n    \"step4_title\": \"完成\",\n    \"stream_mode\": \"串流方式\",\n    \"unknown_display\": \"未知显示器\",\n    \"virtual_display\": \"基地显示器 (ZakoHDR)\",\n    \"virtual_display_desc\": \"使用Zako Display Adapter进行串流, 使用前请确保已安装杂鱼VDD驱动\",\n    \"welcome\": \"欢迎使用 Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"高级\",\n    \"amd\": \"AMD AMF 编码器\",\n    \"av\": \"音频/视频\",\n    \"encoders\": \"编码器\",\n    \"files\": \"配置文件\",\n    \"general\": \"常规\",\n    \"input\": \"输入\",\n    \"network\": \"网络\",\n    \"nv\": \"NVIDIA NVENC 编码器\",\n    \"qsv\": \"Intel QuickSync 编码器\",\n    \"sw\": \"软件编码器\",\n    \"vaapi\": \"VAAPI 编码器\",\n    \"vt\": \"VideoToolbox 编码器\"\n  },\n  \"troubleshooting\": {\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"如果需要立即关闭 Sunshine，可以使用此功能。注意关闭后需要手动开启。\",\n    \"boom_sunshine_success\": \"Sunshine 已关闭\",\n    \"confirm_boom\": \"真的要退出吗\",\n    \"confirm_boom_desc\": \"那么想退出吗？真是拿你没办法呢，继续点一下吧\",\n    \"confirm_logout\": \"确认登出？\",\n    \"confirm_logout_desc\": \"登出后需重新输入密码才能访问管理界面。\",\n    \"copy_config\": \"复制配置\",\n    \"copy_config_error\": \"复制配置失败\",\n    \"copy_config_success\": \"配置已复制到剪贴板\",\n    \"copy_logs\": \"复制日志\",\n    \"download_logs\": \"下载日志\",\n    \"ai_diagnosis\": \"AI 诊断\",\n    \"ai_diagnosis_title\": \"AI 日志诊断\",\n    \"ai_config\": \"AI 配置\",\n    \"ai_provider\": \"供应商\",\n    \"ai_model\": \"模型\",\n    \"ai_key_local\": \"API Key 仅保存在本地，不会上传\",\n    \"ai_start_diagnosis\": \"开始诊断\",\n    \"ai_analyzing\": \"分析中...\",\n    \"ai_analyzing_logs\": \"正在分析日志，请稍候...\",\n    \"ai_error\": \"分析失败\",\n    \"ai_retry\": \"重试\",\n    \"ai_result\": \"诊断结果\",\n    \"ai_copy_result\": \"复制\",\n    \"ai_reanalyze\": \"重新分析\",\n    \"force_close\": \"强制关闭\",\n    \"force_close_desc\": \"当 Moonlight 提示有应用正在运行时，强制关闭该应用通常可以解决问题\",\n    \"force_close_error\": \"关闭应用时发生错误\",\n    \"force_close_success\": \"应用已成功关闭\",\n    \"ignore_case\": \"忽略大小写\",\n    \"logout\": \"登出\",\n    \"logout_desc\": \"退出登录，需要重新输入用户密码。\",\n    \"logout_localhost_tip\": \"当前环境较特殊，不会触发退出登录与密码框。\",\n    \"logs\": \"日志\",\n    \"logs_desc\": \"查看 Sunshine 生成的日志文件\",\n    \"logs_find\": \"查找...\",\n    \"match_contains\": \"包含\",\n    \"match_exact\": \"精确匹配\",\n    \"match_regex\": \"正则表达式\",\n    \"reopen_setup_wizard\": \"重新打开新手引导\",\n    \"reopen_setup_wizard_desc\": \"重新打开新手引导页面，可重新配置基础设置\",\n    \"reopen_setup_wizard_error\": \"重新打开新手引导失败\",\n    \"reset_display_device_desc_windows\": \"如果 Sunshine 无法自动恢复显示设备设置，您可以重置设置并手动调整显示状态。\\n常见原因包括：设备不可用、端口变更等\",\n    \"reset_display_device_error_windows\": \"重置持久化设置时出错\",\n    \"reset_display_device_success_windows\": \"持久化设置已成功重置\",\n    \"reset_display_device_windows\": \"重置显示器设置\",\n    \"restart_sunshine\": \"重启 Sunshine\",\n    \"restart_sunshine_desc\": \"当 Sunshine 运行异常时，可尝试重启服务。注意：这将终止所有当前会话\",\n    \"restart_sunshine_success\": \"Sunshine 正在重启\",\n    \"troubleshooting\": \"故障排除\",\n    \"unpair_all\": \"取消所有配对\",\n    \"unpair_all_error\": \"取消配对时发生错误\",\n    \"unpair_all_success\": \"所有设备配对已成功取消\",\n    \"unpair_desc\": \"删除已配对的设备。取消配对后，设备的活动会话仍保持连接，但无法启动新会话\",\n    \"unpair_single_no_devices\": \"暂无已配对设备\",\n    \"unpair_single_success\": \"设备可能仍在活动会话中。请先结束该设备的所有活动会话，然后重试\",\n    \"unpair_single_unknown\": \"未知客户端\",\n    \"unpair_title\": \"取消设备配对\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"确认密码\",\n    \"create_creds\": \"在开始使用前，请为 Web UI 访问设置用户名和密码\",\n    \"create_creds_alert\": \"以下账户信息用于访问 Sunshine Web UI，请妥善保存，设置后将无法再次查看\",\n    \"creds_local_only\": \"账号密码仅离线存储在本地，不会上传到任何服务器上。\",\n    \"error\": \"错误！\",\n    \"greeting\": \"欢迎使用 Sunshine Foundation！\",\n    \"hide_password\": \"隐藏密码\",\n    \"login\": \"登录\",\n    \"network_error\": \"网络错误，请检查连接\",\n    \"password\": \"密码\",\n    \"password_match\": \"密码匹配\",\n    \"password_mismatch\": \"密码不匹配\",\n    \"server_error\": \"服务器错误\",\n    \"show_password\": \"显示密码\",\n    \"success\": \"成功！\",\n    \"username\": \"用户名\",\n    \"welcome_success\": \"页面即将刷新，浏览器将提示您输入新的账户信息\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/public/assets/locale/zh_TW.json",
    "content": "{\n  \"_common\": {\n    \"apply\": \"套用\",\n    \"auto\": \"自動\",\n    \"autodetect\": \"自動偵測（建議）\",\n    \"beta\": \"（測試版）\",\n    \"cancel\": \"取消\",\n    \"close\": \"關閉\",\n    \"copied\": \"已複製到剪貼板\",\n    \"copy\": \"複製\",\n    \"delete\": \"刪除\",\n    \"description\": \"說明\",\n    \"disabled\": \"已停用\",\n    \"disabled_def\": \"已停用（預設）\",\n    \"dismiss\": \"關閉\",\n    \"do_cmd\": \"執行指令\",\n    \"download\": \"下載\",\n    \"edit\": \"編輯\",\n    \"elevated\": \"提高權限\",\n    \"enabled\": \"已啟用\",\n    \"enabled_def\": \"已啟用（預設值）\",\n    \"error\": \"錯誤！\",\n    \"no_changes\": \"沒有變化\",\n    \"note\": \"請注意：\",\n    \"password\": \"密碼\",\n    \"remove\": \"移除\",\n    \"run_as\": \"以系統管理員身份執行\",\n    \"save\": \"節省\",\n    \"see_more\": \"查看更多資訊\",\n    \"success\": \"成功！\",\n    \"undo_cmd\": \"復原指令\",\n    \"username\": \"使用者名稱\",\n    \"warning\": \"警告！\"\n  },\n  \"apps\": {\n    \"actions\": \"行動\",\n    \"add_cmds\": \"新增指令\",\n    \"add_new\": \"新增\",\n    \"advanced_options\": \"進階選項\",\n    \"app_name\": \"應用程式名稱\",\n    \"app_name_desc\": \"應用程式名稱（在 Moonlight 上顯示的名稱）\",\n    \"applications_desc\": \"應用程式清單僅在用戶端重新啟動時更新\",\n    \"applications_title\": \"應用程式一覽\",\n    \"auto_detach\": \"即使應用程式瞬間關閉，仍會繼續串流。\",\n    \"auto_detach_desc\": \"此功能會自動偵測那些在啟動其他程式或自身的另一個執行個體後，立即關閉的啟動器類應用程式。一旦偵測到這類應用程式，系統會將其視為獨立運行的應用程式。\",\n    \"basic_info\": \"基本資訊\",\n    \"cmd\": \"指令\",\n    \"cmd_desc\": \"這是要啟動的主要應用程式。如果留空，則不會啟動任何程式。\",\n    \"cmd_examples_title\": \"常見範例：\",\n    \"cmd_note\": \"如果指令執行檔的路徑有空格，請將路徑放在引號中。\",\n    \"cmd_prep_desc\": \"這是啟動應用程式前後要執行的指令清單。如果任何準備指令執行失敗，應用程式將無法啟動。\",\n    \"cmd_prep_name\": \"指令準備\",\n    \"command_settings\": \"命令設定\",\n    \"covers_found\": \"找到封面圖片\",\n    \"delete\": \"刪除\",\n    \"delete_confirm\": \"確定要刪除應用 \\\"{name}\\\" 嗎？\",\n    \"detached_cmds\": \"獨立指令\",\n    \"detached_cmds_add\": \"新增獨立指令\",\n    \"detached_cmds_desc\": \"在背景執行的指令清單。\",\n    \"detached_cmds_note\": \"如果指令執行檔的路徑有空格，請將路徑放在引號中。\",\n    \"detached_cmds_remove\": \"移除獨立指令\",\n    \"edit\": \"編輯\",\n    \"env_app_id\": \"應用程式 ID\",\n    \"env_app_name\": \"應用程式名稱\",\n    \"env_client_audio_config\": \"用戶端要求的音訊設定 (2.0/5.1/7.1)\",\n    \"env_client_enable_sops\": \"用戶端要求將遊戲進行最佳化，以達到最佳串流效果 (true/false)\",\n    \"env_client_fps\": \"客戶端幀率（整數）\",\n    \"env_client_gcmap\": \"要求的遊戲手把遮罩，使用位元集合/位元欄位格式 (整數)\",\n    \"env_client_hdr\": \"HDR 由用戶端啟用 (true/false)\",\n    \"env_client_height\": \"用戶端要求的高度（整數）\",\n    \"env_client_host_audio\": \"用戶端要求的主機音訊 (true/false)\",\n    \"env_client_name\": \"客戶端顯示名稱（字串）\",\n    \"env_client_width\": \"用戶端要求的寬度（整數）\",\n    \"env_displayplacer_example\": \"範例 - 用於解析度自動化的 displayplacer：\",\n    \"env_qres_example\": \"範例 - 用於解析度自動化的 QRes：\",\n    \"env_qres_path\": \"qres 路徑\",\n    \"env_var_name\": \"變數名稱\",\n    \"env_vars_about\": \"關於環境變數\",\n    \"env_vars_desc\": \"所有指令預設會獲得這些環境變數：\",\n    \"env_xrandr_example\": \"範例 - 用於解析度自動化的 Xrandr：\",\n    \"exit_timeout\": \"結束逾時設定\",\n    \"exit_timeout_desc\": \"當要求結束時，等待所有應用程式處理程序正常結束的秒數。如果未設定，預設會等待最多 5 秒。如果設為 0，應用程式將立即終止。\",\n    \"file_selector_not_initialized\": \"檔案選擇器未初始化\",\n    \"find_cover\": \"尋找封面圖片\",\n    \"form_invalid\": \"請檢查必填欄位\",\n    \"form_valid\": \"有效的應用程式\",\n    \"global_prep_desc\": \"啟用/停用此應用程式的全域準備指令執行。\",\n    \"global_prep_name\": \"全域準備指令\",\n    \"image\": \"圖片\",\n    \"image_desc\": \"將傳送至用戶端的應用程式圖示/圖片/圖片路徑。圖片必須是 PNG 格式。如果未設定，Sunshine 將傳送預設的盒狀圖示。\",\n    \"image_settings\": \"圖片設定\",\n    \"loading\": \"載入中…\",\n    \"menu_cmd_actions\": \"操作\",\n    \"menu_cmd_add\": \"新增選單指令\",\n    \"menu_cmd_command\": \"指令\",\n    \"menu_cmd_desc\": \"設定後，這些指令將在客戶端的返回選單中可見，允許在不中斷串流的情況下快速執行特定操作，例如啟動輔助程式。\\n範例：顯示名稱-關閉您的電腦；指令-shutdown -s -t 10\",\n    \"menu_cmd_display_name\": \"顯示名稱\",\n    \"menu_cmd_drag_sort\": \"拖曳排序\",\n    \"menu_cmd_name\": \"選單指令\",\n    \"menu_cmd_placeholder_command\": \"指令\",\n    \"menu_cmd_placeholder_display_name\": \"顯示名稱\",\n    \"menu_cmd_placeholder_execute\": \"執行指令\",\n    \"menu_cmd_placeholder_undo\": \"撤銷指令\",\n    \"menu_cmd_remove_menu\": \"刪除選單指令\",\n    \"menu_cmd_remove_prep\": \"刪除準備指令\",\n    \"mouse_mode\": \"滑鼠模式\",\n    \"mouse_mode_auto\": \"自動（使用全域設定）\",\n    \"mouse_mode_desc\": \"選擇此應用程式的滑鼠輸入方式。自動使用全域設定，虛擬滑鼠使用 HID 驅動，SendInput 使用 Windows API。\",\n    \"mouse_mode_sendinput\": \"SendInput（Windows 輸入 API）\",\n    \"mouse_mode_vmouse\": \"虛擬滑鼠\",\n    \"name\": \"名稱\",\n    \"output_desc\": \"指令輸出的儲存檔案。如果未指定，輸出將被忽略\",\n    \"output_name\": \"輸出\",\n    \"run_as_desc\": \"某些需要管理員權限才能正常運行的應用程式，可能需要這個設定。\",\n    \"scan_result_add_all\": \"全部新增\",\n    \"scan_result_edit_title\": \"新增並編輯\",\n    \"scan_result_filter_all\": \"全部\",\n    \"scan_result_filter_epic_title\": \"Epic Games 遊戲\",\n    \"scan_result_filter_executable\": \"可執行\",\n    \"scan_result_filter_executable_title\": \"可執行檔案\",\n    \"scan_result_filter_gog_title\": \"GOG Galaxy 遊戲\",\n    \"scan_result_filter_script\": \"腳本\",\n    \"scan_result_filter_script_title\": \"批次/命令腳本\",\n    \"scan_result_filter_shortcut\": \"捷徑\",\n    \"scan_result_filter_shortcut_title\": \"捷徑\",\n    \"scan_result_filter_steam_title\": \"Steam 遊戲\",\n    \"scan_result_filter_url\": \"URL\",\n    \"scan_result_filter_url_title\": \"URL\",\n    \"scan_result_game\": \"遊戲\",\n    \"scan_result_games_only\": \"僅遊戲\",\n    \"scan_result_matched\": \"符合：{count}\",\n    \"scan_result_no_apps\": \"找不到可新增的應用程式\",\n    \"scan_result_no_matches\": \"找不到符合的應用程式\",\n    \"scan_result_quick_add_title\": \"快速新增\",\n    \"scan_result_remove_title\": \"從清單移除\",\n    \"scan_result_search_placeholder\": \"搜尋應用程式名稱、命令或路徑...\",\n    \"scan_result_show_all\": \"顯示全部\",\n    \"scan_result_title\": \"掃描結果\",\n    \"scan_result_try_different_keywords\": \"嘗試使用不同的搜尋關鍵字\",\n    \"scan_result_type_batch\": \"批次\",\n    \"scan_result_type_command\": \"命令腳本\",\n    \"scan_result_type_executable\": \"可執行檔案\",\n    \"scan_result_type_shortcut\": \"捷徑\",\n    \"scan_result_type_url\": \"URL\",\n    \"search_placeholder\": \"搜尋應用程式...\",\n    \"select\": \"選擇\",\n    \"test_menu_cmd\": \"測試命令\",\n    \"test_menu_cmd_empty\": \"命令不能為空\",\n    \"test_menu_cmd_executing\": \"正在執行命令...\",\n    \"test_menu_cmd_failed\": \"命令執行失敗\",\n    \"test_menu_cmd_success\": \"命令執行成功！\",\n    \"use_desktop_image\": \"使用當前桌面壁紙\",\n    \"wait_all\": \"繼續串流，直到所有應用程式處理程序結束\",\n    \"wait_all_desc\": \"這會繼續串流，直到應用程式啟動的所有處理程序都結束。如果未勾選，當最初的應用程式處理程序結束時，串流就會停止，即使還有其他處理程序在運行。\",\n    \"working_dir\": \"工作目錄\",\n    \"working_dir_desc\": \"這是要傳遞給處理程序的工作目錄。例如，有些應用程式會使用這個目錄來搜尋設定檔。如果沒有設定，Sunshine 會自動使用指令所在的目錄\"\n  },\n  \"config\": {\n    \"adapter_name\": \"顯示卡名稱\",\n    \"adapter_name_desc_linux_1\": \"手動指定用於擷取的 GPU。\",\n    \"adapter_name_desc_linux_2\": \"找出所有支援 VAAPI 的裝置\",\n    \"adapter_name_desc_linux_3\": \"將 ``renderD129`` 替換為上面提到的裝置，以列出裝置的名稱和功能。要被 Sunshine 支援，裝置至少需要具備以下條件：\",\n    \"adapter_name_desc_windows\": \"手動指定用於擷取的 GPU。如果未設定，GPU 將被自動選擇。注意：此 GPU 必須連接並開啟顯示器。如果筆記型電腦無法啟用獨顯直連，請將此處設定為自動。\",\n    \"adapter_name_desc_windows_vdd_hint\": \"如有安裝最新版基地顯示器，可自動關聯 GPU 綁定\",\n    \"adapter_name_placeholder_windows\": \"Radeon RX 580 Series\",\n    \"add\": \"新增\",\n    \"address_family\": \"地址家庭\",\n    \"address_family_both\": \"IPv4+IPv6\",\n    \"address_family_desc\": \"設定 Sunshine 使用的位址族\",\n    \"address_family_ipv4\": \"僅 IPv4\",\n    \"always_send_scancodes\": \"永遠傳送掃描碼\",\n    \"always_send_scancodes_desc\": \"傳送掃描碼可以增強與遊戲和應用程式的相容性，但可能會導致某些未使用美式英語鍵盤佈局的用戶端輸入錯誤。若某些應用程式的鍵盤輸入完全無效，請啟用此選項。若用戶端的鍵盤輸入在主機端產生錯誤輸入，則請停用此選項。\",\n    \"amd_coder\": \"AMF 編碼器 (H264)\",\n    \"amd_coder_desc\": \"允許您選擇熵編碼，以優先品質或編碼速度。僅限 H.264。\",\n    \"amd_enforce_hrd\": \"AMF 假想參考解碼器 (HRD) 強制執行\",\n    \"amd_enforce_hrd_desc\": \"增加速率控制的限制，以符合 HRD 模型的要求。這可大幅減少比特率溢出，但可能會在某些卡上造成編碼假象或降低品質。\",\n    \"amd_preanalysis\": \"AMF 預分析\",\n    \"amd_preanalysis_desc\": \"這可進行速率控制預分析，以增加編碼延遲為代價來提高品質。\",\n    \"amd_quality\": \"AMF 品質\",\n    \"amd_quality_balanced\": \"balanced—平衡（預設）\",\n    \"amd_quality_desc\": \"這可以控制編碼速度和品質之間的平衡。\",\n    \"amd_quality_group\": \"AMF 品質設定\",\n    \"amd_quality_quality\": \"quality—偏好品質\",\n    \"amd_quality_speed\": \"speed—偏好速度\",\n    \"amd_qvbr_quality\": \"AMF QVBR 品質等級\",\n    \"amd_qvbr_quality_desc\": \"QVBR碼率控制模式的品質等級。範圍：1-51（越低越好）。預設：23。僅在碼率控制設為 'qvbr' 時生效。\",\n    \"amd_rc\": \"AMF 速率控制\",\n    \"amd_rc_cbr\": \"cbr—固定位元率（如果啟用 HRD，建議使用）\",\n    \"amd_rc_cqp\": \"cqp—常數 qp 模式\",\n    \"amd_rc_desc\": \"這個選項控制了速率控制方法，確保不超過客戶端的位元率目標。'cqp' 不適用於位元率目標設定，除了 'vbr_latency' 外，其他選項依賴 HRD 強制執行來幫助限制位元率溢出。\",\n    \"amd_rc_group\": \"AMF 速率控制設定\",\n    \"amd_rc_hqcbr\": \"hqcbr -- 高品質固定位元率\",\n    \"amd_rc_hqvbr\": \"hqvbr -- 高品質可變位元率\",\n    \"amd_rc_qvbr\": \"qvbr -- 品質可變位元率（使用QVBR品質等級）\",\n    \"amd_rc_vbr_latency\": \"vbr_latency—受延遲限制的可變位元率（如果停用 HRD，建議使用此選項；預設）\",\n    \"amd_rc_vbr_peak\": \"vbr_peak—峰值受限的可變位元率\",\n    \"amd_usage\": \"AMF 使用情況\",\n    \"amd_usage_desc\": \"這會設定基本的編碼設定檔。以下顯示的所有選項都會覆蓋部分設定檔的設定，但設定檔還包含其他無法在其他地方調整的隱藏設定。\",\n    \"amd_usage_lowlatency\": \"lowlatency—低延遲（最快）\",\n    \"amd_usage_lowlatency_high_quality\": \"lowlatency_high_quality—低延遲、高品質（快速）\",\n    \"amd_usage_transcoding\": \"transcoding—轉碼（最慢）\",\n    \"amd_usage_ultralowlatency\": \"ultralowlatency—超低延遲（最快；預設值）\",\n    \"amd_usage_webcam\": \"webcam—網路攝影機（慢速）\",\n    \"amd_vbaq\": \"AMF 基於方差的自適應量化 (VBAQ)\",\n    \"amd_vbaq_desc\": \"人類的視覺系統通常對高度紋理區域中的壓縮失真較不敏感。在 VBAQ 模式下，像素變異用於指示空間紋理的複雜度，使編碼器能夠將更多位元分配給較平滑的區域。啟用此功能可在某些內容上提升主觀視覺品質。\",\n    \"amf_draw_mouse_cursor\": \"使用 AMF 擷取方法時繪製簡單游標\",\n    \"amf_draw_mouse_cursor_desc\": \"在某些情況下，使用 AMF 擷取不會顯示滑鼠指標。啟用此選項將在螢幕上繪製簡單的滑鼠指標。注意：滑鼠指標的位置只會在內容螢幕更新時更新，因此在非遊戲場景（例如桌面）中，您可能會觀察到滑鼠指標移動遲緩。\",\n    \"apply_note\": \"點選「套用」以重新啟動 Sunshine 並應用變更。這將終止所有正在進行的工作階段。\",\n    \"audio_sink\": \"音訊水槽\",\n    \"audio_sink_desc_linux\": \"用於 Audio Loopback 的音訊輸出的名稱。如果您沒有指定這個變數，pulseaudio 將會選擇預設的監視裝置。您可以使用以下任一指令找出音訊輸出裝置的名稱：\",\n    \"audio_sink_desc_macos\": \"用於音訊環回的音訊槽名稱。由於系統限制，Sunshine 只能在 macOS 上存取麥克風。要使用 Soundflower 或 BlackHole 串流系統音訊。\",\n    \"audio_sink_desc_windows\": \"手動指定要擷取的特定音訊裝置。若未設定，系統會自動選擇裝置。我們強烈建議將此欄位留空以使用自動裝置選擇！若您有多個名稱相同的音訊裝置，您可以使用以下指令來取得裝置 ID：\",\n    \"audio_sink_placeholder_macos\": \"BlackHole 2ch\",\n    \"audio_sink_placeholder_windows\": \"揚聲器（高解析度音訊裝置）\",\n    \"av1_mode\": \"AV1 支援\",\n    \"av1_mode_0\": \"Sunshine 將根據編碼器功能來宣告是否支援 AV1（建議）\",\n    \"av1_mode_1\": \"Sunshine 不會宣告支援 AV1\",\n    \"av1_mode_2\": \"Sunshine 將宣告支援 AV1 Main 8 位元設定檔\",\n    \"av1_mode_3\": \"Sunshine 將宣告支援 AV1 Main 8 位元和 10 位元 (HDR) 設定檔\",\n    \"av1_mode_desc\": \"允許用戶端要求 AV1 Main 8 位元或 10 位元視訊串流。AV1 的編碼較耗費 CPU，因此使用軟體編碼時，啟用此功能可能會降低效能。\",\n    \"back_button_timeout\": \"主畫面/導覽按鈕模擬超時\",\n    \"back_button_timeout_desc\": \"如果按住 Back/Select 按鈕達到指定的毫秒數，系統會模擬 Home/Guide 按鈕的按下動作。若設定為小於 0（預設值），則按住 Back/Select 按鈕不會模擬 Home/Guide 按鈕。\",\n    \"bind_address\": \"綁定位址（測試功能）\",\n    \"bind_address_desc\": \"設定 Sunshine 綁定的 IP 位址。如果留空，Sunshine 將綁定到所有可用介面（0.0.0.0 用於 IPv4 或 :: 用於 IPv6）。\",\n    \"capture\": \"強制使用特定的擷取方式\",\n    \"capture_desc\": \"在自動模式下，Sunshine 會使用第一個有效的驅動程式。NvFBC 需要已修補的 nvidia 驅動程式。\",\n    \"capture_target\": \"擷取目標\",\n    \"capture_target_desc\": \"選擇要擷取的目標類型。選擇「視窗」時，可以擷取特定應用程式視窗（如 AI 插幀軟體），而不是整個顯示器。\",\n    \"capture_target_display\": \"顯示器\",\n    \"capture_target_window\": \"視窗\",\n    \"cert\": \"證書\",\n    \"cert_desc\": \"用於網頁 UI 和 Moonlight 客戶端配對的私鑰。為了確保最佳相容性，應使用 RSA-2048 私鑰。\",\n    \"channels\": \"最大連線用戶端數量\",\n    \"channels_desc_1\": \"Sunshine 可讓單一串流工作階段同時與多個裝置共享。\",\n    \"channels_desc_2\": \"某些硬體編碼器可能會因多重串流而受到性能限制。\",\n    \"close_verify_safe\": \"安全驗證相容舊版用戶端\",\n    \"close_verify_safe_desc\": \"舊版用戶端可能無法連線到 Sunshine, 請關閉此選項或升級用戶端\",\n    \"coder_cabac\": \"cabac—上下文自適應二元算術編碼，更高的品質\",\n    \"coder_cavlc\": \"cavlc—上下文自適應可變長度編碼 - 解碼速度較快\",\n    \"configuration\": \"組態\",\n    \"controller\": \"啟用遊戲手把輸入\",\n    \"controller_desc\": \"允許訪客使用遊戲手把或遊戲控制器操作主機系統。\",\n    \"credentials_file\": \"憑證檔案\",\n    \"credentials_file_desc\": \"將用戶名稱/密碼儲存在與 Sunshine 狀態檔案不同的位置。\",\n    \"display_device_options_note_desc_windows\": \"Windows 會為每個目前作用中顯示器的組合儲存各種顯示設定。\\nSunshine 然後會將變更套用到屬於該顯示器組合的顯示器。\\n如果您在 Sunshine 套用設定時斷開作用中裝置的連接，除非該組合可以在 Sunshine 嘗試還原變更時再次啟動，否則無法還原變更！\",\n    \"display_device_options_note_windows\": \"關於如何套用設定的注意事項\",\n    \"display_device_options_windows\": \"顯示器裝置選項\",\n    \"display_device_prep_ensure_active_desc_windows\": \"如果顯示器未啟動則啟動它\",\n    \"display_device_prep_ensure_active_windows\": \"自動啟動顯示器\",\n    \"display_device_prep_ensure_only_display_desc_windows\": \"停用其他顯示器，只開啟指定的顯示器\",\n    \"display_device_prep_ensure_only_display_windows\": \"停用其他顯示器並僅啟動指定的顯示器\",\n    \"display_device_prep_ensure_primary_desc_windows\": \"確保顯示器啟動並設定為主顯示器\",\n    \"display_device_prep_ensure_primary_windows\": \"自動啟動顯示器並將其設為主顯示器\",\n    \"display_device_prep_ensure_secondary_desc_windows\": \"僅使用基地顯示器進行副屏擴展串流\",\n    \"display_device_prep_ensure_secondary_windows\": \"副屏串流（僅使用基地顯示器支援）\",\n    \"display_device_prep_no_operation_desc_windows\": \"不對顯示器進行任何操作，使用者需自行確保顯示器狀態\",\n    \"display_device_prep_no_operation_windows\": \"已停用\",\n    \"display_device_prep_windows\": \"顯示器準備\",\n    \"display_mode_remapping_default_mode_desc_windows\": \"必須至少指定一個「接收」值和一個「最終」值。\\n「接收」區段中的空欄位表示「符合任何值」。「最終」區段中的空欄位表示「保留接收值」。\\n如果您願意，可以將特定的幀率值對應到特定的解析度...\\n\\n注意：如果 Moonlight 用戶端上未啟用「最佳化遊戲設定」選項，則會忽略包含解析度值的列。\",\n    \"display_mode_remapping_desc_windows\": \"指定應如何將特定解析度和/或重新整理率重新對應到其他值。\\n您可以在較低解析度下串流，同時在主機上以較高解析度渲染以獲得超取樣效果。\\n或者，您可以在較高幀率下串流，同時將主機限制為較低的重新整理率。\\n比對是從上到下執行的。一旦符合項目，就不會再檢查其他項目，但仍會驗證。\",\n    \"display_mode_remapping_final_refresh_rate_windows\": \"最終重新整理率\",\n    \"display_mode_remapping_final_resolution_windows\": \"最終解析度\",\n    \"display_mode_remapping_optional\": \"選用\",\n    \"display_mode_remapping_received_fps_windows\": \"接收的幀率\",\n    \"display_mode_remapping_received_resolution_windows\": \"接收的解析度\",\n    \"display_mode_remapping_resolution_only_mode_desc_windows\": \"注意：如果 Moonlight 用戶端上未啟用「最佳化遊戲設定」選項，則會停用重新對應。\",\n    \"display_mode_remapping_windows\": \"重新對應顯示模式\",\n    \"display_modes\": \"顯示模式\",\n    \"ds4_back_as_touchpad_click\": \"地圖 Back/Select 至觸控板點選\",\n    \"ds4_back_as_touchpad_click_desc\": \"當強制啟用 DS4 模擬時，將返回/選擇按鈕映射為觸控板點擊\",\n    \"dsu_server_port\": \"DSU 伺服器連接埠\",\n    \"dsu_server_port_desc\": \"DSU 伺服器監聽連接埠（預設 26760）。Sunshine 將作為 DSU 伺服器運作，以接收用戶端連線並傳送動作資料。在您的用戶端（Yuzu、Ryujinx 等）中啟用 DSU 伺服器，並設定 DSU 伺服器位址（127.0.0.1）和連接埠（26760）\",\n    \"enable_dsu_server\": \"啟用 DSU 伺服器\",\n    \"enable_dsu_server_desc\": \"啟用 DSU 伺服器以接收用戶端連線並傳送動作資料\",\n    \"encoder\": \"強制指定編碼器\",\n    \"encoder_desc\": \"強制指定特定的編碼器，否則 Sunshine 將選擇最佳的可用選項。注意：如果您在 Windows 上指定硬體編碼器，則必須與顯示器連接的 GPU 符合。\",\n    \"encoder_software\": \"軟體\",\n    \"experimental\": \"實驗性\",\n    \"experimental_features\": \"實驗性功能\",\n    \"external_ip\": \"外部 IP\",\n    \"external_ip_desc\": \"如果未提供外部 IP 位址，Sunshine 會自動偵測外部 IP 位址。\",\n    \"fec_percentage\": \"FEC 比例\",\n    \"fec_percentage_desc\": \"每個視訊影格中每個資料封包的錯誤修正封包百分比。較高的值可以修正更多的網路封包遺失，但代價是增加頻寬使用量。\",\n    \"ffmpeg_auto\": \"auto—由 ffmpeg 決定（預設）\",\n    \"file_apps\": \"應用程式檔案\",\n    \"file_apps_desc\": \"Sunshine 目前的應用程式所儲存的檔案。\",\n    \"file_state\": \"狀態檔案\",\n    \"file_state_desc\": \"儲存目前 Sunshine 狀態的檔案\",\n    \"fps\": \"基地顯示器支援的幀率\",\n    \"gamepad\": \"模擬遊戲手把類型\",\n    \"gamepad_auto\": \"自動選擇選項\",\n    \"gamepad_desc\": \"選擇要在主機上模擬的遊戲手把類型\",\n    \"gamepad_ds4\": \"DS4 (PS4)\",\n    \"gamepad_ds4_manual\": \"DS4 手動選項\",\n    \"gamepad_ds5\": \"DS5 (PS5)\",\n    \"gamepad_manual\": \"手動 DS4 選項\",\n    \"gamepad_switch\": \"Nintendo Pro (Switch)\",\n    \"gamepad_x360\": \"X360 (Xbox 360)\",\n    \"gamepad_xone\": \"XOne (Xbox One)\",\n    \"global_prep_cmd\": \"指令準備\",\n    \"global_prep_cmd_desc\": \"設定在執行任何應用程式之前或之後執行的指令清單。如果任何指定的準備指令失敗，應用程式的啟動程序將會中止。\",\n    \"hdr_luminance_analysis\": \"HDR 動態中繼資料 (HDR10+ / Vivid)\",\n    \"hdr_luminance_analysis_desc\": \"啟用逐幀 GPU 亮度分析，並將 HDR10+ (ST 2094-40) 和 HDR Vivid (CUVA) 動態中繼資料注入編碼位元流。可為支援動態 HDR 的終端提供逐幀色調映射參考。在高解析度下會增加少量 GPU 開銷（約 0.5-1.5ms/幀）。若開啟 HDR 後出現幀率下降，可關閉此選項。\",\n    \"hdr_prep_automatic_windows\": \"根據用戶端要求開啟/關閉 HDR 模式\",\n    \"hdr_prep_no_operation_windows\": \"已停用\",\n    \"hdr_prep_windows\": \"HDR 狀態變更\",\n    \"hevc_mode\": \"HEVC 支援\",\n    \"hevc_mode_0\": \"Sunshine 將根據編碼器的能力宣告是否支援 HEVC（建議）\",\n    \"hevc_mode_1\": \"Sunshine 不會宣告支援 HEVC\",\n    \"hevc_mode_2\": \"Sunshine 將宣告支援 HEVC Main 設定檔\",\n    \"hevc_mode_3\": \"Sunshine 會宣告支援 HEVC Main 和 Main10 (HDR) 設定檔\",\n    \"hevc_mode_desc\": \"允許用戶端要求 HEVC Main 或 HEVC Main10 視訊串流。HEVC 的編碼較耗費 CPU，因此使用軟體編碼時，啟用此功能可能會降低效能。\",\n    \"high_resolution_scrolling\": \"支援高解析度捲動\",\n    \"high_resolution_scrolling_desc\": \"啟用後，Sunshine 會傳遞來自 Moonlight 用戶端的高解析度捲動事件。對於某些在高解析度捲動時捲動速度過快的舊應用程式，建議將此選項停用。\",\n    \"install_steam_audio_drivers\": \"安裝 Steam 音訊驅動程式\",\n    \"install_steam_audio_drivers_desc\": \"如果已安裝 Steam，這將會自動安裝 Steam Streaming Speakers 驅動程式，以支援 5.1/7.1 環繞音效和主機音訊靜音。\",\n    \"key_repeat_delay\": \"按鍵重複延遲\",\n    \"key_repeat_delay_desc\": \"控制按鍵重複的速度。設定按鍵重複前的初始延遲時間，以毫秒為單位。\",\n    \"key_repeat_frequency\": \"按鍵重複頻率\",\n    \"key_repeat_frequency_desc\": \"每秒按鍵重複的頻率。此選項支援小數點。\",\n    \"key_rightalt_to_key_win\": \"將右 Alt 鍵對應到 Windows 鍵\",\n    \"key_rightalt_to_key_win_desc\": \"Moonlight 可能無法直接發送 Windows 鍵。在這種情況下，讓 Sunshine 認為右 Alt 鍵是 Windows 鍵可能會很有用\",\n    \"key_rightalt_to_key_windows\": \"將右 Alt 鍵對應到 Windows 鍵\",\n    \"keyboard\": \"啟用鍵盤輸入\",\n    \"keyboard_desc\": \"允許訪客使用鍵盤控制主機系統\",\n    \"lan_encryption_mode\": \"區域網路加密模式\",\n    \"lan_encryption_mode_1\": \"當用戶端支援時啟用\",\n    \"lan_encryption_mode_2\": \"所有用戶端都需要\",\n    \"lan_encryption_mode_desc\": \"這會決定在本地網路上進行串流時何時使用加密。加密可能會降低串流效能，特別是在較不強大的主機和客戶端上。\",\n    \"locale\": \"語系\",\n    \"locale_desc\": \"Sunshine 使用的使用者介面語言設定。\",\n    \"log_level\": \"日誌層級\",\n    \"log_level_0\": \"詳細\",\n    \"log_level_1\": \"除錯\",\n    \"log_level_2\": \"資訊\",\n    \"log_level_3\": \"警告\",\n    \"log_level_4\": \"錯誤\",\n    \"log_level_5\": \"嚴重錯誤\",\n    \"log_level_6\": \"無\",\n    \"log_level_desc\": \"列印到標準輸出的最小日誌層級\",\n    \"log_path\": \"記錄檔路徑\",\n    \"log_path_desc\": \"儲存目前 Sunshine 記錄的檔案。\",\n    \"max_bitrate\": \"最大位元率\",\n    \"max_bitrate_desc\": \"Sunshine 會以最大位元率（單位為 Kbps）來編碼串流。如果設為0，則會使用Moonlight所要求的位元率。\",\n    \"max_fps_reached\": \"已達到最大幀率值\",\n    \"max_resolutions_reached\": \"已達到最大解析度數量\",\n    \"mdns_broadcast\": \"本地網路發現此電腦\",\n    \"mdns_broadcast_desc\": \"開啟後將允許未連線裝置自動發現此電腦，需要 Moonlight 開啟自動在本地網路中查找電腦\",\n    \"min_threads\": \"最低 CPU 線程數\",\n    \"min_threads_desc\": \"增加該值會稍微降低編碼效率，但為了能使用更多 CPU 核心進行編碼，這樣的折衷通常是值得的。理想的值是在您的硬體上，能以您所需的串流設定進行可靠編碼的最低值。\",\n    \"minimum_fps_target\": \"最小編碼幀率\",\n    \"minimum_fps_target_desc\": \"編碼時要保持的最小幀率（0 = 自動，約為流幀率的一半；1-1000 = 要保持的最小幀率）。啟用可變刷新率時，如果設定為 0，則忽略此設定。\",\n    \"misc\": \"雜項選項\",\n    \"motion_as_ds4\": \"如果客戶端的遊戲手把報告有運動感應器，則會模擬 DS4 遊戲手把\",\n    \"motion_as_ds4_desc\": \"如果禁用，則在選擇遊戲手把類型時不會考慮運動感應器的存在。\",\n    \"mouse\": \"啟用滑鼠輸入\",\n    \"mouse_desc\": \"允許訪客使用滑鼠控制主機系統\",\n    \"native_pen_touch\": \"原生筆/觸控支援\",\n    \"native_pen_touch_desc\": \"啟用後，Sunshine 將從 Moonlight 用戶端傳遞本機筆觸事件。對於沒有原生筆/觸控支援的舊版應用程式來說，停用此功能可能很有用。\",\n    \"no_fps\": \"未添加幀率值\",\n    \"no_resolutions\": \"未添加解析度\",\n    \"notify_pre_releases\": \"發佈前通知\",\n    \"notify_pre_releases_desc\": \"是否接收 Sunshine 的新預發佈版本通知\",\n    \"nvenc_h264_cavlc\": \"在 H.264 中選擇 CAVLC 而非 CABAC\",\n    \"nvenc_h264_cavlc_desc\": \"較簡單的熵編碼形式。CAVLC需要約多10%的位元率才能達到相同的畫質。這僅對非常舊的解碼裝置有影響。\",\n    \"nvenc_latency_over_power\": \"相較於省電，更傾向於較低的編碼延遲\",\n    \"nvenc_latency_over_power_desc\": \"Sunshine會在進行串流時請求最大 GPU 時脈速度，以減少編碼延遲。建議不要停用此選項，因為這樣可能會顯著增加編碼延遲。\",\n    \"nvenc_lookahead_depth\": \"前瞻深度\",\n    \"nvenc_lookahead_depth_desc\": \"編碼時前瞻的幀數（0-32）。前瞻功能可以提升編碼品質，特別是在複雜場景中，透過提供更好的運動估計和位元率分配。數值越高，品質越好，但會增加編碼延遲。設定為 0 可停用前瞻。需要 NVENC SDK 13.0 (1202) 或更新版本。\",\n    \"nvenc_lookahead_level\": \"前瞻級別\",\n    \"nvenc_lookahead_level_0\": \"級別 0（最低品質，最快）\",\n    \"nvenc_lookahead_level_1\": \"級別 1\",\n    \"nvenc_lookahead_level_2\": \"級別 2\",\n    \"nvenc_lookahead_level_3\": \"級別 3（最高品質，最慢）\",\n    \"nvenc_lookahead_level_autoselect\": \"自動選擇（讓驅動選擇最優級別）\",\n    \"nvenc_lookahead_level_desc\": \"前瞻品質級別。更高級別會提升品質，但會降低效能。此選項僅在前瞻深度大於 0 時生效。需要 NVENC SDK 13.0 (1202) 或更新版本。\",\n    \"nvenc_lookahead_level_disabled\": \"停用（等同於級別 0）\",\n    \"nvenc_opengl_vulkan_on_dxgi\": \"在 DXGI 之上呈現 OpenGL/Vulkan\",\n    \"nvenc_opengl_vulkan_on_dxgi_desc\": \"Sunshine 無法以完整幀率捕捉全螢幕的 OpenGL 和 Vulkan 程式，除非它們顯示在 DXGI 之上。這是系統範圍的設定，會在Sunshine程式退出時還原。\",\n    \"nvenc_preset\": \"性能預設參數\",\n    \"nvenc_preset_1\": \"（最快，預設值）\",\n    \"nvenc_preset_7\": \"（最慢）\",\n    \"nvenc_preset_desc\": \"數值越高，在增加編碼延遲的情況下，可提高壓縮率（在指定位元率下的品質）。建議僅在受限於網路或解碼器時才進行調整，否則可以透過增加位元率來達成類似的效果。\",\n    \"nvenc_rate_control\": \"位元率控制模式\",\n    \"nvenc_rate_control_cbr\": \"CBR（固定位元率）- 低延遲\",\n    \"nvenc_rate_control_desc\": \"選擇位元率控制模式。CBR（固定位元率）提供固定位元率，適合低延遲串流。VBR（可變位元率）允許位元率根據場景複雜度變化，在複雜場景中提供更好的品質，但位元率會變化。\",\n    \"nvenc_rate_control_vbr\": \"VBR（可變位元率）- 更高品質\",\n    \"nvenc_realtime_hags\": \"在硬體加速 GPU 排程中使用即時優先順序\",\n    \"nvenc_realtime_hags_desc\": \"目前NVIDIA 驅動程式在啟用 HAGS、使用即時優先權且 VRAM 使用率接近最大值時，可能會在編碼器中凍結。停用此選項會將優先權降低至高，藉此避開凍結，但在 GPU 重度負載時擷取效能會降低。\",\n    \"nvenc_spatial_aq\": \"空間 AQ\",\n    \"nvenc_spatial_aq_desc\": \"為視訊的平坦區域指定較高的 QP 值。建議在以較低位元率串流時啟用。\",\n    \"nvenc_spatial_aq_disabled\": \"已停用（更快，預設）\",\n    \"nvenc_spatial_aq_enabled\": \"已啟用（更慢）\",\n    \"nvenc_split_encode\": \"分割幀編碼\",\n    \"nvenc_split_encode_desc\": \"將每個視訊幀的編碼分佈在多個 NVENC 硬體單元上。顯著降低編碼延遲，但會略微降低壓縮效率。如果您的 GPU 只有一個 NVENC 單元，則忽略此選項。\",\n    \"nvenc_split_encode_driver_decides_def\": \"由驅動程式決定（預設）\",\n    \"nvenc_split_encode_four_strips\": \"強制 4 條帶（需要 4+ NVENC 引擎）\",\n    \"nvenc_split_encode_three_strips\": \"強制 3 條帶（需要 3+ NVENC 引擎）\",\n    \"nvenc_split_encode_two_strips\": \"強制 2 條帶（需要 2+ NVENC 引擎）\",\n    \"nvenc_target_quality\": \"目標品質（VBR 模式）\",\n    \"nvenc_target_quality_desc\": \"VBR 模式的目標品質級別（H.264/HEVC 為 0-51，AV1 為 0-63）。值越低 = 品質越高。設為 0 表示自動品質選擇。僅在碼率控制模式為 VBR 時使用。\",\n    \"nvenc_temporal_aq\": \"時域自適應量化\",\n    \"nvenc_temporal_aq_desc\": \"啟用時域自適應量化。時域 AQ 在時間維度上最佳化量化，提供更好的位元率分配，並改善運動場景的品質。此功能與空間 AQ 配合使用，需要啟用前瞻（前瞻深度 > 0）。需要 NVENC SDK 13.0 (1202) 或更新版本。\",\n    \"nvenc_temporal_filter\": \"時域濾波\",\n    \"nvenc_temporal_filter_4\": \"級別 4（最大強度）\",\n    \"nvenc_temporal_filter_desc\": \"編碼前套用的時域濾波強度。時域濾波可以減少雜訊並提高壓縮效率，特別適合自然內容。更高級別提供更好的降噪效果，但可能會引入輕微的模糊。需要 NVENC SDK 13.0 (1202) 或更新版本。注意：需要 frameIntervalP >= 5，與 zeroReorderDelay 或立體聲 MVC 不相容。\",\n    \"nvenc_temporal_filter_disabled\": \"停用（無時域濾波）\",\n    \"nvenc_twopass\": \"兩次編碼模式\",\n    \"nvenc_twopass_desc\": \"新增初步編碼過程，能夠檢測更多的運動向量，將位元率更均勻地分佈於畫面，並更精確地遵守位元率限制。建議不要停用這個功能，因為停用後可能會偶爾出現位元率超過限制，並導致資料包丟失。\",\n    \"nvenc_twopass_disabled\": \"停用（最快，不建議使用）\",\n    \"nvenc_twopass_full_res\": \"全解析度（較慢）\",\n    \"nvenc_twopass_quarter_res\": \"四分之一解析度（更快，預設值）\",\n    \"nvenc_vbv_increase\": \"單幅 VBV/HRD 百分比增加\",\n    \"nvenc_vbv_increase_desc\": \"預設 sunshine 使用單幀 VBV/HRD，這表示任何編碼視訊幀大小都不會超過要求的位元率除以要求的幀速率。放寬此限制可能有益並可作為低延遲的可變位元率，但如果網路沒有緩衝空間處理比特率峰值，也可能導致封包遺失。可接受的最大值是 400，相當於 5 倍增加的編碼視訊畫格上限。\",\n    \"origin_web_ui_allowed\": \"允許使用 Origin 網頁 UI\",\n    \"origin_web_ui_allowed_desc\": \"用於網頁 UI 和 Moonlight 用戶端配對的憑證。為了確保最佳相容性，建議使用 RSA-2048 公開金鑰。\",\n    \"origin_web_ui_allowed_lan\": \"只有區域網路中的人可以存取網頁 UI\",\n    \"origin_web_ui_allowed_pc\": \"只有 localhost 可以存取網頁 UI\",\n    \"origin_web_ui_allowed_wan\": \"任何人都可以存取網頁 UI\",\n    \"output_name_desc_unix\": \"在 Sunshine 啟動時，您應該會看到檢測到的顯示器清單。請注意：需要使用括弧內的 ID 值。以下是範例，實際輸出可以在「故障排除」分頁中找到。\",\n    \"output_name_desc_windows\": \"手動指定要用於擷取的顯示器設備 ID。如果未設定，則擷取主要顯示器。注意：如果您在上方指定了 GPU，則此顯示器必須連接到該 GPU。在 Sunshine 啟動時，您應該會看到檢測到的顯示器清單。以下是範例，實際輸出可以在「故障排除」分頁中找到。\",\n    \"output_name_unix\": \"顯示號碼\",\n    \"output_name_windows\": \"顯示裝置 ID\",\n    \"ping_timeout\": \"Ping 逾時\",\n    \"ping_timeout_desc\": \"在關閉串流前，等待來自 Moonlight 的資料，以毫秒為單位\",\n    \"pkey\": \"私人密碼匙\",\n    \"pkey_desc\": \"用於網頁 UI 和 Moonlight 客戶端配對的私鑰。為了確保最佳相容性，建議使用 RSA-2048 私鑰。\",\n    \"port\": \"連接埠\",\n    \"port_alert_1\": \"Sunshine 不能使用低於 1024 的連接埠！\",\n    \"port_alert_2\": \"65535 以上的連接埠無法使用！\",\n    \"port_desc\": \"設定 Sunshine 使用的連接埠系列\",\n    \"port_http_port_note\": \"使用此連接埠與 Moonlight 連線。\",\n    \"port_note\": \"注意事項\",\n    \"port_port\": \"連接埠\",\n    \"port_protocol\": \"通訊協定\",\n    \"port_tcp\": \"TCP\",\n    \"port_udp\": \"UDP\",\n    \"port_warning\": \"將網頁 UI 暴露於網際網路存在安全風險！請自行承擔風險！\",\n    \"port_web_ui\": \"Web UI\",\n    \"qp\": \"量化參數\",\n    \"qp_desc\": \"某些裝置可能不支援 Constant Bit Rate。對於這些裝置，會使用 QP 來取代。值越高表示壓縮越多，但品質越低。\",\n    \"qsv_coder\": \"QuickSync 編碼器（H264）\",\n    \"qsv_preset\": \"QuickSync 預設值\",\n    \"qsv_preset_fast\": \"快（低畫質）\",\n    \"qsv_preset_faster\": \"更快（品質較低）\",\n    \"qsv_preset_medium\": \"中（預設值）\",\n    \"qsv_preset_slow\": \"慢（高畫質）\",\n    \"qsv_preset_slower\": \"速度較慢（品質較佳）\",\n    \"qsv_preset_slowest\": \"最慢（最高畫質）\",\n    \"qsv_preset_veryfast\": \"最快（最低畫質）\",\n    \"qsv_slow_hevc\": \"允許慢速 HEVC 編碼\",\n    \"qsv_slow_hevc_desc\": \"這樣可以在較舊的 Intel GPU 上進行 HEVC 編碼，但代價是 GPU 使用量較高，效能較差。\",\n    \"refresh_rate_change_automatic_windows\": \"使用客戶端提供的幀率值\",\n    \"refresh_rate_change_manual_desc_windows\": \"輸入要使用的重新整理率\",\n    \"refresh_rate_change_manual_windows\": \"使用手動輸入的重新整理率\",\n    \"refresh_rate_change_no_operation_windows\": \"已停用\",\n    \"refresh_rate_change_windows\": \"幀率變更\",\n    \"res_fps_desc\": \"Sunshine 公告的顯示模式。某些版本的 Moonlight（例如 Moonlight-nx (Switch)）依賴這些清單來確保支援請求的解析度和幀率。此設定不會變更螢幕串流傳送到 Moonlight 的方式。\",\n    \"resolution_change_automatic_windows\": \"使用用戶端提供的解析度\",\n    \"resolution_change_manual_desc_windows\": \"必須在 Moonlight 用戶端上啟用「最佳化遊戲設定」選項才能運作。\",\n    \"resolution_change_manual_windows\": \"使用手動輸入的解析度\",\n    \"resolution_change_no_operation_windows\": \"已停用\",\n    \"resolution_change_ogs_desc_windows\": \"必須在 Moonlight 用戶端上啟用「最佳化遊戲設定」選項才能運作。\",\n    \"resolution_change_windows\": \"解析度變更\",\n    \"resolutions\": \"基地顯示器支援的解析度\",\n    \"restart_note\": \"Sunshine 正在重新啟動以套用變更。\",\n    \"sleep_mode\": \"睡眠模式\",\n    \"sleep_mode_away\": \"離開模式（關閉螢幕，即時喚醒）\",\n    \"sleep_mode_desc\": \"控制用戶端傳送睡眠指令時的行為。待命(S3)：傳統睡眠，低功耗但需要 WOL 喚醒。休眠(S4)：儲存到磁碟，極低功耗。離開模式：關閉螢幕但系統保持運作，可即時喚醒 - 非常適合遊戲串流伺服器。\",\n    \"sleep_mode_hibernate\": \"休眠（S4）\",\n    \"sleep_mode_suspend\": \"待命（S3 睡眠）\",\n    \"stream_audio\": \"啟用音頻串流\",\n    \"stream_audio_desc\": \"禁用此選項以停用音頻串流。\",\n    \"stream_mic\": \"啟用麥克風串流\",\n    \"stream_mic_desc\": \"禁用此選項以停止麥克風串流。\",\n    \"stream_mic_download_btn\": \"下載虛擬麥克風\",\n    \"stream_mic_download_confirm\": \"即將跳轉到虛擬麥克風下載頁面，是否繼續？\",\n    \"stream_mic_note\": \"啟用此功能需要安裝虛擬麥克風\",\n    \"sunshine_name\": \"Sunshine 名稱\",\n    \"sunshine_name_desc\": \"Moonlight 顯示的名稱。如果未指定，則使用 PC 的主機名稱。\",\n    \"sw_preset\": \"SW 預設值\",\n    \"sw_preset_desc\": \"最佳化編碼速度（每秒編碼幀數）與壓縮效率（位元流中每位元的品質）之間的權衡。預設為 superfast。\",\n    \"sw_preset_fast\": \"快速\",\n    \"sw_preset_faster\": \"更快\",\n    \"sw_preset_medium\": \"中等\",\n    \"sw_preset_slow\": \"慢速\",\n    \"sw_preset_slower\": \"更慢\",\n    \"sw_preset_superfast\": \"超快（預設）\",\n    \"sw_preset_ultrafast\": \"超快\",\n    \"sw_preset_veryfast\": \"非常快\",\n    \"sw_preset_veryslow\": \"非常慢速\",\n    \"sw_tune\": \"SW 調音\",\n    \"sw_tune_animation\": \"animation—適用於卡通，使用較多的去區塊效應處理和更多的參考影格。\",\n    \"sw_tune_desc\": \"調整選項，在預設值之後套用。預設為 zerolatency。\",\n    \"sw_tune_fastdecode\": \"fastdecode—藉由停用某些過濾器來加快解碼速度\",\n    \"sw_tune_film\": \"film—適用於高品質的電影內容，降低去區塊效應處理。\",\n    \"sw_tune_grain\": \"grain—保留老電影畫面的顆粒結構\",\n    \"sw_tune_stillimage\": \"stillimage—適用於類似投影片的內容。\",\n    \"sw_tune_zerolatency\": \"zerolatency—適合快速編碼和低延遲串流（預設值）\",\n    \"system_tray\": \"啟用系統匣\",\n    \"system_tray_desc\": \"是否啟用系統匣。啟用後，Sunshine 將在系統匣中顯示圖示，並可從系統匣進行控制。\",\n    \"touchpad_as_ds4\": \"當用戶端的遊戲手把報告存在觸控板時，模擬 DS4 遊戲手把\",\n    \"touchpad_as_ds4_desc\": \"若停用，選擇遊戲手把類型時將不會考慮觸控板的存在。\",\n    \"unsaved_changes_tooltip\": \"您有未儲存的變更。點擊以儲存。\",\n    \"upnp\": \"UPnP\",\n    \"upnp_desc\": \"自動設定透過網際網路串流的連接埠轉發\",\n    \"variable_refresh_rate\": \"可變刷新率 (VRR)\",\n    \"variable_refresh_rate_desc\": \"允許視訊流幀率與渲染幀率匹配以支援 VRR。啟用後，僅在有新幀可用時進行編碼，允許流跟隨實際渲染幀率。\",\n    \"vdd_reuse_desc_windows\": \"啟用後，所有用戶端將共用同一個 VDD（虛擬顯示裝置）。停用時（預設），每個用戶端取得獨立的 VDD。啟用此選項可加快用戶端切換速度，但所有用戶端將共用相同的顯示設定。\",\n    \"vdd_reuse_windows\": \"所有用戶端共用同一 VDD\",\n    \"virtual_display\": \"基地顯示器\",\n    \"virtual_mouse\": \"虛擬滑鼠驅動\",\n    \"virtual_mouse_desc\": \"啟用後，Sunshine 將使用 Zako 虛擬滑鼠驅動（需已安裝）在 HID 層模擬滑鼠輸入，使 Raw Input 遊戲能正常接收滑鼠事件。停用或未安裝驅動時，回退至 SendInput。\",\n    \"virtual_sink\": \"虛擬音訊輸出\",\n    \"virtual_sink_desc\": \"手動指定要使用的虛擬音訊裝置。如果未設定，則會自動選擇裝置。我們強烈建議將此欄位留空，以使用自動裝置選擇！\",\n    \"virtual_sink_placeholder\": \"Steam Streaming Speakers\",\n    \"vmouse_confirm_install\": \"安裝虛擬滑鼠驅動？\",\n    \"vmouse_confirm_uninstall\": \"解除安裝虛擬滑鼠驅動？\",\n    \"vmouse_install\": \"安裝驅動\",\n    \"vmouse_installing\": \"安裝中...\",\n    \"vmouse_note\": \"虛擬滑鼠驅動需要單獨安裝，請使用 Sunshine 控制面板來安裝或管理驅動。\",\n    \"vmouse_refresh\": \"重新整理狀態\",\n    \"vmouse_status_installed\": \"已安裝（未啟用）\",\n    \"vmouse_status_not_installed\": \"未安裝\",\n    \"vmouse_status_running\": \"執行中\",\n    \"vmouse_uninstall\": \"解除安裝驅動\",\n    \"vmouse_uninstalling\": \"解除安裝中...\",\n    \"vt_coder\": \"VideoToolbox 編碼器\",\n    \"vt_realtime\": \"VideoToolbox 即時編碼\",\n    \"vt_software\": \"VideoToolbox 軟體編碼\",\n    \"vt_software_allowed\": \"允許\",\n    \"vt_software_forced\": \"強制\",\n    \"wan_encryption_mode\": \"WAN 加密模式\",\n    \"wan_encryption_mode_1\": \"對支援的用戶端啟用（預設）\",\n    \"wan_encryption_mode_2\": \"所有客戶都需要\",\n    \"wan_encryption_mode_desc\": \"這會決定在網際網路上串流時，何時會使用加密。加密可能會降低串流效能，尤其是在效能較低的主機和用戶端上。\",\n    \"webhook_curl_command\": \"命令\",\n    \"webhook_curl_command_desc\": \"複製以下命令到終端中執行，可以測試 webhook 是否正常工作：\",\n    \"webhook_curl_copy_failed\": \"複製失敗，請手動選擇並複製\",\n    \"webhook_enabled\": \"Webhook 通知\",\n    \"webhook_enabled_desc\": \"啟用後將向指定的 Webhook URL 發送事件通知\",\n    \"webhook_group\": \"Webhook 通知設定\",\n    \"webhook_skip_ssl_verify\": \"跳過 SSL 憑證驗證\",\n    \"webhook_skip_ssl_verify_desc\": \"跳過 HTTPS 連線的 SSL 憑證驗證，僅用於測試或自簽憑證\",\n    \"webhook_test\": \"測試\",\n    \"webhook_test_failed\": \"Webhook 測試失敗\",\n    \"webhook_test_failed_note\": \"提示：請檢查 URL 是否正確，或查看瀏覽器控制台獲取更多資訊。\",\n    \"webhook_test_success\": \"Webhook 測試成功！\",\n    \"webhook_test_success_cors_note\": \"提示：由於 CORS 限制，無法確認伺服器響應狀態。\\n請求已發送，如果 webhook 配置正確，訊息應該已送達。\\n\\n建議：在瀏覽器開發者工具的 Network 標籤頁中查看請求詳情。\",\n    \"webhook_test_url_required\": \"請先輸入 Webhook URL\",\n    \"webhook_timeout\": \"請求超時時間\",\n    \"webhook_timeout_desc\": \"Webhook 請求的超時時間（毫秒），範圍 100-5000ms\",\n    \"webhook_url\": \"Webhook URL\",\n    \"webhook_url_desc\": \"接收事件通知的 Webhook 位址，支援 HTTP/HTTPS 協定\",\n    \"wgc_checking_mode\": \"檢查中...\",\n    \"wgc_checking_running_mode\": \"正在檢查運行模式...\",\n    \"wgc_control_panel_only\": \"此功能僅在 Sunshine Control Panel 中可用\",\n    \"wgc_mode_switch_failed\": \"模式切換失敗\",\n    \"wgc_mode_switch_started\": \"模式切換已啟動，如果彈出 UAC 提示，請點擊「是」以確認。\",\n    \"wgc_service_mode_warning\": \"WGC 擷取需要在使用者模式下運行。如果當前以服務模式運行，請點擊上方按鈕切換到使用者模式。\",\n    \"wgc_switch_to_service_mode\": \"切換到服務模式\",\n    \"wgc_switch_to_service_mode_tooltip\": \"當前為使用者模式，點擊切換到服務模式\",\n    \"wgc_switch_to_user_mode\": \"切換到使用者模式\",\n    \"wgc_switch_to_user_mode_tooltip\": \"WGC 擷取需要在使用者模式下運行。點擊此按鈕切換到使用者模式。\",\n    \"wgc_user_mode_available\": \"當前已在使用者模式下運行。可以使用 WGC 擷取。\",\n    \"window_title\": \"視窗標題\",\n    \"window_title_desc\": \"要擷取的視窗標題（部分匹配，不區分大小寫）。如果留空，將自動使用當前運行的應用程式名稱。\",\n    \"window_title_placeholder\": \"例如：應用程式名稱\"\n  },\n  \"index\": {\n    \"description\": \"Sunshine 是 Moonlight 的自架遊戲串流主機。\",\n    \"download\": \"下載\",\n    \"installed_version_not_stable\": \"您正在運行的是 Sunshine 的預發行版本。您可能會遇到錯誤或其他問題。請報告您遇到的任何問題。感謝您的協助，讓 Sunshine 成為更好的軟體！\",\n    \"loading_latest\": \"正在載入最新版本…\",\n    \"new_pre_release\": \"提供新的預發行版本！\",\n    \"new_stable\": \"有新的穩定版本可用！\",\n    \"startup_errors\": \"<b>注意！</b> Sunshine 在啟動時檢測到這些錯誤。我們<b>強烈建議</b>在開始串流之前修復它們。\",\n    \"update_download_confirm\": \"即將在瀏覽器中開啟更新下載頁面，是否繼續？\",\n    \"version_dirty\": \"感謝您的協助，讓 Sunshine 成為更好的軟體！\",\n    \"version_latest\": \"您正在執行最新版本的 Sunshine\",\n    \"view_logs\": \"查看日誌\",\n    \"welcome\": \"你好，Sunshine！\"\n  },\n  \"navbar\": {\n    \"applications\": \"應用程式\",\n    \"configuration\": \"組態\",\n    \"home\": \"首頁\",\n    \"password\": \"變更密碼\",\n    \"pin\": \"Pin 碼\",\n    \"theme_auto\": \"自動\",\n    \"theme_dark\": \"深色主題\",\n    \"theme_light\": \"淺色主題\",\n    \"toggle_theme\": \"主題\",\n    \"troubleshoot\": \"疑難排解\"\n  },\n  \"password\": {\n    \"confirm_password\": \"確認密碼\",\n    \"current_creds\": \"目前的憑證\",\n    \"new_creds\": \"新憑證\",\n    \"new_username_desc\": \"如果未指定，使用者名稱不會變更\",\n    \"password_change\": \"密碼變更\",\n    \"success_msg\": \"密碼已成功變更！此頁面將很快重新載入，您的瀏覽器將要求輸入新的憑證。\"\n  },\n  \"pin\": {\n    \"actions\": \"操作\",\n    \"cancel_editing\": \"取消編輯\",\n    \"client_name\": \"名稱\",\n    \"client_settings_info\": \"提示：\",\n    \"confirm_delete\": \"確認刪除\",\n    \"delete_client\": \"刪除用戶端\",\n    \"delete_confirm_message\": \"您確定要刪除 <strong>{name}</strong> 嗎？\",\n    \"delete_warning\": \"此操作無法復原。\",\n    \"device_name\": \"裝置名稱\",\n    \"device_size\": \"裝置尺寸\",\n    \"device_size_info\": \"<strong>裝置尺寸</strong>：設定用戶端裝置的螢幕尺寸類型（小-手機、中-平板、大-電視），用於優化串流體驗和觸控操作。\",\n    \"device_size_large\": \"大 - 電視\",\n    \"device_size_medium\": \"中 - 平板\",\n    \"device_size_small\": \"小 - 手機\",\n    \"edit_client_settings\": \"編輯用戶端設定\",\n    \"hdr_profile\": \"HDR 設定檔\",\n    \"hdr_profile_info\": \"<strong>HDR 設定檔</strong>：選擇用於該用戶端的 HDR 色彩設定檔（ICC 檔案），可確保 HDR 內容在該裝置上正確顯示。\",\n    \"loading\": \"載入中...\",\n    \"loading_clients\": \"正在載入用戶端...\",\n    \"modify_in_gui\": \"請在圖形介面中修改\",\n    \"none\": \"-- 無 --\",\n    \"or_manual_pin\": \"或手動輸入 PIN 碼\",\n    \"pair_failure\": \"配對失敗：請確認 PIN 碼是否輸入正確\",\n    \"pair_success\": \"成功！請檢查 Moonlight 來繼續\",\n    \"pin_pairing\": \"PIN 碼配對\",\n    \"qr_expires_in\": \"剩餘時間\",\n    \"qr_generate\": \"產生 QR Code\",\n    \"qr_paired_success\": \"配對成功！\",\n    \"qr_pairing\": \"QR Code 配對\",\n    \"qr_pairing_desc\": \"產生 QR Code 快速配對。使用 Moonlight 用戶端掃描即可自動配對。\",\n    \"qr_pairing_warning\": \"實驗性測試功能，若無法配對成功，請使用下面的手動輸入 PIN 碼配對。注意！此功能只能在區域網路內使用。\",\n    \"qr_refresh\": \"重新整理 QR Code\",\n    \"remove_paired_devices_desc\": \"移除您已配對的裝置。\",\n    \"save_changes\": \"儲存變更\",\n    \"save_failed\": \"儲存用戶端設定失敗。請重試。\",\n    \"save_or_cancel_first\": \"請先儲存或取消編輯\",\n    \"send\": \"發送\",\n    \"unknown_client\": \"未知的用戶端\",\n    \"unpair_all_confirm\": \"您確定要取消所有用戶端的配對嗎？此操作無法復原。\",\n    \"unsaved_changes\": \"未儲存的變更\",\n    \"warning_msg\": \"請確保您可以存取要配對的用戶端。此軟體可讓對方完全控制您的電腦，請小心使用！\"\n  },\n  \"resource_card\": {\n    \"android_recommended\": \"Android 推薦\",\n    \"client_downloads\": \"客戶端下載\",\n    \"crown_edition\": \"王冠版\",\n    \"github_discussions\": \"GitHub 討論區\",\n    \"gpl_license_text_1\": \"本軟體採用 GPL-3.0 授權。您可以自由使用、修改和分發。\",\n    \"gpl_license_text_2\": \"為保護開源生態系統，請避免使用違反 GPL-3.0 授權的軟體。\",\n    \"harmony_client\": \"鴻蒙Moonlight V+\",\n    \"join_group\": \"加入串流群\",\n    \"join_group_desc\": \"獲取幫助與交流經驗\",\n    \"legal\": \"法律資訊\",\n    \"legal_desc\": \"繼續使用本軟體即表示您同意下列文件中的條款和條件。\",\n    \"license\": \"許可證\",\n    \"lizardbyte_website\": \"LizardByte 網站\",\n    \"official_website\": \"基地官網\",\n    \"official_website_title\": \"瑤光流夢 - 官方網站\",\n    \"open_source\": \"開源專案\",\n    \"open_source_desc\": \"Star & Fork 支持專案發展\",\n    \"quick_start\": \"快速入門\",\n    \"resources\": \"資源\",\n    \"resources_desc\": \"Sunshine 相關資源！\",\n    \"third_party_desc\": \"第三方元件聲明\",\n    \"third_party_moonlight\": \"友情連結\",\n    \"third_party_notice\": \"第三方通知\",\n    \"tutorial\": \"使用教程\",\n    \"tutorial_desc\": \"詳細的配置與使用指南\",\n    \"view_license\": \"檢視完整授權條款\",\n    \"voidlink_title\": \"虛空終端 (VoidLink)\"\n  },\n  \"setup\": {\n    \"adapter_info\": \"組態摘要\",\n    \"android_client\": \"Android 用戶端\",\n    \"base_display_title\": \"虛擬顯示器\",\n    \"choose_adapter\": \"自動\",\n    \"config_saved\": \"組態已成功儲存。\",\n    \"description\": \"讓我們快速開始設定\",\n    \"device_id\": \"裝置 ID\",\n    \"device_state\": \"狀態\",\n    \"download_clients\": \"下載用戶端\",\n    \"finish\": \"完成設定\",\n    \"go_to_apps\": \"設定應用程式\",\n    \"harmony_goto_repo\": \"前往倉庫\",\n    \"harmony_modal_desc\": \"鴻蒙Next Moonlight 請在鴻蒙商店內搜尋 Moonlight V+\",\n    \"harmony_modal_link_notice\": \"此連結將跳轉至專案倉庫\",\n    \"ios_client\": \"iOS 用戶端\",\n    \"load_error\": \"載入組態失敗\",\n    \"next\": \"下一步\",\n    \"physical_display\": \"實體顯示器/EDID 模擬器\",\n    \"physical_display_desc\": \"串流您的實際實體監視器\",\n    \"previous\": \"上一步\",\n    \"restart_countdown_unit\": \"秒\",\n    \"restart_desc\": \"已儲存設定並觸發重新啟動，顯示設定將在重新啟動後生效。\",\n    \"restart_go_now\": \"立即跳轉\",\n    \"restart_title\": \"正在重新啟動 Sunshine\",\n    \"save_error\": \"儲存組態失敗\",\n    \"select_adapter\": \"圖形介面卡\",\n    \"selected_adapter\": \"已選取的介面卡\",\n    \"selected_display\": \"已選取的顯示器\",\n    \"setup_complete\": \"設定完成！\",\n    \"setup_complete_desc\": \"基本設定已生效，您現在可以直接使用 Moonlight 客戶端開始串流了！\",\n    \"skip\": \"跳過設定精靈\",\n    \"skip_confirm\": \"您確定要跳過設定精靈嗎？您稍後可以在設定頁面中設定這些選項。\",\n    \"skip_confirm_title\": \"跳過設定精靈\",\n    \"skip_error\": \"跳過失敗\",\n    \"state_active\": \"啟動\",\n    \"state_inactive\": \"未啟動\",\n    \"state_primary\": \"主要\",\n    \"state_unknown\": \"未知\",\n    \"step0_description\": \"選擇您的介面語言\",\n    \"step0_title\": \"語言\",\n    \"step1_description\": \"選擇要串流的顯示器\",\n    \"step1_title\": \"顯示器選擇\",\n    \"step1_vdd_intro\": \"基地顯示器為Sunshine基地版內建的智慧顯示螢幕，支援任意解析度、幀率和HDR最佳化，是息屏串流、副屏串流的首選。\",\n    \"step2_description\": \"選擇您的圖形介面卡\",\n    \"step2_title\": \"選擇介面卡\",\n    \"step3_description\": \"選擇顯示器裝置準備策略\",\n    \"step3_ensure_active\": \"確保啟動\",\n    \"step3_ensure_active_desc\": \"如果顯示器未啟動則啟動它\",\n    \"step3_ensure_only_display\": \"確保唯一顯示器\",\n    \"step3_ensure_only_display_desc\": \"停用其他顯示器，只開啟指定的顯示器（推薦）\",\n    \"step3_ensure_primary\": \"確保主顯示器\",\n    \"step3_ensure_primary_desc\": \"確保顯示器啟動並設定為主顯示器\",\n    \"step3_ensure_secondary\": \"副屏串流\",\n    \"step3_ensure_secondary_desc\": \"僅使用基地顯示器進行副屏擴展串流\",\n    \"step3_no_operation\": \"無操作\",\n    \"step3_no_operation_desc\": \"不對顯示器進行任何操作，使用者需自行確保顯示器狀態\",\n    \"step3_title\": \"顯示器策略\",\n    \"step4_title\": \"完成\",\n    \"stream_mode\": \"串流模式\",\n    \"unknown_display\": \"未知顯示器\",\n    \"virtual_display\": \"基地顯示器 (ZakoHDR)\",\n    \"virtual_display_desc\": \"使用基地顯示器裝置進行串流（需要安裝 ZakoVDD 驅動程式）\",\n    \"welcome\": \"歡迎來到 Sunshine Foundation\"\n  },\n  \"tabs\": {\n    \"advanced\": \"進階\",\n    \"amd\": \"AMD AMF 編碼器\",\n    \"av\": \"音訊/視訊\",\n    \"encoders\": \"編碼器\",\n    \"files\": \"組態檔\",\n    \"general\": \"一般\",\n    \"input\": \"輸入\",\n    \"network\": \"網路\",\n    \"nv\": \"NVIDIA NVENC 編碼器\",\n    \"qsv\": \"Intel QuickSync 編碼器\",\n    \"sw\": \"軟體編碼器\",\n    \"vaapi\": \"VAAPI 編碼器\",\n    \"vt\": \"VideoToolbox 編碼器\"\n  },\n  \"troubleshooting\": {\n    \"ai_analyzing\": \"分析中...\",\n    \"ai_analyzing_logs\": \"正在分析日誌，請稍候...\",\n    \"ai_config\": \"AI 設定\",\n    \"ai_copy_result\": \"複製\",\n    \"ai_diagnosis\": \"AI 診斷\",\n    \"ai_diagnosis_title\": \"AI 日誌診斷\",\n    \"ai_error\": \"分析失敗\",\n    \"ai_key_local\": \"API Key 僅儲存在本機，不會上傳\",\n    \"ai_model\": \"模型\",\n    \"ai_provider\": \"供應商\",\n    \"ai_reanalyze\": \"重新分析\",\n    \"ai_result\": \"診斷結果\",\n    \"ai_retry\": \"重試\",\n    \"ai_start_diagnosis\": \"開始診斷\",\n    \"boom_sunshine\": \"Boom!\",\n    \"boom_sunshine_desc\": \"如果您需要立即關閉 Sunshine，可以使用此功能。請注意，關閉後需要手動重新啟動。\",\n    \"boom_sunshine_success\": \"Sunshine 已關閉\",\n    \"confirm_boom\": \"真的要退出嗎？\",\n    \"confirm_boom_desc\": \"那麼想退出嗎？真是拿你沒辦法呢，繼續點一下吧\",\n    \"confirm_logout\": \"確認登出？\",\n    \"confirm_logout_desc\": \"登出後需重新輸入密碼才能存取管理介面。\",\n    \"copy_config\": \"複製配置\",\n    \"copy_config_error\": \"複製配置失敗\",\n    \"copy_config_success\": \"配置已複製到剪貼板！\",\n    \"copy_logs\": \"複製日誌\",\n    \"download_logs\": \"下載日誌\",\n    \"force_close\": \"強制關閉\",\n    \"force_close_desc\": \"如果 Moonlight 抱怨目前的應用程式已經在執行，強制關閉該應用程式應該可以解決問題。\",\n    \"force_close_error\": \"關閉應用程式時發生錯誤\",\n    \"force_close_success\": \"應用程式已成功結束！\",\n    \"ignore_case\": \"忽略大小寫\",\n    \"logout\": \"登出\",\n    \"logout_desc\": \"登出。可能需要重新登入。\",\n    \"logout_localhost_tip\": \"目前環境較特殊，不會觸發登出與密碼框。\",\n    \"logs\": \"日誌\",\n    \"logs_desc\": \"查看 Sunshine 上傳的日誌\",\n    \"logs_find\": \"尋找…\",\n    \"match_contains\": \"包含\",\n    \"match_exact\": \"完全符合\",\n    \"match_regex\": \"正則表達式\",\n    \"reopen_setup_wizard\": \"重新開啟新手引導\",\n    \"reopen_setup_wizard_desc\": \"重新開啟新手引導頁面，可以重新設定初始設定。\",\n    \"reopen_setup_wizard_error\": \"重新開啟新手引導失敗\",\n    \"reset_display_device_desc_windows\": \"如果 Sunshine 在嘗試還原已變更的顯示器裝置設定時卡住，您可以重設設定並手動還原顯示器狀態。\\n這可能因各種原因發生：裝置不再可用、已插入不同連接埠等。\",\n    \"reset_display_device_error_windows\": \"重設持續性時發生錯誤！\",\n    \"reset_display_device_success_windows\": \"成功重設持續性！\",\n    \"reset_display_device_windows\": \"重設顯示器記憶體\",\n    \"restart_sunshine\": \"重新啟動 Sunshine\",\n    \"restart_sunshine_desc\": \"如果 Sunshine 沒有正常運作，您可以嘗試重新啟動。這會終止所有正在執行的工作階段。\",\n    \"restart_sunshine_success\": \"Sunshine 正在重新啟動\",\n    \"troubleshooting\": \"疑難排解\",\n    \"unpair_all\": \"全部解除配對\",\n    \"unpair_all_error\": \"解除配對時發生錯誤\",\n    \"unpair_all_success\": \"已取消配對所有裝置。\",\n    \"unpair_desc\": \"刪除已配對的裝置。未配對的裝置若有使用中的工作階段，將保持連線，但無法啟動或恢復工作階段。\",\n    \"unpair_single_no_devices\": \"沒有已配對的裝置。\",\n    \"unpair_single_success\": \"然而，該裝置仍可能處於使用中的工作階段。請使用上方的「強制關閉」按鈕結束任何使用中的工作階段。\",\n    \"unpair_single_unknown\": \"未知的用戶端\",\n    \"unpair_title\": \"解除配對裝置\"\n  },\n  \"welcome\": {\n    \"confirm_password\": \"確認密碼\",\n    \"create_creds\": \"在開始之前，我們需要您建立新的使用者名稱和密碼，以便存取網頁 UI。\",\n    \"create_creds_alert\": \"以下的憑證是存取 Sunshine 網頁介面所需的。請妥善保管，因為您將無法再查看這些憑證！\",\n    \"creds_local_only\": \"帳號密碼僅離線儲存在本地，不會上傳到任何伺服器。\",\n    \"error\": \"錯誤！\",\n    \"greeting\": \"歡迎使用 Sunshine Foundation！\",\n    \"hide_password\": \"隱藏密碼\",\n    \"login\": \"登入\",\n    \"network_error\": \"網路錯誤，請檢查連線\",\n    \"password\": \"密碼\",\n    \"password_match\": \"密碼匹配\",\n    \"password_mismatch\": \"密碼不相符\",\n    \"server_error\": \"伺服器錯誤\",\n    \"show_password\": \"顯示密碼\",\n    \"success\": \"成功！\",\n    \"username\": \"使用者名稱\",\n    \"welcome_success\": \"此頁面將很快重新載入，您的瀏覽器會要求您提供新的憑證\"\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/scripts/extract-welcome-locales.js",
    "content": "#!/usr/bin/env node\n/**\n * 从模板文件生成完整的 welcome.html\n * 提取所有语言的 welcome 翻译并静态嵌入到 HTML 中\n * 注意：只提取 welcome 部分，因为需要的 _common 键已经添加到 welcome 中了\n */\n\nimport fs from 'fs'\nimport path from 'path'\nimport { fileURLToPath } from 'url'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\nconst localeDir = path.join(__dirname, '../public/assets/locale')\nconst templatePath = path.join(__dirname, '../welcome.html.template')\nconst outputPath = path.join(__dirname, '../welcome.html')\nconst output = {}\n\n// 获取所有语言文件\nconst localeFiles = fs.readdirSync(localeDir).filter(file => file.endsWith('.json'))\n\nlocaleFiles.forEach(file => {\n  const locale = file.replace('.json', '')\n  const filePath = path.join(localeDir, file)\n  \n  try {\n    const content = JSON.parse(fs.readFileSync(filePath, 'utf8'))\n    // 只提取 welcome 部分（username, password, error, success 已经在 welcome 中了）\n    if (content.welcome) {\n      output[locale] = {\n        welcome: content.welcome\n      }\n    }\n  } catch (e) {\n    console.error(`Failed to parse ${file}:`, e.message)\n  }\n})\n\n// 生成内联的 script 标签内容\nconst inlineScript = `<script>\n// 自动生成的 welcome 页面翻译数据（构建时生成）\nwindow.__WELCOME_LOCALES__ = ${JSON.stringify(output, null, 2)};\n</script>`\n\n// 读取模板文件并生成完整的 welcome.html\nif (!fs.existsSync(templatePath)) {\n  console.error(`Error: Template file not found: ${templatePath}`)\n  process.exit(1)\n}\n\nlet template = fs.readFileSync(templatePath, 'utf8')\n\n// 替换占位符\nif (template.includes('WELCOME_LOCALES_INLINE_PLACEHOLDER')) {\n  template = template.replace('<!-- WELCOME_LOCALES_INLINE_PLACEHOLDER -->', inlineScript)\n  fs.writeFileSync(outputPath, template, 'utf8')\n  console.log(`Generated welcome.html from template`)\n  console.log(`  Languages: ${Object.keys(output).join(', ')}`)\n} else {\n  console.error(`Error: Placeholder not found in template file`)\n  process.exit(1)\n}\n\n"
  },
  {
    "path": "src_assets/common/assets/web/services/appService.js",
    "content": "import { API_ENDPOINTS } from '../utils/constants.js';\nimport { formatError } from '../utils/helpers.js';\n\n/**\n * 应用服务类\n */\nexport class AppService {\n  /**\n   * 获取应用列表\n   * @returns {Promise<Array>} 应用列表\n   */\n  static async getApps() {\n    try {\n      const response = await fetch(API_ENDPOINTS.APPS);\n      if (!response.ok) {\n        throw new Error(`获取应用列表失败: ${response.status}`);\n      }\n      const data = await response.json();\n      return data.apps || [];\n    } catch (error) {\n      console.error('获取应用列表失败:', error);\n      throw new Error(formatError(error));\n    }\n  }\n\n  /**\n   * 保存应用\n   * @param {Array} apps 应用列表\n   * @param {Object} editApp 编辑的应用（可选）\n   * @returns {Promise<boolean>} 是否保存成功\n   */\n  static async saveApps(apps, editApp = null) {\n    try {\n      const response = await fetch(API_ENDPOINTS.APPS, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n          apps,\n          editApp\n        })\n      });\n      \n      if (!response.ok) {\n        throw new Error(`保存应用失败: ${response.status}`);\n      }\n      \n      return true;\n    } catch (error) {\n      console.error('保存应用失败:', error);\n      throw new Error(formatError(error));\n    }\n  }\n\n  /**\n   * 删除应用\n   * @param {number} index 应用索引\n   * @returns {Promise<boolean>} 是否删除成功\n   */\n  static async deleteApp(index) {\n    try {\n      const response = await fetch(API_ENDPOINTS.APP_DELETE(index), {\n        method: 'DELETE'\n      });\n      \n      if (!response.ok) {\n        throw new Error(`删除应用失败: ${response.status}`);\n      }\n      \n      return true;\n    } catch (error) {\n      console.error('删除应用失败:', error);\n      throw new Error(formatError(error));\n    }\n  }\n\n  /**\n   * 获取平台信息\n   * @returns {Promise<string>} 平台信息\n   */\n  static async getPlatform() {\n    try {\n      const response = await fetch(API_ENDPOINTS.CONFIG);\n      if (!response.ok) {\n        throw new Error(`获取平台信息失败: ${response.status}`);\n      }\n      const data = await response.json();\n      return data.platform || 'windows';\n    } catch (error) {\n      console.error('获取平台信息失败:', error);\n      // 默认返回windows平台\n      return 'windows';\n    }\n  }\n\n  /**\n   * 搜索应用\n   * @param {Array} apps 应用列表\n   * @param {string} query 搜索关键词\n   * @returns {Array} 搜索结果\n   */\n  static searchApps(apps, query) {\n    if (!query || !query.trim()) {\n      return [...apps];\n    }\n    \n    const searchTerm = query.toLowerCase().trim();\n    return apps.filter(app => \n      app.name.toLowerCase().includes(searchTerm) || \n      (app.cmd && app.cmd.toLowerCase().includes(searchTerm))\n    );\n  }\n\n  /**\n   * 验证应用数据\n   * @param {Object} app 应用对象\n   * @returns {Object} 验证结果\n   */\n  static validateApp(app) {\n    const errors = [];\n    \n    if (!app.name || !app.name.trim()) {\n      errors.push('应用名称不能为空');\n    }\n    \n    if (!app.cmd || !app.cmd.trim()) {\n      errors.push('应用命令不能为空');\n    }\n    \n    // 验证退出超时时间\n    if (app['exit-timeout'] !== undefined && \n        (isNaN(app['exit-timeout']) || app['exit-timeout'] < 0)) {\n      errors.push('退出超时时间必须是非负数');\n    }\n    \n    return {\n      isValid: errors.length === 0,\n      errors\n    };\n  }\n\n  /**\n   * 格式化应用数据\n   * @param {Object} app 原始应用数据\n   * @returns {Object} 格式化后的应用数据\n   */\n  static formatAppData(app) {\n    // 过滤掉 do 和 undo 都为空或只包含空格的 prep-cmd 项\n    const filteredPrepCmd = Array.isArray(app['prep-cmd']) \n      ? app['prep-cmd'].filter(cmd => {\n          const hasDo = cmd.do && cmd.do.trim() !== '';\n          const hasUndo = cmd.undo && cmd.undo.trim() !== '';\n          // 至少有一个不为空才保留\n          return hasDo || hasUndo;\n        })\n      : [];\n    \n    return {\n      name: app.name?.trim() || '',\n      output: app.output?.trim() || '',\n      cmd: app.cmd?.trim() || '',\n      'exclude-global-prep-cmd': Boolean(app['exclude-global-prep-cmd']),\n      elevated: Boolean(app.elevated),\n      'auto-detach': Boolean(app['auto-detach']),\n      'wait-all': Boolean(app['wait-all']),\n      'exit-timeout': parseInt(app['exit-timeout']) || 5,\n      'prep-cmd': filteredPrepCmd,\n      'menu-cmd': Array.isArray(app['menu-cmd']) ? app['menu-cmd'] : [],\n      detached: Array.isArray(app.detached) ? app.detached : [],\n      'image-path': app['image-path']?.trim() || '',\n      'working-dir': app['working-dir']?.trim() || ''\n    };\n  }\n} "
  },
  {
    "path": "src_assets/common/assets/web/styles/apps.less",
    "content": "@import './global.less';\n@import './modal-glass.less';\n\n// ============================================================================\n// VARIABLES & MIXINS\n// ============================================================================\n\n// Common transition\n.transition-default() {\n  transition: var(--transition-default);\n}\n\n.transition-transform() {\n  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.transition-bounce() {\n  transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);\n}\n\n// Common backdrop blur\n.glass-effect() {\n  backdrop-filter: blur(10px);\n}\n\n.glass-effect-light() {\n  backdrop-filter: blur(4px);\n}\n\n// Common button base\n.btn-base() {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  border: none;\n  cursor: pointer;\n  .transition-default();\n}\n\n// Common rounded button\n.btn-rounded(@size: 40px) {\n  .btn-base();\n  width: @size;\n  height: @size;\n  border-radius: 50%;\n}\n\n// Hover scale effect\n.hover-scale(@scale: 1.1) {\n  &:hover {\n    transform: scale(@scale);\n  }\n}\n\n// Text ellipsis\n.text-ellipsis() {\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n// Flex center\n.flex-center() {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n// ============================================================================\n// COPY BUTTON\n// ============================================================================\n\n.copy-btn {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0.375rem 0.75rem;\n  font-size: 0.875rem;\n  font-weight: 500;\n  line-height: 1.5;\n  text-align: center;\n  text-decoration: none;\n  vertical-align: middle;\n  cursor: pointer;\n  user-select: none;\n  border: 1.5px solid rgba(102, 126, 234, 0.6);\n  border-radius: 0.375rem;\n  .transition-transform();\n  background: linear-gradient(135deg, #667eea, #764ba2);\n  color: #fff;\n\n  &:hover {\n    background: linear-gradient(135deg, #764ba2, #667eea);\n    border-color: rgba(102, 126, 234, 0.8);\n    color: #fff;\n    transform: translateY(-1px);\n    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35);\n  }\n\n  &:active {\n    transform: translateY(0);\n    box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);\n  }\n\n  &:focus {\n    outline: 0;\n    box-shadow: 0 0 0 0.25rem rgba(102, 126, 234, 0.25);\n  }\n\n  &:disabled {\n    opacity: 0.65;\n    cursor: not-allowed;\n    transform: none;\n  }\n\n  &.absolute {\n    position: absolute;\n    top: 10px;\n    right: 10px;\n    z-index: 10;\n  }\n\n  [data-bs-theme='dark'] & {\n    background: linear-gradient(135deg, #667eea, #764ba2);\n    border-color: rgba(102, 126, 234, 0.7);\n\n    &:hover {\n      background: linear-gradient(135deg, #764ba2, #667eea);\n      border-color: rgba(102, 126, 234, 0.9);\n    }\n  }\n}\n\n// ============================================================================\n// SEARCH & ACTION BUTTONS\n// ============================================================================\n\n.search-container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  gap: 20px;\n  margin-bottom: var(--spacing-xl);\n  flex-wrap: wrap;\n}\n\n.search-box {\n  position: relative;\n  width: 100%;\n  max-width: 500px;\n  flex: 1;\n  min-width: 300px;\n}\n\n.search-input {\n  background: rgba(255, 255, 255, 0.1);\n  border: 2px solid rgba(255, 255, 255, 0.2);\n  border-radius: 25px;\n  padding: 12px 20px 12px 50px;\n  font-size: 1.1rem;\n  color: #fff;\n  .glass-effect-light();\n  .transition-default();\n  box-shadow: var(--box-shadow-sm);\n  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);\n\n  &:focus {\n    outline: none;\n    border-color: rgba(255, 255, 255, 0.5);\n    background: rgba(255, 255, 255, 0.2);\n    box-shadow: var(--box-shadow-lg);\n  }\n\n  &::placeholder {\n    color: rgba(255, 255, 255, 0.6);\n  }\n}\n\n.search-icon {\n  position: absolute;\n  left: 18px;\n  top: 50%;\n  transform: translateY(-50%);\n  color: rgba(255, 255, 255, 0.6);\n  font-size: 1.1rem;\n}\n\n.btn-clear-search {\n  position: absolute;\n  right: 15px;\n  top: 50%;\n  transform: translateY(-50%);\n  background: none;\n  border: none;\n  color: rgba(255, 255, 255, 0.6);\n  cursor: pointer;\n  padding: 5px;\n  border-radius: 50%;\n  .transition-default();\n\n  &:hover {\n    color: #fff;\n    background: rgba(255, 255, 255, 0.1);\n  }\n}\n\n.action-buttons {\n  display: flex;\n  gap: 12px;\n  align-items: center;\n  flex-shrink: 0;\n}\n\n// ============================================================================\n// VIEW TOGGLE\n// ============================================================================\n\n.view-toggle-group {\n  display: flex;\n  background: rgba(255, 255, 255, 0.1);\n  border-radius: 25px;\n  padding: 4px;\n  .glass-effect();\n  border: 1px solid rgba(255, 255, 255, 0.2);\n  box-shadow: var(--box-shadow-sm);\n}\n\n.view-toggle-btn {\n  .btn-rounded();\n  background: transparent;\n  color: rgba(255, 255, 255, 0.6);\n  font-size: var(--font-size-md);\n  transition: all 0.3s ease;\n\n  &:hover {\n    color: rgba(255, 255, 255, 0.9);\n    background: rgba(255, 255, 255, 0.1);\n\n    i {\n      transform: scale(1.1);\n    }\n  }\n\n  &.active {\n    background: var(--gradient-primary);\n    color: #fff;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n    transform: scale(1.05);\n  }\n\n  i {\n    transition: transform 0.3s ease;\n  }\n}\n\n// ============================================================================\n// CUTE BUTTONS\n// ============================================================================\n\n.cute-btn {\n  .btn-rounded(48px);\n  color: #fff;\n  font-size: var(--font-size-lg);\n  .transition-bounce();\n  .glass-effect();\n  box-shadow: var(--box-shadow-md);\n  position: relative;\n  overflow: hidden;\n  animation: bounceIn 0.8s ease-out;\n\n  &::before {\n    content: '';\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    width: 0;\n    height: 0;\n    background: rgba(255, 255, 255, 0.3);\n    border-radius: 50%;\n    transition: all 0.6s ease;\n    transform: translate(-50%, -50%);\n  }\n\n  &:hover {\n    transform: translateY(-3px) scale(1.1);\n    box-shadow: var(--box-shadow-lg);\n\n    &::before {\n      width: 100%;\n      height: 100%;\n    }\n\n    i {\n      transform: scale(1.2);\n    }\n  }\n\n  &:active {\n    transform: translateY(-1px) scale(1.05);\n    transition: all 0.1s ease;\n  }\n\n  i {\n    position: relative;\n    z-index: 2;\n    .transition-default();\n  }\n\n  // Stagger animation\n  &:nth-child(1) {\n    animation-delay: 0.1s;\n  }\n  &:nth-child(2) {\n    animation-delay: 0.2s;\n  }\n  &:nth-child(3) {\n    animation-delay: 0.3s;\n  }\n}\n\n// Cute button variants mixin\n.cute-btn-variant(@gradient, @gradient-hover) {\n  background: @gradient;\n  border: 2px solid var(--cute-btn-border-color);\n\n  &:hover {\n    background: @gradient-hover;\n    border-color: var(--cute-btn-border-hover-color);\n  }\n}\n\n.cute-btn-primary {\n  .cute-btn-variant(var(--gradient-primary), linear-gradient(135deg, #764ba2, #667eea));\n}\n\n.cute-btn-secondary {\n  .cute-btn-variant(\n    linear-gradient(135deg, var(--info-color), #138496),\n    linear-gradient(135deg, #138496, var(--info-color))\n  );\n}\n\n.cute-btn-success {\n  .cute-btn-variant(var(--gradient-success), var(--gradient-success-hover));\n  position: relative;\n\n  &.has-changes {\n    animation: pulse 2s infinite;\n    box-shadow: 0 4px 15px rgba(255, 193, 7, 0.6);\n\n    .unsaved-indicator {\n      position: absolute;\n      top: 4px;\n      right: 4px;\n      width: 8px;\n      height: 8px;\n      background: #ffc107;\n      border-radius: 50%;\n      box-shadow: 0 0 8px rgba(255, 193, 7, 0.8);\n      animation: blink 1.5s infinite;\n    }\n  }\n\n  &:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n    transform: none !important;\n  }\n}\n\n.cute-btn-info {\n  .cute-btn-variant(\n    linear-gradient(135deg, #17a2b8, #138496),\n    linear-gradient(135deg, #138496, #0c6b7a)\n  );\n\n  &:disabled {\n    opacity: 0.6;\n    cursor: not-allowed;\n    transform: none !important;\n  }\n}\n\n// ============================================================================\n// ANIMATIONS\n// ============================================================================\n\n@keyframes pulse {\n  0%,\n  100% {\n    box-shadow: 0 4px 15px rgba(255, 193, 7, 0.4);\n  }\n  50% {\n    box-shadow: 0 4px 20px rgba(255, 193, 7, 0.8);\n  }\n}\n\n@keyframes blink {\n  0%,\n  100% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0.3;\n  }\n}\n\n@keyframes bounceIn {\n  0% {\n    opacity: 0;\n    transform: scale(0.3) translateY(20px);\n  }\n  50% {\n    opacity: 1;\n    transform: scale(1.05) translateY(-5px);\n  }\n  70% {\n    transform: scale(0.9) translateY(0);\n  }\n  100% {\n    opacity: 1;\n    transform: scale(1) translateY(0);\n  }\n}\n\n@keyframes slideInRight {\n  from {\n    transform: translateX(100%);\n    opacity: 0;\n  }\n  to {\n    transform: translateX(0);\n    opacity: 1;\n  }\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n    transform: translateY(20px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes copySuccess {\n  0% {\n    transform: translateY(-1px) scale(1);\n  }\n  50% {\n    transform: translateY(-3px) scale(1.02);\n    background: rgba(40, 167, 69, 0.3);\n  }\n  100% {\n    transform: translateY(-1px) scale(1);\n  }\n}\n\n@keyframes modalSlideUp {\n  from {\n    transform: translateY(20px);\n    opacity: 0;\n  }\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n// ============================================================================\n// APPS GRID & LIST\n// ============================================================================\n\n.apps-grid-container {\n  padding-bottom: var(--spacing-xl);\n}\n\n.apps-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n  gap: 20px;\n\n  .drag-handle {\n    position: absolute;\n    top: 10px;\n    right: 10px;\n    color: rgba(255, 255, 255, 0.4);\n    cursor: move;\n    font-size: var(--font-size-lg);\n    padding: 5px;\n    border-radius: 5px;\n    .transition-default();\n    z-index: 10;\n\n    &:hover {\n      color: rgba(255, 255, 255, 0.8);\n      background: rgba(255, 255, 255, 0.1);\n    }\n  }\n}\n\n.apps-list {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n// ============================================================================\n// APP CARD\n// ============================================================================\n\n.app-card {\n  background: rgba(255, 255, 255, 0.1);\n  border: 1px solid rgba(255, 255, 255, 0.2);\n  border-radius: var(--border-radius-xl);\n  overflow: hidden;\n  .glass-effect-light();\n  .transition-default();\n  cursor: pointer;\n  position: relative;\n  box-shadow: var(--box-shadow-sm);\n  animation: fadeIn 0.5s ease;\n\n  &:hover {\n    transform: translateY(-5px);\n    box-shadow: var(--box-shadow-lg);\n    border-color: rgba(255, 255, 255, 0.3);\n  }\n}\n\n.app-card-inner {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  position: relative;\n  padding: 20px;\n  height: 100%;\n}\n\n// App icon styles\n.app-icon-container {\n  .flex-center();\n  width: 80px;\n  height: 80px;\n  margin: 0 auto 8px;\n  position: relative;\n}\n\n.app-icon {\n  width: 80px;\n  height: 80px;\n  border-radius: var(--border-radius-md);\n  object-fit: cover;\n  box-shadow: var(--box-shadow-md);\n}\n\n.app-icon-placeholder {\n  width: 80px;\n  height: 80px;\n  background: linear-gradient(135deg, #ff6b6b, #ffa500);\n  border-radius: var(--border-radius-md);\n  .flex-center();\n  color: #fff;\n  font-size: var(--font-size-xxl);\n  box-shadow: var(--box-shadow-md);\n}\n\n// App info\n.app-info {\n  text-align: center;\n  margin-bottom: 15px;\n  cursor: pointer;\n  position: relative;\n  padding: 8px;\n  border-radius: var(--border-radius-md);\n  .transition-default();\n\n  &:hover {\n    background: rgba(255, 255, 255, 0.1);\n    .glass-effect-light();\n    transform: translateY(-1px);\n  }\n\n  &:active {\n    transform: translateY(0) scale(0.98);\n    background: rgba(255, 255, 255, 0.2);\n  }\n\n  &:not([title]),\n  &[title=''] {\n    cursor: default;\n    pointer-events: none;\n  }\n\n  &.copy-success {\n    animation: copySuccess 0.4s ease;\n  }\n}\n\n.app-name {\n  font-size: var(--font-size-lg);\n  font-weight: 600;\n  color: #fff;\n  margin-bottom: 8px;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\n  .text-ellipsis();\n}\n\n.app-command {\n  display: inline-block;\n  text-align: left;\n  font-size: var(--font-size-xs);\n  color: rgba(255, 255, 255, 0.7);\n  margin-bottom: 10px;\n  font-family: 'Courier New', monospace;\n  background: rgba(0, 0, 0, 0.2);\n  padding: 5px 10px;\n  border-radius: var(--border-radius-sm);\n  max-width: 100%;\n  .text-ellipsis();\n  .transition-default();\n\n  &:hover {\n    background: rgba(0, 0, 0, 0.35);\n    color: rgba(255, 255, 255, 0.9);\n    transform: translateY(-1px);\n  }\n}\n\n// ============================================================================\n// APP TAGS\n// ============================================================================\n\n.app-tags {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n  gap: 8px;\n  margin-bottom: 15px;\n}\n\n.app-tag {\n  background: rgba(255, 255, 255, 0.4);\n  color: #333;\n  padding: 4px 8px;\n  border-radius: var(--border-radius-md);\n  font-size: var(--font-size-xs);\n  font-weight: 500;\n}\n\n// Tag variants\n.tag-exclude-global-prep-cmd {\n  background: rgba(255, 105, 180, 0.6);\n  color: #fff;\n}\n.tag-menu {\n  background: rgba(147, 112, 219, 0.6);\n  color: #fff;\n}\n.tag-elevated {\n  background: rgba(255, 165, 0, 0.6);\n  color: #fff;\n}\n.tag-detach {\n  background: rgba(32, 178, 170, 0.6);\n  color: #fff;\n}\n\n// ============================================================================\n// APP ACTIONS\n// ============================================================================\n\n.app-actions {\n  display: flex;\n  justify-content: center;\n  gap: 10px;\n\n  .btn {\n    .btn-rounded();\n    color: #fff;\n    font-size: var(--font-size-md);\n    backdrop-filter: blur(5px);\n  }\n}\n\n// Action button variants mixin\n.action-btn-variant(@color) {\n  background: rgba(@color, 0.3);\n  border: 1px solid rgba(@color, 0.5);\n\n  &:hover {\n    background: rgba(@color, 0.5);\n    transform: scale(1.1);\n  }\n}\n\n.btn-edit {\n  .action-btn-variant(rgb(0, 123, 255));\n}\n\n.btn-delete {\n  .action-btn-variant(rgb(220, 53, 69));\n}\n\n// ============================================================================\n// DRAG STATES\n// ============================================================================\n\n.app-card-dragging {\n  opacity: 0.8;\n  transform: scale(0.98);\n}\n.app-card-ghost {\n  opacity: 0.3;\n  transform: scale(0.95);\n  background: rgba(255, 255, 255, 0.05) !important;\n}\n.app-card-chosen {\n  transform: scale(1.02);\n  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3) !important;\n  z-index: 1000;\n}\n.app-card-drag {\n  transform: rotate(5deg) scale(1.05);\n  box-shadow: 0 30px 60px rgba(0, 0, 0, 0.4) !important;\n}\n\n// ============================================================================\n// SEARCH INDICATORS & HINTS\n// ============================================================================\n\n.search-indicator {\n  position: absolute;\n  top: 10px;\n  right: 10px;\n  color: rgba(255, 255, 255, 0.4);\n  font-size: var(--font-size-md);\n  padding: 5px;\n  border-radius: 5px;\n  background: rgba(255, 255, 255, 0.1);\n  z-index: 10;\n}\n\n.search-mode-hint {\n  background: rgba(255, 193, 7, 0.2);\n  border: 1px solid rgba(255, 193, 7, 0.4);\n  border-radius: var(--border-radius-md);\n  padding: 10px 15px;\n  margin-bottom: 20px;\n  color: var(--warning-color);\n  font-size: var(--font-size-sm);\n  text-align: center;\n  backdrop-filter: blur(5px);\n}\n\n// ============================================================================\n// EMPTY STATE\n// ============================================================================\n\n.empty-state {\n  grid-column: 1 / -1;\n  text-align: center;\n  padding: 60px 20px;\n  background: rgba(255, 255, 255, 0.1);\n  border: 2px dashed rgba(255, 255, 255, 0.3);\n  border-radius: var(--border-radius-xl);\n  .glass-effect();\n}\n\n.empty-icon {\n  font-size: 4rem;\n  color: rgba(255, 255, 255, 0.6);\n  margin-bottom: 20px;\n}\n\n.empty-title {\n  font-size: var(--font-size-xl);\n  color: #fff;\n  margin-bottom: 10px;\n}\n\n.empty-subtitle {\n  font-size: var(--font-size-md);\n  color: rgba(255, 255, 255, 0.7);\n  margin-bottom: 20px;\n}\n\n// ============================================================================\n// ALERT TOASTS\n// ============================================================================\n\n.alert-toast {\n  position: fixed;\n  top: 20px;\n  right: 20px;\n  padding: 15px 20px;\n  border-radius: var(--border-radius-md);\n  .glass-effect();\n  box-shadow: var(--box-shadow-lg);\n  z-index: 1100;\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  color: #fff;\n  font-weight: 500;\n  animation: slideInRight 0.3s ease;\n}\n\n// Alert variants mixin\n.alert-variant(@color) {\n  background: rgba(@color, 0.8);\n  border: 1px solid rgba(@color, 0.5);\n}\n\n.alert-success {\n  .alert-variant(rgb(40, 167, 69));\n}\n.alert-error {\n  .alert-variant(rgb(220, 53, 69));\n}\n.alert-warning {\n  .alert-variant(rgb(255, 193, 7));\n}\n.alert-info {\n  .alert-variant(rgb(23, 162, 184));\n}\n\n.btn-close-toast {\n  background: none;\n  border: none;\n  color: #fff;\n  cursor: pointer;\n  padding: 0;\n  margin-left: 10px;\n  opacity: 0.7;\n  transition: opacity 0.3s ease;\n\n  &:hover {\n    opacity: 1;\n  }\n}\n\n// ============================================================================\n// OUTLINE BUTTONS\n// ============================================================================\n\n// Outline button mixin\n.btn-outline-variant(@color, @hover-bg, @focus-shadow) {\n  background: transparent;\n  border: 1.5px solid rgba(@color, 0.6);\n  color: @color;\n  font-weight: 500;\n  .transition-transform();\n\n  &:hover {\n    background: @hover-bg;\n    border-color: rgba(@color, 0.8);\n    color: @color;\n    box-shadow: 0 4px 12px @focus-shadow;\n  }\n\n  &:active {\n    transform: translateY(0);\n    box-shadow: 0 2px 6px @focus-shadow;\n  }\n\n  &:focus {\n    outline: 0;\n    box-shadow: 0 0 0 0.25rem @focus-shadow;\n  }\n\n  &:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n    transform: none;\n  }\n}\n\n.btn-outline-danger {\n  background: var(--danger-color);\n  border: 1px solid rgba(220, 53, 69, 0.5);\n  color: #fff;\n\n  &:hover {\n    transform: translateY(-1px);\n    box-shadow: 0 8px 25px rgba(220, 53, 69, 0.4);\n  }\n}\n\n.btn-outline-secondary {\n  .btn-outline-variant(#6c757d, rgba(108, 117, 125, 0.15), rgba(108, 117, 125, 0.25));\n\n  [data-bs-theme='dark'] & {\n    border-color: rgba(255, 255, 255, 0.4);\n    color: rgba(255, 255, 255, 0.85);\n\n    &:hover {\n      background: rgba(255, 255, 255, 0.12);\n      border-color: rgba(255, 255, 255, 0.6);\n      color: #fff;\n    }\n  }\n}\n\n.btn-outline-info {\n  .btn-outline-variant(#0dcaf0, rgba(13, 202, 240, 0.15), rgba(13, 202, 240, 0.25));\n\n  &:hover {\n    box-shadow: 0 4px 12px rgba(13, 202, 240, 0.3);\n  }\n\n  [data-bs-theme='dark'] & {\n    border-color: rgba(13, 202, 240, 0.7);\n    color: #5dd5f8;\n\n    &:hover {\n      background: rgba(13, 202, 240, 0.2);\n      border-color: rgba(13, 202, 240, 0.9);\n      color: #0dcaf0;\n    }\n  }\n}\n\n// ============================================================================\n// GLASS PANEL & TABLE\n// ============================================================================\n\n.glass-panel {\n  background: rgba(255, 255, 255, 0.1) !important;\n  border: 1px solid rgba(255, 255, 255, 0.2) !important;\n  border-radius: var(--border-radius-md) !important;\n  backdrop-filter: blur(10px) !important;\n  color: #fff !important;\n}\n\n.table-glass {\n  background: rgba(255, 255, 255, 0.1);\n  border-radius: var(--border-radius-md);\n  overflow: hidden;\n  .glass-effect();\n\n  th {\n    background: rgba(255, 255, 255, 0.2);\n    color: #fff;\n    font-weight: 600;\n    border-bottom: 1px solid rgba(255, 255, 255, 0.2);\n  }\n\n  td {\n    color: #fff;\n    border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n  }\n}\n\n// ============================================================================\n// CODE & ENV STYLES\n// ============================================================================\n.env-vars-modal {\n  .env-vars-table {\n    margin-top: 1rem;\n\n    .table {\n      color: var(--modal-text-color, #fff);\n      border-color: var(--modal-border-color, rgba(255, 255, 255, 0.15));\n      margin-bottom: 0;\n\n      th {\n        border-top: none;\n        border-bottom: 1px solid var(--glass-border, rgba(255, 255, 255, 0.2));\n        font-weight: 600;\n        font-size: 0.875rem;\n        padding: 1rem 0.75rem;\n        background: var(--glass-medium, rgba(255, 255, 255, 0.1));\n        color: var(--modal-text-color, #fff);\n      }\n\n      thead th {\n        &:first-child {\n          border-radius: 12px 0 0 0;\n        }\n\n        &:last-child {\n          border-radius: 0 12px 0 0;\n        }\n      }\n\n      tr:last-child td {\n        border-bottom: none;\n      }\n\n      td {\n        vertical-align: middle;\n        border-color: var(--modal-border-color, rgba(255, 255, 255, 0.1));\n        padding: 0.75rem;\n        background: var(--glass-light, rgba(255, 255, 255, 0.05));\n        transition: background 0.3s ease;\n      }\n\n      tbody tr {\n        &:hover td {\n          background: var(--glass-medium, rgba(255, 255, 255, 0.1));\n        }\n\n        &:last-child td {\n          &:first-child {\n            border-radius: 0 0 0 12px;\n          }\n\n          &:last-child {\n            border-radius: 0 0 12px 0;\n          }\n        }\n      }\n    }\n  }\n\n  .env-var-name {\n    background: rgba(0, 0, 0, 0.3) !important;\n    color: var(--warning-color) !important;\n    padding: 4px 8px !important;\n    border-radius: 6px !important;\n    font-family: 'Courier New', monospace !important;\n    font-weight: 600;\n\n    [data-bs-theme='light'] & {\n      background: #e0e7ff !important;\n      color: #6366f1 !important;\n      border: 1px solid rgba(99, 102, 241, 0.3) !important;\n    }\n  }\n\n  .code-example {\n    margin-top: 1rem;\n    background: rgba(0, 0, 0, 0.3) !important;\n    color: #fff !important;\n    padding: 15px !important;\n    border-radius: var(--border-radius-md) !important;\n    border: 1px solid rgba(255, 255, 255, 0.2) !important;\n    font-family: 'Courier New', monospace !important;\n    font-size: var(--font-size-sm) !important;\n\n    [data-bs-theme='light'] & {\n      background: #f5f8ff !important;\n      color: #475569 !important;\n      border: 1px solid rgba(99, 102, 241, 0.2) !important;\n    }\n  }\n}\n\n// ============================================================================\n// RESPONSIVE DESIGN\n// ============================================================================\n\n@media (max-width: 768px) {\n  .apps-grid {\n    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));\n    gap: 15px;\n  }\n\n  .search-container {\n    flex-direction: column;\n    gap: 15px;\n  }\n\n  .search-box {\n    max-width: 100%;\n    min-width: auto;\n  }\n\n  .action-buttons {\n    justify-content: center;\n    flex-wrap: wrap;\n  }\n\n  .cute-btn {\n    width: 44px;\n    height: 44px;\n    font-size: 1.1rem;\n  }\n\n  .page-title {\n    font-size: 2rem;\n  }\n\n  .alert-toast {\n    right: 10px;\n    left: 10px;\n    top: 10px;\n  }\n\n  .app-list-item-inner {\n    padding: 12px 16px;\n    gap: 12px;\n  }\n\n  .app-icon-container-list,\n  .app-icon-list,\n  .app-icon-placeholder-list {\n    width: 50px;\n    height: 50px;\n  }\n\n  .app-icon-placeholder-list {\n    font-size: 1.2rem;\n  }\n  .app-name-list {\n    font-size: var(--font-size-md);\n  }\n  .app-tags-list {\n    gap: 4px;\n  }\n\n  .app-tag-list {\n    font-size: 0.7rem;\n    padding: 2px 6px;\n  }\n\n  .view-toggle-group {\n    order: -1;\n    width: 100%;\n    justify-content: center;\n  }\n}\n\n@media (max-width: 576px) {\n  .app-card-inner {\n    padding: 15px;\n  }\n\n  .app-icon-container,\n  .app-icon,\n  .app-icon-placeholder {\n    width: 60px;\n    height: 60px;\n  }\n\n  .app-name {\n    font-size: 1.1rem;\n  }\n  .search-container {\n    margin-bottom: 1.5rem;\n  }\n\n  .cute-btn {\n    width: 40px;\n    height: 40px;\n    font-size: var(--font-size-md);\n  }\n\n  .action-buttons {\n    gap: 8px;\n  }\n\n  .app-list-item-inner {\n    flex-wrap: wrap;\n    padding: 12px;\n  }\n\n  .drag-handle-list {\n    width: 24px;\n    font-size: var(--font-size-md);\n  }\n\n  .app-icon-container-list,\n  .app-icon-list,\n  .app-icon-placeholder-list {\n    width: 45px;\n    height: 45px;\n  }\n\n  .app-info-list {\n    flex: 1 1 calc(100% - 200px);\n  }\n  .app-actions-list {\n    margin-left: auto;\n  }\n\n  .app-actions-list .btn {\n    width: 36px;\n    height: 36px;\n    font-size: var(--font-size-sm);\n  }\n\n  .app-command-list,\n  .app-working-dir-list {\n    font-size: var(--font-size-xs);\n  }\n\n  .view-toggle-btn {\n    width: 38px;\n    height: 38px;\n  }\n}\n\n// ============================================================================\n// LIST ITEM STYLES\n// ============================================================================\n\n.app-list-item {\n  background: rgba(255, 255, 255, 0.1);\n  border: 1px solid rgba(255, 255, 255, 0.2);\n  border-radius: var(--border-radius-lg);\n  overflow: hidden;\n  .glass-effect();\n  .transition-default();\n  box-shadow: var(--box-shadow-sm);\n  animation: fadeIn 0.5s ease;\n\n  &:hover {\n    transform: translateX(5px);\n    box-shadow: var(--box-shadow-lg);\n    border-color: rgba(255, 255, 255, 0.3);\n  }\n}\n\n.app-list-item-inner {\n  display: flex;\n  align-items: center;\n  padding: 16px 20px;\n  gap: 16px;\n  position: relative;\n}\n\n.drag-handle-list {\n  color: rgba(255, 255, 255, 0.4);\n  cursor: move;\n  font-size: var(--font-size-lg);\n  padding: 5px;\n  border-radius: 5px;\n  .transition-default();\n  .flex-center();\n  width: 30px;\n  flex-shrink: 0;\n\n  &:hover {\n    color: rgba(255, 255, 255, 0.8);\n    background: rgba(255, 255, 255, 0.1);\n  }\n}\n\n.app-icon-container-list {\n  .flex-center();\n  width: 60px;\n  height: 60px;\n  flex-shrink: 0;\n}\n\n.app-icon-list {\n  width: 60px;\n  height: 60px;\n  border-radius: var(--border-radius-md);\n  object-fit: cover;\n  box-shadow: var(--box-shadow-md);\n}\n\n.app-icon-placeholder-list {\n  width: 60px;\n  height: 60px;\n  background: linear-gradient(135deg, #ff6b6b, #ffa500);\n  border-radius: var(--border-radius-md);\n  .flex-center();\n  color: #fff;\n  font-size: 1.5rem;\n  box-shadow: var(--box-shadow-md);\n}\n\n.app-info-list {\n  flex: 1;\n  min-width: 0;\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n}\n\n.app-name-row {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n  flex-wrap: wrap;\n}\n\n.app-name-list {\n  font-size: var(--font-size-lg);\n  font-weight: 600;\n  color: #fff;\n  margin: 0;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\n  .text-ellipsis();\n  flex-shrink: 0;\n}\n\n.app-command-list {\n  font-size: var(--font-size-sm);\n  color: rgba(255, 255, 255, 0.7);\n  margin: 0;\n  font-family: 'Courier New', monospace;\n  background: rgba(0, 0, 0, 0.2);\n  padding: 6px 12px;\n  border-radius: var(--border-radius-sm);\n  .text-ellipsis();\n  .transition-default();\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n\n  &:hover {\n    background: rgba(0, 0, 0, 0.35);\n    color: rgba(255, 255, 255, 0.9);\n    transform: translateX(3px);\n  }\n\n  &.copy-success {\n    animation: copySuccess 0.4s ease;\n  }\n}\n\n.app-working-dir-list {\n  font-size: var(--font-size-sm);\n  color: rgba(255, 255, 255, 0.6);\n  margin: 0;\n  display: flex;\n  align-items: center;\n  .text-ellipsis();\n}\n\n.app-tags-list {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 6px;\n  align-items: center;\n}\n\n.app-tag-list {\n  background: rgba(255, 255, 255, 0.4);\n  color: #333;\n  padding: 3px 8px;\n  border-radius: var(--border-radius-md);\n  font-size: var(--font-size-xs);\n  font-weight: 500;\n  white-space: nowrap;\n  display: inline-flex;\n  align-items: center;\n}\n\n.app-actions-list {\n  display: flex;\n  gap: 8px;\n  flex-shrink: 0;\n\n  .btn {\n    .btn-rounded(38px);\n    color: #fff;\n    font-size: var(--font-size-md);\n    backdrop-filter: blur(5px);\n  }\n}\n\n.btn-edit-list {\n  background: rgba(0, 123, 255, 0.3);\n  border: 1px solid rgba(0, 123, 255, 0.5);\n  .hover-scale();\n\n  &:hover {\n    background: rgba(0, 123, 255, 0.5);\n  }\n}\n\n.btn-delete-list {\n  background: rgba(220, 53, 69, 0.3);\n  border: 1px solid rgba(220, 53, 69, 0.5);\n  .hover-scale();\n\n  &:hover {\n    background: rgba(220, 53, 69, 0.5);\n  }\n}\n\n.search-indicator-list {\n  position: absolute;\n  top: 10px;\n  right: 10px;\n  color: rgba(255, 255, 255, 0.4);\n  font-size: var(--font-size-sm);\n  padding: 4px 8px;\n  border-radius: 5px;\n  background: rgba(255, 255, 255, 0.1);\n}\n\n// List drag states\n.app-list-item-dragging {\n  opacity: 0.8;\n}\n.app-list-item-ghost {\n  opacity: 0.3;\n  background: rgba(255, 255, 255, 0.05) !important;\n}\n.app-list-item-chosen {\n  transform: scale(1.02);\n  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3) !important;\n  z-index: 1000;\n}\n.app-list-item-drag {\n  transform: rotate(2deg) scale(1.03);\n  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4) !important;\n}\n\n// ============================================================================\n// LEGACY COMPATIBILITY\n// ============================================================================\n\n.precmd-head {\n  width: 200px;\n}\n.monospace {\n  font-family: 'Courier New', monospace;\n}\n\n.cover-finder .cover-results {\n  max-height: 400px;\n  overflow-x: hidden;\n  overflow-y: auto;\n\n  &.busy * {\n    cursor: wait !important;\n    pointer-events: none;\n  }\n}\n\n.cover-container {\n  padding-top: 133.33%;\n  position: relative;\n\n  &.result {\n    cursor: pointer;\n  }\n\n  img {\n    display: block;\n    position: absolute;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n  }\n}\n\n.spinner-border {\n  position: absolute;\n  left: 0;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  margin: auto;\n}\n\n.config-page {\n  padding: 1em;\n  border: 1px solid #dee2e6;\n  border-top: none;\n}\n\ntbody {\n  border: none;\n}\ntd {\n  padding: 0 0.5em;\n}\n\n.env-table td {\n  padding: 0.25em;\n  border-bottom: rgba(0, 0, 0, 0.25) 1px solid;\n  vertical-align: top;\n}\n\n.table .item-app {\n  cursor: move;\n  &:hover {\n    background-color: #d38d9d91;\n  }\n}\n\n// ============================================================================\n// MODAL & FORM STYLES\n// ============================================================================\n\n.modal-xl {\n  max-width: 1200px;\n}\n\n.form-section {\n  margin-bottom: var(--spacing-xl);\n}\n\n.section-title {\n  font-size: var(--font-size-lg);\n  font-weight: 600;\n  margin-bottom: var(--spacing-md);\n  color: #495057;\n  border-bottom: 2px solid #e9ecef;\n  padding-bottom: var(--spacing-xs);\n}\n\n.form-group-enhanced {\n  margin-bottom: var(--spacing-lg);\n}\n\n.form-label-enhanced {\n  font-weight: 600;\n  margin-bottom: var(--spacing-xs);\n  color: #7b8188;\n}\n\n.form-control-enhanced {\n  border-radius: var(--border-radius-sm);\n  border: 1px solid #ced4da;\n  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n\n  &:focus {\n    border-color: #80bdff;\n    box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n  }\n}\n\n.required-field::after {\n  content: ' *';\n  color: var(--danger-color);\n}\n\n.field-hint {\n  margin-top: var(--spacing-xs);\n  font-size: var(--font-size-sm);\n  color: var(--secondary-color);\n}\n\n.form-check-label {\n  font-size: var(--font-size-md);\n  color: var(--secondary-color);\n}\n\n.command-table {\n  padding: var(--spacing-xs);\n  margin-top: var(--spacing-md);\n  animation: fadeIn 0.6s ease-out;\n\n  [data-bs-theme='light'] & {\n    background: rgba(248, 249, 250, 0.9);\n    color: #000;\n\n    th,\n    td {\n      color: #000;\n    }\n\n    .form-control,\n    .form-control-sm {\n      color: #000;\n      background-color: #fff;\n      border-color: rgba(0, 0, 0, 0.2);\n\n      &::placeholder {\n        color: rgba(0, 0, 0, 0.5);\n      }\n    }\n\n    label,\n    .form-label,\n    .form-check-label {\n      color: #000;\n    }\n\n    span,\n    p,\n    div:not(.btn):not(.badge) {\n      color: #000;\n    }\n  }\n}\n\n.modal-footer-enhanced {\n  border-top: 1px solid #dee2e6;\n  padding: var(--spacing-md) var(--spacing-lg);\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.save-status {\n  font-size: var(--font-size-sm);\n  color: var(--secondary-color);\n}\n\n.accordion-button {\n  &:not(.collapsed) {\n    color: #0c63e4;\n    background-color: #e7f1ff;\n    border-bottom-color: #86b7fe;\n  }\n\n  &:focus {\n    z-index: 3;\n    border-color: #86b7fe;\n    outline: 0;\n    box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);\n  }\n}\n\n// Validation states\n.is-invalid {\n  border-color: #dc3545;\n}\n.is-valid {\n  border-color: #198754;\n}\n\n.invalid-feedback {\n  display: block;\n  font-size: 0.875rem;\n  color: #dc3545;\n  margin-top: 0.25rem;\n}\n\n.valid-feedback {\n  display: block;\n  font-size: 0.875rem;\n  color: #198754;\n  margin-top: 0.25rem;\n}\n\n// ============================================================================\n// STEAM COVER FINDER\n// ============================================================================\n\n.cover-source-selector {\n  margin-bottom: 1rem;\n}\n\n.cover-source-tabs {\n  display: flex;\n  border-bottom: 1px solid #dee2e6;\n  margin-bottom: 1rem;\n}\n\n.cover-source-tab {\n  padding: 0.5rem 1rem;\n  background: none;\n  border: none;\n  border-bottom: 2px solid transparent;\n  cursor: pointer;\n  color: #6c757d;\n  transition: all 0.15s ease-in-out;\n\n  &.active {\n    color: #0d6efd;\n    border-bottom-color: #0d6efd;\n  }\n\n  &:hover {\n    color: #0d6efd;\n  }\n}\n\n.steam-app-info {\n  font-size: 0.875rem;\n  color: #6c757d;\n  margin-top: 0.25rem;\n}\n\n// ============================================================================\n// SCAN RESULT MODAL\n// ============================================================================\n\n.scan-result-overlay {\n  position: fixed;\n  inset: 0;\n  background: var(--overlay-bg, rgba(0, 0, 0, 0.7));\n  backdrop-filter: blur(8px);\n  z-index: 9999;\n  display: flex;\n  align-items: flex-start;\n  justify-content: center;\n  padding: var(--spacing-lg, 20px);\n  padding-top: 50px;\n  overflow-y: auto;\n}\n\n.scan-result-modal {\n  background: var(--modal-bg, rgba(30, 30, 50, 0.95));\n  border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.2));\n  border-radius: var(--border-radius-xl);\n  width: 100%;\n  max-width: 700px;\n  max-height: 80vh;\n  display: flex;\n  flex-direction: column;\n  backdrop-filter: blur(20px);\n  box-shadow: var(--shadow-xl, 0 25px 50px rgba(0, 0, 0, 0.5));\n  animation: modalSlideUp 0.3s ease;\n\n  [data-bs-theme='light'] & {\n    background: #f0f4ff;\n    border: 1px solid rgba(99, 102, 241, 0.25);\n    box-shadow: 0 20px 40px rgba(99, 102, 241, 0.15);\n  }\n}\n\n.scan-result-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-bottom: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  h5 {\n    margin: 0;\n    color: var(--text-primary, #fff);\n    font-size: var(--font-size-lg);\n    font-weight: 600;\n    display: flex;\n    align-items: center;\n    gap: var(--spacing-sm, 8px);\n  }\n\n  [data-bs-theme='light'] & {\n    border-bottom: 1px solid rgba(0, 0, 0, 0.08);\n\n    h5 {\n      color: #1e293b;\n    }\n  }\n}\n\n.scan-result-search {\n  padding: var(--spacing-sm, 12px) var(--spacing-md, 20px);\n  border-bottom: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.08));\n  background: linear-gradient(180deg, rgba(0, 0, 0, 0.15) 0%, rgba(0, 0, 0, 0.08) 100%);\n\n  .search-box {\n    position: relative;\n    width: 100%;\n    max-width: 100%;\n    min-width: auto;\n  }\n\n  .search-input {\n    background: rgba(255, 255, 255, 0.06);\n    border: 1px solid rgba(255, 255, 255, 0.12);\n    border-radius: var(--border-radius-lg, 12px);\n    padding: 12px 44px;\n    font-size: var(--font-size-sm, 14px);\n    font-weight: 400;\n    letter-spacing: 0.01em;\n    color: var(--text-primary, #fff);\n    backdrop-filter: blur(8px);\n    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n    width: 100%;\n    box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n\n    &:hover {\n      background: rgba(255, 255, 255, 0.08);\n      border-color: rgba(255, 255, 255, 0.18);\n    }\n\n    &:focus {\n      outline: none;\n      background: rgba(255, 255, 255, 0.1);\n      border-color: var(--color-primary, #667eea);\n      box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 0 3px rgba(102, 126, 234, 0.15);\n    }\n\n    &::placeholder {\n      color: rgba(255, 255, 255, 0.4);\n      font-weight: 400;\n    }\n  }\n\n  .search-icon {\n    position: absolute;\n    left: 14px;\n    top: 50%;\n    transform: translateY(-50%);\n    color: rgba(255, 255, 255, 0.45);\n    font-size: var(--font-size-sm, 14px);\n    pointer-events: none;\n    transition: color 0.2s ease;\n  }\n\n  .search-input:focus ~ .search-icon,\n  .search-box:focus-within .search-icon {\n    color: var(--color-primary, #667eea);\n  }\n\n  .btn-clear-search {\n    position: absolute;\n    right: 10px;\n    top: 50%;\n    transform: translateY(-50%);\n    background: rgba(255, 255, 255, 0.08);\n    border: none;\n    color: rgba(255, 255, 255, 0.5);\n    cursor: pointer;\n    padding: 0;\n    border-radius: var(--border-radius-sm, 6px);\n    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n    width: 26px;\n    height: 26px;\n    .flex-center();\n    font-size: 11px;\n\n    &:hover {\n      color: #fff;\n      background: rgba(255, 255, 255, 0.15);\n      transform: translateY(-50%) scale(1.05);\n    }\n\n    &:active {\n      transform: translateY(-50%) scale(0.95);\n    }\n  }\n\n  [data-bs-theme='light'] & {\n    border-bottom: 1px solid rgba(99, 102, 241, 0.2);\n    background: #e8efff;\n\n    .search-input {\n      background: #fff;\n      border: 1px solid rgba(99, 102, 241, 0.25);\n      color: #1e293b;\n      border-radius: 12px;\n\n      &:hover {\n        border-color: rgba(99, 102, 241, 0.35);\n      }\n\n      &:focus {\n        background: #fff;\n        border-color: #6366f1;\n        box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);\n      }\n\n      &::placeholder {\n        color: #94a3b8;\n      }\n    }\n\n    .search-icon {\n      color: #6366f1;\n    }\n\n    .btn-clear-search {\n      background: rgba(99, 102, 241, 0.15);\n      color: #6366f1;\n\n      &:hover {\n        color: #fff;\n        background: #6366f1;\n      }\n    }\n  }\n}\n\n.scan-result-body {\n  flex: 1;\n  overflow-y: auto;\n  padding: var(--spacing-sm, 12px) var(--spacing-md, 20px);\n}\n\n.scan-result-list {\n  display: flex;\n  flex-direction: column;\n  gap: var(--spacing-sm, 8px);\n}\n\n.scan-result-item {\n  display: flex;\n  align-items: stretch;\n  gap: var(--spacing-md, 16px);\n  padding: 0;\n  background: var(--card-bg-hover, rgba(255, 255, 255, 0.05));\n  border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n  border-radius: var(--border-radius-md);\n  .transition-default();\n  overflow: hidden;\n\n  &:hover {\n    background: var(--card-bg-active, rgba(255, 255, 255, 0.1));\n    border-color: var(--border-color-hover, rgba(255, 255, 255, 0.2));\n    transform: translateX(4px);\n  }\n\n  [data-bs-theme='light'] & {\n    background: #f5f8ff;\n    border: 1px solid rgba(99, 102, 241, 0.2);\n    box-shadow: 0 3px 6px rgba(99, 102, 241, 0.08);\n    border-radius: 14px;\n\n    &:hover {\n      background: #eef2ff;\n      border-color: rgba(99, 102, 241, 0.3);\n      box-shadow: 0 8px 20px rgba(99, 102, 241, 0.15);\n      transform: translateY(-3px);\n    }\n  }\n}\n\n.scan-app-icon {\n  overflow: hidden;\n  .flex-center();\n  flex-shrink: 0;\n  box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.2));\n  min-width: 56px;\n\n  img {\n    height: 80px;\n    aspect-ratio: 1 / 1;\n    object-fit: cover;\n  }\n}\n\n.scan-app-info {\n  display: flex;\n  flex-direction: column;\n  gap: var(--spacing-xs, 2px);\n  flex: 1;\n  min-width: 0;\n  padding: var(--spacing-xs, 2px) 0;\n}\n\n.scan-app-name {\n  font-size: var(--font-size-sm);\n  font-weight: 600;\n  color: var(--text-primary, #fff);\n  .text-ellipsis();\n\n  [data-bs-theme='light'] & {\n    color: #1e293b;\n  }\n}\n\n.scan-app-cmd {\n  font-size: var(--font-size-xs, 12px);\n  color: var(--text-primary, rgba(255, 255, 255, 0.85));\n  font-family: var(--font-mono, 'Courier New', monospace);\n  background: var(--code-bg, rgba(0, 0, 0, 0.3));\n  border-radius: var(--border-radius-xs, 4px);\n  .text-ellipsis();\n  padding: var(--spacing-xs, 4px) var(--spacing-xs, 2px);\n  display: inline-block;\n  max-width: 100%;\n  letter-spacing: 0.02em;\n  line-height: 1.3;\n\n  [data-bs-theme='light'] & {\n    color: #475569;\n    background: #e0e7ff;\n    border: 1px solid rgba(99, 102, 241, 0.2);\n    border-radius: 6px;\n  }\n}\n\n.scan-app-path {\n  font-size: var(--font-size-xs, 12px);\n  color: var(--text-secondary, rgba(255, 255, 255, 0.65));\n  .text-ellipsis();\n  line-height: 1.3;\n\n  [data-bs-theme='light'] & {\n    color: #64748b;\n  }\n}\n\n.scan-app-actions {\n  display: flex;\n  gap: var(--spacing-xs, 6px);\n  flex-shrink: 0;\n  padding: var(--spacing-sm, 8px) var(--spacing-sm, 12px);\n  align-items: center;\n\n  .btn {\n    .btn-rounded(28px);\n    padding: 0;\n    .hover-scale();\n  }\n}\n\n.scan-result-footer {\n  display: flex;\n  justify-content: flex-end;\n  gap: var(--spacing-md, 12px);\n  padding: var(--spacing-sm, 12px) var(--spacing-md, 20px);\n  border-top: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  .btn {\n    padding: var(--spacing-sm, 10px) var(--spacing-lg, 20px);\n    border-radius: var(--border-radius-md);\n    font-weight: 500;\n    .transition-default();\n  }\n\n  .btn-primary {\n    background: var(--gradient-primary);\n    border: none;\n    color: var(--text-primary, #fff);\n\n    &:hover {\n      transform: translateY(-2px);\n      box-shadow: var(--shadow-primary, 0 8px 25px rgba(102, 126, 234, 0.4));\n    }\n  }\n\n  .btn-secondary {\n    background: var(--btn-secondary-bg, rgba(255, 255, 255, 0.1));\n    border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.2));\n    color: var(--text-primary, #fff);\n\n    &:hover {\n      background: var(--btn-secondary-bg-hover, rgba(255, 255, 255, 0.15));\n    }\n  }\n\n  [data-bs-theme='light'] & {\n    border-top: 1px solid rgba(99, 102, 241, 0.2);\n    background: #e0e7ff;\n\n    .btn-secondary {\n      background: #f5f8ff;\n      border: 1px solid rgba(99, 102, 241, 0.25);\n      color: #475569;\n\n      &:hover {\n        background: #e8efff;\n        border-color: rgba(99, 102, 241, 0.35);\n        color: #6366f1;\n      }\n    }\n\n    .btn-primary {\n      background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%);\n      color: #fff;\n      border: none;\n\n      &:hover {\n        box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);\n        transform: translateY(-1px);\n      }\n    }\n  }\n}\n\n.scan-result-empty {\n  text-align: center;\n  padding: var(--spacing-xxl, 40px) var(--spacing-lg, 20px);\n  color: var(--text-muted, rgba(255, 255, 255, 0.5));\n\n  i {\n    font-size: 3rem;\n    margin-bottom: var(--spacing-md, 16px);\n    opacity: 0.5;\n  }\n\n  p {\n    margin: 0;\n    font-size: var(--font-size-md);\n  }\n\n  [data-bs-theme='light'] & {\n    color: #6c757d;\n  }\n}\n\n// ============================================================================\n// DELETE APP CONFIRM MODAL\n// ============================================================================\n\n.delete-app-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 100vw;\n  height: 100vh;\n  margin: 0;\n  background: var(--overlay-bg, rgba(0, 0, 0, 0.7));\n  backdrop-filter: blur(8px);\n  z-index: 9999;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: var(--spacing-lg, 20px);\n  overflow: hidden;\n\n  [data-bs-theme='light'] & {\n    background: rgba(0, 0, 0, 0.5);\n  }\n}\n\n.delete-app-modal {\n  background: var(--modal-bg, rgba(30, 30, 50, 0.95));\n  border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.2));\n  border-radius: var(--border-radius-xl, 12px);\n  width: 100%;\n  max-width: 500px;\n  display: flex;\n  flex-direction: column;\n  backdrop-filter: blur(20px);\n  box-shadow: var(--shadow-xl, 0 25px 50px rgba(0, 0, 0, 0.5));\n  animation: modalSlideUp 0.3s ease;\n\n  [data-bs-theme='light'] & {\n    background: rgba(255, 255, 255, 0.95);\n    border: 1px solid rgba(0, 0, 0, 0.15);\n    box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);\n  }\n}\n\n.delete-app-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-bottom: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  h5 {\n    margin: 0;\n    color: var(--text-primary, #fff);\n    font-size: var(--font-size-lg, 1.1rem);\n    font-weight: 600;\n    display: flex;\n    align-items: center;\n  }\n\n  [data-bs-theme='light'] & {\n    border-bottom: 1px solid rgba(0, 0, 0, 0.1);\n\n    h5 {\n      color: #000000;\n    }\n  }\n}\n\n.delete-app-body {\n  padding: var(--spacing-lg, 24px);\n  font-size: var(--font-size-md, 0.95rem);\n  line-height: 1.5;\n  color: var(--text-primary, #fff);\n\n  [data-bs-theme='light'] & {\n    color: #000000;\n  }\n}\n\n.delete-app-footer {\n  display: flex;\n  justify-content: flex-end;\n  gap: 10px;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-top: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  [data-bs-theme='light'] & {\n    border-top: 1px solid rgba(0, 0, 0, 0.1);\n  }\n\n  button {\n    padding: 8px 16px;\n    font-size: 0.9rem;\n  }\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/styles/global.less",
    "content": "@import './var.css';\n\n// Scrollbar stability\n// html {\n//   scrollbar-gutter: stable;\n\n//   @supports not (scrollbar-gutter: stable) {\n//     overflow-y: scroll;\n//   }\n// }\n\nbody {\n  min-height: 100vh;\n  font-family: var(--font-family-base);\n  background-color: #5496dd;\n  background-position: center;\n  background-size: cover;\n  background-repeat: no-repeat;\n  background-attachment: fixed;\n  transition: background 0.3s ease;\n  position: relative;\n  z-index: 0;\n\n  &.bg-light {\n    background-color: #5496dd !important;\n  }\n\n  &::after {\n    content: '';\n    position: fixed;\n    inset: 0;\n    background: rgba(0, 0, 0, 0.15);\n    pointer-events: none;\n    z-index: -1;\n\n    [data-bs-theme='dark'] & {\n      background: rgba(0, 0, 0, 0.4);\n    }\n  }\n}\n\n// Theme variables\n[data-bs-theme='light'] {\n  --bs-body-bg: rgba(255, 255, 255, 0.41);\n  --bs-dropdown-bg: rgba(255, 255, 255, 0.7);\n}\n\n[data-bs-theme='dark'] {\n  --bs-body-bg: rgba(0, 0, 0, 0.65);\n  --bs-dropdown-bg: rgba(0, 0, 0, 0.7);\n}\n\n// Drag and drop indicator\n.dragover {\n  outline: 4px dashed #ffc400;\n  outline-offset: -20px;\n}\n\n// Layout\n.container {\n  padding: 1rem;\n}\n\n// Page typography\n.page-title {\n  font-size: 1.9rem;\n  font-weight: 100;\n  margin-bottom: 0.5rem;\n  letter-spacing: -0.02em;\n  color: var(--text-title-color, #333);\n  transition: color 0.5s ease;\n\n  [data-bs-theme='dark'] &,\n  [data-bs-theme='light'] &,\n  body.bg-dark &,\n  body.bg-light & {\n    color: var(--text-title-color) !important;\n  }\n}\n\n.page-subtitle {\n  font-size: 1rem;\n  font-weight: 100;\n  margin-bottom: 0;\n  color: var(--text-primary-color, #fff);\n  transition: color 0.5s ease;\n}\n\n// Color transition utility\n:root.text-color-transitioning * {\n  transition: color 0.5s ease !important;\n}\n\n// Card component\n.card {\n  border: none;\n  border-radius: 12px;\n\n  &-header {\n    border-radius: 12px 12px 0 0 !important;\n    padding: 1rem 1.25rem;\n    background: rgba(var(--bs-body-bg-rgb), 0.6);\n  }\n\n  &-title {\n    font-size: 1.1rem;\n    font-weight: 600;\n  }\n\n  &-body {\n    padding: 1.25rem;\n    background: rgba(var(--bs-body-bg-rgb), 0.4);\n\n    p {\n      font-size: 0.9rem;\n      line-height: 1.6;\n\n      &.pre-line {\n        white-space: pre-line;\n      }\n    }\n  }\n}"
  },
  {
    "path": "src_assets/common/assets/web/styles/modal-glass.less",
    "content": "/* 统一模态框样式 */\n:root {\n  /* 背景与边框 */\n  --modal-backdrop-bg: rgba(0, 0, 0, 0.7);\n  --modal-content-bg: rgba(30, 30, 50, 0.95);\n  --modal-border-color: rgba(255, 255, 255, 0.2);\n  --modal-text-color: #fff;\n  --modal-text-secondary: rgba(255, 255, 255, 0.85);\n  --modal-text-muted: rgba(255, 255, 255, 0.6);\n  --modal-shadow: rgba(0, 0, 0, 0.5);\n  --modal-footer-bg: rgba(0, 0, 0, 0.2);\n\n  /* 按钮渐变 */\n  --btn-primary-gradient: linear-gradient(135deg, #667eea, #764ba2);\n  --btn-primary-gradient-hover: linear-gradient(135deg, #764ba2, #667eea);\n  --btn-secondary-gradient: rgba(255, 255, 255, 0.2);\n  --btn-secondary-gradient-hover: rgba(255, 255, 255, 0.3);\n  --btn-danger-gradient: linear-gradient(135deg, #dc3545, #c82333);\n  --btn-danger-gradient-hover: linear-gradient(135deg, #c82333, #dc3545);\n  --btn-success-gradient: linear-gradient(135deg, #28a745, #20c997);\n  --btn-success-gradient-hover: linear-gradient(135deg, #20c997, #28a745);\n  --btn-outline-primary-border: rgba(102, 126, 234, 0.5);\n  --btn-outline-primary-hover: rgba(102, 126, 234, 0.2);\n\n  /* 玻璃效果 */\n  --glass-light: rgba(255, 255, 255, 0.06);\n  --glass-medium: rgba(255, 255, 255, 0.1);\n  --glass-border: rgba(255, 255, 255, 0.2);\n  --glass-dark: rgba(0, 0, 0, 0.3);\n}\n\n[data-bs-theme='light'] {\n  --modal-content-bg: #f0f4ff;\n  --modal-border-color: rgba(99, 102, 241, 0.25);\n  --modal-shadow: rgba(99, 102, 241, 0.15);\n  --modal-footer-bg: #e0e7ff;\n  --modal-text-color: #1e293b;\n  --modal-text-secondary: #475569;\n  --modal-text-muted: #64748b;\n\n  --glass-light: #f5f8ff;\n  --glass-medium: #e8efff;\n  --glass-border: rgba(99, 102, 241, 0.2);\n  --glass-dark: rgba(99, 102, 241, 0.08);\n\n  --btn-secondary-gradient: #fff;\n  --btn-secondary-gradient-hover: #f5f8ff;\n  --btn-outline-primary-border: rgba(99, 102, 241, 0.4);\n  --btn-outline-primary-hover: rgba(99, 102, 241, 0.12);\n}\n\n[data-bs-theme='dark'] {\n  --modal-content-bg: rgba(20, 20, 35, 0.98);\n  --modal-border-color: rgba(255, 255, 255, 0.15);\n  --modal-shadow: rgba(0, 0, 0, 0.6);\n  --modal-footer-bg: rgba(0, 0, 0, 0.3);\n  --modal-text-color: #fff;\n  --modal-text-secondary: rgba(255, 255, 255, 0.9);\n  --modal-text-muted: rgba(255, 255, 255, 0.65);\n\n  --glass-light: rgba(255, 255, 255, 0.05);\n  --glass-medium: rgba(255, 255, 255, 0.08);\n  --glass-border: rgba(255, 255, 255, 0.15);\n}\n\n/* 背景遮罩 */\n.modal-backdrop {\n  background: var(--modal-backdrop-bg);\n  backdrop-filter: blur(8px);\n}\n\n/* 模态框内容 */\n.modal-content {\n  background: var(--modal-content-bg);\n  border: 1px solid var(--modal-border-color);\n  border-radius: 20px;\n  backdrop-filter: blur(20px);\n  box-shadow: 0 20px 40px var(--modal-shadow);\n  color: var(--modal-text-color);\n\n  small,\n  .text-muted {\n    color: var(--modal-text-muted);\n  }\n}\n\n.modal-title {\n  color: var(--modal-text-color);\n  font-weight: 600;\n}\n\n.modal-body {\n  color: var(--modal-text-color);\n\n  .text-muted,\n  small {\n    color: var(--modal-text-muted);\n  }\n}\n\n.modal-footer {\n  border-top: 1px solid var(--modal-border-color);\n  background: var(--modal-footer-bg);\n  border-radius: 0 0 20px 20px;\n}\n\n/* 关闭按钮 */\n.btn-close {\n  opacity: 0.7;\n  transition: opacity 0.2s;\n\n  &:hover {\n    opacity: 1;\n  }\n\n  [data-bs-theme='dark'] & {\n    filter: invert(1);\n  }\n}\n\n/* 模态框按钮 */\n.modal {\n  padding: 0 20px;\n\n  .btn {\n    color: #6366f1;\n    border: 1.5px solid var(--glass-border);\n    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n    box-shadow: none;\n    background: transparent;\n    font-weight: 500;\n\n    &:hover {\n      background-color: rgba(102, 126, 234, 0.15);\n    }\n\n    &:active {\n      transform: scale(0.98);\n    }\n\n    &-primary {\n      border-color: rgba(102, 126, 234, 0.6);\n      color: #667eea;\n\n      &:hover {\n        background: rgba(102, 126, 234, 0.15);\n        border-color: rgba(102, 126, 234, 0.8);\n      }\n\n      [data-bs-theme='dark'] & {\n        color: #8b9aff;\n        border-color: rgba(139, 154, 255, 0.6);\n\n        &:hover {\n          background: rgba(139, 154, 255, 0.15);\n          border-color: rgba(139, 154, 255, 0.8);\n          color: #a8b4ff;\n        }\n      }\n    }\n\n    &.add-command-btn {\n      border-style: dashed;\n    }\n\n    &-outline-primary {\n      border-color: var(--btn-outline-primary-border);\n      color: #667eea;\n\n      &:hover {\n        background: var(--btn-outline-primary-hover);\n        border-color: rgba(102, 126, 234, 0.7);\n      }\n\n      [data-bs-theme='dark'] & {\n        color: #8b9aff;\n\n        &:hover {\n          color: var(--modal-text-color);\n        }\n      }\n    }\n\n    &-secondary {\n      border: 1.5px solid var(--glass-border);\n      color: var(--modal-text-secondary);\n\n      &:hover {\n        background: rgba(108, 117, 125, 0.15);\n        border-color: rgba(108, 117, 125, 0.6);\n        color: var(--modal-text-color);\n      }\n\n      &:focus {\n        box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.25);\n      }\n\n      [data-bs-theme='dark'] & {\n        border-color: rgba(255, 255, 255, 0.4);\n        color: rgba(255, 255, 255, 0.85);\n\n        &:hover {\n          background: rgba(255, 255, 255, 0.12);\n          border-color: rgba(255, 255, 255, 0.6);\n          color: #fff;\n        }\n      }\n    }\n\n    &-danger {\n      border-color: rgba(220, 53, 69, 0.6);\n      color: #dc3545;\n\n      &:hover {\n        background: rgba(220, 53, 69, 0.15);\n        border-color: rgba(220, 53, 69, 0.8);\n      }\n\n      [data-bs-theme='dark'] & {\n        color: #ff6b7a;\n        border-color: rgba(255, 107, 122, 0.6);\n\n        &:hover {\n          background: rgba(255, 107, 122, 0.15);\n          border-color: rgba(255, 107, 122, 0.8);\n          color: #ff8a96;\n        }\n      }\n    }\n\n    &-success {\n      border-color: rgba(40, 167, 69, 0.6);\n      color: #28a745;\n\n      &:hover {\n        background: rgba(40, 167, 69, 0.15);\n        border-color: rgba(40, 167, 69, 0.8);\n      }\n\n      [data-bs-theme='dark'] & {\n        color: #5dd879;\n        border-color: rgba(93, 216, 121, 0.6);\n\n        &:hover {\n          background: rgba(93, 216, 121, 0.15);\n          border-color: rgba(93, 216, 121, 0.8);\n          color: #7ae094;\n        }\n      }\n    }\n  }\n\n  /* 表单元素 */\n  .form-control,\n  .form-select {\n    background: var(--glass-light);\n    border: 1px solid var(--glass-border);\n    color: var(--modal-text-color);\n    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n\n    &:hover {\n      background: var(--glass-medium);\n    }\n\n    &:focus {\n      background: var(--glass-medium);\n      border-color: rgba(102, 126, 234, 0.6);\n      box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.15);\n      color: var(--modal-text-color);\n    }\n\n    &::placeholder {\n      color: var(--modal-text-muted);\n    }\n  }\n\n  .form-label {\n    color: var(--modal-text-color);\n    font-weight: 600;\n  }\n\n  /* 警告框 */\n  .alert {\n    background: var(--glass-light);\n    border: 1px solid var(--glass-border);\n    border-radius: 12px;\n    color: var(--modal-text-color);\n\n    &-info {\n      background: rgba(23, 162, 184, 0.15);\n      border-color: rgba(23, 162, 184, 0.4);\n\n      [data-bs-theme='light'] & {\n        background: #e0f7fa;\n        border-color: #4dd0e1;\n        color: #006064;\n      }\n    }\n\n    &-warning {\n      background: rgba(255, 193, 7, 0.15);\n      border-color: rgba(255, 193, 7, 0.4);\n\n      [data-bs-theme='light'] & {\n        background: #fff8e1;\n        border-color: #ffca28;\n        color: #795548;\n      }\n    }\n\n    &-danger {\n      background: rgba(220, 53, 69, 0.15);\n      border-color: rgba(220, 53, 69, 0.4);\n\n      [data-bs-theme='light'] & {\n        background: #ffebee;\n        border-color: #ef5350;\n        color: #b71c1c;\n      }\n    }\n  }\n\n  /* 选项卡 */\n  .nav-tabs {\n    border-bottom: 1px solid var(--glass-border);\n\n    .nav-link {\n      background: var(--glass-light);\n      border: 1px solid var(--modal-border-color);\n      color: var(--modal-text-muted);\n\n      &:hover {\n        color: var(--modal-text-secondary);\n      }\n\n      &.active {\n        background: var(--glass-medium);\n        color: var(--modal-text-color);\n      }\n    }\n  }\n\n  /* 手风琴 */\n  .accordion-item {\n    background: var(--glass-light);\n    border: 1px solid var(--modal-border-color);\n    border-radius: 12px;\n    overflow: hidden;\n    margin-bottom: 0.75rem;\n\n    [data-bs-theme='light'] & {\n      background: #f5f8ff;\n      border-color: rgba(99, 102, 241, 0.2);\n      box-shadow: 0 3px 8px rgba(99, 102, 241, 0.08);\n    }\n  }\n\n  .accordion-button {\n    background: var(--glass-light);\n    color: var(--modal-text-color);\n    border-radius: 12px !important;\n    font-weight: 500;\n\n    &:hover {\n      background: var(--glass-medium);\n    }\n\n    &:not(.collapsed) {\n      background: var(--glass-medium);\n    }\n\n    [data-bs-theme='light'] & {\n      background: #e8efff;\n      color: #1e293b;\n\n      &:hover {\n        background: #dce4ff;\n      }\n\n      &:not(.collapsed) {\n        background: #dce4ff;\n        color: #6366f1;\n        font-weight: 600;\n      }\n    }\n  }\n\n  /* 代码块 */\n  pre,\n  code {\n    background: var(--glass-light);\n    color: var(--modal-text-color);\n    border: 1px solid var(--modal-border-color);\n    border-radius: 8px;\n\n    [data-bs-theme='light'] & {\n      background: #e8efff;\n      color: #6366f1;\n      border-color: rgba(99, 102, 241, 0.2);\n    }\n  }\n\n  pre[data-bs-theme='light'] {\n    padding: 1rem;\n    color: #475569;\n  }\n\n  /* 表格 */\n  .table {\n    color: var(--modal-text-color);\n\n    [data-bs-theme='light'] & {\n      color: #1e293b;\n    }\n\n    th {\n      background: var(--glass-medium);\n\n      [data-bs-theme='light'] & {\n        background: #e0e7ff;\n        color: #6366f1;\n        font-weight: 600;\n        border-bottom: 1px solid rgba(99, 102, 241, 0.2);\n      }\n    }\n\n    td[data-bs-theme='light'] {\n      border-color: rgba(99, 102, 241, 0.1);\n    }\n\n    tbody tr[data-bs-theme='light']:hover {\n      background: rgba(99, 102, 241, 0.05);\n    }\n  }\n}\n\n/* 响应式 */\n@media (max-width: 768px) {\n  .modal-content {\n    margin: 10px;\n  }\n\n  .modal-header,\n  .modal-footer,\n  .modal-body {\n    padding: 1rem;\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/styles/var.css",
    "content": "/* 全局变量 */\n:root {\n  --primary-color: #0d6efd;\n  --secondary-color: #6c757d;\n  --success-color: #28a745;\n  --danger-color: #dc3545;\n  --warning-color: #ffc107;\n  --info-color: #17a2b8;\n  --light-color: #f8f9fa;\n  --dark-color: #343a40;\n  --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n  --gradient-success: linear-gradient(135deg, #28a745, #20c997);\n  --gradient-success-hover: linear-gradient(135deg, #20c997, #28a745);\n  --box-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);\n  --box-shadow-md: 0 8px 16px rgba(0, 0, 0, 0.1);\n  --box-shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.2);\n  --transition-default: all 0.3s ease;\n  --border-radius-sm: 0.25rem;\n  --border-radius-md: 0.5rem;\n  --border-radius-lg: 1rem;\n  --border-radius-xl: 1.5rem;\n  --border-radius-pill: 50rem;\n  --border-radius-circle: 50%;\n  --font-family-base: 'Microsoft YaHei', 'Segoe UI', sans-serif;\n  --font-size-xs: 0.75rem;\n  --font-size-sm: 0.875rem;\n  --font-size-md: 1rem;\n  --font-size-lg: 1.25rem;\n  --font-size-xl: 1.5rem;\n  --font-size-xxl: 2rem;\n  --spacing-xs: 0.25rem;\n  --spacing-sm: 0.5rem;\n  --spacing-md: 1rem;\n  --spacing-lg: 1.5rem;\n  --spacing-xl: 2rem;\n\n  --cute-btn-border-color: rgba(255, 255, 255, 0.3);\n  --cute-btn-border-hover-color: rgba(255, 255, 255, 0.5);\n\n  /* 动态文字颜色变量 - 根据背景图片亮度自动设置 */\n  --text-primary-color: #333333;\n  --text-secondary-color: #666666;\n  --text-muted-color: #999999;\n  --text-title-color: #1a1a1a;\n}\n\n/* data-bs-theme=\"dark\" 适配 */\n[data-bs-theme=\"dark\"] {\n  --primary-color: #375a7f;\n  --secondary-color: #444950;\n  --success-color: #00bc8c;\n  --danger-color: #e74c3c;\n  --warning-color: #f39c12;\n  --info-color: #3498db;\n  --light-color: #23272b;\n  --dark-color: #f8f9fa;\n  --gradient-success: linear-gradient(135deg, #00bc8c, #16a085);\n  --gradient-success-hover: linear-gradient(135deg, #16a085, #00bc8c);\n  --box-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.4);\n  --box-shadow-md: 0 8px 16px rgba(0, 0, 0, 0.3);\n  --box-shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);\n  --font-size-xs: 0.75rem;\n  --font-size-sm: 0.875rem;\n  --font-size-md: 1rem;\n  --font-size-lg: 1.25rem;\n  --font-size-xl: 1.5rem;\n  --font-size-xxl: 2rem;\n  --spacing-xs: 0.25rem;\n  --spacing-sm: 0.5rem;\n  --spacing-md: 1rem;\n  --spacing-lg: 1.5rem;\n  --spacing-xl: 2rem;\n\n  --cute-btn-border-color: rgba(0, 0, 0, 0.2);\n  --cute-btn-border-hover-color: rgba(0, 0, 0, 0.1);\n\n  /* 深色模式文字颜色 - 使用浅色 */\n  --text-primary-color: #ffffff;\n  --text-secondary-color: rgba(255, 255, 255, 0.85);\n  --text-muted-color: rgba(255, 255, 255, 0.6);\n  --text-title-color: #ffffff;\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/styles/welcome.less",
    "content": "/* 导入手写字体 */\n@import url('../fonts/fonts.css');\n\n/* 手绘风格颜色变量 */\n:root {\n  --sketch-black: #2c2c2c;\n  --sketch-blue: #4169e1;\n  --sketch-green: #3cb371;\n  --sketch-red: #dc143c;\n  --sketch-yellow: #ffd700;\n  --paper-bg: #fffef9;\n  --paper-bg-alt: #faf9f6;\n  --pencil-gray: #8b8b8b;\n  --texture-opacity: 0.02;\n  --texture-size: 2px;\n}\n\n/* 纸张背景效果 */\nbody {\n  background: linear-gradient(to bottom, var(--paper-bg), var(--paper-bg-alt));\n  font-family: 'Kalam', 'KaiTi', 'STXingkai', 'Kaiti SC', cursive;\n  position: relative;\n  min-height: 100vh;\n\n  /* 纸张纹理 */\n  &::before {\n    content: '';\n    position: fixed;\n    inset: 0;\n    background-image: \n      repeating-linear-gradient(\n        0deg,\n        transparent,\n        transparent var(--texture-size),\n        rgba(0, 0, 0, var(--texture-opacity)) var(--texture-size),\n        rgba(0, 0, 0, var(--texture-opacity)) calc(var(--texture-size) * 2)\n      ),\n      repeating-linear-gradient(\n        90deg,\n        transparent,\n        transparent var(--texture-size),\n        rgba(0, 0, 0, var(--texture-opacity)) var(--texture-size),\n        rgba(0, 0, 0, var(--texture-opacity)) calc(var(--texture-size) * 2)\n      );\n    pointer-events: none;\n    z-index: 0;\n  }\n}\n\n/* 打印样式优化 */\n@media print {\n  body::before {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/sunshine_version.js",
    "content": "class SunshineVersion {\n  constructor(release = null, version = null) {\n    if (release) {\n      this.release = release\n      this.version = release.tag_name\n      this.versionName = release.name\n      this.versionTag = release.tag_tag\n    } else if (version) {\n      this.release = null\n      this.version = version\n      this.versionName = null\n      this.versionTag = null\n    } else {\n      throw new Error('Either release or version must be provided')\n    }\n    this.versionParts = this.parseVersion(this.version)\n    this.versionMajor = this.versionParts ? this.versionParts[0] : null\n    this.versionMinor = this.versionParts ? this.versionParts[1] : null\n    this.versionPatch = this.versionParts ? this.versionParts[2] : null\n  }\n\n  parseVersion(version) {\n    if (!version) {\n      return null\n    }\n    let v = version\n    if (v.indexOf('v') === 0) {\n      v = v.substring(1)\n    }\n    return v.split('.').map(Number)\n  }\n\n  isGreater(otherVersion) {\n    let otherVersionParts\n    if (otherVersion instanceof SunshineVersion) {\n      otherVersionParts = otherVersion.versionParts\n    } else if (typeof otherVersion === 'string') {\n      otherVersionParts = this.parseVersion(otherVersion)\n    } else {\n      throw new Error('Invalid argument: otherVersion must be a SunshineVersion object or a version string')\n    }\n\n    if (!this.versionParts || !otherVersionParts) {\n      return false\n    }\n    for (let i = 0; i < Math.min(3, this.versionParts.length, otherVersionParts.length); i++) {\n      const v1 = this.versionParts[i]\n      const v2 = otherVersionParts[i]\n      if (v1 > v2) {\n        return true\n      }\n      if (v1 < v2) {\n        return false\n      }\n    }\n    return false\n  }\n}\n\nexport default SunshineVersion\n"
  },
  {
    "path": "src_assets/common/assets/web/template_header.html",
    "content": "<!-- TEMPLATE_HEADER - Used by Every UI Page -->\n<meta charset=\"UTF-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<title>Sunshine</title>\n<link rel=\"icon\" type=\"image/x-icon\" href=\"/images/sunshine.ico\">\n<link href=\"@fortawesome/fontawesome-free/css/all.min.css\" rel=\"stylesheet\">\n<link href=\"bootstrap/dist/css/bootstrap.min.css\" rel=\"stylesheet\" />\n<link href=\"/assets/css/sunshine.css\" rel=\"stylesheet\" />\n"
  },
  {
    "path": "src_assets/common/assets/web/template_header_dev.html",
    "content": "<!-- TEMPLATE_HEADER - 开发环境版本 -->\n<meta charset=\"UTF-8\" />\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<title>Sunshine - 开发环境</title>\n<link rel=\"icon\" type=\"image/x-icon\" href=\"/images/sunshine.ico\">\n<!-- 使用CDN加载CSS，避免路径问题 -->\n<link href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css\" rel=\"stylesheet\">\n<link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css\" rel=\"stylesheet\" />\n<link href=\"/assets/css/sunshine.css\" rel=\"stylesheet\" />\n\n<!-- 开发环境标识 -->\n<style>\nbody::before {\n    content: \"🔧 开发环境\";\n    position: fixed;\n    top: 0;\n    right: 0;\n    background: #ffc107;\n    color: #000;\n    padding: 4px 8px;\n    font-size: 12px;\n    z-index: 10000;\n    border-radius: 0 0 0 4px;\n}\n</style> "
  },
  {
    "path": "src_assets/common/assets/web/troubleshooting.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bs-theme=\"auto\">\n  <head>\n    <%- header %>\n  </head>\n\n  <body id=\"app\" v-cloak>\n    <!-- Vue 应用挂载点 -->\n  </body>\n\n  <script type=\"module\">\n    import { createApp } from 'vue'\n    import { initApp } from './init'\n    import Troubleshooting from './views/Troubleshooting.vue'\n\n    const app = createApp(Troubleshooting)\n    initApp(app)\n  </script>\n</html>\n"
  },
  {
    "path": "src_assets/common/assets/web/utils/README.md",
    "content": "# 工具模块文档\n\n## 文件选择模块 (fileSelection.js)\n\n提供跨平台的文件和目录选择功能，支持 Electron 和浏览器环境。\n\n### 快速开始\n\n```javascript\nimport { createFileSelector } from './utils/fileSelection.js';\n\n// 创建文件选择器实例\nconst fileSelector = createFileSelector({\n  platform: 'windows', // 'windows', 'linux', 'macos'\n  onSuccess: (message) => console.log(message),\n  onError: (error) => console.error(error),\n  onInfo: (info) => console.info(info)\n});\n\n// 选择文件\nfileSelector.selectFile('cmd', fileInputRef, (fieldName, filePath) => {\n  console.log(`选择的文件: ${filePath}`);\n});\n\n// 选择目录\nfileSelector.selectDirectory('working-dir', dirInputRef, (fieldName, dirPath) => {\n  console.log(`选择的目录: ${dirPath}`);\n});\n```\n\n### 简化使用\n\n```javascript\nimport { selectFile, selectDirectory } from './utils/fileSelection.js';\n\n// 直接选择文件\nselectFile({\n  fieldName: 'cmd',\n  fileInput: fileInputRef,\n  platform: 'windows',\n  callback: (fieldName, filePath) => {\n    // 处理选择的文件\n  }\n});\n\n// 直接选择目录\nselectDirectory({\n  fieldName: 'working-dir',\n  dirInput: dirInputRef,\n  platform: 'windows',\n  callback: (fieldName, dirPath) => {\n    // 处理选择的目录\n  }\n});\n```\n\n### 环境检查\n\n```javascript\nimport { checkEnvironmentSupport } from './utils/fileSelection.js';\n\nconst support = checkEnvironmentSupport();\nconsole.log('文件选择支持:', support.fileSelection);\nconsole.log('目录选择支持:', support.directorySelection);\nconsole.log('Electron环境:', support.isElectron);\nconsole.log('开发环境:', support.isDevelopment);\n```\n\n### 在 Vue 组件中使用\n\n```javascript\nimport { createFileSelector } from '../utils/fileSelection.js';\n\nexport default {\n  data() {\n    return {\n      fileSelector: null\n    };\n  },\n  \n  mounted() {\n    this.fileSelector = createFileSelector({\n      platform: this.platform,\n      onSuccess: this.showSuccess,\n      onError: this.showError,\n      onInfo: this.showInfo\n    });\n  },\n  \n  methods: {\n    selectFile(fieldName) {\n      this.fileSelector.selectFile(\n        fieldName,\n        this.$refs.fileInput,\n        this.onFileSelected\n      );\n    },\n    \n    onFileSelected(fieldName, filePath) {\n      this.formData[fieldName] = filePath;\n      this.validateField(fieldName);\n    }\n  }\n};\n```\n\n### 支持的平台\n\n- **Electron**: 完整的原生文件/目录选择对话框\n- **浏览器**: HTML5 文件API，有安全限制\n- **开发环境**: 模拟完整路径，便于测试\n\n### 注意事项\n\n- 浏览器环境下无法获取完整系统路径\n- 开发环境会自动模拟完整路径\n- Electron环境提供最佳的用户体验\n\n## 表单验证模块 (validation.js)\n\n提供表单字段验证功能。\n\n### 使用方法\n\n```javascript\nimport { validateField, validateAppForm } from './utils/validation.js';\n\n// 验证单个字段\nconst result = validateField('appName', 'MyApp');\nconsole.log(result.isValid); // true/false\nconsole.log(result.message); // 错误消息\n\n// 验证整个表单\nconst formResult = validateAppForm(formData);\nconsole.log(formResult.isValid); // true/false\nconsole.log(formResult.errors); // 错误列表\n```\n\n## Steam API 模块 (steamApi.js)\n\n提供Steam Store API集成功能。\n\n### 使用方法\n\n```javascript\nimport { searchSteamApps, findAppCover } from './utils/steamApi.js';\n\n// 搜索Steam应用\nconst apps = await searchSteamApps('Half-Life');\n\n// 查找应用封面\nconst cover = await findAppCover('Half-Life 2');\n``` "
  },
  {
    "path": "src_assets/common/assets/web/utils/constants.js",
    "content": "// 应用管理相关常量\nexport const APP_CONSTANTS = {\n  // 消息类型\n  MESSAGE_TYPES: {\n    SUCCESS: 'success',\n    ERROR: 'error',\n    WARNING: 'warning',\n    INFO: 'info'\n  },\n  \n  // 消息图标映射\n  MESSAGE_ICONS: {\n    success: 'fa-check-circle',\n    error: 'fa-exclamation-circle',\n    warning: 'fa-exclamation-triangle',\n    info: 'fa-info-circle'\n  },\n  \n  // 默认应用配置\n  DEFAULT_APP: {\n    name: \"\",\n    output: \"\",\n    cmd: \"\",\n    index: -1,\n    \"exclude-global-prep-cmd\": false,\n    elevated: false,\n    \"auto-detach\": true,\n    \"wait-all\": true,\n    \"exit-timeout\": 5,\n    \"prep-cmd\": [],\n    \"menu-cmd\": [],\n    detached: [],\n    \"image-path\": \"\",\n    \"working-dir\": \"\"\n  },\n  \n  // 支持的平台\n  PLATFORMS: {\n    WINDOWS: 'windows',\n    LINUX: 'linux',\n    MACOS: 'macos'\n  },\n  \n  // 视图模式\n  VIEW_MODES: {\n    GRID: 'grid',\n    LIST: 'list'\n  },\n  \n  // 消息自动隐藏时间\n  MESSAGE_AUTO_HIDE_TIME: 3000,\n  \n  // 拖拽动画时间\n  DRAG_ANIMATION_TIME: 300,\n  \n  // 复制成功动画时间\n  COPY_SUCCESS_ANIMATION_TIME: 400,\n  \n  // 搜索防抖时间\n  SEARCH_DEBOUNCE_TIME: 300,\n  \n  // 文本截断长度\n  TEXT_TRUNCATE_LENGTH: 50\n};\n\n// 环境变量配置\nexport const ENV_VARS_CONFIG = {\n  'SUNSHINE_APP_ID': 'apps.env_app_id',\n  'SUNSHINE_APP_NAME': 'apps.env_app_name',\n  'SUNSHINE_CLIENT_NAME': 'apps.env_client_name',\n  'SUNSHINE_CLIENT_WIDTH': 'apps.env_client_width',\n  'SUNSHINE_CLIENT_HEIGHT': 'apps.env_client_height',\n  'SUNSHINE_CLIENT_FPS': 'apps.env_client_fps',\n  'SUNSHINE_CLIENT_HDR': 'apps.env_client_hdr',\n  'SUNSHINE_CLIENT_GCMAP': 'apps.env_client_gcmap',\n  'SUNSHINE_CLIENT_HOST_AUDIO': 'apps.env_client_host_audio',\n  'SUNSHINE_CLIENT_ENABLE_SOPS': 'apps.env_client_enable_sops',\n  'SUNSHINE_CLIENT_AUDIO_CONFIGURATION': 'apps.env_client_audio_config'\n};\n\n// API端点\nexport const API_ENDPOINTS = {\n  APPS: '/api/apps',\n  CONFIG: '/api/config',\n  APP_DELETE: (index) => `/api/apps/${index}`\n}; "
  },
  {
    "path": "src_assets/common/assets/web/utils/coverSearch.js",
    "content": "/**\n * 封面搜索工具模块\n * 提供统一的IGDB和Steam封面搜索功能\n */\n\nimport { searchSteamCovers } from './steamApi.js'\n\n// 共享缓存（模块级别，避免重复创建）\nconst bucketCache = new Map()\nconst gameCache = new Map()\n\n// IGDB 相关常量\nconst IGDB_BASE_URL = 'https://lizardbyte.github.io/GameDB'\nconst IGDB_IMAGE_URL = 'https://images.igdb.com/igdb/image/upload/t_cover_big_2x'\n\n// 缓存 Tauri 环境检测结果\nlet _isTauriEnv = null\n\n/**\n * 检测是否在 Tauri 环境中\n * @returns {boolean} 是否在 Tauri 环境\n */\nfunction isTauriEnv() {\n  if (_isTauriEnv === null) {\n    _isTauriEnv = typeof window !== 'undefined' && !!(window.isTauri || window.__TAURI__)\n  }\n  return _isTauriEnv\n}\n\n/**\n * 构建代理 URL（用于绕过 CORS 限制）\n * @param {string} url 原始 URL\n * @returns {string} 代理 URL 或原始 URL\n */\nfunction buildProxyUrl(url) {\n  return isTauriEnv() ? `/_proxy/?url=${encodeURIComponent(url)}` : url\n}\n\n/**\n * 获取搜索bucket（用于IGDB搜索）\n * 注意：IGDB bucket 只支持英文字母和数字，中文等非ASCII字符会返回 '@'\n * @param {string} name 应用名称\n * @returns {string} bucket标识符\n */\nexport function getSearchBucket(name) {\n  const bucket = name\n    .substring(0, 2)\n    .toLowerCase()\n    .replace(/[^a-z\\d]/g, '')\n  return bucket || '@'\n}\n\n/**\n * 检查搜索词是否适合IGDB搜索\n * IGDB的bucket系统只支持英文，中文等非ASCII字符无法正确匹配\n * @param {string} name 搜索名称\n * @returns {boolean} 是否适合IGDB搜索\n */\nfunction isValidForIGDB(name) {\n  if (!name) return false\n  // 检查是否包含至少一个英文字母或数字\n  return /[a-zA-Z\\d]/.test(name)\n}\n\n// 预编译正则表达式\nconst SEPARATOR_REGEX = /[:\\-_''\"\"]/g\nconst WHITESPACE_REGEX = /\\s+/g\n\n/**\n * 规范化搜索字符串\n * @param {string} str 原始字符串\n * @returns {string} 规范化后的字符串\n */\nfunction normalizeSearchString(str) {\n  return str.toLowerCase().replace(SEPARATOR_REGEX, ' ').replace(WHITESPACE_REGEX, ' ').trim()\n}\n\n/**\n * 生成字符串的bigrams集合\n * @param {string} str 输入字符串\n * @returns {Set<string>} bigrams集合\n */\nfunction getBigrams(str) {\n  const bigrams = new Set()\n  const len = str.length - 1\n  for (let i = 0; i < len; i++) {\n    bigrams.add(str.substring(i, i + 2))\n  }\n  return bigrams\n}\n\n/**\n * 计算字符串相似度（Dice系数）\n * @param {string} str1 字符串1\n * @param {string} str2 字符串2\n * @returns {number} 相似度 0-1\n */\nfunction calculateSimilarity(str1, str2) {\n  const s1 = normalizeSearchString(str1)\n  const s2 = normalizeSearchString(str2)\n\n  if (s1 === s2) return 1\n  if (s1.length < 2 || s2.length < 2) return 0\n\n  const bigrams1 = getBigrams(s1)\n  const bigrams2 = getBigrams(s2)\n\n  let intersection = 0\n  for (const bigram of bigrams1) {\n    if (bigrams2.has(bigram)) intersection++\n  }\n\n  return (2 * intersection) / (bigrams1.size + bigrams2.size)\n}\n\n/**\n * 检查是否匹配搜索词\n * @param {string} gameName 游戏名称\n * @param {string} searchTerm 搜索词\n * @returns {{match: boolean, score: number}} 匹配结果和分数\n */\nfunction matchesSearch(gameName, searchTerm) {\n  const normalizedGame = normalizeSearchString(gameName)\n  const normalizedSearch = normalizeSearchString(searchTerm)\n\n  // 完全匹配\n  if (normalizedGame === normalizedSearch) {\n    return { match: true, score: 1 }\n  }\n\n  // 前缀匹配（高优先级）\n  if (normalizedGame.startsWith(normalizedSearch)) {\n    return { match: true, score: 0.95 }\n  }\n\n  // 包含匹配\n  if (normalizedGame.includes(normalizedSearch)) {\n    return { match: true, score: 0.85 }\n  }\n\n  // 单词匹配（搜索词的所有单词都在游戏名中）\n  const searchWords = normalizedSearch.split(' ').filter((w) => w.length > 1)\n  if (searchWords.length > 0) {\n    const gameWords = normalizedGame.split(' ')\n    const allWordsMatch = searchWords.every((sw) => gameWords.some((gw) => gw.startsWith(sw) || gw.includes(sw)))\n    if (allWordsMatch) {\n      return { match: true, score: 0.8 }\n    }\n  }\n\n  // 相似度匹配\n  const similarity = calculateSimilarity(gameName, searchTerm)\n  if (similarity > 0.5) {\n    return { match: true, score: similarity * 0.7 }\n  }\n\n  return { match: false, score: 0 }\n}\n\n/**\n * 带缓存的fetch函数\n * @param {Map} cache 缓存Map\n * @param {string} key 缓存键\n * @param {Function} fetchFn 获取数据的函数\n * @returns {Promise<any>} 数据\n */\nasync function fetchWithCache(cache, key, fetchFn) {\n  const cached = cache.get(key)\n  if (cached !== undefined) return cached\n  const data = await fetchFn()\n  cache.set(key, data)\n  return data\n}\n\n/**\n * 从封面URL提取hash并构建完整URL\n * @param {string} thumbUrl 缩略图URL\n * @param {string} size 图片尺寸\n * @param {string} ext 文件扩展名\n * @returns {string} 完整的图片URL\n */\nfunction buildIGDBImageUrl(thumbUrl, size = 't_cover_big_2x', ext = 'png') {\n  const lastSlash = thumbUrl.lastIndexOf('/')\n  const lastDot = thumbUrl.lastIndexOf('.')\n  const hash = thumbUrl.substring(lastSlash + 1, lastDot)\n  return `https://images.igdb.com/igdb/image/upload/${size}/${hash}.${ext}`\n}\n\n/**\n * 搜索IGDB封面（单个结果，返回URL字符串）\n * @param {string} searchName 搜索名称\n * @param {string} bucket bucket标识符\n * @returns {Promise<string>} 封面URL，未找到返回空字符串\n */\nexport async function searchIGDBCover(searchName, bucket) {\n  // 检查搜索词是否适合IGDB搜索\n  if (!isValidForIGDB(searchName)) {\n    return ''\n  }\n\n  try {\n    const maps = await fetchWithCache(bucketCache, bucket, async () => {\n      const url = `${IGDB_BASE_URL}/buckets/${bucket}.json`\n      const response = await fetch(buildProxyUrl(url))\n      return response.ok ? response.json() : null\n    })\n\n    if (!maps) return ''\n\n    let bestMatch = null\n    let bestScore = 0\n\n    const ids = Object.keys(maps)\n    for (let i = 0; i < ids.length; i++) {\n      const id = ids[i]\n      const { match, score } = matchesSearch(maps[id].name, searchName)\n      if (match && score > bestScore) {\n        bestScore = score\n        bestMatch = id\n      }\n    }\n\n    if (!bestMatch) return ''\n\n    const game = await fetchWithCache(gameCache, bestMatch, async () => {\n      const url = `${IGDB_BASE_URL}/games/${bestMatch}.json`\n      const res = await fetch(buildProxyUrl(url))\n      return res.ok ? res.json() : null\n    })\n\n    if (!game?.cover?.url) return ''\n\n    return buildIGDBImageUrl(game.cover.url)\n  } catch (error) {\n    console.warn(`搜索IGDB封面失败: ${searchName}`, error)\n    return ''\n  }\n}\n\n/**\n * 搜索IGDB封面（多个结果，返回数组）\n * @param {string} name 应用名称\n * @param {AbortSignal} signal 可选的AbortSignal用于取消请求\n * @param {number} maxResults 最大结果数量\n * @returns {Promise<Array>} 封面结果数组\n */\nexport async function searchIGDBCovers(name, signal = null, maxResults = 20) {\n  if (!name) return []\n\n  // 检查搜索词是否适合IGDB搜索（IGDB只支持英文搜索）\n  if (!isValidForIGDB(name)) {\n    console.debug(`IGDB搜索跳过：搜索词 \"${name}\" 不包含英文字符`)\n    return []\n  }\n\n  const bucket = getSearchBucket(name)\n\n  try {\n    const maps = await fetchWithCache(bucketCache, bucket, async () => {\n      const url = `${IGDB_BASE_URL}/buckets/${bucket}.json`\n      const response = await fetch(buildProxyUrl(url), { signal })\n      if (!response.ok) {\n        // 404 表示该 bucket 不存在，这是正常情况，返回空对象\n        if (response.status === 404) {\n          return {}\n        }\n        throw new Error('Failed to search covers')\n      }\n      return response.json()\n    })\n\n    // 使用改进的匹配算法，收集所有匹配项并按分数排序\n    const matches = []\n    const ids = Object.keys(maps)\n    for (let i = 0; i < ids.length; i++) {\n      const id = ids[i]\n      const { match, score } = matchesSearch(maps[id].name, name)\n      if (match) {\n        matches.push({ id, score, name: maps[id].name })\n      }\n    }\n\n    // 按分数降序排序，取前maxResults个\n    matches.sort((a, b) => b.score - a.score)\n    const matchedIds = matches.slice(0, maxResults).map((m) => m.id)\n\n    // 并行获取游戏详情，使用缓存\n    const games = await Promise.all(\n      matchedIds.map(async (id) => {\n        return fetchWithCache(gameCache, id, async () => {\n          try {\n            const url = `${IGDB_BASE_URL}/games/${id}.json`\n            const res = await fetch(buildProxyUrl(url), { signal })\n            return res.json()\n          } catch {\n            return null\n          }\n        })\n      })\n    )\n\n    const results = []\n    for (let i = 0; i < games.length; i++) {\n      const game = games[i]\n      if (game?.cover?.url) {\n        const thumb = game.cover.url\n        results.push({\n          name: game.name,\n          key: `igdb_${game.id}`,\n          source: 'igdb',\n          url: buildIGDBImageUrl(thumb, 't_cover_big', 'jpg'),\n          saveUrl: buildIGDBImageUrl(thumb),\n        })\n      }\n    }\n    return results\n  } catch (error) {\n    if (error.name === 'AbortError') {\n      throw error\n    }\n    console.error('搜索IGDB封面失败:', error)\n    return []\n  }\n}\n\n/**\n * 搜索封面图片（单个结果，用于useApps.js）\n * 同时搜索IGDB和Steam，返回第一个找到的结果\n * @param {string} appName 应用名称\n * @returns {Promise<string>} 封面URL，未找到返回空字符串\n */\nexport async function searchCoverImage(appName) {\n  if (!appName) return ''\n\n  const bucket = getSearchBucket(appName)\n\n  try {\n    const [igdbResult, steamResult] = await Promise.allSettled([\n      searchIGDBCover(appName, bucket),\n      searchSteamCovers(appName, 1).then((results) => results[0]?.saveUrl || ''),\n    ])\n\n    return (\n      (igdbResult.status === 'fulfilled' && igdbResult.value) ||\n      (steamResult.status === 'fulfilled' && steamResult.value) ||\n      ''\n    )\n  } catch (error) {\n    console.warn(`搜索封面失败: ${appName}`, error)\n    return ''\n  }\n}\n\n/**\n * 批量搜索封面图片\n * @param {Array} appList 应用列表\n * @returns {Promise<Array>} 带封面URL的应用列表\n */\nexport async function batchSearchCoverImages(appList) {\n  const results = await Promise.allSettled(\n    appList.map(async (app) => ({\n      ...app,\n      'image-path': await searchCoverImage(encodeURIComponent(app.name)),\n    }))\n  )\n  return results.map((result, index) => (result.status === 'fulfilled' ? result.value : appList[index]))\n}\n\n/**\n * 同时搜索IGDB和Steam封面（多个结果，用于CoverFinder.vue）\n * @param {string} name 应用名称\n * @param {AbortSignal} signal 可选的AbortSignal用于取消请求\n * @returns {Promise<{igdb: Array, steam: Array}>} 包含IGDB和Steam结果的对象\n */\nexport async function searchAllCovers(name, signal = null) {\n  if (!name) {\n    return { igdb: [], steam: [] }\n  }\n\n  try {\n    const [igdbResults, steamResults] = await Promise.allSettled([\n      searchIGDBCovers(name, signal),\n      searchSteamCovers(name),\n    ])\n\n    return {\n      igdb: igdbResults.status === 'fulfilled' ? igdbResults.value : [],\n      steam: steamResults.status === 'fulfilled' ? steamResults.value : [],\n    }\n  } catch (error) {\n    if (error.name === 'AbortError') {\n      throw error\n    }\n    console.error('搜索封面失败:', error)\n    return { igdb: [], steam: [] }\n  }\n}\n\n/**\n * 清除缓存\n */\nexport function clearCache() {\n  bucketCache.clear()\n  gameCache.clear()\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/utils/errorHandler.js",
    "content": "import { APP_CONSTANTS } from './constants.js';\n\n/**\n * 统一错误处理类\n */\nexport class ErrorHandler {\n  /**\n   * 处理网络错误\n   * @param {Error} error 错误对象\n   * @param {string} context 错误上下文\n   * @returns {string} 用户友好的错误信息\n   */\n  static handleNetworkError(error, context = '操作') {\n    console.error(`${context}失败:`, error);\n    \n    if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {\n      return `网络连接失败，请检查网络连接后重试`;\n    }\n    \n    if (error.message.includes('404')) {\n      return `${context}失败：请求的资源不存在`;\n    }\n    \n    if (error.message.includes('500')) {\n      return `${context}失败：服务器内部错误`;\n    }\n    \n    if (error.message.includes('403')) {\n      return `${context}失败：权限不足`;\n    }\n    \n    return `${context}失败：${error.message || '未知错误'}`;\n  }\n\n  /**\n   * 处理验证错误\n   * @param {Array} errors 错误数组\n   * @returns {string} 格式化的错误信息\n   */\n  static handleValidationErrors(errors) {\n    if (!Array.isArray(errors) || errors.length === 0) {\n      return '验证失败';\n    }\n    \n    return errors.join('；');\n  }\n\n  /**\n   * 处理应用操作错误\n   * @param {Error} error 错误对象\n   * @param {string} operation 操作类型\n   * @param {string} appName 应用名称\n   * @returns {string} 格式化的错误信息\n   */\n  static handleAppError(error, operation, appName = '') {\n    const appContext = appName ? `\"${appName}\"` : '';\n    \n    switch(operation) {\n      case 'save':\n        return this.handleNetworkError(error, `保存应用${appContext}`);\n      case 'delete':\n        return this.handleNetworkError(error, `删除应用${appContext}`);\n      case 'load':\n        return this.handleNetworkError(error, `加载应用${appContext}`);\n      default:\n        return this.handleNetworkError(error, `操作应用${appContext}`);\n    }\n  }\n\n  /**\n   * 创建错误弹窗\n   * @param {string} message 错误信息\n   * @param {string} title 标题\n   */\n  static showErrorDialog(message, title = '错误') {\n    // 如果需要更复杂的错误弹窗，可以在这里实现\n    // 目前使用简单的 alert\n    alert(`${title}\\n\\n${message}`);\n  }\n\n  /**\n   * 创建确认弹窗\n   * @param {string} message 确认信息\n   * @param {string} title 标题\n   * @returns {boolean} 用户是否确认\n   */\n  static showConfirmDialog(message, title = '确认') {\n    return confirm(`${title}\\n\\n${message}`);\n  }\n\n  /**\n   * 记录错误日志\n   * @param {Error} error 错误对象\n   * @param {string} context 错误上下文\n   * @param {Object} metadata 额外的元数据\n   */\n  static logError(error, context = '', metadata = {}) {\n    const errorInfo = {\n      message: error.message,\n      stack: error.stack,\n      context,\n      timestamp: new Date().toISOString(),\n      ...metadata\n    };\n    \n    console.error('应用错误:', errorInfo);\n    \n    // 如果需要发送到日志服务，可以在这里实现\n    // this.sendToLogService(errorInfo);\n  }\n\n  /**\n   * 处理异步操作错误\n   * @param {Promise} promise Promise对象\n   * @param {string} context 错误上下文\n   * @returns {Promise} 包装后的Promise\n   */\n  static async handleAsyncError(promise, context = '') {\n    try {\n      return await promise;\n    } catch (error) {\n      this.logError(error, context);\n      throw error;\n    }\n  }\n} "
  },
  {
    "path": "src_assets/common/assets/web/utils/fileSelection.js",
    "content": "/**\n * 文件选择工具模块\n * 提供跨平台的文件和目录选择功能\n */\n\nconst FILE_FILTERS = [\n  { name: '可执行文件', extensions: ['exe', 'app', 'sh', 'bat', 'cmd'] },\n  { name: '所有文件', extensions: ['*'] },\n]\n\nconst PLACEHOLDERS = {\n  windows: { cmd: 'C:\\\\Program Files\\\\App\\\\app.exe', 'working-dir': 'C:\\\\Program Files\\\\App' },\n  default: { cmd: '/usr/bin/app', 'working-dir': '/usr/bin' },\n}\n\n/**\n * 文件选择器类\n */\nexport class FileSelector {\n  constructor(options = {}) {\n    this.platform = options.platform || 'linux'\n    this.onSuccess = options.onSuccess || (() => {})\n    this.onError = options.onError || (() => {})\n    this.onInfo = options.onInfo || (() => {})\n    this.currentField = null\n    this.selectionType = null\n  }\n\n  /**\n   * 通用选择方法\n   */\n  async select(fieldName, input, callback, isDirectory = false) {\n    this.currentField = fieldName\n    this.selectionType = isDirectory ? 'directory' : 'file'\n\n    if (this.isTauriEnvironment()) {\n      return isDirectory ? this.selectDirectoryTauri(fieldName, callback) : this.selectFileTauri(fieldName, callback)\n    }\n\n    if (this.isElectronEnvironment()) {\n      return isDirectory\n        ? this.selectDirectoryElectron(fieldName, callback)\n        : this.selectFileElectron(fieldName, callback)\n    }\n\n    return isDirectory ? this.selectDirectoryBrowser(input, callback) : this.selectFileBrowser(input, callback)\n  }\n\n  /**\n   * 选择文件\n   */\n  async selectFile(fieldName, fileInput, callback) {\n    return this.select(fieldName, fileInput, callback, false)\n  }\n\n  /**\n   * 选择目录\n   */\n  async selectDirectory(fieldName, dirInput, callback) {\n    return this.select(fieldName, dirInput, callback, true)\n  }\n\n  /**\n   * 浏览器环境下选择文件/目录\n   */\n  selectBrowser(input, callback, isDirectory) {\n    if (!input) {\n      this.onError(isDirectory ? '目录输入元素不存在' : '文件输入元素不存在')\n      return\n    }\n\n    input.value = ''\n    input.click()\n\n    const handleSelected = (event) => {\n      const files = event.target.files\n      const hasSelection = isDirectory ? files.length > 0 : files[0]\n\n      if (hasSelection && this.currentField) {\n        try {\n          const path = isDirectory ? this.processDirectoryPath(files[0]) : this.processFilePath(files[0])\n\n          callback?.(this.currentField, path)\n          this.onSuccess(`${isDirectory ? '目录' : '文件'}选择成功: ${path}`)\n\n          if (!this.isElectronEnvironment()) {\n            this.onInfo('浏览器环境下无法获取完整路径，请检查并手动调整路径')\n          }\n        } catch (error) {\n          console.error(`${isDirectory ? '目录' : '文件'}选择处理失败:`, error)\n          this.onError(`${isDirectory ? '目录' : '文件'}选择处理失败，请重试`)\n        }\n      }\n\n      this.resetState()\n      input.removeEventListener('change', handleSelected)\n    }\n\n    input.addEventListener('change', handleSelected)\n  }\n\n  selectFileBrowser(fileInput, callback) {\n    return this.selectBrowser(fileInput, callback, false)\n  }\n\n  selectDirectoryBrowser(dirInput, callback) {\n    return this.selectBrowser(dirInput, callback, true)\n  }\n\n  /**\n   * 检查是否在 Tauri 环境\n   */\n  isTauriEnvironment() {\n    const tauri = typeof window !== 'undefined' ? window.__TAURI__ : null\n    return !!(tauri?.dialog?.open || tauri?.core?.invoke)\n  }\n\n  /**\n   * 检查是否在 Electron 环境\n   */\n  isElectronEnvironment() {\n    return typeof window !== 'undefined' && window.process?.type === 'renderer'\n  }\n\n  /**\n   * Tauri 环境下选择\n   */\n  async selectTauri(fieldName, callback, isDirectory) {\n    const tauri = window.__TAURI__\n    if (!tauri?.dialog?.open) {\n      this.onError('Tauri 对话框 API 不可用')\n      this.resetState()\n      return null\n    }\n\n    try {\n      const options = isDirectory\n        ? { title: '选择目录', multiple: false, directory: true }\n        : { title: '选择文件', filters: FILE_FILTERS, multiple: false, directory: false }\n\n      const selected = await tauri.dialog.open(options)\n\n      if (selected) {\n        callback?.(fieldName, selected)\n        this.onSuccess(`${isDirectory ? '目录' : '文件'}选择成功: ${selected}`)\n        this.resetState()\n        return selected\n      }\n    } catch (error) {\n      console.error(`Tauri ${isDirectory ? '目录' : '文件'}选择失败:`, error)\n      this.onError(`${isDirectory ? '目录' : '文件'}选择失败，请手动输入路径`)\n    }\n\n    this.resetState()\n    return null\n  }\n\n  async selectFileTauri(fieldName, callback) {\n    return this.selectTauri(fieldName, callback, false)\n  }\n\n  async selectDirectoryTauri(fieldName, callback) {\n    return this.selectTauri(fieldName, callback, true)\n  }\n\n  /**\n   * Electron 环境下选择\n   */\n  async selectElectron(fieldName, callback, isDirectory) {\n    try {\n      const { dialog } = window.require('electron').remote\n      const options = isDirectory\n        ? { properties: ['openDirectory'] }\n        : { properties: ['openFile'], filters: FILE_FILTERS }\n\n      const result = await dialog.showOpenDialog(options)\n\n      if (!result.canceled && result.filePaths.length > 0) {\n        const path = result.filePaths[0]\n        callback?.(fieldName, path)\n        this.onSuccess(`${isDirectory ? '目录' : '文件'}选择成功: ${path}`)\n        return path\n      }\n    } catch (error) {\n      console.error(`${isDirectory ? '目录' : '文件'}选择失败:`, error)\n      this.onError(`${isDirectory ? '目录' : '文件'}选择失败，请手动输入路径`)\n    }\n\n    this.resetState()\n    return null\n  }\n\n  async selectFileElectron(fieldName, callback) {\n    return this.selectElectron(fieldName, callback, false)\n  }\n\n  async selectDirectoryElectron(fieldName, callback) {\n    return this.selectElectron(fieldName, callback, true)\n  }\n\n  /**\n   * 处理文件路径\n   */\n  processFilePath(file) {\n    return file.webkitRelativePath || file.name\n  }\n\n  /**\n   * 处理目录路径\n   */\n  processDirectoryPath(firstFile) {\n    if (!firstFile.webkitRelativePath) return ''\n    const parts = firstFile.webkitRelativePath.split('/')\n    return parts.slice(0, -1).join('/')\n  }\n\n  /**\n   * 检查是否是开发环境\n   */\n  isDevelopmentEnvironment() {\n    if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') {\n      return true\n    }\n    if (typeof window !== 'undefined') {\n      const { hostname } = window.location\n      return hostname === 'localhost' || hostname === '127.0.0.1'\n    }\n    return false\n  }\n\n  /**\n   * 重置状态\n   */\n  resetState() {\n    this.currentField = null\n    this.selectionType = null\n  }\n\n  /**\n   * 检查文件选择支持\n   */\n  checkFileSelectionSupport() {\n    if (typeof window === 'undefined') return false\n    return !!(window.File && window.FileReader && window.FileList && window.Blob)\n  }\n\n  /**\n   * 检查目录选择支持\n   */\n  checkDirectorySelectionSupport(dirInput) {\n    return !!(dirInput && 'webkitdirectory' in dirInput)\n  }\n\n  /**\n   * 获取字段占位符文本\n   */\n  getPlaceholderText(fieldName) {\n    const platformPlaceholders = this.platform === 'windows' ? PLACEHOLDERS.windows : PLACEHOLDERS.default\n    return platformPlaceholders[fieldName] || ''\n  }\n\n  /**\n   * 获取按钮标题文本\n   */\n  getButtonTitle(type) {\n    return type === 'file' ? '选择文件' : type === 'directory' ? '选择目录' : '选择'\n  }\n\n  /**\n   * 清理文件输入\n   */\n  cleanupFileInputs(fileInput, dirInput) {\n    if (fileInput) fileInput.value = ''\n    if (dirInput) dirInput.value = ''\n  }\n}\n\n/**\n * 创建文件选择器实例的工厂函数\n */\nexport function createFileSelector(options = {}) {\n  return new FileSelector(options)\n}\n\n/**\n * 简化的文件选择函数\n */\nexport async function selectFile(options = {}) {\n  const selector = createFileSelector(options)\n  return selector.selectFile(options.fieldName, options.fileInput, options.callback)\n}\n\n/**\n * 简化的目录选择函数\n */\nexport async function selectDirectory(options = {}) {\n  const selector = createFileSelector(options)\n  return selector.selectDirectory(options.fieldName, options.dirInput, options.callback)\n}\n\n/**\n * 检查环境支持\n */\nexport function checkEnvironmentSupport() {\n  const selector = createFileSelector()\n  return {\n    fileSelection: selector.checkFileSelectionSupport(),\n    directorySelection: selector.checkDirectorySelectionSupport(),\n    isTauri: selector.isTauriEnvironment(),\n    isElectron: selector.isElectronEnvironment(),\n    isDevelopment: selector.isDevelopmentEnvironment(),\n  }\n}\n\nexport default FileSelector\n"
  },
  {
    "path": "src_assets/common/assets/web/utils/helpers.js",
    "content": "/**\n * 防抖函数\n * @param {Function} func 需要防抖的函数\n * @param {number} wait 等待时间（毫秒）\n * @returns {Function} 防抖后的函数\n */\nexport function debounce(func, wait) {\n  let timeout;\n  return function executedFunction(...args) {\n    const later = () => {\n      clearTimeout(timeout);\n      func(...args);\n    };\n    clearTimeout(timeout);\n    timeout = setTimeout(later, wait);\n  };\n}\n\n/**\n * 异步延迟函数\n * @param {number} ms 延迟时间（毫秒）\n * @returns {Promise} Promise对象\n */\nexport function delay(ms) {\n  return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * 深拷贝函数\n * @param {*} obj 需要深拷贝的对象\n * @returns {*} 深拷贝后的对象\n */\nexport function deepClone(obj) {\n  if (obj === null || typeof obj !== 'object') return obj;\n  if (obj instanceof Date) return new Date(obj);\n  if (obj instanceof Array) return obj.map(item => deepClone(item));\n  if (typeof obj === 'object') {\n    const clonedObj = {};\n    for (const key in obj) {\n      if (obj.hasOwnProperty(key)) {\n        clonedObj[key] = deepClone(obj[key]);\n      }\n    }\n    return clonedObj;\n  }\n}\n\n/**\n * 安全的JSON解析\n * @param {string} str JSON字符串\n * @param {*} defaultValue 解析失败时的默认值\n * @returns {*} 解析结果或默认值\n */\nexport function safeJsonParse(str, defaultValue = null) {\n  try {\n    return JSON.parse(str);\n  } catch (error) {\n    console.warn('JSON解析失败:', error);\n    return defaultValue;\n  }\n}\n\n/**\n * 格式化错误信息\n * @param {Error|string} error 错误对象或错误信息\n * @returns {string} 格式化后的错误信息\n */\nexport function formatError(error) {\n  if (typeof error === 'string') return error;\n  if (error && error.message) return error.message;\n  return '未知错误';\n}\n\n/**\n * 检查是否为有效的URL\n * @param {string} url URL字符串\n * @returns {boolean} 是否为有效URL\n */\nexport function isValidUrl(url) {\n  try {\n    new URL(url);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n/**\n * 获取文件扩展名\n * @param {string} filename 文件名\n * @returns {string} 文件扩展名\n */\nexport function getFileExtension(filename) {\n  return filename.split('.').pop().toLowerCase();\n}\n\n/**\n * 格式化文件大小\n * @param {number} bytes 字节数\n * @param {number} decimals 小数位数\n * @returns {string} 格式化后的大小\n */\nexport function formatFileSize(bytes, decimals = 2) {\n  if (bytes === 0) return '0 Bytes';\n  const k = 1024;\n  const dm = decimals < 0 ? 0 : decimals;\n  const sizes = ['Bytes', 'KB', 'MB', 'GB'];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];\n}\n\n/**\n * 生成随机ID\n * @param {number} length ID长度\n * @returns {string} 随机ID\n */\nexport function generateRandomId(length = 8) {\n  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n  let result = '';\n  for (let i = 0; i < length; i++) {\n    result += chars.charAt(Math.floor(Math.random() * chars.length));\n  }\n  return result;\n}\n\n/**\n * 验证必填字段\n * @param {Object} obj 要验证的对象\n * @param {Array} requiredFields 必填字段数组\n * @returns {Object} 验证结果 { isValid: boolean, missingFields: Array }\n */\nexport function validateRequiredFields(obj, requiredFields) {\n  const missingFields = requiredFields.filter(field => \n    !obj.hasOwnProperty(field) || obj[field] === '' || obj[field] === null || obj[field] === undefined\n  );\n  \n  return {\n    isValid: missingFields.length === 0,\n    missingFields\n  };\n} \n\n/**\n * 检测是否在 Tauri 环境中\n * @returns {boolean} 是否在 Tauri 环境\n */\nexport function isTauriEnv() {\n  return typeof window !== 'undefined' && !!(window.isTauri || window.__TAURI__);\n}\n\n/**\n * 打开外部链接（支持 Tauri 和浏览器环境）\n * @param {string} url 要打开的 URL\n * @returns {Promise<void>}\n */\nexport async function openExternalUrl(url) {\n  if (!isValidUrl(url)) {\n    throw new Error('Invalid URL');\n  }\n\n  if (isTauriEnv()) {\n    try {\n      await window.__TAURI__.shell.open(url);\n    } catch (error) {\n      console.error('Failed to open URL with Tauri shell:', error);\n      // 降级到 window.open\n      window.open(url, '_blank');\n    }\n  } else {\n    // 非 Tauri 环境，使用 window.open\n    window.open(url, '_blank');\n  }\n} "
  },
  {
    "path": "src_assets/common/assets/web/utils/imageUtils.js",
    "content": "/**\n * 图片工具函数\n * 用于处理应用图片URL的标准化逻辑\n */\n/**\n * 获取图片预览URL\n * @param {string} imagePath 图片路径\n * @returns {string} 预览URL\n */\nexport function getImagePreviewUrl(imagePath = 'box.png') {\n  if (imagePath === 'desktop') {\n    return '/boxart/desktop.png'\n  }\n  // 如果路径不包含分隔符,说明是boxart资源ID\n  if (!/[/\\\\]/.test(imagePath)) {\n    return `/boxart/${encodeURIComponent(imagePath)}`\n  }\n\n  return isLocalImagePath(imagePath) ? `file://${imagePath}` : imagePath\n}\n\n/**\n * 检查图片路径是否为本地文件路径\n * @param {string} imagePath 图片路径\n * @returns {boolean} 是否为本地文件路径\n */\nexport function isLocalImagePath(imagePath) {\n  if (!imagePath) {\n    return false\n  }\n\n  // 如果是网络URL或blob/data URL，不是本地路径\n  if (\n    imagePath.startsWith('http://') ||\n    imagePath.startsWith('https://') ||\n    imagePath.startsWith('blob:') ||\n    imagePath.startsWith('data:')\n  ) {\n    return false\n  }\n\n  return true\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/utils/steamApi.js",
    "content": "/**\n * Steam API工具模块\n * 提供Steam应用搜索和封面获取功能\n *\n */\n\n// Steam CDN 基础URL\nconst STEAM_CDN_BASE = 'https://cdn.cloudflare.steamstatic.com/steam/apps'\n\n// SteamGridDB API 基础URL\nconst STEAMGRIDDB_API_BASE = '/steamgriddb'\n\n// 封面URL缓存\nconst coverUrlCache = new Map()\n\n// SteamGridDB 缓存\nconst steamGridDBCache = new Map()\n\n// 图片存在性缓存\nconst imageExistsCache = new Map()\n\n/**\n * 构建URL查询参数\n * @param {Object} options 参数对象\n * @returns {string} 查询字符串\n */\nfunction buildQueryString(options) {\n  const params = new URLSearchParams()\n  Object.entries(options).forEach(([key, value]) => {\n    if (value !== undefined && value !== null) {\n      params.append(key, value)\n    }\n  })\n  const str = params.toString()\n  return str ? `?${str}` : ''\n}\n\n/**\n * 通用fetch请求封装\n * @param {string} url 请求URL\n * @param {Object} options fetch选项\n * @returns {Promise<Object|null>} 响应数据\n */\nasync function fetchJson(url, options = {}) {\n  try {\n    const response = await fetch(url, options)\n    if (!response.ok) {\n      return null\n    }\n    return await response.json()\n  } catch {\n    return null\n  }\n}\n\n/**\n * 搜索Steam应用 (使用Steam Store搜索API)\n * @param {string} searchName 搜索名称\n * @param {number} maxResults 最大结果数量\n * @returns {Promise<Array>} 匹配的Steam应用列表\n */\nexport async function searchSteamApps(searchName, maxResults = 20) {\n  if (!searchName?.trim()) {\n    return []\n  }\n\n  const data = await fetchJson(`/steam-store/api/storesearch/?term=${encodeURIComponent(searchName)}&l=schinese&cc=CN`)\n\n  if (!data?.items?.length) {\n    return []\n  }\n\n  return data.items\n    .filter((item) => item.type === 'app')\n    .slice(0, maxResults)\n    .map(({ id, name, tiny_image, platforms, price, metascore }) => ({\n      appid: id,\n      name,\n      tiny_image,\n      platforms,\n      price,\n      metascore,\n    }))\n}\n\n/**\n * 加载Steam应用列表 (已弃用，保留兼容性)\n * @deprecated 使用 searchSteamApps 代替\n * @returns {Promise<Array>} 空数组\n */\nexport async function loadSteamApps() {\n  console.warn('loadSteamApps 已弃用，请使用 searchSteamApps 直接搜索')\n  return []\n}\n\n/**\n * 获取Steam应用详情\n * @param {number} appId Steam应用ID\n * @returns {Promise<Object|null>} Steam应用详情\n */\nexport async function getSteamAppDetails(appId) {\n  const data = await fetchJson(`/steam-store/api/appdetails?appids=${appId}&l=schinese`)\n  return data?.[appId]?.success ? data[appId].data : null\n}\n\n/**\n * 搜索Steam应用封面（快速模式）\n * 优化：直接使用CDN URL，不再获取详情API，大幅提升速度\n * @param {string} name 应用名称\n * @param {number} maxResults 最大结果数量\n * @returns {Promise<Array>} 封面列表\n */\nexport async function searchSteamCovers(name, maxResults = 20) {\n  if (!name) {\n    return []\n  }\n\n  const matches = await searchSteamApps(name, maxResults)\n\n  if (!matches.length) {\n    return []\n  }\n\n  const coverPromises = matches.map(async ({ appid, name: appName }) => ({\n    name: appName,\n    appid,\n    source: 'steam',\n    url: getSteamCoverUrl(appid, 'header'),\n    saveUrl: await getCachedBestCoverUrl(appid),\n    key: `steam_${appid}`,\n  }))\n\n  return Promise.all(coverPromises)\n}\n\n/**\n * 搜索Steam应用封面（完整模式，包含详情）\n * @param {string} name 应用名称\n * @param {number} maxResults 最大结果数量\n * @returns {Promise<Array>} 封面列表（包含详情）\n */\nexport async function searchSteamCoversWithDetails(name, maxResults = 20) {\n  if (!name) {\n    return []\n  }\n\n  const matches = await searchSteamApps(name, maxResults)\n\n  if (!matches.length) {\n    return []\n  }\n\n  const detailPromises = matches.map(async ({ appid }) => {\n    const gameData = await getSteamAppDetails(appid)\n\n    if (!gameData) {\n      return null\n    }\n\n    const headerImage = gameData.header_image || gameData.capsule_image || gameData.capsule_imagev5\n    const saveUrl = await getCachedBestCoverUrl(appid)\n\n    return {\n      name: gameData.name,\n      appid,\n      source: 'steam',\n      url: headerImage,\n      saveUrl,\n      key: `steam_${appid}`,\n      type: gameData.type || 'game',\n      shortDescription: gameData.short_description || '',\n      developers: gameData.developers || [],\n      publishers: gameData.publishers || [],\n      releaseDate: gameData.release_date || null,\n    }\n  })\n\n  const results = await Promise.all(detailPromises)\n  return results.filter((item) => item?.url)\n}\n\n// 封面类型映射表\nconst COVER_TYPE_MAP = {\n  header: 'header.jpg',\n  header_292x136: 'header_292x136.jpg',\n  capsule: 'capsule_231x87.jpg',\n  capsule_231x87: 'capsule_231x87.jpg',\n  capsule_616x353: 'capsule_616x353.jpg',\n  library: 'library_600x900.jpg',\n  library_600x900: 'library_600x900.jpg',\n  library_2x: 'library_600x900_2x.jpg',\n  library_600x900_2x: 'library_600x900_2x.jpg',\n  library_hero: 'library_hero.jpg',\n  library_hero_2x: 'library_hero_2x.jpg',\n  logo: 'logo.png',\n  page_bg: 'page_bg_generated_v6b.jpg',\n}\n\n/**\n * 获取Steam封面图片URL\n * @param {number} appId Steam应用ID\n * @param {string} type 封面类型\n * @returns {string} 封面图片URL\n */\nexport function getSteamCoverUrl(appId, type = 'header') {\n  const filename = COVER_TYPE_MAP[type] || COVER_TYPE_MAP.header\n  return `${STEAM_CDN_BASE}/${appId}/${filename}`\n}\n\n/**\n * 检查图片URL是否有效（带缓存）\n * @param {string} url 图片URL\n * @returns {Promise<boolean>} 是否有效\n */\nexport async function checkImageExists(url) {\n  if (imageExistsCache.has(url)) {\n    return imageExistsCache.get(url)\n  }\n\n  try {\n    const response = await fetch(url, { method: 'HEAD' })\n    const exists = response.ok\n    imageExistsCache.set(url, exists)\n    return exists\n  } catch {\n    imageExistsCache.set(url, false)\n    return false\n  }\n}\n\n/**\n * 获取最佳可用的Steam封面URL（带缓存）\n * @param {number} appId Steam应用ID\n * @returns {Promise<string>} 最佳封面URL\n */\nexport async function getCachedBestCoverUrl(appId) {\n  if (coverUrlCache.has(appId)) {\n    return coverUrlCache.get(appId)\n  }\n\n  const libraryUrl = getSteamCoverUrl(appId, 'library')\n  const libraryExists = await checkImageExists(libraryUrl)\n  const bestUrl = libraryExists ? libraryUrl : getSteamCoverUrl(appId, 'header')\n\n  coverUrlCache.set(appId, bestUrl)\n  return bestUrl\n}\n\n/**\n * 获取最佳可用的Steam封面URL\n * @param {number} appId Steam应用ID\n * @param {string} headerImage header图片URL (从API获取的)\n * @returns {Promise<string>} 最佳封面URL\n */\nexport async function getBestCoverUrl(appId, headerImage) {\n  const libraryUrl = getSteamCoverUrl(appId, 'library')\n  const libraryExists = await checkImageExists(libraryUrl)\n  return libraryExists ? libraryUrl : headerImage || getSteamCoverUrl(appId, 'header')\n}\n\n/**\n * 批量获取Steam封面URL（优化版本）\n * @param {Array<number>} appIds Steam应用ID数组\n * @returns {Promise<Map<number, string>>} appId到封面URL的映射\n */\nexport async function batchGetCoverUrls(appIds) {\n  const results = new Map()\n  const uncachedIds = []\n\n  for (const appId of appIds) {\n    if (coverUrlCache.has(appId)) {\n      results.set(appId, coverUrlCache.get(appId))\n    } else {\n      uncachedIds.push(appId)\n    }\n  }\n\n  if (uncachedIds.length > 0) {\n    const fetched = await Promise.all(\n      uncachedIds.map(async (appId) => ({\n        appId,\n        url: await getCachedBestCoverUrl(appId),\n      }))\n    )\n    fetched.forEach(({ appId, url }) => results.set(appId, url))\n  }\n\n  return results\n}\n\n/**\n * 清除封面URL缓存\n */\nexport function clearCoverCache() {\n  coverUrlCache.clear()\n  steamGridDBCache.clear()\n  imageExistsCache.clear()\n}\n\n/**\n * 验证Steam应用ID\n * @param {number|string} appId 应用ID\n * @returns {boolean} 是否有效\n */\nexport function isValidSteamAppId(appId) {\n  const id = parseInt(appId)\n  return !isNaN(id) && id > 0 && id < 2147483647\n}\n\n/**\n * 格式化Steam应用信息\n * @param {Object} appData Steam应用数据\n * @returns {Object} 格式化后的应用信息\n */\nexport function formatSteamAppInfo(appData) {\n  return {\n    id: appData.steam_appid,\n    name: appData.name,\n    type: appData.type,\n    description: appData.short_description,\n    developers: appData.developers || [],\n    publishers: appData.publishers || [],\n    releaseDate: appData.release_date?.date || null,\n    price: appData.price_overview || null,\n    categories: appData.categories || [],\n    genres: appData.genres || [],\n    screenshots: appData.screenshots || [],\n    movies: appData.movies || [],\n    achievements: appData.achievements || [],\n    platforms: appData.platforms || {},\n    metacritic: appData.metacritic || null,\n    recommendations: appData.recommendations || null,\n  }\n}\n\n// ==================== SteamGridDB 支持 ====================\n\n/**\n * 通用SteamGridDB资源映射函数\n * @param {Object} item 资源项\n * @returns {Object} 映射后的对象\n */\nfunction mapSteamGridDBItem(item) {\n  return {\n    id: item.id,\n    url: item.url,\n    thumb: item.thumb,\n    width: item.width,\n    height: item.height,\n    style: item.style,\n    nsfw: item.nsfw,\n    humor: item.humor,\n    author: item.author,\n    language: item.language,\n    score: item.score,\n    ...(item.lock !== undefined && { lock: item.lock }),\n    ...(item.epilepsy !== undefined && { epilepsy: item.epilepsy }),\n  }\n}\n\n/**\n * 通用SteamGridDB资源获取函数\n * @param {string} resourceType 资源类型 (grids, heroes, logos, icons)\n * @param {number} gameId 游戏ID\n * @param {Object} options 选项\n * @returns {Promise<Array>} 资源列表\n */\nasync function fetchSteamGridDBResource(resourceType, gameId, options = {}) {\n  if (!gameId) {\n    return []\n  }\n\n  const queryString = buildQueryString(options)\n  const url = `${STEAMGRIDDB_API_BASE}/${resourceType}/game/${gameId}${queryString}`\n  const data = await fetchJson(url)\n\n  if (!data?.success || !data?.data) {\n    return []\n  }\n\n  return data.data.map(mapSteamGridDBItem)\n}\n\n/**\n * 在SteamGridDB上搜索游戏\n * @param {string} searchTerm 搜索词\n * @returns {Promise<Array>} 游戏列表\n */\nexport async function searchSteamGridDB(searchTerm) {\n  if (!searchTerm?.trim()) {\n    return []\n  }\n\n  const data = await fetchJson(`${STEAMGRIDDB_API_BASE}/search/autocomplete/${encodeURIComponent(searchTerm)}`)\n\n  if (!data?.success || !data?.data) {\n    return []\n  }\n\n  return data.data.map((game) => ({\n    id: game.id,\n    name: game.name,\n    releaseDate: game.release_date,\n    types: game.types || [],\n    verified: game.verified || false,\n  }))\n}\n\n/**\n * 通过Steam AppID获取SteamGridDB游戏ID\n * @param {number} steamAppId Steam应用ID\n * @returns {Promise<number|null>} SteamGridDB游戏ID\n */\nexport async function getSteamGridDBGameId(steamAppId) {\n  const cacheKey = `steam_${steamAppId}`\n  if (steamGridDBCache.has(cacheKey)) {\n    return steamGridDBCache.get(cacheKey)\n  }\n\n  const data = await fetchJson(`${STEAMGRIDDB_API_BASE}/games/steam/${steamAppId}`)\n\n  if (data?.success && data?.data) {\n    const gameId = data.data.id\n    steamGridDBCache.set(cacheKey, gameId)\n    return gameId\n  }\n\n  return null\n}\n\n/**\n * 获取SteamGridDB封面（Grids）\n * @param {number} gameId SteamGridDB游戏ID\n * @param {Object} options 选项\n * @returns {Promise<Array>} 封面列表\n */\nexport function getSteamGridDBGrids(gameId, options = {}) {\n  return fetchSteamGridDBResource('grids', gameId, options)\n}\n\n/**\n * 获取SteamGridDB英雄图（Heroes）\n * @param {number} gameId SteamGridDB游戏ID\n * @param {Object} options 选项\n * @returns {Promise<Array>} 英雄图列表\n */\nexport function getSteamGridDBHeroes(gameId, options = {}) {\n  return fetchSteamGridDBResource('heroes', gameId, options)\n}\n\n/**\n * 获取SteamGridDB Logo\n * @param {number} gameId SteamGridDB游戏ID\n * @param {Object} options 选项\n * @returns {Promise<Array>} Logo列表\n */\nexport function getSteamGridDBLogos(gameId, options = {}) {\n  return fetchSteamGridDBResource('logos', gameId, options)\n}\n\n/**\n * 获取SteamGridDB图标（Icons）\n * @param {number} gameId SteamGridDB游戏ID\n * @param {Object} options 选项\n * @returns {Promise<Array>} 图标列表\n */\nexport function getSteamGridDBIcons(gameId, options = {}) {\n  return fetchSteamGridDBResource('icons', gameId, options)\n}\n\n// 默认SteamGridDB选项\nconst DEFAULT_GRID_OPTIONS = {\n  dimensions: '600x900',\n  types: 'static',\n  nsfw: 'false',\n  humor: 'false',\n}\n\n/**\n * 搜索SteamGridDB封面（综合搜索）\n * @param {string} name 游戏名称\n * @param {number} maxResults 最大结果数量\n * @param {Object} gridOptions 封面选项\n * @returns {Promise<Array>} 封面列表\n */\nexport async function searchSteamGridDBCovers(name, maxResults = 20, gridOptions = {}) {\n  if (!name) {\n    return []\n  }\n\n  const games = await searchSteamGridDB(name)\n\n  if (!games.length) {\n    return []\n  }\n\n  const options = { ...DEFAULT_GRID_OPTIONS, ...gridOptions }\n\n  const coverPromises = games.slice(0, 10).map(async (game) => {\n    const grids = await getSteamGridDBGrids(game.id, options)\n\n    return grids.slice(0, 3).map((grid) => ({\n      name: game.name,\n      gameId: game.id,\n      source: 'steamgriddb',\n      url: grid.thumb || grid.url,\n      saveUrl: grid.url,\n      key: `sgdb_${game.id}_${grid.id}`,\n      width: grid.width,\n      height: grid.height,\n      style: grid.style,\n      author: grid.author,\n      score: grid.score,\n    }))\n  })\n\n  const results = await Promise.all(coverPromises)\n  return results.flat().slice(0, maxResults)\n}\n\n/**\n * 通过Steam AppID获取SteamGridDB封面\n * @param {number} steamAppId Steam应用ID\n * @param {Object} gridOptions 封面选项\n * @returns {Promise<Array>} 封面列表\n */\nexport async function getSteamGridDBCoversBySteamId(steamAppId, gridOptions = {}) {\n  const gameId = await getSteamGridDBGameId(steamAppId)\n\n  if (!gameId) {\n    return []\n  }\n\n  const options = { ...DEFAULT_GRID_OPTIONS, ...gridOptions }\n  const grids = await getSteamGridDBGrids(gameId, options)\n\n  return grids.map((grid) => ({\n    gameId,\n    steamAppId,\n    source: 'steamgriddb',\n    url: grid.thumb || grid.url,\n    saveUrl: grid.url,\n    key: `sgdb_${gameId}_${grid.id}`,\n    width: grid.width,\n    height: grid.height,\n    style: grid.style,\n    author: grid.author,\n    score: grid.score,\n  }))\n}\n\nexport default {\n  loadSteamApps,\n  searchSteamApps,\n  getSteamAppDetails,\n  searchSteamCovers,\n  searchSteamCoversWithDetails,\n  getSteamCoverUrl,\n  checkImageExists,\n  getBestCoverUrl,\n  getCachedBestCoverUrl,\n  batchGetCoverUrls,\n  clearCoverCache,\n  isValidSteamAppId,\n  formatSteamAppInfo,\n  // SteamGridDB\n  searchSteamGridDB,\n  getSteamGridDBGameId,\n  getSteamGridDBGrids,\n  getSteamGridDBHeroes,\n  getSteamGridDBLogos,\n  getSteamGridDBIcons,\n  searchSteamGridDBCovers,\n  getSteamGridDBCoversBySteamId,\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/utils/theme.js",
    "content": "const getStoredTheme = () => localStorage.getItem('theme')\nexport const setStoredTheme = (theme) => localStorage.setItem('theme', theme)\n\nexport const getPreferredTheme = () => {\n  const storedTheme = getStoredTheme()\n  if (storedTheme) {\n    return storedTheme\n  }\n\n  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n}\n\nexport const setTheme = (theme) => {\n  if (theme === 'auto') {\n    document.documentElement.setAttribute(\n      'data-bs-theme',\n      window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n    )\n  } else {\n    document.documentElement.setAttribute('data-bs-theme', theme)\n  }\n}\n\nexport const showActiveTheme = (theme, focus = false) => {\n  const themeSwitcher = document.querySelector('#bd-theme')\n\n  if (!themeSwitcher) {\n    return\n  }\n\n  const themeSwitcherText = document.querySelector('#bd-theme-text')\n  const activeThemeIcon = document.querySelector('.theme-icon-active i')\n  const btnToActive = document.querySelector(`[data-bs-theme-value=\"${theme}\"]`)\n  \n  if (!btnToActive) {\n    return\n  }\n  \n  const classListOfActiveBtn = btnToActive.querySelector('i').classList\n\n  document.querySelectorAll('[data-bs-theme-value]').forEach((element) => {\n    element.classList.remove('active')\n    element.setAttribute('aria-pressed', 'false')\n  })\n\n  btnToActive.classList.add('active')\n  btnToActive.setAttribute('aria-pressed', 'true')\n  activeThemeIcon.classList.remove(...activeThemeIcon.classList.values())\n  activeThemeIcon.classList.add(...classListOfActiveBtn)\n  const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.textContent.trim()})`\n  themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)\n\n  if (focus) {\n    themeSwitcher.focus()\n  }\n}\n\n// 单例标志，确保全局事件监听器只添加一次\nlet isAutoThemeInitialized = false\nlet mediaQueryHandler = null\nlet domContentLoadedHandler = null\n\nexport function loadAutoTheme() {\n  // 设置主题\n  setTheme(getPreferredTheme())\n\n  // 只在第一次调用时添加全局事件监听器\n  if (!isAutoThemeInitialized) {\n    // 处理系统主题变化\n    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')\n    mediaQueryHandler = () => {\n    const storedTheme = getStoredTheme()\n    if (storedTheme !== 'light' && storedTheme !== 'dark') {\n      setTheme(getPreferredTheme())\n    }\n    }\n    mediaQuery.addEventListener('change', mediaQueryHandler)\n\n    // 处理 DOMContentLoaded 事件（如果文档已经加载完成，则立即执行）\n    domContentLoadedHandler = () => {\n    showActiveTheme(getPreferredTheme())\n    }\n    if (document.readyState === 'loading') {\n      window.addEventListener('DOMContentLoaded', domContentLoadedHandler)\n    } else {\n      // 文档已经加载完成，直接执行\n      domContentLoadedHandler()\n    }\n\n    isAutoThemeInitialized = true\n  }\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/utils/validation.js",
    "content": "/**\n * 表单验证工具模块\n * 提供应用表单的各种验证规则和方法\n */\n\n/**\n * 验证规则对象\n */\nexport const validationRules = {\n  appName: {\n    required: true,\n    minLength: 1,\n    maxLength: 100,\n    pattern: /^[^<>:\"\\\\|?*\\x00-\\x1F]+$/,\n    message: '应用名称不能为空，且不能包含特殊字符',\n  },\n  command: {\n    required: false,\n    minLength: 0,\n    maxLength: 1000,\n    message: '命令不规范，请输入正确的命令',\n  },\n  workingDir: {\n    required: false,\n    maxLength: 500,\n    message: '工作目录路径过长',\n  },\n  outputName: {\n    required: false,\n    maxLength: 100,\n    pattern: /^[a-zA-Z0-9_\\-\\.]*$/,\n    message: '输出名称只能包含字母、数字、下划线、连字符和点',\n  },\n  timeout: {\n    required: false,\n    min: 0,\n    max: 3600,\n    message: '超时时间必须在0-3600秒之间',\n  },\n  imagePath: {\n    required: false,\n    maxLength: 500,\n    allowedTypes: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'],\n    message: '图片路径无效或格式不支持',\n  },\n}\n\n/**\n * 验证单个字段\n * @param {string} fieldName 字段名称\n * @param {any} value 字段值\n * @param {Object} customRules 自定义验证规则\n * @returns {Object} 验证结果 {isValid: boolean, message: string}\n */\nexport function validateField(fieldName, value, customRules = {}) {\n  const rules = { ...validationRules[fieldName], ...customRules }\n\n  if (!rules) {\n    return { isValid: true, message: '' }\n  }\n\n  const strValue = value?.toString().trim() ?? ''\n  const isEmpty = strValue === ''\n\n  // 必填验证\n  if (rules.required && isEmpty) {\n    return { isValid: false, message: rules.message || '此字段为必填项' }\n  }\n\n  // 如果字段为空且不是必填，则跳过其他验证\n  if (isEmpty) {\n    return { isValid: true, message: '' }\n  }\n\n  // 长度验证\n  if (rules.minLength && strValue.length < rules.minLength) {\n    return { isValid: false, message: `最少需要${rules.minLength}个字符` }\n  }\n\n  if (rules.maxLength && strValue.length > rules.maxLength) {\n    return { isValid: false, message: `最多允许${rules.maxLength}个字符` }\n  }\n\n  // 数值验证\n  if (rules.min !== undefined || rules.max !== undefined) {\n    const numValue = Number(value)\n    if (isNaN(numValue)) {\n      return { isValid: false, message: '请输入有效的数字' }\n    }\n    if (rules.min !== undefined && numValue < rules.min) {\n      return { isValid: false, message: `最小值为${rules.min}` }\n    }\n    if (rules.max !== undefined && numValue > rules.max) {\n      return { isValid: false, message: `最大值为${rules.max}` }\n    }\n  }\n\n  // 正则表达式验证\n  if (rules.pattern && !rules.pattern.test(strValue)) {\n    return { isValid: false, message: rules.message || '格式不正确' }\n  }\n\n  // 文件类型验证\n  if (rules.allowedTypes && fieldName === 'imagePath' && strValue !== 'desktop') {\n    const lastDotIndex = strValue.lastIndexOf('.')\n    if (lastDotIndex > 0) {\n      const extension = strValue\n        .slice(lastDotIndex + 1)\n        .split(/[?#]/)[0]\n        .toLowerCase()\n      if (extension && !rules.allowedTypes.includes(extension)) {\n        return {\n          isValid: false,\n          message: `只支持以下格式：${rules.allowedTypes.join(', ')}`,\n        }\n      }\n    }\n  }\n\n  return { isValid: true, message: '' }\n}\n\n// 字段映射配置\nconst FIELD_MAPPINGS = [\n  { key: 'name', rule: 'appName', label: '应用名称' },\n  { key: 'cmd', rule: 'command', label: '命令' },\n  { key: 'working-dir', rule: 'workingDir', label: '工作目录' },\n  { key: 'output', rule: 'outputName', label: '输出名称' },\n  { key: 'exit-timeout', rule: 'timeout', label: '超时时间' },\n  { key: 'image-path', rule: 'imagePath', label: '图片路径' },\n]\n\n/**\n * 验证应用表单\n * @param {Object} formData 表单数据\n * @returns {Object} 验证结果\n */\nexport function validateAppForm(formData) {\n  const results = {}\n  const errors = []\n\n  // 验证基础字段\n  for (const { key, rule, label } of FIELD_MAPPINGS) {\n    const result = validateField(rule, formData[key])\n    results[key] = result\n    if (!result.isValid) {\n      errors.push(`${label}: ${result.message}`)\n    }\n  }\n\n  // 验证准备命令\n  formData['prep-cmd']?.forEach((cmd, index) => {\n    if (!cmd.do?.trim() && !cmd.undo?.trim()) {\n      errors.push(`准备命令 ${index + 1}: 打开时执行命令或退出应用时要执行的命令至少需要填写一个`)\n    }\n  })\n\n  // 验证菜单命令\n  formData['menu-cmd']?.forEach((cmd, index) => {\n    if (!cmd.name?.trim()) {\n      errors.push(`菜单命令 ${index + 1}: 显示名称不能为空`)\n    }\n    if (!cmd.cmd?.trim()) {\n      errors.push(`菜单命令 ${index + 1}: 命令不能为空`)\n    }\n  })\n\n  // 验证独立命令\n  formData.detached?.forEach((cmd, index) => {\n    if (cmd && !cmd.trim()) {\n      errors.push(`独立命令 ${index + 1}: 命令不能为空`)\n    }\n  })\n\n  return {\n    isValid: errors.length === 0,\n    errors,\n    fieldResults: results,\n  }\n}\n\n/**\n * 验证文件\n * @param {File} file 文件对象\n * @param {Object} options 验证选项\n * @returns {Object} 验证结果\n */\nexport function validateFile(file, options = {}) {\n  const {\n    allowedTypes = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/bmp', 'image/webp'],\n    maxSize = 10 * 1024 * 1024,\n    minSize = 0,\n  } = options\n\n  if (!file) {\n    return { isValid: false, message: '请选择文件' }\n  }\n\n  if (!allowedTypes.includes(file.type)) {\n    return {\n      isValid: false,\n      message: `不支持的文件类型。支持的格式：${allowedTypes.join(', ')}`,\n    }\n  }\n\n  if (file.size > maxSize) {\n    return {\n      isValid: false,\n      message: `文件大小不能超过 ${(maxSize / (1024 * 1024)).toFixed(1)}MB`,\n    }\n  }\n\n  if (file.size < minSize) {\n    return {\n      isValid: false,\n      message: `文件大小不能小于 ${(minSize / 1024).toFixed(1)}KB`,\n    }\n  }\n\n  return { isValid: true, message: '' }\n}\n\n/**\n * 实时验证混合器\n * @param {Object} formData 表单数据\n * @param {Array} watchFields 需要监听的字段\n * @returns {Object} 验证状态\n */\nexport function createFormValidator(formData, watchFields = []) {\n  const validationStates = Object.fromEntries(watchFields.map((field) => [field, { isValid: true, message: '' }]))\n\n  return {\n    validateField(fieldName, value) {\n      const result = validateField(fieldName, value)\n      validationStates[fieldName] = result\n      return result\n    },\n\n    validateForm() {\n      return validateAppForm(formData)\n    },\n\n    getFieldState(fieldName) {\n      return validationStates[fieldName] ?? { isValid: true, message: '' }\n    },\n\n    getAllStates() {\n      return { ...validationStates }\n    },\n\n    resetValidation() {\n      for (const key of Object.keys(validationStates)) {\n        validationStates[key] = { isValid: true, message: '' }\n      }\n    },\n  }\n}\n\n/**\n * 创建防抖验证器\n * @param {Function} validationFn 验证函数\n * @param {number} delay 防抖延迟时间\n * @returns {Function} 防抖后的验证函数\n */\nexport function createDebouncedValidator(validationFn, delay = 300) {\n  let timeoutId\n\n  return function (...args) {\n    clearTimeout(timeoutId)\n    return new Promise((resolve) => {\n      timeoutId = setTimeout(() => resolve(validationFn(...args)), delay)\n    })\n  }\n}\n\nexport default {\n  validationRules,\n  validateField,\n  validateAppForm,\n  validateFile,\n  createFormValidator,\n  createDebouncedValidator,\n}\n"
  },
  {
    "path": "src_assets/common/assets/web/views/Apps.vue",
    "content": "<template>\n  <div>\n    <Navbar />\n    <div class=\"container-fluid px-4\">\n      <div class=\"my-4\">\n        <h1 class=\"page-title\">{{ $t('apps.applications_title') }}</h1>\n        <p class=\"page-subtitle\">{{ $t('apps.applications_desc') }}</p>\n      </div>\n\n      <!-- 搜索栏和功能按钮 -->\n      <div class=\"search-container mb-4\">\n        <div class=\"search-box\">\n          <i class=\"fas fa-search search-icon\"></i>\n          <input\n            type=\"text\"\n            class=\"form-control search-input\"\n            :placeholder=\"$t('apps.search_placeholder')\"\n            v-model=\"searchQuery\"\n            @input=\"debouncedSearch\"\n          />\n          <button v-if=\"searchQuery\" class=\"btn-clear-search\" @click=\"clearSearch\">\n            <i class=\"fas fa-times\"></i>\n          </button>\n        </div>\n\n        <!-- 功能按钮组 -->\n        <div class=\"action-buttons\">\n          <div class=\"view-toggle-group\">\n            <button\n              class=\"view-toggle-btn\"\n              :class=\"{ active: viewMode === 'grid' }\"\n              @click=\"viewMode = 'grid'\"\n              title=\"网格视图\"\n            >\n              <i class=\"fas fa-th\"></i>\n            </button>\n            <button\n              class=\"view-toggle-btn\"\n              :class=\"{ active: viewMode === 'list' }\"\n              @click=\"viewMode = 'list'\"\n              title=\"列表视图\"\n            >\n              <i class=\"fas fa-list\"></i>\n            </button>\n          </div>\n\n          <button class=\"cute-btn cute-btn-primary\" @click=\"newApp\" :title=\"$t('apps.add_new')\">\n            <i class=\"fas fa-plus\"></i>\n          </button>\n          <button\n            v-if=\"isTauriEnv()\"\n            class=\"cute-btn cute-btn-info\"\n            @click=\"scanGameLibraries()\"\n            :disabled=\"isScanning\"\n            title=\"扫描游戏平台库 (Steam/Epic/GOG)\"\n            aria-label=\"扫描游戏平台库 (Steam/Epic/GOG)\"\n          >\n            <i class=\"fas\" :class=\"isScanning ? 'fa-spinner fa-spin' : 'fa-gamepad'\"></i>\n          </button>\n          <button\n            class=\"cute-btn cute-btn-secondary\"\n            data-bs-toggle=\"modal\"\n            data-bs-target=\"#envVarsModal\"\n            title=\"环境变量说明\"\n          >\n            <i class=\"fas fa-info-circle\"></i>\n          </button>\n          <button \n            class=\"cute-btn cute-btn-success\" \n            :class=\"{ 'has-changes': hasUnsavedChanges() }\"\n            @click=\"save\" \n            :disabled=\"!hasUnsavedChanges() || isSaving\"\n            :title=\"hasUnsavedChanges() ? $t('_common.save') : $t('_common.no_changes')\"\n          >\n            <i class=\"fas fa-save\"></i>\n            <span v-if=\"hasUnsavedChanges()\" class=\"unsaved-indicator\"></span>\n          </button>\n        </div>\n      </div>\n\n      <!-- 应用卡片列表 -->\n      <div class=\"apps-grid-container\">\n        <!-- 网格视图 - 拖拽模式 -->\n        <draggable\n          v-if=\"viewMode === 'grid' && !searchQuery\"\n          v-model=\"apps\"\n          item-key=\"name\"\n          class=\"apps-grid\"\n          :animation=\"300\"\n          :delay=\"0\"\n          :disabled=\"false\"\n          ghost-class=\"app-card-ghost\"\n          chosen-class=\"app-card-chosen\"\n          drag-class=\"app-card-drag\"\n          @start=\"onDragStart\"\n          @end=\"onDragEnd\"\n        >\n          <template #item=\"{ element: app, index }\">\n            <AppCard\n              :app=\"app\"\n              :draggable=\"true\"\n              :is-drag-result=\"false\"\n              :is-dragging=\"isDragging\"\n              @edit=\"editApp(index)\"\n              @delete=\"showDeleteForm(index)\"\n              @copy-success=\"handleCopySuccess\"\n              @copy-error=\"handleCopyError\"\n            />\n          </template>\n        </draggable>\n\n        <!-- 网格视图 - 搜索模式 -->\n        <div v-else-if=\"viewMode === 'grid' && searchQuery\" class=\"apps-grid\">\n          <AppCard\n            v-for=\"(app, index) in filteredApps\"\n            :key=\"`search-grid-${app.name}-${index}`\"\n            :app=\"app\"\n            :draggable=\"false\"\n            :is-search-result=\"true\"\n            :is-dragging=\"false\"\n            @edit=\"editApp(getOriginalIndex(app, index))\"\n            @delete=\"showDeleteForm(getOriginalIndex(app, index))\"\n            @copy-success=\"handleCopySuccess\"\n            @copy-error=\"handleCopyError\"\n          />\n        </div>\n\n        <!-- 列表视图 - 拖拽模式 -->\n        <draggable\n          v-else-if=\"viewMode === 'list' && !searchQuery\"\n          v-model=\"apps\"\n          item-key=\"name\"\n          class=\"apps-list\"\n          :animation=\"300\"\n          :delay=\"0\"\n          :disabled=\"false\"\n          ghost-class=\"app-list-item-ghost\"\n          chosen-class=\"app-list-item-chosen\"\n          drag-class=\"app-list-item-drag\"\n          @start=\"onDragStart\"\n          @end=\"onDragEnd\"\n        >\n          <template #item=\"{ element: app, index }\">\n            <AppListItem\n              :app=\"app\"\n              :draggable=\"true\"\n              :is-dragging=\"isDragging\"\n              @edit=\"editApp(index)\"\n              @delete=\"showDeleteForm(index)\"\n              @copy-success=\"handleCopySuccess\"\n              @copy-error=\"handleCopyError\"\n            />\n          </template>\n        </draggable>\n\n        <!-- 列表视图 - 搜索模式 -->\n        <div v-else-if=\"viewMode === 'list' && searchQuery\" class=\"apps-list\">\n          <AppListItem\n            v-for=\"(app, index) in filteredApps\"\n            :key=\"`search-list-${app.name}-${index}`\"\n            :app=\"app\"\n            :draggable=\"false\"\n            :is-search-result=\"true\"\n            :is-dragging=\"false\"\n            @edit=\"editApp(getOriginalIndex(app, index))\"\n            @delete=\"showDeleteForm(getOriginalIndex(app, index))\"\n            @copy-success=\"handleCopySuccess\"\n            @copy-error=\"handleCopyError\"\n          />\n        </div>\n\n        <!-- 空状态 - 搜索无结果 -->\n        <div v-if=\"searchQuery && filteredApps.length === 0\" class=\"empty-state\">\n          <div class=\"empty-icon\">\n            <i class=\"fas fa-search\"></i>\n          </div>\n          <h3 class=\"empty-title\">未找到匹配的应用</h3>\n          <p class=\"empty-subtitle\">尝试使用不同的搜索关键词</p>\n        </div>\n\n        <!-- 空状态 - 无应用 -->\n        <div v-if=\"!searchQuery && apps.length === 0 && isLoaded\" class=\"empty-state\">\n          <div class=\"empty-icon\">\n            <i class=\"fas fa-rocket\"></i>\n          </div>\n          <h3 class=\"empty-title\">暂无应用</h3>\n          <p class=\"empty-subtitle\">点击下方按钮添加第一个应用</p>\n          <button class=\"btn btn-primary\" @click=\"newApp\">\n            <i class=\"fas fa-plus me-1\"></i>{{ $t('apps.add_new') }}\n          </button>\n        </div>\n      </div>\n\n      <!-- 应用编辑器 -->\n      <AppEditor\n        v-if=\"editingApp\"\n        :app=\"editingApp\"\n        :platform=\"platform\"\n        :disabled=\"isSaving\"\n        @save-app=\"handleSaveApp\"\n        @close=\"closeAppEditor\"\n      />\n\n      <!-- 提示消息 -->\n      <div v-if=\"message\" class=\"alert-toast\" :class=\"messageClass\">\n        <i class=\"fas\" :class=\"getMessageIcon()\"></i>\n        <span>{{ message }}</span>\n        <button class=\"btn-close-toast\" @click=\"message = ''\">\n          <i class=\"fas fa-times\"></i>\n        </button>\n      </div>\n\n      <!-- 扫描结果模态框 -->\n      <ScanResultModal\n        :show=\"showScanResult\"\n        :apps=\"scannedApps\"\n        :saving=\"isSaving\"\n        @close=\"closeScanResult\"\n        @edit=\"handleScanEdit\"\n        @quick-add=\"quickAddScannedApp\"\n        @remove=\"removeScannedApp\"\n        @add-all=\"addAllScannedApps\"\n      />\n\n      <!-- 环境变量说明模态框 -->\n      <div id=\"envVarsModal\" class=\"modal fade\" tabindex=\"-1\">\n        <div class=\"modal-dialog modal-lg env-vars-modal\">\n          <div class=\"modal-content\">\n            <div class=\"modal-header\">\n              <h5 id=\"envVarsModalLabel\" class=\"modal-title\">\n                <i class=\"fas fa-info-circle me-2\"></i>{{ $t('apps.env_vars_about') }}\n              </h5>\n              <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>\n            </div>\n            <div class=\"modal-body\">\n              <div class=\"alert alert-info\">\n                <div class=\"form-text\">\n                  <h6>{{ $t('apps.env_vars_about') }}</h6>\n                  {{ $t('apps.env_vars_desc') }}\n                </div>\n              </div>\n              <div class=\"env-vars-table\">\n                <div class=\"table-responsive\">\n                  <table class=\"table table-sm\">\n                    <thead>\n                      <tr>\n                        <th>\n                          <i class=\"fas fa-code me-1\"></i>{{ $t('apps.env_var_name') }}\n                        </th>\n                        <th>\n                          <i class=\"fas fa-info-circle me-1\"></i>{{ $t('_common.description') }}\n                        </th>\n                      </tr>\n                    </thead>\n                    <tbody>\n                      <tr v-for=\"(desc, varName) in envVars\" :key=\"varName\">\n                        <td>\n                          <code class=\"env-var-name\">{{ varName }}</code>\n                        </td>\n                        <td>{{ desc }}</td>\n                      </tr>\n                    </tbody>\n                  </table>\n                </div>\n              </div>\n              <div class=\"mt-3\">\n                <template v-if=\"platform === 'windows'\">\n                  <div class=\"form-text\">\n                    <strong>{{ $t('apps.env_qres_example') }}</strong>\n                    <pre class=\"code-example\">\ncmd /C &lt;{{\n                        $t('apps.env_qres_path')\n                      }}&gt;\\QRes.exe /X:%SUNSHINE_CLIENT_WIDTH% /Y:%SUNSHINE_CLIENT_HEIGHT% /R:%SUNSHINE_CLIENT_FPS%</pre\n                    >\n                  </div>\n                </template>\n                <template v-else-if=\"platform === 'linux'\">\n                  <div class=\"form-text\">\n                    <strong>{{ $t('apps.env_xrandr_example') }}</strong>\n                    <pre class=\"code-example\">\nsh -c \"xrandr --output HDMI-1 --mode \\\"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}\\\" --rate ${SUNSHINE_CLIENT_FPS}\"</pre\n                    >\n                  </div>\n                </template>\n                <template v-else-if=\"platform === 'macos'\">\n                  <div class=\"form-text\">\n                    <strong>{{ $t('apps.env_displayplacer_example') }}</strong>\n                    <pre class=\"code-example\">\nsh -c \"displayplacer \"id:&lt;screenId&gt; res:${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} hz:${SUNSHINE_CLIENT_FPS} scaling:on origin:(0,0) degree:0\"\"</pre\n                    >\n                  </div>\n                </template>\n              </div>\n            </div>\n            <div class=\"modal-footer\">\n              <a\n                href=\"https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2app__examples.html\"\n                target=\"_blank\"\n                class=\"btn btn-outline-primary\"\n              >\n                <i class=\"fas fa-external-link-alt me-1\"></i>{{ $t('_common.see_more') }}\n              </a>\n              <button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">\n                <i class=\"fas fa-times me-1\"></i>关闭\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- 删除确认对话框 -->\n    <Transition name=\"fade\">\n      <div v-if=\"deleteConfirmIndex !== null\" class=\"delete-app-overlay\" @click.self=\"cancelDeleteApp\">\n        <div class=\"delete-app-modal\">\n          <div class=\"delete-app-header\">\n            <h5>\n              <i class=\"fas fa-exclamation-triangle me-2\"></i>{{ $t('_common.delete') }}\n            </h5>\n            <button class=\"btn-close\" @click=\"cancelDeleteApp\"></button>\n          </div>\n          <div class=\"delete-app-body\">\n            <p>{{ $t('apps.delete_confirm', { name: apps[deleteConfirmIndex]?.name || '' }) }}</p>\n          </div>\n          <div class=\"delete-app-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" @click=\"cancelDeleteApp\">{{ $t('_common.cancel') }}</button>\n            <button type=\"button\" class=\"btn btn-danger\" @click=\"confirmDeleteApp\">\n              {{ $t('_common.delete') }}\n            </button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n  </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, watch } from 'vue'\nimport draggable from 'vuedraggable-es'\nimport Navbar from '../components/layout/Navbar.vue'\nimport AppEditor from '../components/AppEditor.vue'\nimport AppCard from '../components/AppCard.vue'\nimport AppListItem from '../components/AppListItem.vue'\nimport ScanResultModal from '../components/ScanResultModal.vue'\nimport { useApps } from '../composables/useApps.js'\nimport { initFirebase, trackEvents } from '../config/firebase.js'\nimport { useI18n } from 'vue-i18n'\n\nconst { t } = useI18n()\n\nconst isLoaded = ref(false)\n\nconst {\n  apps,\n  filteredApps,\n  searchQuery,\n  editingApp,\n  platform,\n  isSaving,\n  isDragging,\n  viewMode,\n  message,\n  envVars,\n  debouncedSearch,\n  messageClass,\n  isScanning,\n  scannedApps,\n  showScanResult,\n  loadApps,\n  loadPlatform,\n  clearSearch,\n  getOriginalIndex,\n  newApp,\n  editApp,\n  closeAppEditor,\n  handleSaveApp,\n  showDeleteForm,\n  cancelDeleteApp,\n  confirmDeleteApp,\n  deleteConfirmIndex,\n  save,\n  hasUnsavedChanges,\n  onDragStart,\n  onDragEnd,\n  scanDirectory,\n  scanGameLibraries,\n  addScannedApp,\n  addAllScannedApps,\n  closeScanResult,\n  removeScannedApp,\n  quickAddScannedApp,\n  isTauriEnv,\n  getMessageIcon,\n  handleCopySuccess,\n  handleCopyError,\n  init,\n} = useApps()\n\nconst initEnvVarsModal = () => {\n  try {\n    const modalElement = document.getElementById('envVarsModal')\n    if (modalElement && window.bootstrap?.Modal) {\n      new window.bootstrap.Modal(modalElement)\n    }\n  } catch (error) {\n    console.warn('Environment variables modal initialization failed:', error)\n  }\n}\n\nonMounted(async () => {\n  initFirebase()\n  trackEvents.pageView('applications')\n  init(t)\n  initEnvVarsModal()\n\n  await Promise.all([loadApps(), loadPlatform()])\n  isLoaded.value = true\n})\n\nwatch(searchQuery, () => {\n  debouncedSearch.value?.()\n})\n\n// 处理扫描结果编辑\nconst handleScanEdit = (app) => {\n  addScannedApp(app)\n  closeScanResult()\n}\n</script>\n\n<style>\n@import '../styles/apps.less';\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/views/Config.vue",
    "content": "<template>\n  <div class=\"page-config\">\n    <Navbar />\n    <div class=\"config-floating-buttons\">\n      <button\n        class=\"cute-btn cute-btn-primary\"\n        :class=\"{ 'has-unsaved': hasUnsaved }\"\n        @click=\"save\"\n        :title=\"hasUnsaved ? $t('config.unsaved_changes_tooltip') : $t('_common.save')\"\n      >\n        <i class=\"fas fa-save\"></i>\n      </button>\n      <button v-if=\"saved && !restarted\" class=\"cute-btn cute-btn-success\" @click=\"apply\" :title=\"$t('_common.apply')\">\n        <i class=\"fas fa-check\"></i>\n      </button>\n      <div class=\"floating-toast-container\">\n        <Transition name=\"toast\">\n          <div\n            v-if=\"showSaveToast\"\n            class=\"toast align-items-center text-bg-success border-0 show\"\n            role=\"alert\"\n            aria-live=\"assertive\"\n            aria-atomic=\"true\"\n          >\n            <div class=\"d-flex\">\n              <div class=\"toast-body\">\n                <i class=\"fas fa-check-circle me-2\"></i>\n                <b>{{ $t('_common.success') }}</b> {{ $t('config.apply_note') }}\n              </div>\n              <button\n                type=\"button\"\n                class=\"btn-close btn-close-white me-2 m-auto\"\n                @click=\"showSaveToast = false\"\n                aria-label=\"Close\"\n              ></button>\n            </div>\n          </div>\n        </Transition>\n        <Transition name=\"toast\">\n          <div\n            v-if=\"showRestartToast\"\n            class=\"toast align-items-center text-bg-success border-0 mt-2 show\"\n            role=\"alert\"\n            aria-live=\"assertive\"\n            aria-atomic=\"true\"\n          >\n            <div class=\"d-flex\">\n              <div class=\"toast-body\">\n                <i class=\"fas fa-check-circle me-2\"></i>\n                <b>{{ $t('_common.success') }}</b> {{ $t('config.restart_note') }}\n              </div>\n              <button\n                type=\"button\"\n                class=\"btn-close btn-close-white me-2 m-auto\"\n                @click=\"showRestartToast = false\"\n                aria-label=\"Close\"\n              ></button>\n            </div>\n          </div>\n        </Transition>\n      </div>\n    </div>\n    <div class=\"container\">\n      <h1 class=\"my-4 page-title\">{{ $t('config.configuration') }}</h1>\n\n      <div v-if=\"!config\" class=\"form card config-skeleton\">\n        <div class=\"card-header skeleton-header\">\n          <div class=\"skeleton-tabs\">\n            <div v-for=\"n in 6\" :key=\"n\" class=\"skeleton-tab\"></div>\n          </div>\n        </div>\n        <div class=\"config-page skeleton-body\">\n          <div class=\"skeleton-section\">\n            <div class=\"skeleton-title\"></div>\n            <div v-for=\"n in 4\" :key=\"n\" class=\"skeleton-row\">\n              <div class=\"skeleton-label\"></div>\n              <div class=\"skeleton-input\"></div>\n            </div>\n          </div>\n          <div class=\"skeleton-section\">\n            <div class=\"skeleton-title\"></div>\n            <div v-for=\"n in 3\" :key=\"n\" class=\"skeleton-row\">\n              <div class=\"skeleton-label\"></div>\n              <div class=\"skeleton-input\"></div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div v-else class=\"form card\">\n        <ul class=\"nav nav-tabs config-tabs card-header\">\n          <template v-for=\"tab in tabs\" :key=\"tab.id\">\n            <li\n              v-if=\"tab.type === 'group' && tab.children\"\n              class=\"nav-item dropdown\"\n              :class=\"{ active: isEncoderTabActive(tab), show: expandedDropdown === tab.id }\"\n            >\n              <a\n                class=\"nav-link dropdown-toggle\"\n                :class=\"{ active: isEncoderTabActive(tab) }\"\n                href=\"#\"\n                role=\"button\"\n                :aria-expanded=\"expandedDropdown === tab.id\"\n                @click.prevent=\"toggleEncoderDropdown(tab.id, $event)\"\n              >\n                {{ $t(`tabs.${tab.id}`) || tab.name }}\n              </a>\n              <ul class=\"dropdown-menu\" :class=\"{ show: expandedDropdown === tab.id }\">\n                <li v-for=\"childTab in tab.children\" :key=\"childTab.id\">\n                  <a\n                    class=\"dropdown-item\"\n                    :class=\"[{ active: currentTab === childTab.id }, `encoder-item-${childTab.id}`]\"\n                    href=\"#\"\n                    @click.prevent=\"selectEncoderTab(childTab.id, $event)\"\n                  >\n                    {{ $t(`tabs.${childTab.id}`) || childTab.name }}\n                  </a>\n                </li>\n              </ul>\n            </li>\n            <li v-else class=\"nav-item\">\n              <a\n                class=\"nav-link\"\n                :class=\"{ active: tab.id === currentTab }\"\n                href=\"#\"\n                @click.prevent=\"currentTab = tab.id\"\n              >\n                {{ $t(`tabs.${tab.id}`) || tab.name }}\n              </a>\n            </li>\n          </template>\n        </ul>\n\n        <General\n          v-if=\"currentTab === 'general'\"\n          :config=\"config\"\n          :global-prep-cmd=\"global_prep_cmd\"\n          :platform=\"platform\"\n        />\n        <Inputs v-if=\"currentTab === 'input'\" :config=\"config\" :platform=\"platform\" />\n        <AudioVideo\n          v-if=\"currentTab === 'av'\"\n          :config=\"config\"\n          :platform=\"platform\"\n          :resolutions=\"resolutions\"\n          :fps=\"fps\"\n          :display-mode-remapping=\"display_mode_remapping\"\n        />\n        <Network v-if=\"currentTab === 'network'\" :config=\"config\" :platform=\"platform\" />\n        <Files v-if=\"currentTab === 'files'\" :config=\"config\" :platform=\"platform\" />\n        <Advanced v-if=\"currentTab === 'advanced'\" :config=\"config\" :platform=\"platform\" />\n        <ContainerEncoders :current-tab=\"currentTab\" :config=\"config\" :platform=\"platform\" />\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { ref, watch, onMounted, provide, computed, onUnmounted } from 'vue'\nimport Navbar from '../components/layout/Navbar.vue'\nimport General from '../configs/tabs/General.vue'\nimport Inputs from '../configs/tabs/Inputs.vue'\nimport Network from '../configs/tabs/Network.vue'\nimport Files from '../configs/tabs/Files.vue'\nimport Advanced from '../configs/tabs/Advanced.vue'\nimport AudioVideo from '../configs/tabs/AudioVideo.vue'\nimport ContainerEncoders from '../configs/tabs/ContainerEncoders.vue'\nimport { useConfig } from '../composables/useConfig.js'\nimport { initFirebase, trackEvents } from '../config/firebase.js'\n\ninitFirebase()\n\nconst {\n  platform,\n  saved,\n  restarted,\n  config,\n  fps,\n  resolutions,\n  currentTab,\n  global_prep_cmd,\n  display_mode_remapping,\n  tabs,\n  initTabs,\n  loadConfig,\n  save,\n  apply,\n  handleHash,\n  hasUnsavedChanges,\n} = useConfig()\n\nconst showSaveToast = ref(false)\nconst showRestartToast = ref(false)\nconst expandedDropdown = ref(null)\n\nconst hasUnsaved = computed(() => {\n  if (!config.value) return false\n  void config.value\n  void fps.value\n  void resolutions.value\n  void global_prep_cmd.value\n  void display_mode_remapping.value\n  return hasUnsavedChanges()\n})\n\nconst isEncoderTabActive = (tab) => tab.type === 'group' && tab.children?.some((child) => child.id === currentTab.value)\n\nconst toggleEncoderDropdown = (tabId, event) => {\n  event.stopPropagation()\n\n  if (expandedDropdown.value === tabId) {\n    expandedDropdown.value = null\n    return\n  }\n\n  expandedDropdown.value = tabId\n\n  const encoderGroup = tabs.value.find((t) => t.id === tabId && t.type === 'group')\n  const children = encoderGroup?.children\n\n  if (children?.length && !children.some((child) => child.id === currentTab.value)) {\n    currentTab.value = children[0].id\n  }\n}\n\nconst selectEncoderTab = (childTabId, event) => {\n  event.stopPropagation()\n  currentTab.value = childTabId\n  expandedDropdown.value = null\n}\n\nconst showToast = (toastRef, duration = 5000) => {\n  toastRef.value = true\n  setTimeout(() => {\n    toastRef.value = false\n  }, duration)\n}\n\nwatch(saved, (newVal) => {\n  if (newVal && !restarted.value) {\n    showToast(showSaveToast)\n  }\n})\n\nwatch(restarted, (newVal) => {\n  if (newVal) {\n    showSaveToast.value = false\n    showToast(showRestartToast)\n  }\n})\n\nprovide(\n  'platform',\n  computed(() => platform.value)\n)\n\nconst handleOutsideClick = (event) => {\n  if (expandedDropdown.value && !event.target.closest('.dropdown')) {\n    expandedDropdown.value = null\n  }\n}\n\nonMounted(async () => {\n  trackEvents.pageView('configuration')\n  initTabs()\n  await loadConfig()\n  handleHash()\n\n  window.addEventListener('hashchange', handleHash)\n  document.addEventListener('click', handleOutsideClick)\n})\n\nonUnmounted(() => {\n  window.removeEventListener('hashchange', handleHash)\n  document.removeEventListener('click', handleOutsideClick)\n})\n</script>\n\n<style lang=\"less\">\n@import '../styles/global.less';\n\n// Variables\n@transition-fast: 0.3s;\n@border-radius-sm: 2px;\n@border-radius-md: 10px;\n@border-radius-lg: 12px;\n@btn-size: 56px;\n@btn-size-mobile: 48px;\n@cubic-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);\n@cubic-smooth: cubic-bezier(0.4, 0, 0.2, 1);\n\n// Encoder brand colors\n@color-nvidia: #76b900;\n@color-amd: #ed1c24;\n@color-intel: #0071c5;\n\n// Mixins\n.flex-center() {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.transition(@properties: all) {\n  transition: @properties @transition-fast @cubic-smooth;\n}\n\n.skeleton-gradient(@light: 0.08, @mid: 0.12) {\n  background: linear-gradient(90deg, rgba(0, 0, 0, @light) 25%, rgba(0, 0, 0, @mid) 50%, rgba(0, 0, 0, @light) 75%);\n  background-size: 200% 100%;\n  animation: skeleton-shimmer 1.5s infinite;\n}\n\n.config-page {\n  padding: 1em;\n  border: 1px solid transparent;\n  border-top: none;\n}\n\n.config-skeleton {\n  .skeleton-header {\n    background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.1));\n    border-radius: @border-radius-lg @border-radius-lg 0 0;\n    padding: 0.5rem 1rem;\n  }\n\n  .skeleton-tabs {\n    display: flex;\n    gap: 0.5rem;\n    padding: 0.5rem 0;\n  }\n\n  .skeleton-tab {\n    width: 80px;\n    height: 38px;\n    .skeleton-gradient();\n    border-radius: @border-radius-md;\n  }\n\n  .skeleton-body {\n    padding: 1.5rem;\n  }\n\n  .skeleton-section {\n    margin-bottom: 2rem;\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  .skeleton-title {\n    width: 150px;\n    height: 24px;\n    .skeleton-gradient();\n    border-radius: 4px;\n    margin-bottom: 1rem;\n  }\n\n  .skeleton-row {\n    display: flex;\n    align-items: center;\n    gap: 1rem;\n    margin-bottom: 1rem;\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  .skeleton-label {\n    width: 120px;\n    height: 16px;\n    .skeleton-gradient(0.06, 0.1);\n    border-radius: 4px;\n    flex-shrink: 0;\n  }\n\n  .skeleton-input {\n    flex: 1;\n    height: 38px;\n    .skeleton-gradient(0.06, 0.1);\n    border-radius: 6px;\n    max-width: 300px;\n  }\n}\n\n@keyframes skeleton-shimmer {\n  0% {\n    background-position: 200% 0;\n  }\n  100% {\n    background-position: -200% 0;\n  }\n}\n\n[data-bs-theme='dark'] .config-skeleton {\n  .skeleton-header {\n    background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));\n  }\n\n  .skeleton-tab,\n  .skeleton-title,\n  .skeleton-label,\n  .skeleton-input {\n    background: linear-gradient(\n      90deg,\n      rgba(255, 255, 255, 0.06) 25%,\n      rgba(255, 255, 255, 0.1) 50%,\n      rgba(255, 255, 255, 0.06) 75%\n    );\n    background-size: 200% 100%;\n    animation: skeleton-shimmer 1.5s infinite;\n  }\n}\n\n.page-config {\n  .nav-tabs {\n    border: none;\n  }\n\n  .ms-item {\n    border: 1px solid;\n    border-radius: @border-radius-sm;\n    font-size: 12px;\n    font-weight: bold;\n  }\n\n  .config-tabs {\n    background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.1));\n    border-radius: @border-radius-lg @border-radius-lg 0 0;\n    padding: 0.5rem 1rem 0;\n    gap: 0.5rem;\n    border-bottom: 1px solid rgba(0, 0, 0, 0.1);\n    position: relative;\n    z-index: 10;\n    overflow: visible;\n\n    .nav-item {\n      margin-bottom: -1px;\n\n      &.dropdown {\n        position: relative;\n        &.show .dropdown-menu {\n          display: block;\n        }\n      }\n    }\n\n    .nav-link {\n      border: none;\n      border-radius: @border-radius-md @border-radius-md 0 0;\n      padding: 0.75rem 1.5rem;\n      font-weight: 500;\n      color: var(--bs-secondary-color);\n      background: transparent;\n      position: relative;\n      overflow: hidden;\n      .transition();\n\n      &::before {\n        content: '';\n        position: absolute;\n        bottom: 0;\n        left: 50%;\n        transform: translateX(-50%) scaleX(0);\n        width: 80%;\n        height: 3px;\n        background: linear-gradient(90deg, var(--bs-primary), var(--bs-info));\n        border-radius: 3px 3px 0 0;\n        .transition(transform);\n      }\n\n      &:hover {\n        color: var(--bs-primary);\n        background: rgba(var(--bs-primary-rgb), 0.08);\n      }\n\n      &.active {\n        color: var(--bs-primary);\n        background: var(--bs-body-bg);\n        box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);\n        font-weight: 600;\n\n        &::before {\n          transform: translateX(-50%) scaleX(1);\n        }\n      }\n\n      &.dropdown-toggle::after {\n        margin-left: 0.5em;\n        .transition(transform);\n      }\n\n      &.dropdown-toggle[aria-expanded='true']::after {\n        transform: rotate(180deg);\n      }\n    }\n\n    .dropdown-menu {\n      display: none;\n      position: absolute;\n      top: 100%;\n      left: 0;\n      z-index: 1050;\n      min-width: 200px;\n      margin-top: 0.25rem;\n      padding: 0.5rem 0;\n      border-radius: @border-radius-md;\n      border: 1px solid rgba(0, 0, 0, 0.1);\n      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n      background: rgba(var(--bs-body-bg-rgb), 0.95);\n      backdrop-filter: blur(10px);\n\n      &.show {\n        display: block;\n      }\n\n      .dropdown-item {\n        display: flex;\n        align-items: center;\n        padding: 0.5rem 1.5rem;\n        font-weight: 500;\n        text-decoration: none;\n        .transition();\n\n        &.encoder-item-nv {\n          color: @color-nvidia;\n        }\n        &.encoder-item-amd {\n          color: @color-amd;\n        }\n        &.encoder-item-qsv {\n          color: @color-intel;\n        }\n        &.encoder-item-sw {\n          color: var(--bs-secondary-color);\n        }\n\n        &:hover {\n          background: rgba(var(--bs-primary-rgb), 0.08);\n        }\n        &.active {\n          background: rgba(var(--bs-primary-rgb), 0.15);\n          font-weight: 600;\n        }\n      }\n    }\n  }\n}\n\n// Toast transitions\n.toast-enter-active,\n.toast-leave-active {\n  transition: opacity @transition-fast ease-in-out;\n}\n\n.toast-enter-from,\n.toast-leave-to {\n  opacity: 0;\n}\n\n.toast.show {\n  opacity: 1;\n}\n\n.config-floating-buttons {\n  position: sticky;\n  top: 80%;\n  right: 2rem;\n  float: right;\n  clear: right;\n  margin: 2rem 0;\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  z-index: 1000;\n\n  .floating-toast-container {\n    position: absolute;\n    right: calc(100% + 1rem);\n    top: 0;\n    width: max-content;\n    max-width: 300px;\n\n    .toast {\n      margin-bottom: 0.5rem;\n    }\n  }\n\n  .cute-btn {\n    width: @btn-size;\n    height: @btn-size;\n    border-radius: 50%;\n    border: 3px solid rgba(255, 255, 255, 0.4);\n    color: #fff;\n    font-size: 1.25rem;\n    cursor: pointer;\n    position: relative;\n    overflow: hidden;\n    .transition();\n    .flex-center();\n\n    &::before {\n      content: '';\n      position: absolute;\n      top: -50%;\n      left: -50%;\n      width: 200%;\n      height: 200%;\n      background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);\n      opacity: 0;\n      .transition(opacity);\n    }\n\n    &::after {\n      content: '';\n      position: absolute;\n      top: 20%;\n      left: 20%;\n      width: 30%;\n      height: 30%;\n      background: radial-gradient(circle, rgba(255, 255, 255, 0.6) 0%, transparent 70%);\n      border-radius: 50%;\n      opacity: 0.8;\n    }\n\n    &:hover {\n      transform: scale(1.1) translateY(-2px);\n      box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2), 0 0 30px rgba(255, 255, 255, 0.3);\n\n      &::before,\n      &::after {\n        opacity: 1;\n      }\n    }\n\n    &:active {\n      transform: scale(0.95) translateY(0);\n      transition: transform 0.1s @cubic-bounce;\n    }\n\n    &-primary {\n      background: linear-gradient(135deg, #ff6b9d, #c44569, #f093fb);\n      background-size: 200% 200%;\n      box-shadow: 0 4px 15px rgba(255, 107, 157, 0.4), 0 0 20px rgba(255, 107, 157, 0.2),\n        inset 0 1px 0 rgba(255, 255, 255, 0.3);\n\n      &:hover {\n        animation: gradient-shift 1.5s ease infinite;\n        box-shadow: 0 8px 25px rgba(255, 107, 157, 0.6), 0 0 40px rgba(255, 107, 157, 0.4),\n          inset 0 1px 0 rgba(255, 255, 255, 0.4);\n      }\n\n      &.has-unsaved {\n        animation: pulse-warning 2s ease-in-out 3;\n        box-shadow: 0 4px 15px rgba(255, 107, 157, 0.4), 0 0 20px rgba(255, 107, 157, 0.2),\n          0 0 0 3px rgba(255, 193, 7, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.3);\n\n        &:hover {\n          animation: gradient-shift 1.5s ease infinite, pulse-warning 2s ease-in-out 3;\n          box-shadow: 0 8px 25px rgba(255, 107, 157, 0.6), 0 0 40px rgba(255, 107, 157, 0.4),\n            0 0 0 4px rgba(255, 193, 7, 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.4);\n        }\n      }\n    }\n\n    &-success {\n      background: linear-gradient(135deg, #4facfe, #00f2fe, #43e97b);\n      background-size: 200% 200%;\n      box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4), 0 0 20px rgba(79, 172, 254, 0.2),\n        inset 0 1px 0 rgba(255, 255, 255, 0.3);\n\n      &:hover {\n        animation: gradient-shift 1.5s ease infinite;\n        box-shadow: 0 8px 25px rgba(79, 172, 254, 0.6), 0 0 40px rgba(79, 172, 254, 0.4),\n          inset 0 1px 0 rgba(255, 255, 255, 0.4);\n      }\n    }\n\n    i {\n      position: relative;\n      z-index: 1;\n      .transition(transform);\n    }\n\n    &:hover i {\n      transform: scale(1.2) rotate(5deg);\n    }\n  }\n\n  @keyframes gradient-shift {\n    0%,\n    100% {\n      background-position: 0% 50%;\n    }\n    50% {\n      background-position: 100% 50%;\n    }\n  }\n\n  @keyframes pulse-warning {\n    0%,\n    100% {\n      box-shadow: 0 4px 15px rgba(255, 107, 157, 0.4), 0 0 20px rgba(255, 107, 157, 0.2),\n        0 0 0 3px rgba(255, 193, 7, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.3);\n    }\n    50% {\n      box-shadow: 0 4px 15px rgba(255, 107, 157, 0.4), 0 0 20px rgba(255, 107, 157, 0.2),\n        0 0 0 5px rgba(255, 193, 7, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.3);\n    }\n  }\n}\n\n// Dark mode\n[data-bs-theme='dark'] .page-config .config-tabs {\n  background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));\n  border-bottom-color: rgba(255, 255, 255, 0.1);\n\n  .nav-link {\n    &:hover {\n      background: rgba(var(--bs-primary-rgb), 0.15);\n    }\n    &.active {\n      box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);\n    }\n  }\n\n  .dropdown-menu {\n    border-color: rgba(255, 255, 255, 0.1);\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n\n    .dropdown-item {\n      &:hover {\n        background: rgba(var(--bs-primary-rgb), 0.15);\n      }\n      &.active {\n        background: rgba(var(--bs-primary-rgb), 0.25);\n      }\n    }\n  }\n}\n\n[data-bs-theme='dark'] .config-floating-buttons .cute-btn {\n  border-color: rgba(255, 255, 255, 0.5);\n\n  &-primary {\n    box-shadow: 0 4px 15px rgba(255, 107, 157, 0.5), 0 0 25px rgba(255, 107, 157, 0.3),\n      inset 0 1px 0 rgba(255, 255, 255, 0.4);\n\n    &:hover {\n      box-shadow: 0 8px 25px rgba(255, 107, 157, 0.7), 0 0 50px rgba(255, 107, 157, 0.5),\n        inset 0 1px 0 rgba(255, 255, 255, 0.5);\n    }\n  }\n\n  &-success {\n    box-shadow: 0 4px 15px rgba(79, 172, 254, 0.5), 0 0 25px rgba(79, 172, 254, 0.3),\n      inset 0 1px 0 rgba(255, 255, 255, 0.4);\n\n    &:hover {\n      box-shadow: 0 8px 25px rgba(79, 172, 254, 0.7), 0 0 50px rgba(79, 172, 254, 0.5),\n        inset 0 1px 0 rgba(255, 255, 255, 0.5);\n    }\n  }\n}\n\n// Responsive\n@media (max-width: 768px) {\n  .config-floating-buttons {\n    position: fixed;\n    right: 1rem;\n    bottom: 1rem;\n    top: auto;\n    float: none;\n    margin: 0;\n    gap: 0.75rem;\n\n    .floating-toast-container {\n      right: auto;\n      left: auto;\n      top: auto;\n      bottom: calc(100% + 1rem);\n      max-width: calc(100vw - 2rem);\n    }\n\n    .cute-btn {\n      width: @btn-size-mobile;\n      height: @btn-size-mobile;\n      font-size: 1.1rem;\n    }\n  }\n\n  .page-config .config-tabs {\n    padding: 0.5rem 0.5rem 0;\n    gap: 0.25rem;\n    overflow-x: auto;\n    flex-wrap: nowrap;\n\n    .nav-link {\n      padding: 0.5rem 1rem;\n      font-size: 0.875rem;\n      white-space: nowrap;\n    }\n  }\n}\n\n// 无障碍：减少动态效果\n@media (prefers-reduced-motion: reduce) {\n  .config-floating-buttons .cute-btn {\n    animation: none !important;\n    transition: none !important;\n\n    &:hover {\n      animation: none !important;\n    }\n  }\n\n  .config-skeleton .skeleton-tab,\n  .config-skeleton .skeleton-title,\n  .config-skeleton .skeleton-label,\n  .config-skeleton .skeleton-input {\n    animation: none !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/views/Home.vue",
    "content": "<template>\n  <div>\n    <Navbar v-if=\"!showSetupWizard\" />\n\n    <!-- 首次设置向导 -->\n    <SetupWizard\n      v-if=\"showSetupWizard\"\n      :adapters=\"adapters\"\n      :display-devices=\"displayDevices\"\n      :has-locale=\"hasLocale\"\n      @setup-complete=\"onSetupComplete\"\n    />\n\n    <!-- 正常首页内容 -->\n    <div v-if=\"!showSetupWizard\" id=\"content\" class=\"container\">\n      <div class=\"page-header mt-2 mb-4\">\n        <h1 class=\"page-title\">\n          {{ $t('index.welcome') }}\n        </h1>\n        <p class=\"page-subtitle\">{{ $t('index.description') }}</p>\n      </div>\n\n      <!-- 错误日志 -->\n      <ErrorLogs :fatal-logs=\"fatalLogs\" />\n\n      <!-- 版本信息 -->\n      <VersionCard\n        :version=\"version\"\n        :github-version=\"githubVersion\"\n        :pre-release-version=\"preReleaseVersion\"\n        :notify-pre-releases=\"notifyPreReleases\"\n        :loading=\"loading\"\n        :installed-version-not-stable=\"installedVersionNotStable\"\n        :stable-build-available=\"stableBuildAvailable\"\n        :pre-release-build-available=\"preReleaseBuildAvailable\"\n        :build-version-is-dirty=\"buildVersionIsDirty\"\n        :parsed-stable-body=\"parsedStableBody\"\n        :parsed-pre-release-body=\"parsedPreReleaseBody\"\n      />\n\n      <!-- 资源卡片 -->\n      <div class=\"my-4\">\n        <ResourceCard />\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { onMounted } from 'vue'\nimport Navbar from '../components/layout/Navbar.vue'\nimport SetupWizard from '../components/SetupWizard.vue'\nimport ResourceCard from '../components/common/ResourceCard.vue'\nimport ErrorLogs from '../components/common/ErrorLogs.vue'\nimport VersionCard from '../components/common/VersionCard.vue'\nimport { useVersion } from '../composables/useVersion.js'\nimport { useLogs } from '../composables/useLogs.js'\nimport { useSetupWizard } from '../composables/useSetupWizard.js'\nimport { trackEvents } from '../config/firebase.js'\n\n// 使用组合式函数\nconst {\n  version,\n  githubVersion,\n  preReleaseVersion,\n  notifyPreReleases,\n  loading,\n  installedVersionNotStable,\n  stableBuildAvailable,\n  preReleaseBuildAvailable,\n  buildVersionIsDirty,\n  parsedStableBody,\n  parsedPreReleaseBody,\n  fetchVersions,\n} = useVersion()\n\nconst { fatalLogs, fetchLogs } = useLogs()\n\nconst { showSetupWizard, adapters, displayDevices, hasLocale, checkSetupWizard, onSetupComplete } = useSetupWizard()\n\n// 上报显卡信息\nconst reportGPUInfo = (config) => {\n  try {\n    const adapters = config.adapters || []\n    const adapterNames = adapters.map((a) => (typeof a === 'string' ? a : a?.name || String(a))).join(', ')\n\n    const gpuInfo = {\n      platform: config.platform || 'unknown',\n      adapter_count: adapters.length,\n      adapters: adapterNames,\n      selected_adapter: config.adapter_name || (adapters.length ? 'auto' : 'none'),\n      has_selected_adapter: !!config.adapter_name,\n    }\n\n    trackEvents.gpuReported(gpuInfo)\n  } catch (error) {\n    console.error('上报显卡信息失败:', error)\n  }\n}\n\n// 初始化\nonMounted(async () => {\n  // 记录页面访问\n  trackEvents.pageView('home')\n\n  try {\n    const config = await fetch('/api/config').then((r) => r.json())\n\n    setTimeout(() => {\n      reportGPUInfo(config)\n    }, 1000)\n\n    // 检查是否需要显示设置向导\n    if (checkSetupWizard(config)) {\n      return\n    }\n\n    // 获取版本信息\n    await fetchVersions(config)\n\n    // 获取日志\n    await fetchLogs()\n\n    // 更新页面标题\n    if (version.value) {\n      document.title += ` Ver ${version.value.version}`\n    }\n  } catch (e) {\n    // 在预览模式下，API 不可用是正常的，只记录警告\n    if (e?.message?.includes('JSON') || e?.message?.includes('<!DOCTYPE')) {\n      console.warn('API not available in preview mode:', e.message)\n    } else {\n      console.error('Failed to initialize:', e)\n      trackEvents.errorOccurred('home_initialization', e.message)\n    }\n  }\n})\n</script>\n\n<style>\n@import '../styles/global.less';\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/views/Password.vue",
    "content": "<template>\n  <div>\n    <Navbar />\n    <div class=\"container py-4\">\n      <div class=\"row justify-content-center\">\n        <div class=\"col-lg-8 col-xl-7\">\n          <div class=\"text-center mb-4\">\n            <div class=\"icon-wrapper mb-2\">\n              <Icon name=\"lock\" :size=\"32\" icon-class=\"text-primary\" />\n            </div>\n            <h1 class=\"h4 page-title\">{{ $t('password.password_change') }}</h1>\n            <p class=\"text-muted small mb-0\">{{ $t('password.new_username_desc') }}</p>\n          </div>\n\n          <form @submit.prevent=\"save\" autocomplete=\"off\">\n            <div class=\"card border-0 shadow-sm rounded-3\">\n              <div class=\"card-body p-3 p-md-4\">\n                <div class=\"row g-3\">\n                  <!-- Current Credentials Section -->\n                  <div class=\"col-12\">\n                    <div class=\"section-header d-flex align-items-center mb-3\">\n                      <div class=\"section-icon me-2\">\n                        <Icon name=\"clock\" :size=\"20\" icon-class=\"text-primary\" />\n                      </div>\n                      <h6 class=\"mb-0 fw-semibold\">{{ $t('password.current_creds') }}</h6>\n                    </div>\n                    <div class=\"row g-2\">\n                      <div class=\"col-md-6\">\n                        <label for=\"currentUsername\" class=\"form-label fw-medium small\">\n                          <i class=\"bi bi-person me-1 text-muted\"></i>\n                          {{ $t('_common.username') }}\n                        </label>\n                        <input\n                          required\n                          type=\"text\"\n                          class=\"form-control\"\n                          id=\"currentUsername\"\n                          v-model=\"passwordData.currentUsername\"\n                          :placeholder=\"$t('_common.username')\"\n                        />\n                      </div>\n                      <div class=\"col-md-6\">\n                        <label for=\"currentPassword\" class=\"form-label fw-medium small\">\n                          <i class=\"bi bi-key me-1 text-muted\"></i>\n                          {{ $t('_common.password') }}\n                        </label>\n                        <input\n                          autocomplete=\"current-password\"\n                          type=\"password\"\n                          class=\"form-control\"\n                          id=\"currentPassword\"\n                          v-model=\"passwordData.currentPassword\"\n                          :placeholder=\"$t('_common.password')\"\n                        />\n                      </div>\n                    </div>\n                  </div>\n\n                  <div class=\"col-12\">\n                    <hr class=\"my-1\" />\n                  </div>\n\n                  <!-- New Credentials Section -->\n                  <div class=\"col-12\">\n                    <div class=\"section-header d-flex align-items-center mb-3\">\n                      <div class=\"section-icon section-icon-success me-2\">\n                        <Icon name=\"star\" :size=\"20\" icon-class=\"text-success\" />\n                      </div>\n                      <h6 class=\"mb-0 fw-semibold\">{{ $t('password.new_creds') }}</h6>\n                    </div>\n                    <div class=\"row g-2\">\n                      <div class=\"col-12\">\n                        <label for=\"newUsername\" class=\"form-label fw-medium small\">\n                          <i class=\"bi bi-person me-1 text-muted\"></i>\n                          {{ $t('_common.username') }}\n                          <span class=\"text-muted fw-normal ms-1\">({{ $t('_common.auto') }})</span>\n                        </label>\n                        <input\n                          type=\"text\"\n                          class=\"form-control\"\n                          id=\"newUsername\"\n                          v-model=\"passwordData.newUsername\"\n                          :placeholder=\"$t('_common.username')\"\n                        />\n                      </div>\n                      <div class=\"col-md-6\">\n                        <label for=\"newPassword\" class=\"form-label fw-medium small\">\n                          <i class=\"bi bi-lock me-1 text-muted\"></i>\n                          {{ $t('_common.password') }}\n                        </label>\n                        <input\n                          autocomplete=\"new-password\"\n                          required\n                          type=\"password\"\n                          class=\"form-control\"\n                          id=\"newPassword\"\n                          v-model=\"passwordData.newPassword\"\n                          :placeholder=\"$t('_common.password')\"\n                        />\n                      </div>\n                      <div class=\"col-md-6\">\n                        <label for=\"confirmNewPassword\" class=\"form-label fw-medium small\">\n                          <i class=\"bi bi-lock-fill me-1 text-muted\"></i>\n                          {{ $t('password.confirm_password') }}\n                        </label>\n                        <input\n                          autocomplete=\"new-password\"\n                          required\n                          type=\"password\"\n                          class=\"form-control\"\n                          id=\"confirmNewPassword\"\n                          v-model=\"passwordData.confirmNewPassword\"\n                          :placeholder=\"$t('password.confirm_password')\"\n                        />\n                      </div>\n                    </div>\n                  </div>\n                </div>\n\n                <!-- Alerts -->\n                <div class=\"alert alert-danger d-flex align-items-center mt-3 rounded-3 py-2\" v-if=\"error\">\n                  <i class=\"bi bi-exclamation-triangle-fill me-2\"></i>\n                  <div class=\"small\">\n                    <strong>{{ $t('_common.error') }}</strong> {{ error }}\n                  </div>\n                </div>\n                <div class=\"alert alert-success d-flex align-items-center mt-3 rounded-3 py-2\" v-if=\"success\">\n                  <i class=\"bi bi-check-circle-fill me-2\"></i>\n                  <div class=\"small\">\n                    <strong>{{ $t('_common.success') }}</strong> {{ $t('password.success_msg') }}\n                  </div>\n                </div>\n\n                <!-- Submit Button -->\n                <div class=\"d-grid mt-3\">\n                  <button class=\"btn btn-primary py-2 rounded-3\" type=\"submit\">\n                    <i class=\"bi bi-save me-2\"></i>{{ $t('_common.save') }}\n                  </button>\n                </div>\n              </div>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { ref } from 'vue'\nimport Navbar from '../components/layout/Navbar.vue'\nimport Icon from '../components/common/Icon.vue'\n\nconst error = ref(null)\nconst success = ref(false)\nconst passwordData = ref({\n  currentUsername: '',\n  currentPassword: '',\n  newUsername: '',\n  newPassword: '',\n  confirmNewPassword: '',\n})\n\nasync function save() {\n  error.value = null\n  try {\n    const response = await fetch('/api/password', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify(passwordData.value),\n    })\n\n    const result = await response.json()\n    if (response.ok && result.status?.toString() === 'true') {\n      success.value = true\n      setTimeout(() => document.location.reload(), 5000)\n    } else {\n      error.value = result.error || 'Internal Server Error'\n    }\n  } catch (err) {\n    error.value = err.message || 'Internal Server Error'\n  }\n}\n</script>\n\n<style>\n@import '../styles/global.less';\n</style>\n\n<style lang=\"less\" scoped>\n@transition-duration: 0.2s;\n@transition-timing: ease;\n@primary-bg-opacity: 0.1;\n@success-bg-opacity: 0.1;\n\n.icon-wrapper {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 56px;\n  height: 56px;\n  background: rgba(var(--bs-primary-rgb), @primary-bg-opacity);\n  border-radius: 50%;\n}\n\n.section-icon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 32px;\n  height: 32px;\n  background: rgba(var(--bs-primary-rgb), @primary-bg-opacity);\n  border-radius: 8px;\n  font-size: 1rem;\n\n  &-success {\n    background: rgba(var(--bs-success-rgb), @success-bg-opacity);\n  }\n}\n\n.form-control {\n  padding: 0.5rem 0.75rem;\n  border-radius: 0.375rem;\n  transition: border-color @transition-duration @transition-timing,\n              box-shadow @transition-duration @transition-timing;\n\n  &:focus {\n    box-shadow: 0 0 0 0.15rem rgba(var(--bs-primary-rgb), 0.15);\n  }\n}\n\n.card {\n  transition: transform @transition-duration @transition-timing;\n}\n\n.btn-primary {\n  transition: transform @transition-duration @transition-timing,\n              box-shadow @transition-duration @transition-timing;\n\n  &:hover {\n    transform: translateY(-1px);\n    box-shadow: 0 4px 12px rgba(var(--bs-primary-rgb), 0.35);\n  }\n}\n\nhr {\n  opacity: 0.1;\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/views/Pin.vue",
    "content": "<template>\n  <div>\n    <Navbar />\n    <div id=\"content\" class=\"container\">\n      <h1 class=\"my-4 text-center page-title\">{{ $t('pin.pin_pairing') }}</h1>\n\n      <!-- QR Code Pairing -->\n      <div class=\"card mb-4 qr-pair-card\">\n        <div class=\"card-body text-center\">\n          <h5 class=\"card-title mb-3\">\n            <i class=\"fas fa-qrcode me-2\"></i>{{ $t('pin.qr_pairing') }}\n          </h5>\n          <p class=\"text-muted mb-3\">{{ $t('pin.qr_pairing_desc') }}</p>\n          <div class=\"alert alert-danger d-flex align-items-start mb-3\" style=\"font-size: 0.85rem;\">\n            <i class=\"fas fa-exclamation-triangle me-2 mt-1\"></i>\n            <span>{{ $t('pin.qr_pairing_warning') }}</span>\n          </div>\n\n          <!-- QR Code Display -->\n          <div v-if=\"qrActive\" class=\"qr-display\">\n            <div class=\"qr-image-wrapper mb-3\">\n              <img :src=\"qrDataUrl\" alt=\"QR Code\" class=\"qr-image\" />\n            </div>\n            <div class=\"qr-info\">\n              <div class=\"mb-2\">\n                <span class=\"text-muted\">PIN:</span>\n                <span class=\"fw-bold fs-4 ms-2 font-monospace\">{{ qrPin }}</span>\n              </div>\n              <div class=\"mb-3\">\n                <span class=\"badge\" :class=\"qrRemaining <= 30 ? 'bg-warning text-dark' : 'bg-info'\">\n                  <i class=\"fas fa-clock me-1\"></i>\n                  {{ $t('pin.qr_expires_in', { seconds: qrRemaining }) }}\n                </span>\n              </div>\n            </div>\n            <div class=\"d-flex gap-2 justify-content-center\">\n              <button class=\"btn btn-outline-primary btn-sm\" @click=\"generateQrCode\" :disabled=\"qrLoading\">\n                <i class=\"fas fa-sync-alt me-1\"></i>{{ $t('pin.qr_refresh') }}\n              </button>\n              <button class=\"btn btn-outline-secondary btn-sm\" @click=\"cancelQrCode\">\n                <i class=\"fas fa-times me-1\"></i>{{ $t('_common.cancel') }}\n              </button>\n            </div>\n          </div>\n\n          <!-- Pairing Success -->\n          <div v-else-if=\"qrPaired\" class=\"qr-success\">\n            <div class=\"mb-3\">\n              <i class=\"fas fa-check-circle text-success\" style=\"font-size: 3rem;\"></i>\n            </div>\n            <h5 class=\"text-success mb-3\">{{ $t('pin.qr_paired_success') }}</h5>\n            <button class=\"btn btn-outline-primary btn-sm\" @click=\"qrPaired = false\">\n              {{ $t('_common.ok') }}\n            </button>\n          </div>\n\n          <!-- Generate Button -->\n          <div v-else>\n            <div v-if=\"qrError\" class=\"alert alert-danger mb-3\">{{ qrError }}</div>\n            <button class=\"btn btn-primary\" @click=\"generateQrCode\" :disabled=\"qrLoading\">\n              <span v-if=\"qrLoading\" class=\"spinner-border spinner-border-sm me-2\"></span>\n              <i v-else class=\"fas fa-qrcode me-2\"></i>\n              {{ $t('pin.qr_generate') }}\n            </button>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"divider-text mb-4\">\n        <span>{{ $t('pin.or_manual_pin') }}</span>\n      </div>\n\n      <form action=\"\" class=\"form d-flex flex-column align-items-center\" id=\"form\">\n        <div class=\"card flex-column d-flex p-4 mb-4\">\n          <input\n            type=\"text\"\n            pattern=\"\\d*\"\n            :placeholder=\"`${$t('navbar.pin')}`\"\n            autofocus\n            id=\"pin-input\"\n            class=\"form-control mt-2\"\n            required\n          />\n          <input\n            type=\"text\"\n            v-model=\"pairingDeviceName\"\n            :placeholder=\"`${$t('pin.device_name')}`\"\n            id=\"name-input\"\n            class=\"form-control my-4\"\n            required\n          />\n          <button class=\"btn btn-primary\">{{ $t('pin.send') }}</button>\n        </div>\n        <div class=\"alert alert-warning\">\n          <b>{{ $t('_common.warning') }}</b> {{ $t('pin.warning_msg') }}\n        </div>\n        <div id=\"status\"></div>\n      </form>\n\n      <!-- Unpair all Clients -->\n      <div class=\"card my-4\">\n        <div class=\"card-body\">\n          <div class=\"p-2\">\n            <div class=\"d-flex justify-content-end align-items-center mb-3\">\n              <h2 id=\"unpair\" class=\"text-center me-auto mb-0\">{{ $t('troubleshooting.unpair_title') }}</h2>\n              <button class=\"btn btn-danger\" :disabled=\"unpairAllPressed || loading\" @click=\"handleUnpairAll\">\n                <span v-if=\"unpairAllPressed\" class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></span>\n                {{ $t('troubleshooting.unpair_all') }}\n              </button>\n            </div>\n            <div\n              id=\"apply-alert\"\n              class=\"alert alert-success d-flex align-items-center mt-3\"\n              :style=\"{ display: showApplyMessage ? 'flex !important' : 'none !important' }\"\n            >\n              <div class=\"me-2\">\n                <b>{{ $t('_common.success') }}</b> {{ $t('troubleshooting.unpair_single_success') }}\n              </div>\n              <button class=\"btn btn-success ms-auto apply\" @click=\"clickedApplyBanner\">\n                {{ $t('_common.dismiss') }}\n              </button>\n            </div>\n            <div class=\"alert alert-success\" v-if=\"unpairAllStatus === true\">\n              {{ $t('troubleshooting.unpair_all_success') }}\n            </div>\n            <div class=\"alert alert-danger\" v-if=\"unpairAllStatus === false\">\n              {{ $t('troubleshooting.unpair_all_error') }}\n            </div>\n            <p class=\"mb-3 text-muted\">{{ $t('pin.remove_paired_devices_desc') }}</p>\n          </div>\n\n          <!-- 加载状态 -->\n          <div v-if=\"loading && clients.length === 0\" class=\"text-center py-5\">\n            <div class=\"spinner-border text-primary\" role=\"status\">\n              <span class=\"visually-hidden\">{{ $t('pin.loading') }}</span>\n            </div>\n            <p class=\"mt-3 text-muted\">{{ $t('pin.loading_clients') }}</p>\n          </div>\n\n          <!-- 客户端列表 -->\n          <div id=\"client-list\" v-else-if=\"clients && clients.length > 0\" class=\"client-list-container\">\n            <div class=\"table-responsive\">\n              <table class=\"table table-hover table-bordered align-middle mb-0\">\n                <thead class=\"table-dark\">\n                  <tr>\n                    <th scope=\"col\" width=\"20%\" class=\"ps-3\">{{ $t('pin.client_name') }}</th>\n                    <th scope=\"col\" class=\"ps-3\">\n                      <span class=\"d-inline-flex align-items-center gap-1\">\n                        {{ $t('pin.hdr_profile') }}\n                        <i\n                          class=\"fas fa-info-circle text-info\"\n                          data-tooltip=\"hdr-profile\"\n                          style=\"cursor: help; font-size: 0.875rem;\"\n                        ></i>\n                      </span>\n                    </th>\n                    <th scope=\"col\" class=\"ps-3\">\n                      <span class=\"d-inline-flex align-items-center gap-1\">\n                        {{ $t('pin.device_size') }}\n                        <i\n                          class=\"fas fa-info-circle text-info\"\n                          data-tooltip=\"device-size\"\n                          style=\"cursor: help; font-size: 0.875rem;\"\n                        ></i>\n                      </span>\n                    </th>\n                    <th scope=\"col\" width=\"30%\" class=\"text-center\">{{ $t('pin.actions') }}</th>\n                  </tr>\n                </thead>\n                <tbody>\n                  <tr\n                    v-for=\"client in clients\"\n                    :key=\"client.uuid\"\n                    :class=\"{ 'table-warning': editingStates[client.uuid] }\"\n                  >\n                    <td class=\"fw-medium ps-3\">{{ client.name || $t('pin.unknown_client') }}</td>\n                    <td class=\"ps-3\">\n                      <select\n                        class=\"form-select form-select-sm\"\n                        v-model=\"client.hdrProfile\"\n                        :disabled=\"!editingStates[client.uuid]\"\n                        @change=\"onProfileChange(client.uuid)\"\n                      >\n                        <option v-if=\"!hasIccFileList\" value=\"\" disabled>{{ $t('pin.modify_in_gui') }}</option>\n                        <option v-else value=\"\">{{ $t('pin.none') }}</option>\n                        <option v-for=\"item in hdrProfileList\" :value=\"item\" :key=\"item\">{{ item }}</option>\n                      </select>\n                    </td>\n                    <td class=\"ps-3\">\n                      <select\n                        class=\"form-select form-select-sm\"\n                        v-model=\"client.deviceSize\"\n                        :disabled=\"!editingStates[client.uuid]\"\n                        @change=\"onSizeChange(client.uuid)\"\n                      >\n                        <option value=\"small\">{{ $t('pin.device_size_small') }}</option>\n                        <option value=\"medium\">{{ $t('pin.device_size_medium') }}</option>\n                        <option value=\"large\">{{ $t('pin.device_size_large') }}</option>\n                      </select>\n                    </td>\n                    <td class=\"text-center\">\n                      <div class=\"btn-toolbar justify-content-center\" role=\"toolbar\">\n                        <!-- 编辑模式按钮 -->\n                        <template v-if=\"!editingStates[client.uuid]\">\n                          <button\n                            class=\"btn btn-sm btn-outline-primary me-1\"\n                            @click=\"startEdit(client.uuid)\"\n                            :disabled=\"saving || deleting.has(client.uuid)\"\n                            :title=\"$t('pin.edit_client_settings')\"\n                          >\n                            <i class=\"fas fa-edit me-1\"></i> {{ $t('_common.edit') }}\n                          </button>\n                        </template>\n                        <!-- 保存/取消按钮 -->\n                        <template v-else>\n                          <button\n                            class=\"btn btn-sm btn-success me-1\"\n                            @click=\"handleSave(client.uuid)\"\n                            :disabled=\"saving || deleting.has(client.uuid)\"\n                            :title=\"$t('pin.save_changes')\"\n                          >\n                            <span v-if=\"saving\" class=\"spinner-border spinner-border-sm me-1\"></span>\n                            <i v-else class=\"fas fa-check me-1\"></i> {{ $t('_common.save') }}\n                          </button>\n                          <button\n                            class=\"btn btn-sm btn-secondary me-1\"\n                            @click=\"handleCancelEdit(client.uuid)\"\n                            :disabled=\"saving || deleting.has(client.uuid)\"\n                            :title=\"$t('pin.cancel_editing')\"\n                          >\n                            <i class=\"fas fa-times me-1\"></i> {{ $t('_common.cancel') }}\n                          </button>\n                        </template>\n                        <!-- 删除按钮 -->\n                        <button\n                          class=\"btn btn-sm btn-outline-danger\"\n                          @click=\"handleDelete(client)\"\n                          :disabled=\"saving || deleting.has(client.uuid) || editingStates[client.uuid]\"\n                          :title=\"editingStates[client.uuid] ? $t('pin.save_or_cancel_first') : $t('pin.delete_client')\"\n                        >\n                          <span v-if=\"deleting.has(client.uuid)\" class=\"spinner-border spinner-border-sm me-1\"></span>\n                          <i v-else class=\"fas fa-trash me-1\"></i> {{ $t('_common.delete') }}\n                        </button>\n                      </div>\n                      <!-- 未保存更改提示 -->\n                      <div\n                        v-if=\"editingStates[client.uuid] && hasUnsavedChanges(client.uuid)\"\n                        class=\"text-warning small mt-2\"\n                      >\n                        <i class=\"fas fa-exclamation-triangle me-1\"></i> {{ $t('pin.unsaved_changes') }}\n                      </div>\n                    </td>\n                  </tr>\n                </tbody>\n              </table>\n            </div>\n          </div>\n          <!-- 空状态 -->\n          <div v-else-if=\"!loading\" class=\"list-group list-group-flush list-group-item-light\">\n            <div class=\"list-group-item p-5 text-center\">\n              <i class=\"fas fa-inbox fa-3x text-muted mb-3\"></i>\n              <p class=\"mb-0\">\n                <em>{{ $t('troubleshooting.unpair_single_no_devices') }}</em>\n              </p>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- 删除确认对话框 -->\n      <Transition name=\"fade\">\n        <div v-if=\"clientToDelete\" class=\"delete-client-overlay\" @click.self=\"clientToDelete = null\">\n          <div class=\"delete-client-modal\">\n            <div class=\"delete-client-header\">\n              <h5>\n                <i class=\"fas fa-exclamation-triangle me-2\"></i>{{ $t('pin.confirm_delete') }}\n              </h5>\n              <button class=\"btn-close\" @click=\"clientToDelete = null\"></button>\n            </div>\n            <div class=\"delete-client-body\">\n              <p v-html=\"$t('pin.delete_confirm_message', { name: clientToDelete.name || $t('pin.unknown_client') })\"></p>\n              <p class=\"text-muted small mb-0\">{{ $t('pin.delete_warning') }}</p>\n            </div>\n            <div class=\"delete-client-footer\">\n              <button type=\"button\" class=\"btn btn-secondary\" @click=\"clientToDelete = null\">{{ $t('_common.cancel') }}</button>\n              <button type=\"button\" class=\"btn btn-danger\" @click=\"confirmDelete\">\n                <span v-if=\"deleting.has(clientToDelete.uuid)\" class=\"spinner-border spinner-border-sm me-2\"></span>\n                {{ $t('_common.delete') }}\n              </button>\n            </div>\n          </div>\n        </div>\n      </Transition>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { onMounted, ref, nextTick, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { Tooltip } from 'bootstrap'\nimport Navbar from '../components/layout/Navbar.vue'\nimport { usePin } from '../composables/usePin.js'\nimport { useQrPair } from '../composables/useQrPair.js'\n\nconst { t } = useI18n()\n\nconst {\n  pairingDeviceName,\n  unpairAllPressed,\n  unpairAllStatus,\n  showApplyMessage,\n  clients,\n  hdrProfileList,\n  hasIccFileList,\n  loading,\n  saving,\n  deleting,\n  editingStates,\n  refreshClients,\n  unpairAll,\n  unpairSingle,\n  saveClient,\n  startEdit,\n  cancelEdit,\n  hasUnsavedChanges,\n  initPinForm,\n  clickedApplyBanner,\n  loadConfig,\n} = usePin()\n\nconst {\n  qrDataUrl,\n  qrPin,\n  qrRemaining,\n  qrLoading,\n  qrError,\n  qrPaired,\n  qrActive,\n  generateQrCode,\n  cancelQrCode,\n} = useQrPair()\n\nconst clientToDelete = ref(null)\n\nconst handleDelete = (client) => {\n  if (editingStates[client.uuid]) return\n  clientToDelete.value = client\n}\n\nconst confirmDelete = async () => {\n  if (!clientToDelete.value) return\n  const success = await unpairSingle(clientToDelete.value.uuid)\n  if (success) clientToDelete.value = null\n}\n\nconst handleSave = async (uuid) => {\n  const success = await saveClient(uuid)\n  if (!success) alert(t('pin.save_failed'))\n}\n\nconst handleCancelEdit = (uuid) => cancelEdit(uuid)\n\nconst handleUnpairAll = async () => {\n  if (confirm(t('pin.unpair_all_confirm'))) await unpairAll()\n}\n\nconst initTooltips = () => {\n  nextTick(() => {\n    const tooltipConfigs = [\n      { selector: '[data-tooltip=\"hdr-profile\"]', title: t('pin.hdr_profile_info') },\n      { selector: '[data-tooltip=\"device-size\"]', title: t('pin.device_size_info') }\n    ]\n    \n    tooltipConfigs.forEach(({ selector, title }) => {\n      const el = document.querySelector(selector)\n      if (!el) return\n      \n      Tooltip.getInstance(el)?.dispose()\n      new Tooltip(el, { html: true, placement: 'top', title })\n    })\n  })\n}\n\nonMounted(async () => {\n  await loadConfig()\n  await refreshClients()\n\n  initPinForm(() => setTimeout(refreshClients, 0))\n\n  if (window.electron?.getIccFileList) {\n    hasIccFileList.value = true\n    window.electron.getIccFileList((files = []) => {\n      hdrProfileList.value = files.filter(file => /.icc$/.test(file))\n    })\n  } else {\n    hasIccFileList.value = false\n  }\n\n  initTooltips()\n})\n\nwatch(clients, initTooltips, { deep: true })\n</script>\n\n<style>\n@import '../styles/global.less';\n</style>\n\n<style scoped lang=\"less\">\n.client-list-container {\n  margin-top: 1rem;\n\n  .table-responsive {\n    border-radius: var(--border-radius-md, 8px);\n    overflow: hidden;\n  }\n\n  .table {\n    border-radius: var(--border-radius-md, 12px);\n    overflow: hidden;\n    margin-bottom: 0;\n  }\n}\n\n.table-warning {\n  background-color: rgba(255, 193, 7, 0.1) !important;\n}\n\n/* Delete Client Modal - 使用 ScanResultModal 样式 */\n.delete-client-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 100vw;\n  height: 100vh;\n  margin: 0;\n  background: var(--overlay-bg, rgba(0, 0, 0, 0.7));\n  backdrop-filter: blur(8px);\n  z-index: 9999;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: var(--spacing-lg, 20px);\n  overflow: hidden;\n\n  [data-bs-theme='light'] & {\n    background: rgba(0, 0, 0, 0.5);\n  }\n}\n\n.delete-client-modal {\n  background: var(--modal-bg, rgba(30, 30, 50, 0.95));\n  border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.2));\n  border-radius: var(--border-radius-xl, 12px);\n  width: 100%;\n  max-width: 500px;\n  max-height: 80vh;\n  display: flex;\n  flex-direction: column;\n  backdrop-filter: blur(20px);\n  box-shadow: var(--shadow-xl, 0 25px 50px rgba(0, 0, 0, 0.5));\n  animation: modalSlideUp 0.3s ease;\n\n  [data-bs-theme='light'] & {\n    background: rgba(255, 255, 255, 0.95);\n    border: 1px solid rgba(0, 0, 0, 0.15);\n    box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);\n  }\n}\n\n@keyframes modalSlideUp {\n  from {\n    transform: translateY(20px);\n    opacity: 0;\n  }\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n.delete-client-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-bottom: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  h5 {\n    margin: 0;\n    color: var(--text-primary, #fff);\n    font-size: var(--font-size-lg, 1.1rem);\n    font-weight: 600;\n    display: flex;\n    align-items: center;\n    gap: var(--spacing-sm, 8px);\n  }\n\n  [data-bs-theme='light'] & {\n    border-bottom: 1px solid rgba(0, 0, 0, 0.1);\n\n    h5 {\n      color: #000000;\n    }\n  }\n}\n\n.delete-client-body {\n  padding: var(--spacing-lg, 24px);\n  font-size: var(--font-size-md, 0.95rem);\n  line-height: 1.5;\n  overflow-y: auto;\n  flex: 1;\n  color: var(--text-primary, #fff);\n\n  [data-bs-theme='light'] & {\n    color: #000000;\n  }\n}\n\n.delete-client-footer {\n  display: flex;\n  justify-content: flex-end;\n  gap: 10px;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-top: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  [data-bs-theme='light'] & {\n    border-top: 1px solid rgba(0, 0, 0, 0.1);\n  }\n\n  button {\n    padding: 8px 16px;\n    font-size: 0.9rem;\n  }\n}\n\n/* Vue 过渡动画 */\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n\n/* 响应式优化 */\n@media (max-width: 768px) {\n  .btn-toolbar {\n    flex-direction: column;\n\n    .btn {\n      width: 100%;\n      margin-bottom: 0.25rem;\n    }\n  }\n\n  .table-responsive {\n    font-size: 0.875rem;\n  }\n}\n\n/* QR Code Pairing Styles */\n.qr-pair-card {\n  max-width: 480px;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.qr-image-wrapper {\n  display: inline-block;\n  padding: 12px;\n  background: #fff;\n  border-radius: 12px;\n  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);\n}\n\n.qr-image {\n  display: block;\n  width: 280px;\n  height: 280px;\n  border-radius: 4px;\n}\n\n.divider-text {\n  display: flex;\n  align-items: center;\n  text-align: center;\n  color: var(--bs-body-color, #dee2e6);\n  font-size: 0.875rem;\n\n  &::before,\n  &::after {\n    content: '';\n    flex: 1;\n    border-bottom: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.15));\n  }\n\n  span {\n    padding: 0 1rem;\n  }\n\n  [data-bs-theme='light'] & {\n    &::before,\n    &::after {\n      border-bottom-color: rgba(0, 0, 0, 0.15);\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/views/Troubleshooting.vue",
    "content": "<template>\n  <div>\n    <Navbar />\n    <div class=\"container py-4\">\n      <div class=\"page-header mb-4\">\n        <h1 class=\"page-title\">\n          <i class=\"fas fa-tools me-3\"></i>\n          {{ $t('troubleshooting.troubleshooting') }}\n        </h1>\n      </div>\n\n      <!-- Row 1: 重新打开新手引导 | 登出 -->\n      <div class=\"row mb-4\">\n        <div class=\"col-lg-6 d-flex\">\n          <TroubleshootingCard\n            class=\"flex-fill\"\n            icon=\"fa-magic\"\n            color=\"success\"\n            :title=\"$t('troubleshooting.reopen_setup_wizard')\"\n            :description=\"$t('troubleshooting.reopen_setup_wizard_desc')\"\n          >\n            <button class=\"btn btn-success\" @click=\"handleReopenSetupWizard\">\n              <i class=\"fas fa-redo me-2\"></i>\n              {{ $t('troubleshooting.reopen_setup_wizard') }}\n            </button>\n          </TroubleshootingCard>\n        </div>\n        <div class=\"col-lg-6 d-flex\">\n          <TroubleshootingCard\n            class=\"flex-fill\"\n            icon=\"fa-sign-out-alt\"\n            color=\"danger\"\n            :title=\"$t('troubleshooting.logout')\"\n            :description=\"$t('troubleshooting.logout_desc')\"\n          >\n            <button class=\"btn btn-danger\" @click=\"showLogoutModal\">\n              <i class=\"fas fa-sign-out-alt me-2\"></i>\n              {{ $t('troubleshooting.logout') }}\n            </button>\n          </TroubleshootingCard>\n        </div>\n      </div>\n\n      <!-- Row 2: 强制关闭 | Boom -->\n      <div class=\"row mb-4\">\n        <div class=\"col-lg-6 d-flex\">\n          <TroubleshootingCard\n            class=\"flex-fill\"\n            icon=\"fa-times-circle\"\n            color=\"warning\"\n            :title=\"$t('troubleshooting.force_close')\"\n            :description=\"$t('troubleshooting.force_close_desc')\"\n          >\n            <template #alerts>\n              <div class=\"alert alert-success d-flex align-items-center\" v-if=\"closeAppStatus === true\">\n                <i class=\"fas fa-check-circle me-2\"></i>\n                {{ $t('troubleshooting.force_close_success') }}\n              </div>\n              <div class=\"alert alert-danger d-flex align-items-center\" v-if=\"closeAppStatus === false\">\n                <i class=\"fas fa-exclamation-circle me-2\"></i>\n                {{ $t('troubleshooting.force_close_error') }}\n              </div>\n            </template>\n            <button class=\"btn btn-warning\" :disabled=\"closeAppPressed\" @click=\"closeApp\">\n              <i class=\"fas fa-times me-2\"></i>\n              {{ $t('troubleshooting.force_close') }}\n            </button>\n          </TroubleshootingCard>\n        </div>\n        <div class=\"col-lg-6 d-flex\">\n          <TroubleshootingCard\n            class=\"flex-fill\"\n            icon=\"fa-bomb\"\n            color=\"danger\"\n            :title=\"$t('troubleshooting.boom_sunshine')\"\n            :description=\"$t('troubleshooting.boom_sunshine_desc')\"\n          >\n            <template #alerts>\n              <div class=\"alert alert-success d-flex align-items-center\" v-if=\"boomPressed\">\n                <i class=\"fas fa-check-circle me-2\"></i>\n                {{ $t('troubleshooting.boom_sunshine_success') }}\n              </div>\n            </template>\n            <button class=\"btn btn-danger\" :disabled=\"boomPressed\" @click=\"showBoomModal\">\n              <i class=\"fas fa-bomb me-2\"></i>\n              {{ $t('troubleshooting.boom_sunshine') }}\n            </button>\n          </TroubleshootingCard>\n        </div>\n      </div>\n\n      <!-- Row 3: 重置显示器设置 | 重启 Sunshine -->\n      <div class=\"row mb-4\">\n        <div class=\"col-lg-6 d-flex\" v-if=\"platform === 'windows'\">\n          <TroubleshootingCard\n            class=\"flex-fill\"\n            icon=\"fa-desktop\"\n            color=\"info\"\n            :title=\"$t('troubleshooting.reset_display_device_windows')\"\n            :description=\"$t('troubleshooting.reset_display_device_desc_windows')\"\n            pre-line\n          >\n            <template #alerts>\n              <div class=\"alert alert-success d-flex align-items-center\" v-if=\"resetDisplayDeviceStatus === true\">\n                <i class=\"fas fa-check-circle me-2\"></i>\n                {{ $t('troubleshooting.reset_display_device_success_windows') }}\n              </div>\n              <div class=\"alert alert-danger d-flex align-items-center\" v-if=\"resetDisplayDeviceStatus === false\">\n                <i class=\"fas fa-exclamation-circle me-2\"></i>\n                {{ $t('troubleshooting.reset_display_device_error_windows') }}\n              </div>\n            </template>\n            <button\n              class=\"btn btn-info text-white\"\n              :disabled=\"resetDisplayDevicePressed\"\n              @click=\"resetDisplayDevicePersistence\"\n            >\n              <i class=\"fas fa-undo me-2\"></i>\n              {{ $t('troubleshooting.reset_display_device_windows') }}\n            </button>\n          </TroubleshootingCard>\n        </div>\n        <div class=\"col-lg-6 d-flex\">\n          <TroubleshootingCard\n            class=\"flex-fill\"\n            icon=\"fa-sync-alt\"\n            color=\"info\"\n            :title=\"$t('troubleshooting.restart_sunshine')\"\n            :description=\"$t('troubleshooting.restart_sunshine_desc')\"\n          >\n            <template #alerts>\n              <div class=\"alert alert-success d-flex align-items-center\" v-if=\"restartPressed\">\n                <i class=\"fas fa-check-circle me-2\"></i>\n                {{ $t('troubleshooting.restart_sunshine_success') }}\n              </div>\n            </template>\n            <button class=\"btn btn-info text-white\" :disabled=\"restartPressed\" @click=\"restart\">\n              <i class=\"fas fa-redo me-2\"></i>\n              {{ $t('troubleshooting.restart_sunshine') }}\n            </button>\n          </TroubleshootingCard>\n        </div>\n      </div>\n\n      <!-- Logs Section - Full Width -->\n      <LogsSection\n        v-model:logFilter=\"logFilter\"\n        v-model:matchMode=\"matchMode\"\n        v-model:ignoreCase=\"ignoreCase\"\n        :actualLogs=\"actualLogs\"\n        :copyLogs=\"copyLogs\"\n        :copyConfig=\"handleCopyConfig\"\n        @openDiagnosis=\"openDiagnosis\"\n      />\n    </div>\n\n    <!-- AI Diagnosis Modal -->\n    <LogDiagnosisModal\n      :show=\"showDiagnosisModal\"\n      :config=\"aiConfig\"\n      :providers=\"aiProviders\"\n      :isLoading=\"aiLoading\"\n      :result=\"aiResult\"\n      :error=\"aiError\"\n      :onProviderChange=\"aiProviderChange\"\n      :getAvailableModels=\"aiGetModels\"\n      @close=\"showDiagnosisModal = false\"\n      @diagnose=\"handleDiagnose\"\n    />\n\n    <!-- Boom Confirm Modal -->\n    <Transition name=\"fade\">\n      <div v-if=\"showBoomConfirmModal\" class=\"boom-confirm-overlay\" @click.self=\"closeBoomModal\">\n        <div class=\"boom-confirm-modal\">\n          <div class=\"boom-confirm-header\">\n            <h5>\n              <i class=\"fas fa-bomb me-2\"></i>{{ $t('troubleshooting.confirm_boom') }}\n            </h5>\n            <button class=\"btn-close\" @click=\"closeBoomModal\"></button>\n          </div>\n          <div class=\"boom-confirm-body\">\n            <p>{{ $t('troubleshooting.confirm_boom_desc') }}</p>\n          </div>\n          <div class=\"boom-confirm-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" @click=\"closeBoomModal\">\n              {{ $t('_common.cancel') }}\n            </button>\n            <button type=\"button\" class=\"btn btn-danger\" @click=\"confirmBoom\">\n              <i class=\"fas fa-bomb me-2\"></i>{{ $t('troubleshooting.boom_sunshine') }}\n            </button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n\n    <!-- Logout Confirm Modal -->\n    <Transition name=\"fade\">\n      <div v-if=\"showLogoutConfirmModal\" class=\"boom-confirm-overlay\" @click.self=\"closeLogoutModal\">\n        <div class=\"boom-confirm-modal\">\n          <div class=\"boom-confirm-header\">\n            <h5>\n              <i class=\"fas fa-sign-out-alt me-2\"></i>{{ $t('troubleshooting.confirm_logout') }}\n            </h5>\n            <button class=\"btn-close\" @click=\"closeLogoutModal\"></button>\n          </div>\n          <div class=\"boom-confirm-body\">\n            <p>{{ $t('troubleshooting.confirm_logout_desc') }}</p>\n          </div>\n          <div class=\"boom-confirm-footer\">\n            <button type=\"button\" class=\"btn btn-secondary\" @click=\"closeLogoutModal\">\n              {{ $t('_common.cancel') }}\n            </button>\n            <button type=\"button\" class=\"btn btn-danger\" @click=\"confirmLogout\">\n              <i class=\"fas fa-sign-out-alt me-2\"></i>{{ $t('troubleshooting.logout') }}\n            </button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n\n    <!-- Localhost 登出提醒弹窗（200 时显示） -->\n    <Transition name=\"fade\">\n      <div v-if=\"showLocalhostLogoutModal\" class=\"boom-confirm-overlay\" @click.self=\"closeLocalhostLogoutModal\">\n        <div class=\"boom-confirm-modal\">\n          <div class=\"boom-confirm-header\">\n            <h5>\n              <i class=\"fas fa-info-circle me-2\"></i>{{ $t('troubleshooting.logout') }}\n            </h5>\n            <button class=\"btn-close\" @click=\"closeLocalhostLogoutModal\"></button>\n          </div>\n          <div class=\"boom-confirm-body\">\n            <p>{{ $t('troubleshooting.logout_localhost_tip') }}</p>\n          </div>\n          <div class=\"boom-confirm-footer\">\n            <button type=\"button\" class=\"btn btn-primary\" @click=\"closeLocalhostLogoutModal\">\n              {{ $t('_common.close') }}\n            </button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n  </div>\n</template>\n\n<script setup>\nimport { onMounted, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport Navbar from '../components/layout/Navbar.vue'\nimport TroubleshootingCard from '../components/TroubleshootingCard.vue'\nimport LogsSection from '../components/LogsSection.vue'\nimport LogDiagnosisModal from '../components/LogDiagnosisModal.vue'\nimport { useTroubleshooting } from '../composables/useTroubleshooting.js'\nimport { useLogout } from '../composables/useLogout.js'\nimport { useAiDiagnosis } from '../composables/useAiDiagnosis.js'\n\nconst { t } = useI18n()\nconst { logout } = useLogout()\n\nconst {\n  config: aiConfig,\n  providers: aiProviders,\n  isLoading: aiLoading,\n  result: aiResult,\n  error: aiError,\n  onProviderChange: aiProviderChange,\n  getAvailableModels: aiGetModels,\n  diagnose: aiDiagnose,\n} = useAiDiagnosis()\n\nconst showDiagnosisModal = ref(false)\n\nconst {\n  platform,\n  closeAppPressed,\n  closeAppStatus,\n  restartPressed,\n  boomPressed,\n  resetDisplayDevicePressed,\n  resetDisplayDeviceStatus,\n  logFilter,\n  matchMode,\n  ignoreCase,\n  actualLogs,\n  refreshLogs,\n  closeApp,\n  restart,\n  boom,\n  resetDisplayDevicePersistence,\n  copyLogs,\n  copyConfig,\n  reopenSetupWizard,\n  loadPlatform,\n  startLogRefresh,\n  stopLogRefresh,\n} = useTroubleshooting()\n\nconst showBoomConfirmModal = ref(false)\nconst showLogoutConfirmModal = ref(false)\nconst showLocalhostLogoutModal = ref(false)\n\nconst showBoomModal = () => {\n  showBoomConfirmModal.value = true\n}\n\nconst closeBoomModal = () => {\n  showBoomConfirmModal.value = false\n}\n\nconst confirmBoom = () => {\n  closeBoomModal()\n  boom()\n}\n\nconst showLogoutModal = () => {\n  showLogoutConfirmModal.value = true\n}\n\nconst closeLogoutModal = () => {\n  showLogoutConfirmModal.value = false\n}\n\nconst closeLocalhostLogoutModal = () => {\n  showLocalhostLogoutModal.value = false\n}\n\nconst confirmLogout = () => {\n  closeLogoutModal()\n  showLocalhostLogoutModal.value = false\n  stopLogRefresh() // 避免登出后 log 轮询再发请求触发第二次登录框\n  logout({\n    onLocalhost: () => {\n      showLocalhostLogoutModal.value = true\n    },\n  })\n}\n\nconst handleCopyConfig = () => copyConfig(t)\n\nconst handleReopenSetupWizard = () => reopenSetupWizard(t)\n\nconst openDiagnosis = () => {\n  showDiagnosisModal.value = true\n}\n\nconst handleDiagnose = () => {\n  aiDiagnose(actualLogs.value)\n}\n\nonMounted(async () => {\n  await Promise.all([loadPlatform(), refreshLogs()])\n  startLogRefresh()\n})\n</script>\n\n<style>\n@import '../styles/global.less';\n</style>\n\n<style scoped>\n.btn {\n  border-radius: 8px;\n  padding: 0.5rem 1rem;\n  font-weight: 500;\n  transition: all 0.2s ease;\n}\n\n.btn:hover {\n  transform: translateY(-1px);\n}\n\n.alert {\n  border-radius: 8px;\n  font-size: 0.9rem;\n  padding: 0.75rem 1rem;\n}\n\n/* Boom Confirm Modal - 使用 ScanResultModal 样式 */\n.boom-confirm-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 100vw;\n  height: 100vh;\n  margin: 0;\n  background: var(--overlay-bg, rgba(0, 0, 0, 0.7));\n  backdrop-filter: blur(8px);\n  z-index: 9999;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: var(--spacing-lg, 20px);\n  overflow: hidden;\n  \n  [data-bs-theme='light'] & {\n    background: rgba(0, 0, 0, 0.5);\n  }\n}\n\n.boom-confirm-modal {\n  background: var(--modal-bg, rgba(30, 30, 50, 0.95));\n  border: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.2));\n  border-radius: var(--border-radius-xl, 12px);\n  width: 100%;\n  max-width: 500px;\n  max-height: 80vh;\n  display: flex;\n  flex-direction: column;\n  backdrop-filter: blur(20px);\n  box-shadow: var(--shadow-xl, 0 25px 50px rgba(0, 0, 0, 0.5));\n  animation: modalSlideUp 0.3s ease;\n  \n  [data-bs-theme='light'] & {\n    background: rgba(255, 255, 255, 0.95);\n    border: 1px solid rgba(0, 0, 0, 0.15);\n    box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);\n  }\n}\n\n@keyframes modalSlideUp {\n  from {\n    transform: translateY(20px);\n    opacity: 0;\n  }\n  to {\n    transform: translateY(0);\n    opacity: 1;\n  }\n}\n\n.boom-confirm-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-bottom: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n\n  h5 {\n    margin: 0;\n    color: var(--text-primary, #fff);\n    font-size: var(--font-size-lg, 1.1rem);\n    font-weight: 600;\n    display: flex;\n    align-items: center;\n    gap: var(--spacing-sm, 8px);\n  }\n  \n  [data-bs-theme='light'] & {\n    border-bottom: 1px solid rgba(0, 0, 0, 0.1);\n    \n    h5 {\n      color: #000000;\n    }\n  }\n}\n\n.boom-confirm-body {\n  padding: var(--spacing-lg, 24px);\n  font-size: var(--font-size-md, 0.95rem);\n  line-height: 1.5;\n  overflow-y: auto;\n  flex: 1;\n  color: var(--text-primary, #fff);\n  \n  [data-bs-theme='light'] & {\n    color: #000000;\n  }\n}\n\n.boom-confirm-footer {\n  display: flex;\n  justify-content: flex-end;\n  gap: 10px;\n  padding: var(--spacing-md, 20px) var(--spacing-lg, 24px);\n  border-top: 1px solid var(--border-color-light, rgba(255, 255, 255, 0.1));\n  \n  [data-bs-theme='light'] & {\n    border-top: 1px solid rgba(0, 0, 0, 0.1);\n  }\n}\n\n.boom-confirm-footer button {\n  padding: 8px 16px;\n  font-size: 0.9rem;\n}\n\n/* Vue 过渡动画 */\n.fade-enter-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n\n@media (max-width: 991.98px) {\n  .page-title {\n    font-size: 1.5rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/views/Welcome.vue",
    "content": "<template>\n  <div id=\"content\" class=\"container\">\n    <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"position:absolute;width:0;height:0\">\n      <defs>\n        <filter id=\"pencilTexture\">\n          <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.8\" numOctaves=\"4\" result=\"noise\" />\n          <feDisplacementMap in=\"SourceGraphic\" in2=\"noise\" scale=\"1\" xChannelSelector=\"R\" yChannelSelector=\"G\" />\n        </filter>\n      </defs>\n    </svg>\n\n    <div class=\"row justify-content-center my-4\">\n      <div class=\"col-lg-10\">\n        <!-- 语言选择器 -->\n        <div class=\"text-end mb-3\">\n          <div class=\"language-selector\">\n            <label for=\"localeSelect\" class=\"form-label me-2\">\n              <i class=\"fas fa-language\"></i>\n            </label>\n            <select\n              id=\"localeSelect\"\n              class=\"form-select form-select-sm d-inline-block\"\n              style=\"width: auto;\"\n              v-model=\"selectedLocale\"\n              @change=\"changeLanguage\"\n            >\n              <option value=\"en\">English</option>\n              <option value=\"en_GB\">English (UK)</option>\n              <option value=\"en_US\">English (US)</option>\n              <option value=\"zh\">简体中文</option>\n              <option value=\"zh_TW\">繁體中文</option>\n              <option value=\"de\">Deutsch</option>\n              <option value=\"fr\">Français</option>\n              <option value=\"es\">Español</option>\n              <option value=\"it\">Italiano</option>\n              <option value=\"ja\">日本語</option>\n              <option value=\"ko\">한국어</option>\n              <option value=\"ru\">Русский</option>\n              <option value=\"uk\">Українська</option>\n              <option value=\"pt\">Português</option>\n              <option value=\"pt_BR\">Português (Brasil)</option>\n              <option value=\"pl\">Polski</option>\n              <option value=\"sv\">Svenska</option>\n              <option value=\"tr\">Türkçe</option>\n              <option value=\"cs\">Čeština</option>\n              <option value=\"bg\">Български</option>\n            </select>\n          </div>\n        </div>\n\n        <div class=\"card my-4\">\n          <div class=\"card-body\">\n            <header class=\"text-center mb-4\">\n              <h1 class=\"mb-3\">\n                {{ $t('welcome.greeting') }}\n              </h1>\n              <p class=\"lead text-muted\">{{ $t('welcome.create_creds') }}</p>\n            </header>\n\n            <div class=\"alert alert-warning\">\n              <i class=\"fas fa-exclamation-triangle me-2\"></i>\n              {{ $t('welcome.create_creds_alert') }}\n              <br>\n              <i class=\"fas fa-shield-alt me-2 mt-2\"></i>\n              {{ $t('welcome.creds_local_only') }}\n            </div>\n\n            <form @submit.prevent=\"save\">\n              <div class=\"row justify-content-center\">\n                <div class=\"col-md-8\">\n                  <div class=\"mb-3\">\n                    <label for=\"usernameInput\" class=\"form-label\">\n                      <i class=\"fas fa-user me-2\"></i>{{ $t('welcome.username') }}\n                    </label>\n                    <input\n                      type=\"text\"\n                      class=\"form-control\"\n                      id=\"usernameInput\"\n                      autocomplete=\"username\"\n                      v-model=\"passwordData.newUsername\"\n                      :placeholder=\"$t('welcome.username')\"\n                    />\n                  </div>\n\n                  <div class=\"mb-3\">\n                    <label for=\"passwordInput\" class=\"form-label\">\n                      <i class=\"fas fa-lock me-2\"></i>{{ $t('welcome.password') }}\n                    </label>\n                    <div class=\"input-group\">\n                      <input\n                        :type=\"showPassword ? 'text' : 'password'\"\n                        class=\"form-control\"\n                        id=\"passwordInput\"\n                        autocomplete=\"new-password\"\n                        v-model=\"passwordData.newPassword\"\n                        :placeholder=\"$t('welcome.password')\"\n                        required\n                      />\n                      <button class=\"btn btn-outline-secondary toggle-password\" type=\"button\" @click=\"showPassword = !showPassword\" :aria-label=\"showPassword ? $t('welcome.hide_password') : $t('welcome.show_password')\">\n                        <i :class=\"showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'\"></i>\n                      </button>\n                    </div>\n                  </div>\n\n                  <div class=\"mb-3\">\n                    <label for=\"confirmPasswordInput\" class=\"form-label\">\n                      <i class=\"fas fa-check-circle me-2\"></i>{{ $t('welcome.confirm_password') }}\n                    </label>\n                    <div class=\"input-group\" :class=\"{ 'is-invalid': !passwordsMatch && passwordData.confirmNewPassword }\">\n                      <input\n                        :type=\"showConfirmPassword ? 'text' : 'password'\"\n                        class=\"form-control\"\n                        id=\"confirmPasswordInput\"\n                        autocomplete=\"new-password\"\n                        v-model=\"passwordData.confirmNewPassword\"\n                        :placeholder=\"$t('welcome.confirm_password')\"\n                        required\n                      />\n                      <button class=\"btn btn-outline-secondary toggle-password\" type=\"button\" @click=\"showConfirmPassword = !showConfirmPassword\" :aria-label=\"showConfirmPassword ? $t('welcome.hide_password') : $t('welcome.show_password')\">\n                        <i :class=\"showConfirmPassword ? 'fas fa-eye-slash' : 'fas fa-eye'\"></i>\n                      </button>\n                    </div>\n                    <div class=\"invalid-feedback d-block\" v-if=\"!passwordsMatch && passwordData.confirmNewPassword\">\n                      <i class=\"fas fa-exclamation-circle me-1\"></i>{{ $t('welcome.password_mismatch') }}\n                    </div>\n                    <div\n                      class=\"valid-feedback d-block\"\n                      v-if=\"passwordsMatch && passwordData.confirmNewPassword && passwordData.newPassword\"\n                    >\n                      <i class=\"fas fa-check-circle me-1\"></i>{{ $t('welcome.password_match') }}\n                    </div>\n                  </div>\n\n                  <button type=\"submit\" class=\"btn btn-primary w-100 mb-3\" :disabled=\"loading || !isFormValid\">\n                    <span\n                      v-if=\"loading\"\n                      class=\"spinner-border spinner-border-sm me-2\"\n                      role=\"status\"\n                      aria-hidden=\"true\"\n                    ></span>\n                    <i v-else class=\"fas fa-sign-in-alt me-2\"></i>\n                    {{ $t('welcome.login') }}\n                  </button>\n\n                  <transition name=\"fade\">\n                    <div class=\"alert alert-danger\" v-if=\"error\">\n                      <i class=\"fas fa-exclamation-circle me-2\"></i>\n                      <strong>{{ $t('welcome.error') }}</strong>\n                      <span v-if=\"error.startsWith('welcome.')\">{{ $t(error) }}</span>\n                      <span v-else>{{ error }}</span>\n                    </div>\n                  </transition>\n\n                  <transition name=\"fade\">\n                    <div class=\"alert alert-success\" v-if=\"success\">\n                      <i class=\"fas fa-check-circle me-2\"></i>\n                      <strong>{{ $t('welcome.success') }}</strong> {{ $t('welcome.welcome_success') }}\n                    </div>\n                  </transition>\n                </div>\n              </div>\n            </form>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { ref, onMounted } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { useWelcome } from '../composables/useWelcome.js'\n\nconst { locale, setLocaleMessage } = useI18n()\nconst selectedLocale = ref('en')\n\nconst { error, success, loading, passwordData, passwordsMatch, isFormValid, save } = useWelcome()\nconst showPassword = ref(false)\nconst showConfirmPassword = ref(false)\n\n// 加载语言（使用静态嵌入的翻译数据）\nconst loadLanguage = (lang) => {\n  const welcomeLocales = window.__WELCOME_LOCALES__\n  \n  if (welcomeLocales && welcomeLocales[lang]) {\n    // 使用嵌入的翻译数据设置到 i18n\n    setLocaleMessage(lang, welcomeLocales[lang])\n    locale.value = lang\n    document.querySelector('html').setAttribute('lang', lang)\n  } else {\n    // 如果没有找到，使用英文\n    locale.value = 'en'\n    document.querySelector('html').setAttribute('lang', 'en')\n  }\n}\n\n// 加载当前语言设置\nonMounted(async () => {\n  try {\n    // 使用 /api/configLocale，这个 API 不需要认证（在 welcome 页面时可能还没有账号）\n    const config = await fetch('/api/configLocale').then((r) => r.json())\n    if (config.locale) {\n      selectedLocale.value = config.locale\n      loadLanguage(config.locale)\n    } else {\n      loadLanguage('en')\n    }\n  } catch (e) {\n    console.error('Failed to load locale config', e)\n    loadLanguage('en')\n  }\n})\n\n// 切换语言\nconst changeLanguage = () => {\n  loadLanguage(selectedLocale.value)\n}\n</script>\n\n<style scoped>  \n:root {\n  --sketch-black: #2c2c2c;\n  --sketch-blue: #4169e1;\n  --sketch-green: #3cb371;\n  --sketch-red: #dc143c;\n  --sketch-yellow: #ffd700;\n  --paper-bg: #fffef9;\n  --pencil-gray: #8b8b8b;\n}\n\n/* 容器定位 */\n#content {\n  position: relative;\n  z-index: 1;\n}\n\n/* 手绘风格卡片 */\n.card {\n  background: #fff;\n  border: 3px solid var(--sketch-black);\n  border-radius: 15px;\n  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.1), 3px 3px 0px rgba(0, 0, 0, 0.05);\n  position: relative;\n  transform: rotate(-0.5deg);\n  filter: url(#pencilTexture);\n}\n\n/* 手绘边框效果 */\n.card::before {\n  content: '';\n  position: absolute;\n  top: -3px;\n  left: -3px;\n  right: -3px;\n  bottom: -3px;\n  border: 2px dashed rgba(0, 0, 0, 0.1);\n  border-radius: 15px;\n  pointer-events: none;\n}\n\n/* 装饰性涂鸦 */\n.card::after {\n  content: '✨';\n  position: absolute;\n  top: -15px;\n  right: 20px;\n  font-size: 2rem;\n  transform: rotate(15deg);\n}\n\n/* 手写标题 */\nh1 {\n  font-family: 'Patrick Hand', 'KaiTi', 'STXingkai', 'Kaiti SC', cursive;\n  color: var(--sketch-black);\n  font-weight: 700;\n  text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.05);\n  transform: rotate(-1deg);\n  position: relative;\n}\n\n/* 标题下划线效果 */\nh1::after {\n  content: '';\n  position: absolute;\n  bottom: -5px;\n  left: 10%;\n  right: 10%;\n  height: 3px;\n  background: var(--sketch-blue);\n  border-radius: 50%;\n  opacity: 0.3;\n}\n\n/* Logo 效果 */\nheader img {\n  filter: drop-shadow(3px 3px 0px rgba(0, 0, 0, 0.1));\n}\n\n/* 副标题 */\n.lead {\n  font-family: 'Indie Flower', 'KaiTi', 'STXingkai', 'Kaiti SC', cursive;\n  color: var(--pencil-gray);\n  font-size: 1.1rem;\n  transform: rotate(0.5deg);\n}\n\n/* 手绘警告框 */\n.alert-warning {\n  background: linear-gradient(135deg, #fff9e6 0%, #fffaeb 100%);\n  border: 3px solid #ff8c00;\n  border-radius: 12px;\n  box-shadow: 5px 5px 0px rgba(255, 140, 0, 0.4), 3px 3px 0px rgba(255, 140, 0, 0.2), 0 0 20px rgba(255, 140, 0, 0.15);\n  transform: rotate(-0.3deg);\n  position: relative;\n  font-family: 'Kalam', 'KaiTi', 'STXingkai', 'Kaiti SC', cursive;\n  color: #d97706;\n  font-size: 1.05rem;\n  font-weight: 600;\n  padding: 1rem 1.25rem;\n}\n\n.alert-warning i {\n  color: var(--sketch-yellow);\n  filter: drop-shadow(1px 1px 0px rgba(0, 0, 0, 0.2));\n}\n\n/* 手绘表单标签 */\n.form-label {\n  font-family: 'Patrick Hand', 'KaiTi', 'STXingkai', 'Kaiti SC', cursive;\n  color: var(--sketch-black);\n  font-size: 1.1rem;\n  font-weight: 700;\n  transform: rotate(-0.5deg);\n  display: inline-block;\n  position: relative;\n}\n\n/* 标签强调下划线 */\n.form-label::after {\n  content: '';\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  width: 100%;\n  height: 2px;\n  background: var(--sketch-blue);\n  opacity: 0.3;\n  transform: scaleX(0);\n  transition: transform 0.3s ease;\n}\n\n.form-label:hover::after {\n  transform: scaleX(1);\n}\n\n.form-label i {\n  color: var(--sketch-blue);\n  margin-right: 0.3rem;\n}\n\n.toggle-password {\n  border: 2px solid var(--sketch-black) !important;\n  border-left: none !important;\n  border-radius: 0 8px 8px 0 !important;\n  background: #fff !important;\n  color: var(--pencil-gray) !important;\n  padding: 0 14px;\n  transition: all 0.2s ease;\n  outline: none !important;\n  box-shadow: none !important;\n}\n\n.toggle-password:hover,\n.toggle-password:focus,\n.toggle-password:active {\n  background: var(--paper-bg-alt, #faf9f6) !important;\n  color: var(--sketch-black) !important;\n  border-color: var(--sketch-black) !important;\n  border-left: none !important;\n  outline: none !important;\n  box-shadow: none !important;\n}\n\n.input-group .form-control {\n  border-radius: 8px 0 0 8px;\n  border-right: none;\n}\n\n/* 手绘输入框 */\n.form-control {\n  background: #fff;\n  border: 2px solid var(--sketch-black);\n  border-radius: 8px;\n  padding: 12px 16px;\n  font-family: 'Kalam', 'KaiTi', 'STXingkai', 'Kaiti SC', cursive;\n  font-size: 1rem;\n  color: var(--sketch-black);\n  box-shadow: inset 2px 2px 0px rgba(0, 0, 0, 0.05), 2px 2px 0px rgba(0, 0, 0, 0.1);\n  transition: all 0.3s ease;\n  position: relative;\n}\n\n.form-control:focus {\n  outline: none;\n  border-color: var(--sketch-blue);\n  background: #f0f8ff;\n  box-shadow: inset 2px 2px 0px rgba(65, 105, 225, 0.1), 3px 3px 0px rgba(65, 105, 225, 0.2);\n  transform: translateY(-2px);\n}\n\n.form-control::placeholder {\n  color: var(--pencil-gray);\n  opacity: 0.6;\n}\n\n/* 验证状态 */\n.form-control.is-invalid {\n  border-color: var(--sketch-red);\n  background: #fff5f5;\n  animation: shake 0.5s;\n}\n\n@keyframes shake {\n  0%,\n  100% {\n    transform: translateX(0) rotate(0deg);\n  }\n  25% {\n    transform: translateX(-5px) rotate(-1deg);\n  }\n  75% {\n    transform: translateX(5px) rotate(1deg);\n  }\n}\n\n.form-control:focus.is-invalid {\n  border-color: var(--sketch-red);\n  box-shadow: inset 2px 2px 0px rgba(220, 20, 60, 0.1), 3px 3px 0px rgba(220, 20, 60, 0.2);\n}\n\n/* 反馈信息 */\n.invalid-feedback,\n.valid-feedback {\n  display: block;\n  margin-top: 0.5rem;\n  font-family: 'Kalam', 'KaiTi', 'STXingkai', 'Kaiti SC', cursive;\n  font-size: 0.9rem;\n  font-weight: 600;\n}\n\n.invalid-feedback {\n  color: var(--sketch-red);\n}\n\n.valid-feedback {\n  color: var(--sketch-green);\n}\n\n.invalid-feedback i,\n.valid-feedback i {\n  display: inline-block;\n}\n\n/* 手绘按钮 */\n.btn-primary {\n  background: var(--sketch-blue);\n  border: 3px solid var(--sketch-black);\n  border-radius: 10px;\n  padding: 14px 32px;\n  font-family: 'Patrick Hand', 'KaiTi', 'STXingkai', 'Kaiti SC', cursive;\n  font-size: 1.2rem;\n  font-weight: 700;\n  color: #fff;\n  text-shadow: 1px 1px 0px rgba(0, 0, 0, 0.2);\n  box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.2), inset -2px -2px 0px rgba(0, 0, 0, 0.1);\n  transition: all 0.2s ease;\n  position: relative;\n  overflow: hidden;\n}\n\n/* 按钮装饰线条 */\n.btn-primary::before {\n  content: '';\n  position: absolute;\n  top: 3px;\n  left: 3px;\n  right: 3px;\n  bottom: 3px;\n  border: 1px dashed rgba(255, 255, 255, 0.3);\n  border-radius: 7px;\n  pointer-events: none;\n}\n\n.btn-primary:hover:not(:disabled) {\n  transform: rotate(-0.5deg) translateY(-3px);\n  box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.25), inset -2px -2px 0px rgba(0, 0, 0, 0.1);\n}\n\n.btn-primary:active:not(:disabled) {\n  transform: rotate(-0.5deg) translateY(0);\n  box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.2), inset -2px -2px 0px rgba(0, 0, 0, 0.1);\n}\n\n.btn-primary:disabled {\n  opacity: 0.5;\n  cursor: not-allowed;\n  background: var(--pencil-gray);\n}\n\n.btn-primary i {\n  display: inline-block;\n}\n\n/* 加载动画 */\n.spinner-border {\n  border-color: rgba(255, 255, 255, 0.3);\n  border-right-color: #fff;\n}\n\n/* 手绘错误提示 */\n.alert-danger {\n  background: #fff0f0;\n  border: 2px solid var(--sketch-red);\n  border-radius: 10px;\n  color: var(--sketch-red);\n  box-shadow: 3px 3px 0px rgba(220, 20, 60, 0.2);\n  transform: rotate(0.5deg);\n  animation: errorPop 0.5s ease;\n  position: relative;\n}\n\n@keyframes errorPop {\n  0% {\n    transform: scale(0.8) rotate(0.5deg);\n    opacity: 0;\n  }\n  50% {\n    transform: scale(1.05) rotate(0.5deg);\n  }\n  100% {\n    transform: scale(1) rotate(0.5deg);\n    opacity: 1;\n  }\n}\n\n.alert-danger::before {\n  content: '❌';\n  position: absolute;\n  top: -12px;\n  left: 10px;\n  font-size: 1.5rem;\n}\n\n/* 手绘成功提示 */\n.alert-success {\n  background: #f0fff4;\n  border: 2px solid var(--sketch-green);\n  border-radius: 10px;\n  color: var(--sketch-green);\n  box-shadow: 3px 3px 0px rgba(60, 179, 113, 0.2);\n  transform: rotate(-0.5deg);\n  animation: successPop 0.6s ease;\n  position: relative;\n}\n\n@keyframes successPop {\n  0% {\n    transform: scale(0.8) rotate(-0.5deg);\n    opacity: 0;\n  }\n  50% {\n    transform: scale(1.05) rotate(-0.5deg);\n  }\n  100% {\n    transform: scale(1) rotate(-0.5deg);\n    opacity: 1;\n  }\n}\n\n.alert-success::before {\n  content: '✓';\n  position: absolute;\n  top: -12px;\n  left: 10px;\n  font-size: 1.8rem;\n  font-weight: bold;\n  color: var(--sketch-green);\n}\n\n/* Vue 过渡动画 */\n.fade-enter-active {\n  animation: sketchIn 0.5s ease;\n}\n\n.fade-leave-active {\n  animation: sketchOut 0.3s ease;\n}\n\n@keyframes sketchIn {\n  0% {\n    opacity: 0;\n    transform: translateY(20px) rotate(-2deg) scale(0.95);\n  }\n  100% {\n    opacity: 1;\n    transform: translateY(0) rotate(0deg) scale(1);\n  }\n}\n\n@keyframes sketchOut {\n  0% {\n    opacity: 1;\n    transform: translateY(0) rotate(0deg);\n  }\n  100% {\n    opacity: 0;\n    transform: translateY(-20px) rotate(2deg);\n  }\n}\n\n/* SVG 滤镜 */\nsvg {\n  position: absolute;\n  width: 0;\n  height: 0;\n}\n\n/* 响应式优化 */\n@media (max-width: 768px) {\n  .card {\n    transform: rotate(0deg);\n    margin: 1rem 0.5rem;\n  }\n\n  h1 {\n    font-size: 1.5rem;\n  }\n\n  .btn-primary {\n    font-size: 1rem;\n    padding: 12px 24px;\n  }\n\n  .col-md-8 {\n    padding-left: 0.5rem;\n    padding-right: 0.5rem;\n  }\n}\n\n/* 语言选择器样式 */\n.language-selector {\n  display: inline-block;\n  position: relative;\n}\n\n.language-selector .form-label {\n  font-family: 'Patrick Hand', 'KaiTi', 'STXingkai', 'Kaiti SC', cursive;\n  color: var(--sketch-black);\n  font-size: 1rem;\n  font-weight: 600;\n  margin-bottom: 0;\n  vertical-align: middle;\n}\n\n.language-selector .form-select-sm {\n  font-family: 'Patrick Hand', 'KaiTi', 'STXingkai', 'Kaiti SC', cursive;\n  border: 2px solid var(--sketch-black);\n  border-radius: 8px;\n  padding: 6px 12px;\n  font-size: 0.95rem;\n  color: var(--sketch-black);\n  background: #fff;\n  box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.1);\n  transition: all 0.3s ease;\n  cursor: pointer;\n}\n\n.language-selector .form-select-sm:focus {\n  outline: none;\n  border-color: var(--sketch-blue);\n  background: #f0f8ff;\n  box-shadow: 3px 3px 0px rgba(65, 105, 225, 0.2);\n  transform: translateY(-1px);\n}\n\n.language-selector .form-select-sm:hover {\n  transform: translateY(-1px);\n  box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.15);\n}\n\n/* 打印样式优化 */\n@media print {\n  .card::after,\n  .alert-warning::before,\n  .alert-danger::before,\n  .alert-success::before,\n  .language-selector {\n    display: none;\n  }\n}\n</style>\n"
  },
  {
    "path": "src_assets/common/assets/web/welcome.html.template",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bs-theme=\"auto\">\n  <head>\n    <%- header %>\n    <link rel=\"stylesheet\" href=\"./styles/welcome.less\" />\n  </head>\n\n  <body id=\"app\" v-cloak>\n    <!-- Vue 应用挂载点 -->\n  </body>\n\n  <!-- WELCOME_LOCALES_INLINE_PLACEHOLDER -->\n\n  <script type=\"module\">\n    import { createApp } from 'vue'\n    import { initApp } from './init'\n    import Welcome from './views/Welcome.vue'\n\n    const app = createApp(Welcome)\n    initApp(app)\n  </script>\n</html>\n\n"
  },
  {
    "path": "src_assets/linux/assets/apps.json",
    "content": "{\n  \"env\": {\n    \"PATH\": \"$(PATH):$(HOME)/.local/bin\"\n  },\n  \"apps\": [\n    {\n      \"name\": \"Desktop\",\n      \"image-path\": \"desktop.png\"\n    },\n    {\n      \"name\": \"Low Res Desktop\",\n      \"image-path\": \"desktop.png\",\n      \"prep-cmd\": [\n        {\n          \"do\": \"xrandr --output HDMI-1 --mode 1920x1080\",\n          \"undo\": \"xrandr --output HDMI-1 --mode 1920x1200\"\n        }\n      ]\n    },\n    {\n      \"name\": \"Steam Big Picture\",\n      \"detached\": [\n        \"setsid steam steam://open/bigpicture\"\n      ],\n      \"image-path\": \"steam.png\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src_assets/linux/assets/shaders/opengl/ConvertUV.frag",
    "content": "#version 300 es\n\n#ifdef GL_ES\nprecision lowp float;\n#endif\n\nuniform sampler2D image;\n\nlayout(shared) uniform ColorMatrix {\n  vec4 color_vec_y;\n  vec4 color_vec_u;\n  vec4 color_vec_v;\n  vec2 range_y;\n  vec2 range_uv;\n};\n\nin vec3 uuv;\nlayout(location = 0) out vec2 color;\n\n//--------------------------------------------------------------------------------------\n// Pixel Shader\n//--------------------------------------------------------------------------------------\nvoid main() {\n  vec3 rgb_left  = texture(image, uuv.xz).rgb;\n  vec3 rgb_right = texture(image, uuv.yz).rgb;\n  vec3 rgb       = (rgb_left + rgb_right) * 0.5;\n\n  float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w;\n  float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w;\n\n  u = u * range_uv.x + range_uv.y;\n  v = v * range_uv.x + range_uv.y;\n\n  color = vec2(u, v);\n}"
  },
  {
    "path": "src_assets/linux/assets/shaders/opengl/ConvertUV.vert",
    "content": "#version 300 es\n\n#ifdef GL_ES\nprecision mediump float;\n#endif\n\nuniform float width_i;\n\nout vec3 uuv;\n//--------------------------------------------------------------------------------------\n// Vertex Shader\n//--------------------------------------------------------------------------------------\nvoid main()\n{\n\tfloat idHigh = float(gl_VertexID >> 1);\n\tfloat idLow = float(gl_VertexID & int(1));\n\n\tfloat x = idHigh * 4.0 - 1.0;\n\tfloat y = idLow * 4.0 - 1.0;\n\n\tfloat u_right = idHigh * 2.0;\n\tfloat u_left = u_right - width_i;\n\tfloat v = idLow * 2.0;\n\n\tuuv = vec3(u_left, u_right, v);\n\tgl_Position = vec4(x, y, 0.0, 1.0);\n}"
  },
  {
    "path": "src_assets/linux/assets/shaders/opengl/ConvertY.frag",
    "content": "#version 300 es\n\n#ifdef GL_ES\nprecision lowp float;\n#endif\n\nuniform sampler2D image;\n\nlayout(shared) uniform ColorMatrix {\n  vec4 color_vec_y;\n  vec4 color_vec_u;\n  vec4 color_vec_v;\n  vec2 range_y;\n  vec2 range_uv;\n};\n\nin vec2 tex;\nlayout(location = 0) out float color;\n\nvoid main()\n{\n\tvec3 rgb = texture(image, tex).rgb;\n\tfloat y = dot(color_vec_y.xyz, rgb);\n\n\tcolor = y * range_y.x + range_y.y;\n}"
  },
  {
    "path": "src_assets/linux/assets/shaders/opengl/Scene.frag",
    "content": "#version 300 es\n\n#ifdef GL_ES\nprecision lowp float;\n#endif\n\nuniform sampler2D image;\n\nin vec2 tex;\nlayout(location = 0) out vec4 color;\nvoid main()\n{\n\tcolor = texture(image, tex);\n}"
  },
  {
    "path": "src_assets/linux/assets/shaders/opengl/Scene.vert",
    "content": "#version 300 es\n\n#ifdef GL_ES\nprecision mediump float;\n#endif\n\nout vec2 tex;\n\nvoid main()\n{\n\tfloat idHigh = float(gl_VertexID >> 1);\n\tfloat idLow = float(gl_VertexID & int(1));\n\n\tfloat x = idHigh * 4.0 - 1.0;\n\tfloat y = idLow * 4.0 - 1.0;\n\n\tfloat u = idHigh * 2.0;\n\tfloat v = idLow * 2.0;\n\n\tgl_Position = vec4(x, y, 0.0, 1.0);\n\ttex = vec2(u, v);\n}"
  },
  {
    "path": "src_assets/linux/misc/60-sunshine.rules",
    "content": "KERNEL==\"uinput\", SUBSYSTEM==\"misc\", OPTIONS+=\"static_node=uinput\", TAG+=\"uaccess\"\nKERNEL==\"uhid\", TAG+=\"uaccess\"\n"
  },
  {
    "path": "src_assets/linux/misc/postinst",
    "content": "#!/bin/sh\n\n# Ensure Sunshine can grab images from KMS\npath_to_setcap=$(which setcap)\npath_to_sunshine=$(readlink -f $(which sunshine))\nif [ -x \"$path_to_setcap\" ] ; then\n  echo \"$path_to_setcap cap_sys_admin+p $path_to_sunshine\"\n        $path_to_setcap cap_sys_admin+p $path_to_sunshine\nfi\n\n# Trigger udev rule reload for /dev/uinput and /dev/uhid\npath_to_udevadm=$(which udevadm)\nif [ -x \"$path_to_udevadm\" ] ; then\n  $path_to_udevadm control --reload-rules\n  $path_to_udevadm trigger --property-match=DEVNAME=/dev/uinput\n  $path_to_udevadm trigger --property-match=DEVNAME=/dev/uhid\nfi\n"
  },
  {
    "path": "src_assets/macos/assets/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CFBundleIdentifier</key>\n  <string>com.alkaidlab.sunshine</string>\n  <key>CFBundleName</key>\n  <string>Foundation Sunshine</string>\n  <key>NSMicrophoneUsageDescription</key>\n  <string>This app requires access to your microphone to stream audio.</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "src_assets/macos/assets/apps.json",
    "content": "{\n  \"env\": {\n    \"PATH\": \"$(PATH):$(HOME)/.local/bin\"\n  },\n  \"apps\": [\n    {\n      \"name\": \"Desktop\",\n      \"image-path\": \"desktop.png\"\n    },\n    {\n      \"name\": \"Steam Big Picture\",\n      \"detached\": [\n        \"open steam://open/bigpicture\"\n      ],\n      \"image-path\": \"steam.png\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src_assets/macos/misc/uninstall_pkg.sh",
    "content": "#!/bin/bash -e\n\n# note: this file was used to remove files when using the pkg/dmg, it is no longer used, but left for reference\n\nset -e\n\npackage_name=org.macports.Sunshine\n\necho \"Removing files now...\"\nFILES=$(pkgutil --files $package_name --only-files)\n\nfor file in ${FILES}; do\n    file=\"/$file\"\n    echo \"removing: $file\"\n    rm -f \"$file\"\ndone\n\necho \"Removing directories now...\"\nDIRECTORIES=$(pkgutil --files org.macports.Sunshine --only-dirs)\n\nfor dir in ${DIRECTORIES}; do\n    dir=\"/$dir\"\n    echo \"Checking if empty directory: $dir\"\n\n    # check if directory is empty... could just use ${DIRECTORIES} here if pkgutils added the `/` prefix\n    empty_dir=$(find \"$dir\" -depth 0 -type d -empty)\n\n    # remove the directory if it is empty\n    if [[ $empty_dir != \"\" ]]; then  # prevent the loop from running and failing if no directories found\n        for i in \"${empty_dir}\"; do  # don't split words as we already know this will be a single directory\n            echo \"Removing empty directory: ${i}\"\n            rmdir \"${i}\"\n        done\n    fi\ndone\n\necho \"Forgetting Sunshine...\"\npkgutil --forget $package_name\n\necho \"Sunshine has been uninstalled...\"\n"
  },
  {
    "path": "src_assets/windows/assets/apps.json",
    "content": "{\n  \"env\": {},\n  \"apps\": [\n    {\n      \"name\": \"Desktop\",\n      \"image-path\": \"desktop\",\n      \"exclude-global-prep-cmd\": \"false\",\n      \"elevated\": \"\",\n      \"auto-detach\": \"true\",\n      \"wait-all\": \"true\",\n      \"exit-timeout\": \"5\",\n      \"menu-cmd\": [\n        {\n          \"id\": \"kcENAT5r9P\",\n          \"name\": \"触摸键盘\",\n          \"cmd\": \".\\\\tools\\\\qiin-tabtip.exe\",\n          \"elevated\": \"false\"\n        },\n        {\n          \"id\": \"rjeOKHmcdL\",\n          \"name\": \"桌宠\",\n          \"cmd\": \".\\\\assets\\\\gui\\\\sunshine-gui.exe --toolbar\",\n          \"elevated\": \"false\"\n        }\n      ]\n    },\n    {\n      \"name\": \"Steam Big Picture\",\n      \"cmd\": \"steam://open/bigpicture\",\n      \"auto-detach\": \"true\",\n      \"wait-all\": \"true\",\n      \"image-path\": \"steam.png\"\n    },\n    {\n      \"name\": \"Xbox Game\",\n      \"cmd\": \"cmd \\/c \\\"start xbox:\\\"\",\n      \"auto-detach\": \"true\",\n      \"wait-all\": \"true\",\n      \"image-path\": \"box.png\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_bicubic_ps.hlsl",
    "content": "#include \"include/convert_base.hlsl\"\n\n#include \"include/convert_yuv420_packed_uv_bicubic_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_bicubic_ps_hybrid_log_gamma.hlsl",
    "content": "#include \"include/convert_hybrid_log_gamma_base.hlsl\"\n\n#include \"include/convert_yuv420_packed_uv_bicubic_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_bicubic_ps_linear.hlsl",
    "content": "#include \"include/convert_linear_base.hlsl\"\n\n#include \"include/convert_yuv420_packed_uv_bicubic_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_bicubic_ps_perceptual_quantizer.hlsl",
    "content": "#include \"include/convert_perceptual_quantizer_base.hlsl\"\n\n#include \"include/convert_yuv420_packed_uv_bicubic_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_bicubic_vs.hlsl",
    "content": "cbuffer rotate_texture_steps_cbuffer : register(b1) {\n    int rotate_texture_steps;\n};\n\n#include \"include/base_vs.hlsl\"\n\nvertex_t main_vs(uint vertex_id : SV_VertexID)\n{\n    // For bicubic sampling, we don't need subsample_offset, just use standard texture coordinates\n    return generate_fullscreen_triangle_vertex(vertex_id, float2(0, 0), rotate_texture_steps);\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0_ps.hlsl",
    "content": "#include \"include/convert_base.hlsl\"\n\n#define LEFT_SUBSAMPLING\n\n#include \"include/convert_yuv420_packed_uv_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0_ps_hybrid_log_gamma.hlsl",
    "content": "#include \"include/convert_hybrid_log_gamma_base.hlsl\"\n\n#define LEFT_SUBSAMPLING\n\n#include \"include/convert_yuv420_packed_uv_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0_ps_linear.hlsl",
    "content": "#include \"include/convert_linear_base.hlsl\"\n\n#define LEFT_SUBSAMPLING\n\n#include \"include/convert_yuv420_packed_uv_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0_ps_perceptual_quantizer.hlsl",
    "content": "#include \"include/convert_perceptual_quantizer_base.hlsl\"\n\n#define LEFT_SUBSAMPLING\n\n#include \"include/convert_yuv420_packed_uv_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0_vs.hlsl",
    "content": "cbuffer subsample_offset_cbuffer : register(b0) {\n    float2 subsample_offset;\n};\n\ncbuffer rotate_texture_steps_cbuffer : register(b1) {\n    int rotate_texture_steps;\n};\n\n#define LEFT_SUBSAMPLING\n#include \"include/base_vs.hlsl\"\n\nvertex_t main_vs(uint vertex_id : SV_VertexID)\n{\n    return generate_fullscreen_triangle_vertex(vertex_id, subsample_offset, rotate_texture_steps);\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0s_ps.hlsl",
    "content": "#include \"include/convert_base.hlsl\"\n\n#define LEFT_SUBSAMPLING_SCALE\n\n#include \"include/convert_yuv420_packed_uv_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0s_ps_hybrid_log_gamma.hlsl",
    "content": "#include \"include/convert_hybrid_log_gamma_base.hlsl\"\n\n#define LEFT_SUBSAMPLING_SCALE\n\n#include \"include/convert_yuv420_packed_uv_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0s_ps_linear.hlsl",
    "content": "#include \"include/convert_linear_base.hlsl\"\n\n#define LEFT_SUBSAMPLING_SCALE\n\n#include \"include/convert_yuv420_packed_uv_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0s_ps_perceptual_quantizer.hlsl",
    "content": "#include \"include/convert_perceptual_quantizer_base.hlsl\"\n\n#define LEFT_SUBSAMPLING_SCALE\n\n#include \"include/convert_yuv420_packed_uv_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_packed_uv_type0s_vs.hlsl",
    "content": "cbuffer subsample_offset_cbuffer : register(b0) {\n    float2 subsample_offset;\n};\n\ncbuffer rotate_texture_steps_cbuffer : register(b1) {\n    int rotate_texture_steps;\n};\n\n#define LEFT_SUBSAMPLING_SCALE\n#include \"include/base_vs.hlsl\"\n\nvertex_t main_vs(uint vertex_id : SV_VertexID)\n{\n    return generate_fullscreen_triangle_vertex(vertex_id, subsample_offset, rotate_texture_steps);\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_bicubic_ps.hlsl",
    "content": "#include \"include/convert_base.hlsl\"\n\n#include \"include/convert_yuv420_planar_y_bicubic_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_bicubic_ps_hybrid_log_gamma.hlsl",
    "content": "#include \"include/convert_hybrid_log_gamma_base.hlsl\"\n\n#include \"include/convert_yuv420_planar_y_bicubic_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_bicubic_ps_linear.hlsl",
    "content": "#include \"include/convert_linear_base.hlsl\"\n\n#include \"include/convert_yuv420_planar_y_bicubic_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_bicubic_ps_perceptual_quantizer.hlsl",
    "content": "#include \"include/convert_perceptual_quantizer_base.hlsl\"\n\n#include \"include/convert_yuv420_planar_y_bicubic_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_ps.hlsl",
    "content": "#include \"include/convert_base.hlsl\"\n\n#include \"include/convert_yuv420_planar_y_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_ps_hybrid_log_gamma.hlsl",
    "content": "#include \"include/convert_hybrid_log_gamma_base.hlsl\"\n\n#include \"include/convert_yuv420_planar_y_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_ps_linear.hlsl",
    "content": "#include \"include/convert_linear_base.hlsl\"\n\n#include \"include/convert_yuv420_planar_y_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_ps_perceptual_quantizer.hlsl",
    "content": "#include \"include/convert_perceptual_quantizer_base.hlsl\"\n\n#include \"include/convert_yuv420_planar_y_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv420_planar_y_vs.hlsl",
    "content": "cbuffer rotate_texture_steps_cbuffer : register(b1) {\n    int rotate_texture_steps;\n};\n\n#include \"include/base_vs.hlsl\"\n\nvertex_t main_vs(uint vertex_id : SV_VertexID)\n{\n    return generate_fullscreen_triangle_vertex(vertex_id, float2(0, 0), rotate_texture_steps);\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_packed_ayuv_ps.hlsl",
    "content": "#include \"include/convert_base.hlsl\"\n\n#include \"include/convert_yuv444_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_packed_ayuv_ps_linear.hlsl",
    "content": "#include \"include/convert_linear_base.hlsl\"\n\n#include \"include/convert_yuv444_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_packed_vs.hlsl",
    "content": "cbuffer rotate_texture_steps_cbuffer : register(b1) {\n    int rotate_texture_steps;\n};\n\n#include \"include/base_vs.hlsl\"\n\nvertex_t main_vs(uint vertex_id : SV_VertexID)\n{\n    return generate_fullscreen_triangle_vertex(vertex_id, float2(0, 0), rotate_texture_steps);\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_packed_y410_ps.hlsl",
    "content": "#include \"include/convert_base.hlsl\"\n\n#define Y410\n#include \"include/convert_yuv444_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_packed_y410_ps_hybrid_log_gamma.hlsl",
    "content": "#include \"include/convert_hybrid_log_gamma_base.hlsl\"\n\n#define Y410\n#include \"include/convert_yuv444_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_packed_y410_ps_linear.hlsl",
    "content": "#include \"include/convert_linear_base.hlsl\"\n\n#define Y410\n#include \"include/convert_yuv444_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_packed_y410_ps_perceptual_quantizer.hlsl",
    "content": "#include \"include/convert_perceptual_quantizer_base.hlsl\"\n\n#define Y410\n#include \"include/convert_yuv444_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_planar_ps.hlsl",
    "content": "#include \"include/convert_base.hlsl\"\n\n#define PLANAR_VIEWPORTS\n#include \"include/convert_yuv444_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_planar_ps_hybrid_log_gamma.hlsl",
    "content": "#include \"include/convert_hybrid_log_gamma_base.hlsl\"\n\n#define PLANAR_VIEWPORTS\n#include \"include/convert_yuv444_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_planar_ps_linear.hlsl",
    "content": "#include \"include/convert_linear_base.hlsl\"\n\n#define PLANAR_VIEWPORTS\n#include \"include/convert_yuv444_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_planar_ps_perceptual_quantizer.hlsl",
    "content": "#include \"include/convert_perceptual_quantizer_base.hlsl\"\n\n#define PLANAR_VIEWPORTS\n#include \"include/convert_yuv444_ps_base.hlsl\"\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/convert_yuv444_planar_vs.hlsl",
    "content": "cbuffer rotate_texture_steps_cbuffer : register(b1) {\n    int rotate_texture_steps;\n};\n\ncbuffer color_matrix_cbuffer : register(b3) {\n    float4 color_vec_y;\n    float4 color_vec_u;\n    float4 color_vec_v;\n    float2 range_y;\n    float2 range_uv;\n};\n\n#define PLANAR_VIEWPORTS\n#include \"include/base_vs.hlsl\"\n\nvertex_t main_vs(uint vertex_id : SV_VertexID)\n{\n    vertex_t output = generate_fullscreen_triangle_vertex(vertex_id % 3, float2(0, 0), rotate_texture_steps);\n\n    output.viewport = vertex_id / 3;\n\n    if (output.viewport == 0) {\n        output.color_vec = color_vec_y;\n    }\n    else if (output.viewport == 1) {\n        output.color_vec = color_vec_u;\n    }\n    else {\n        output.color_vec = color_vec_v;\n    }\n\n    return output;\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/cursor_ps.hlsl",
    "content": "Texture2D cursor : register(t0);\nSamplerState def_sampler : register(s0);\n\n#include \"include/base_vs_types.hlsl\"\n\nfloat4 main_ps(vertex_t input) : SV_Target\n{\n    return cursor.Sample(def_sampler, input.tex_coord, 0);\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/cursor_ps_normalize_white.hlsl",
    "content": "Texture2D cursor : register(t0);\nSamplerState def_sampler : register(s0);\n\ncbuffer normalize_white_cbuffer : register(b1) {\n    float white_multiplier;\n};\n\n#include \"include/base_vs_types.hlsl\"\n\nfloat4 main_ps(vertex_t input) : SV_Target\n{\n    float4 output = cursor.Sample(def_sampler, input.tex_coord, 0);\n\n    output.rgb = output.rgb * white_multiplier;\n\n    return output;\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/cursor_vs.hlsl",
    "content": "cbuffer rotate_texture_steps_cbuffer : register(b2) {\n    int rotate_texture_steps;\n};\n\n#include \"include/base_vs.hlsl\"\n\nvertex_t main_vs(uint vertex_id : SV_VertexID)\n{\n    return generate_fullscreen_triangle_vertex(vertex_id, float2(0, 0), rotate_texture_steps);\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/hdr_luminance_analysis_cs.hlsl",
    "content": "/**\n * @file hdr_luminance_analysis_cs.hlsl\n * @brief GPU compute shader for per-frame HDR luminance analysis.\n *\n * Analyzes captured scRGB FP16 frames to extract per-frame luminance statistics\n * for generating accurate HDR dynamic metadata (CUVA HDR Vivid / HDR10+).\n *\n * Input: scRGB FP16 texture (R16G16B16A16_FLOAT)\n *   - scRGB uses BT.709 primaries in linear light\n *   - 1.0 in scRGB = 80 nits (SDR reference white)\n *\n * Output: Per-group reduction results in a structured buffer.\n *   Each thread group (16x16 = 256 threads) processes one tile and writes\n *   {min, max, sum, count, histogram[128]} of maxRGB values (in nits)\n *   to the output buffer. A second-pass shader reduces all groups to one.\n *\n * Histogram: 128 bins covering 0-10000 nits, each bin = 78.125 nits wide.\n *   Used for P95/P99 percentile computation for stable peak luminance.\n *\n * Thread group size: 16x16 = 256 threads\n * Dispatch: (ceil(analysisWidth/16), ceil(analysisHeight/16), 1)\n */\n\n// scRGB to nits conversion factor\nstatic const float SCRGB_NITS_PER_UNIT = 80.0;\n\n// Histogram parameters\nstatic const uint HISTOGRAM_BINS = 128;\nstatic const float HISTOGRAM_MAX_NITS = 10000.0;\nstatic const float NITS_PER_BIN = HISTOGRAM_MAX_NITS / HISTOGRAM_BINS;  // 78.125\n\n// Input texture (scRGB FP16)\nTexture2D<float4> inputTexture : register(t0);\nSamplerState linearSampler : register(s0);\n\ncbuffer AnalysisParams : register(b0) {\n    uint analysisWidth;\n    uint analysisHeight;\n    uint2 _pad;\n};\n\n// Per-group reduction results\nstruct GroupResult {\n    float minMaxRGB;                  // Minimum of max(R,G,B) in nits\n    float maxMaxRGB;                  // Maximum of max(R,G,B) in nits\n    float sumMaxRGB;                  // Sum of max(R,G,B) in nits (for average)\n    uint  pixelCount;                 // Number of valid pixels processed\n    uint  histogram[HISTOGRAM_BINS];  // Luminance histogram (128 bins)\n};\n\nRWStructuredBuffer<GroupResult> groupResults : register(u0);\n\n// Shared memory for intra-group parallel reduction\ngroupshared float gs_min[256];\ngroupshared float gs_max[256];\ngroupshared float gs_sum[256];\ngroupshared uint  gs_count[256];\ngroupshared uint  gs_histogram[HISTOGRAM_BINS];\n\n[numthreads(16, 16, 1)]\nvoid main_cs(uint3 DTid : SV_DispatchThreadID,\n             uint3 GTid : SV_GroupThreadID,\n             uint3 Gid  : SV_GroupID,\n             uint  GIndex : SV_GroupIndex)\n{\n    // Initialize shared histogram bins (each thread zeroes ~1 bin, 256 threads > 128 bins)\n    if (GIndex < HISTOGRAM_BINS) {\n        gs_histogram[GIndex] = 0;\n    }\n\n    // Compute maxRGB for this analysis sample\n    float maxRGB_nits = 0.0;\n    bool valid = (DTid.x < analysisWidth && DTid.y < analysisHeight);\n\n    if (valid) {\n        // Sample the full-resolution frame on a lower-resolution analysis grid.\n        float2 uv = (float2(DTid.xy) + 0.5) / float2(analysisWidth, analysisHeight);\n        float4 pixel = inputTexture.SampleLevel(linearSampler, uv, 0.0);\n\n        // maxRGB = max(R, G, B) — the brightest channel per pixel\n        // This is the key statistic used by CUVA HDR Vivid and HDR10+\n        float maxRGB = max(max(pixel.r, pixel.g), pixel.b);\n\n        // Clamp negative values (out-of-gamut in scRGB)\n        maxRGB = max(maxRGB, 0.0);\n\n        // Convert to nits: scRGB 1.0 = 80 nits\n        maxRGB_nits = maxRGB * SCRGB_NITS_PER_UNIT;\n    }\n\n    // Initialize shared memory for min/max/sum/count reduction\n    gs_min[GIndex] = valid ? maxRGB_nits : 100000.0;  // Large sentinel for min\n    gs_max[GIndex] = valid ? maxRGB_nits : 0.0;\n    gs_sum[GIndex] = valid ? maxRGB_nits : 0.0;\n    gs_count[GIndex] = valid ? 1u : 0u;\n\n    GroupMemoryBarrierWithGroupSync();\n\n    // Accumulate into shared histogram using atomic add\n    if (valid) {\n        uint bin = min((uint)(maxRGB_nits / NITS_PER_BIN), HISTOGRAM_BINS - 1);\n        InterlockedAdd(gs_histogram[bin], 1);\n    }\n\n    GroupMemoryBarrierWithGroupSync();\n\n    // Parallel reduction for min/max/sum/count (log2(256) = 8 steps)\n    [unroll]\n    for (uint stride = 128; stride > 0; stride >>= 1) {\n        if (GIndex < stride) {\n            gs_min[GIndex] = min(gs_min[GIndex], gs_min[GIndex + stride]);\n            gs_max[GIndex] = max(gs_max[GIndex], gs_max[GIndex + stride]);\n            gs_sum[GIndex] += gs_sum[GIndex + stride];\n            gs_count[GIndex] += gs_count[GIndex + stride];\n        }\n        GroupMemoryBarrierWithGroupSync();\n    }\n\n    // Thread 0 writes the group's result (including histogram)\n    if (GIndex == 0) {\n        // Compute flat group index\n        uint dispatchWidth = (analysisWidth + 15) / 16;\n        uint groupIndex = Gid.y * dispatchWidth + Gid.x;\n\n        GroupResult result;\n        result.minMaxRGB = gs_min[0];\n        result.maxMaxRGB = gs_max[0];\n        result.sumMaxRGB = gs_sum[0];\n        result.pixelCount = gs_count[0];\n\n        [unroll]\n        for (uint i = 0; i < HISTOGRAM_BINS; i++) {\n            result.histogram[i] = gs_histogram[i];\n        }\n\n        groupResults[groupIndex] = result;\n    }\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/hdr_luminance_reduce_cs.hlsl",
    "content": "/**\n * @file hdr_luminance_reduce_cs.hlsl\n * @brief Second-pass GPU reduction shader for HDR luminance analysis.\n *\n * Reduces per-group results from the first-pass analysis shader into a single\n * final result. This eliminates CPU iteration over thousands of groups.\n *\n * Input:  StructuredBuffer of GroupResult from first pass (N groups)\n * Output: RWStructuredBuffer with 1 FinalResult containing:\n *         - Global min/max/sum/count\n *         - Merged 128-bin histogram\n *\n * Dispatch: (1, 1, 1) — single thread group of 256 threads\n * Each thread processes ceil(N/256) groups.\n *\n * cbuffer provides numGroups so the shader knows how many to reduce.\n */\n\nstatic const uint HISTOGRAM_BINS = 128;\n\nstruct GroupResult {\n    float minMaxRGB;\n    float maxMaxRGB;\n    float sumMaxRGB;\n    uint  pixelCount;\n    uint  histogram[HISTOGRAM_BINS];\n};\n\nstruct FinalResult {\n    float minMaxRGB;\n    float maxMaxRGB;\n    float sumMaxRGB;\n    uint  pixelCount;\n    uint  histogram[HISTOGRAM_BINS];\n};\n\n// Input: per-group results from first pass (SRV)\nStructuredBuffer<GroupResult> groupResults : register(t0);\n\n// Output: single merged result (UAV)\nRWStructuredBuffer<FinalResult> finalResult : register(u0);\n\n// Number of groups to reduce\ncbuffer ReduceParams : register(b0) {\n    uint numGroups;\n    uint3 _pad;\n};\n\n// Shared memory for parallel reduction\ngroupshared float gs_min[256];\ngroupshared float gs_max[256];\ngroupshared float gs_sum[256];\ngroupshared uint  gs_count[256];\ngroupshared uint  gs_histogram[HISTOGRAM_BINS];\n\n[numthreads(256, 1, 1)]\nvoid main_cs(uint GIndex : SV_GroupIndex)\n{\n    // Initialize histogram bins (256 threads > 128 bins, first 128 threads init)\n    if (GIndex < HISTOGRAM_BINS) {\n        gs_histogram[GIndex] = 0;\n    }\n\n    // Each thread sequentially processes its assigned groups\n    float local_min = 100000.0;\n    float local_max = 0.0;\n    float local_sum = 0.0;\n    uint  local_count = 0;\n\n    // Distribute groups across 256 threads\n    uint groupsPerThread = (numGroups + 255) / 256;\n    uint startGroup = GIndex * groupsPerThread;\n    uint endGroup = min(startGroup + groupsPerThread, numGroups);\n\n    for (uint g = startGroup; g < endGroup; g++) {\n        GroupResult gr = groupResults[g];\n        if (gr.pixelCount > 0) {\n            local_min = min(local_min, gr.minMaxRGB);\n            local_max = max(local_max, gr.maxMaxRGB);\n            local_sum += gr.sumMaxRGB;\n            local_count += gr.pixelCount;\n        }\n    }\n\n    gs_min[GIndex] = local_min;\n    gs_max[GIndex] = local_max;\n    gs_sum[GIndex] = local_sum;\n    gs_count[GIndex] = local_count;\n\n    GroupMemoryBarrierWithGroupSync();\n\n    // Merge histogram bins: each of the first 128 threads handles one bin\n    // across all groups (sequential accumulation per bin)\n    if (GIndex < HISTOGRAM_BINS) {\n        uint binSum = 0;\n        for (uint g = 0; g < numGroups; g++) {\n            binSum += groupResults[g].histogram[GIndex];\n        }\n        gs_histogram[GIndex] = binSum;\n    }\n\n    GroupMemoryBarrierWithGroupSync();\n\n    // Parallel reduction of min/max/sum/count (log2(256) = 8 steps)\n    [unroll]\n    for (uint stride = 128; stride > 0; stride >>= 1) {\n        if (GIndex < stride) {\n            gs_min[GIndex] = min(gs_min[GIndex], gs_min[GIndex + stride]);\n            gs_max[GIndex] = max(gs_max[GIndex], gs_max[GIndex + stride]);\n            gs_sum[GIndex] += gs_sum[GIndex + stride];\n            gs_count[GIndex] += gs_count[GIndex + stride];\n        }\n        GroupMemoryBarrierWithGroupSync();\n    }\n\n    // Thread 0 writes final merged result (scalars only; histogram written below by 128 threads)\n    if (GIndex == 0) {\n        finalResult[0].minMaxRGB = gs_min[0];\n        finalResult[0].maxMaxRGB = gs_max[0];\n        finalResult[0].sumMaxRGB = gs_sum[0];\n        finalResult[0].pixelCount = gs_count[0];\n    }\n\n    GroupMemoryBarrierWithGroupSync();\n\n    // First 128 threads write histogram bins to output\n    if (GIndex < HISTOGRAM_BINS) {\n        finalResult[0].histogram[GIndex] = gs_histogram[GIndex];\n    }\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/base_vs.hlsl",
    "content": "#include \"include/base_vs_types.hlsl\"\r\n\r\nvertex_t generate_fullscreen_triangle_vertex(uint vertex_id, float2 subsample_offset, int rotate_texture_steps)\r\n{\r\n    vertex_t output;\r\n    float2 tex_coord;\r\n\r\n    if (vertex_id == 0) {\r\n        output.viewpoint_pos = float4(-1, -1, 0, 1);\r\n        tex_coord = float2(0, 1);\r\n    }\r\n    else if (vertex_id == 1) {\r\n        output.viewpoint_pos = float4(-1, 3, 0, 1);\r\n        tex_coord = float2(0, -1);\r\n    }\r\n    else {\r\n        output.viewpoint_pos = float4(3, -1, 0, 1);\r\n        tex_coord = float2(2, 1);\r\n    }\r\n\r\n    if (rotate_texture_steps != 0) {\r\n        float rotation_radians = radians(90 * rotate_texture_steps);\r\n        float2x2 rotation_matrix = { cos(rotation_radians), -sin(rotation_radians),\r\n                                     sin(rotation_radians), cos(rotation_radians) };\r\n        float2 rotation_center = { 0.5, 0.5 };\r\n        tex_coord = round(rotation_center + mul(rotation_matrix, tex_coord - rotation_center));\r\n\r\n        // Swap the xy offset coordinates if the texture is rotated an odd number of times.\r\n        if (rotate_texture_steps & 1) {\r\n            subsample_offset.xy = subsample_offset.yx;\r\n        }\r\n    }\r\n\r\n#if defined(LEFT_SUBSAMPLING)\r\n    output.tex_right_left_center = float3(tex_coord.x, tex_coord.x - subsample_offset.x, tex_coord.y);\r\n#elif defined(LEFT_SUBSAMPLING_SCALE)\r\n    float2 halfsample_offset = subsample_offset / 2;\r\n    float3 right_center_left = float3(tex_coord.x + halfsample_offset.x,\r\n                                      tex_coord.x - halfsample_offset.x,\r\n                                      tex_coord.x - 3 * halfsample_offset.x);\r\n    float2 top_bottom = float2(tex_coord.y - halfsample_offset.y,\r\n                               tex_coord.y + halfsample_offset.y);\r\n    output.tex_right_center_left_top = float4(right_center_left, top_bottom.x);\r\n    output.tex_right_center_left_bottom = float4(right_center_left, top_bottom.y);\r\n#elif defined(TOPLEFT_SUBSAMPLING)\r\n    output.tex_right_left_top = float3(tex_coord.x, tex_coord.x - subsample_offset.x, tex_coord.y - subsample_offset.y);\r\n    output.tex_right_left_bottom = float3(tex_coord.x, tex_coord.x - subsample_offset.x, tex_coord.y);\r\n#else\r\n    output.tex_coord = tex_coord;\r\n#endif\r\n\r\n    return output;\r\n}"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/base_vs_types.hlsl",
    "content": "struct vertex_t\r\n{\r\n    float4 viewpoint_pos : SV_Position;\r\n#if defined(LEFT_SUBSAMPLING)\r\n    float3 tex_right_left_center : TEXCOORD;\r\n#elif defined(LEFT_SUBSAMPLING_SCALE)\r\n    float4 tex_right_center_left_top : TEXCOORD0;\r\n    float4 tex_right_center_left_bottom : TEXCOORD1;\r\n#elif defined(TOPLEFT_SUBSAMPLING)\r\n    float3 tex_right_left_top : TEXCOORD0;\r\n    float3 tex_right_left_bottom : TEXCOORD1;\r\n#else\r\n    float2 tex_coord : TEXCOORD;\r\n#endif\r\n#ifdef PLANAR_VIEWPORTS\r\n    uint viewport : SV_ViewportArrayIndex;\r\n    nointerpolation float4 color_vec : COLOR0;\r\n#endif\r\n};\r\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/common.hlsl",
    "content": "// This is a fast sRGB approximation from Microsoft's ColorSpaceUtility.hlsli\r\nfloat3 ApplySRGBCurve(float3 x)\r\n{\r\n    return x < 0.0031308 ? 12.92 * x : 1.13005 * sqrt(x - 0.00228) - 0.13448 * x + 0.005719;\r\n}\r\n\r\nfloat3 NitsToPQ(float3 L)\r\n{\r\n    // Constants from SMPTE 2084 PQ\r\n    static const float m1 = 2610.0 / 4096.0 / 4;\r\n    static const float m2 = 2523.0 / 4096.0 * 128;\r\n    static const float c1 = 3424.0 / 4096.0;\r\n    static const float c2 = 2413.0 / 4096.0 * 32;\r\n    static const float c3 = 2392.0 / 4096.0 * 32;\r\n\r\n    float3 Lp = pow(saturate(L / 10000.0), m1);\r\n    return pow((c1 + c2 * Lp) / (1 + c3 * Lp), m2);\r\n}\r\n\r\nfloat3 Rec709toRec2020(float3 rec709)\r\n{\r\n    static const float3x3 ConvMat =\r\n    {\r\n        0.627402, 0.329292, 0.043306,\r\n        0.069095, 0.919544, 0.011360,\r\n        0.016394, 0.088028, 0.895578\r\n    };\r\n    return mul(ConvMat, rec709);\r\n}\r\n\r\nfloat3 scRGBTo2100PQ(float3 rgb)\r\n{\r\n    // Convert from Rec 709 primaries (used by scRGB) to Rec 2020 primaries (used by Rec 2100)\r\n    rgb = Rec709toRec2020(rgb);\r\n\r\n    // 1.0f is defined as 80 nits in the scRGB colorspace\r\n    rgb *= 80;\r\n\r\n    // Apply the PQ transfer function on the raw color values in nits\r\n    return NitsToPQ(rgb);\r\n}\r\n\r\n// HLG (Hybrid Log-Gamma) OETF as defined in ARIB STD-B67 / ITU-R BT.2100\r\n// Optimized: branchless vectorized implementation\r\n//\r\n// Note: HLG OETF is mathematically valid for L > 1, but output > 1 will be\r\n// clipped by the encoder (10-bit can only represent [0, 1]). We don't clip\r\n// in the shader to preserve precision; the encoder handles clipping.\r\nfloat3 LinearToHLG(float3 L)\r\n{\r\n    // HLG constants from ARIB STD-B67\r\n    static const float a = 0.17883277;\r\n    static const float b = 0.28466892;  // 1 - 4 * a\r\n    static const float c = 0.55991073;  // 0.5 - a * ln(4 * a)\r\n    static const float threshold = 1.0 / 12.0;\r\n\r\n    // Clamp negative values only (out of gamut), allow > 1 for HDR headroom\r\n    L = max(L, 0.0);\r\n\r\n    // Compute both branches for all channels (branchless)\r\n    // Low range: sqrt(3 * L)\r\n    float3 lowRange = sqrt(3.0 * L);\r\n\r\n    // High range: a * log(12 * L - b) + c\r\n    // For L > 1, this produces output > 1 which is fine (encoder clips later)\r\n    float3 highRange = a * log(max(12.0 * L - b, 1e-6)) + c;\r\n\r\n    // Branchless select using step function\r\n    float3 selector = step(threshold, L);\r\n\r\n    // Return unclamped result - let encoder handle clipping\r\n    return lerp(lowRange, highRange, selector);\r\n}\r\n\r\nfloat3 scRGBTo2100HLG(float3 rgb)\r\n{\r\n    // Convert from Rec 709 primaries (used by scRGB) to Rec 2020 primaries (used by Rec 2100)\r\n    rgb = Rec709toRec2020(rgb);\r\n\r\n    // scRGB luminance mapping to HLG:\r\n    // - scRGB 1.0 = 80 nits (SDR reference white)\r\n    // - HLG is scene-referred, OETF expects normalized scene light [0, 1]\r\n    // - For a 1000 nits peak display, HLG signal 1.0 maps to ~1000 nits\r\n    // - HLG reference white (75% signal) is ~203 nits\r\n    //\r\n    // Mapping strategy:\r\n    // - scRGB 1.0 (80 nits) should map to HLG ~0.5 (SDR-compatible level)\r\n    // - scRGB 12.5 (1000 nits) should map to HLG 1.0 (peak white)\r\n    //\r\n    // Scale factor: 80 nits / 1000 nits = 0.08\r\n    // This maps the full HDR range [0, 1000 nits] to HLG input [0, 1]\r\n    \r\n    static const float HDR_PEAK_NITS = 1000.0;\r\n    static const float SCRGB_NITS_PER_UNIT = 80.0;\r\n    static const float scaleToHLG = SCRGB_NITS_PER_UNIT / HDR_PEAK_NITS;  // 0.08\r\n    \r\n    // Convert scRGB to normalized scene light for HLG\r\n    // Negative values are clamped (out of gamut), but > 1.0 is preserved\r\n    // This allows content > 1000 nits to pass through (soft rolloff)\r\n    rgb = max(rgb, 0.0) * scaleToHLG;\r\n\r\n    // Apply the HLG OETF\r\n    // For input > 1.0, output will exceed 1.0 and be clipped by encoder\r\n    // This provides a natural \"soft knee\" rolloff for super-bright content\r\n    return LinearToHLG(rgb);\r\n}\r\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/convert_base.hlsl",
    "content": "#define CONVERT_FUNCTION saturate\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/convert_hybrid_log_gamma_base.hlsl",
    "content": "#include \"include/common.hlsl\"\n\n#define CONVERT_FUNCTION scRGBTo2100HLG\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/convert_linear_base.hlsl",
    "content": "#include \"include/common.hlsl\"\n\nfloat3 CONVERT_FUNCTION(float3 input)\n{\n    return ApplySRGBCurve(saturate(input));\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/convert_perceptual_quantizer_base.hlsl",
    "content": "#include \"include/common.hlsl\"\n\n#define CONVERT_FUNCTION scRGBTo2100PQ\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/convert_yuv420_packed_uv_bicubic_ps_base.hlsl",
    "content": "Texture2D image : register(t0);\nSamplerState def_sampler : register(s0);\n// Point sampler used by high-quality resampling to avoid double-filtering.\nSamplerState point_sampler : register(s1);\n\ncbuffer color_matrix_cbuffer : register(b0) {\n    float4 color_vec_y;\n    float4 color_vec_u;\n    float4 color_vec_v;\n    float2 range_y;\n    float2 range_uv;\n};\n\n// Optimized Mitchell-Netravali bicubic weight function (B=1/3, C=1/3)\n// Branchless implementation using precomputed coefficients for better performance\n// Better for HDR than Catmull-Rom as it reduces ringing artifacts\n// while maintaining good sharpness\nfloat bicubic_weight(float x) {\n    x = abs(x);\n    \n    // Precomputed coefficients for B=1/3, C=1/3 (Mitchell-Netravali)\n    // For x < 1.0: (7/6)*x^3 - 2*x^2 + (8/9)\n    // For 1.0 <= x < 2.0: (-7/18)*x^3 + 2*x^2 - (10/3)*x + (16/9)\n    // For x >= 2.0: 0\n    \n    // Use step() and lerp() to avoid branches\n    // step(a, x) returns 1.0 if x >= a, else 0.0\n    float in_range_1 = 1.0 - step(1.0, x);  // 1.0 if x < 1.0, else 0.0\n    float in_range_2 = step(1.0, x) * (1.0 - step(2.0, x));  // 1.0 if 1.0 <= x < 2.0, else 0.0\n    \n    // Calculate both polynomial branches\n    float x2 = x * x;\n    float x3 = x2 * x;\n    \n    // Branch 1: x < 1.0\n    float weight1 = (7.0 / 6.0) * x3 - 2.0 * x2 + (8.0 / 9.0);\n    \n    // Branch 2: 1.0 <= x < 2.0\n    float weight2 = (-7.0 / 18.0) * x3 + 2.0 * x2 - (10.0 / 3.0) * x + (16.0 / 9.0);\n    \n    // Select result based on range (branchless)\n    return in_range_1 * weight1 + in_range_2 * weight2;\n}\n\n// Separable bicubic interpolation (optimized: weight calculations reduced from 32 to 8)\n// First applies horizontal filtering (4 samples per row), then vertical (combines 4 rows)\n// This reduces weight calculations by 75% (8 vs 32) while maintaining identical quality\n// Texture fetches remain 16 (4x4 grid), but weight computation is optimized\nfloat3 bicubic_sample(Texture2D tex, float2 uv, float2 texel_size) {\n    // Get the base pixel coordinate\n    float2 pixel_coord = uv / texel_size;\n    float2 base_coord = floor(pixel_coord - 0.5) + 0.5;\n    float2 frac_part = pixel_coord - base_coord;\n    \n    // Precompute horizontal weights (used for all 4 rows)\n    float w_h0 = bicubic_weight(frac_part.x - (-1));\n    float w_h1 = bicubic_weight(frac_part.x - 0);\n    float w_h2 = bicubic_weight(frac_part.x - 1);\n    float w_h3 = bicubic_weight(frac_part.x - 2);\n    \n    // Precompute vertical weights\n    float w_v0 = bicubic_weight(frac_part.y - (-1));\n    float w_v1 = bicubic_weight(frac_part.y - 0);\n    float w_v2 = bicubic_weight(frac_part.y - 1);\n    float w_v3 = bicubic_weight(frac_part.y - 2);\n    \n    // Step 1: Horizontal filtering - sample 4 rows, each with 4 horizontal samples\n    // Row -1\n    float2 coord_y1 = (base_coord + float2(0, -1)) * texel_size;\n    float3 row_m1 = tex.Sample(point_sampler, coord_y1 + float2(-1, 0) * texel_size).rgb * w_h0\n                   + tex.Sample(point_sampler, coord_y1).rgb * w_h1\n                   + tex.Sample(point_sampler, coord_y1 + float2(1, 0) * texel_size).rgb * w_h2\n                   + tex.Sample(point_sampler, coord_y1 + float2(2, 0) * texel_size).rgb * w_h3;\n    \n    // Row 0\n    float2 coord_y0 = base_coord * texel_size;\n    float3 row_0 = tex.Sample(point_sampler, coord_y0 + float2(-1, 0) * texel_size).rgb * w_h0\n                 + tex.Sample(point_sampler, coord_y0).rgb * w_h1\n                 + tex.Sample(point_sampler, coord_y0 + float2(1, 0) * texel_size).rgb * w_h2\n                 + tex.Sample(point_sampler, coord_y0 + float2(2, 0) * texel_size).rgb * w_h3;\n    \n    // Row 1\n    float2 coord_y1_pos = (base_coord + float2(0, 1)) * texel_size;\n    float3 row_1 = tex.Sample(point_sampler, coord_y1_pos + float2(-1, 0) * texel_size).rgb * w_h0\n                 + tex.Sample(point_sampler, coord_y1_pos).rgb * w_h1\n                 + tex.Sample(point_sampler, coord_y1_pos + float2(1, 0) * texel_size).rgb * w_h2\n                 + tex.Sample(point_sampler, coord_y1_pos + float2(2, 0) * texel_size).rgb * w_h3;\n    \n    // Row 2\n    float2 coord_y2 = (base_coord + float2(0, 2)) * texel_size;\n    float3 row_2 = tex.Sample(point_sampler, coord_y2 + float2(-1, 0) * texel_size).rgb * w_h0\n                 + tex.Sample(point_sampler, coord_y2).rgb * w_h1\n                 + tex.Sample(point_sampler, coord_y2 + float2(1, 0) * texel_size).rgb * w_h2\n                 + tex.Sample(point_sampler, coord_y2 + float2(2, 0) * texel_size).rgb * w_h3;\n    \n    // Step 2: Vertical filtering - combine the 4 horizontally-filtered rows\n    float3 result = row_m1 * w_v0 + row_0 * w_v1 + row_1 * w_v2 + row_2 * w_v3;\n    \n    // Anti-ringing clamp: find min/max from the 4 rows to prevent overshoot/undershoot\n    float3 min_rgb = min(min(row_m1, row_0), min(row_1, row_2));\n    float3 max_rgb = max(max(row_m1, row_0), max(row_1, row_2));\n    \n    // Clamp result to prevent ringing artifacts (especially visible in HDR UI/text)\n    return clamp(result, min_rgb, max_rgb);\n}\n\nstruct bicubic_vertex_t {\n    float4 viewpoint_pos : SV_Position;\n    float2 tex_coord : TEXCOORD;\n};\n\nfloat2 main_ps(bicubic_vertex_t input) : SV_Target\n{\n    // Get texture dimensions for texel size calculation\n    uint width, height;\n    image.GetDimensions(width, height);\n    float2 texel_size = float2(1.0 / width, 1.0 / height);\n    \n    // Use bicubic interpolation\n    float3 rgb = bicubic_sample(image, input.tex_coord, texel_size);\n\n    rgb = CONVERT_FUNCTION(rgb);\n\n    float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w;\n    float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w;\n\n    u = u * range_uv.x + range_uv.y;\n    v = v * range_uv.x + range_uv.y;\n\n    return float2(u, v);\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/convert_yuv420_packed_uv_ps_base.hlsl",
    "content": "Texture2D image : register(t0);\nSamplerState def_sampler : register(s0);\n\ncbuffer color_matrix_cbuffer : register(b0) {\n    float4 color_vec_y;\n    float4 color_vec_u;\n    float4 color_vec_v;\n    float2 range_y;\n    float2 range_uv;\n};\n\n#include \"include/base_vs_types.hlsl\"\n\nfloat2 main_ps(vertex_t input) : SV_Target\n{\n#if defined(LEFT_SUBSAMPLING)\n    float3 rgb_left = image.Sample(def_sampler, input.tex_right_left_center.xz).rgb;\n    float3 rgb_right = image.Sample(def_sampler, input.tex_right_left_center.yz).rgb;\n    float3 rgb = CONVERT_FUNCTION((rgb_left + rgb_right) * 0.5);\n#elif defined(LEFT_SUBSAMPLING_SCALE)\n    float3 rgb = image.Sample(def_sampler, input.tex_right_center_left_top.yw).rgb; // top-center\n    rgb += image.Sample(def_sampler, input.tex_right_center_left_bottom.yw).rgb; // bottom-center\n    rgb *= 2;\n    rgb += image.Sample(def_sampler, input.tex_right_center_left_top.xw).rgb; // top-right\n    rgb += image.Sample(def_sampler, input.tex_right_center_left_top.zw).rgb; // top-left\n    rgb += image.Sample(def_sampler, input.tex_right_center_left_bottom.xw).rgb; // bottom-right\n    rgb += image.Sample(def_sampler, input.tex_right_center_left_bottom.zw).rgb; // bottom-left\n    rgb = CONVERT_FUNCTION(rgb * (1./8));\n#elif defined(TOPLEFT_SUBSAMPLING)\n    float3 rgb_top_left = image.Sample(def_sampler, input.tex_right_left_top.xz).rgb;\n    float3 rgb_top_right = image.Sample(def_sampler, input.tex_right_left_top.yz).rgb;\n    float3 rgb_bottom_left = image.Sample(def_sampler, input.tex_right_left_bottom.xz).rgb;\n    float3 rgb_bottom_right = image.Sample(def_sampler, input.tex_right_left_bottom.yz).rgb;\n    float3 rgb = CONVERT_FUNCTION((rgb_top_left + rgb_top_right + rgb_bottom_left + rgb_bottom_right) * 0.25);\n#endif\n\n    float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w;\n    float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w;\n\n    u = u * range_uv.x + range_uv.y;\n    v = v * range_uv.x + range_uv.y;\n\n    return float2(u, v);\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/convert_yuv420_planar_y_bicubic_ps_base.hlsl",
    "content": "Texture2D image : register(t0);\nSamplerState def_sampler : register(s0);\n// Point sampler used by high-quality resampling to avoid double-filtering.\nSamplerState point_sampler : register(s1);\n\ncbuffer color_matrix_cbuffer : register(b0) {\n    float4 color_vec_y;\n    float4 color_vec_u;\n    float4 color_vec_v;\n    float2 range_y;\n    float2 range_uv;\n};\n\n// Optimized Mitchell-Netravali bicubic weight function (B=1/3, C=1/3)\n// Branchless implementation using precomputed coefficients for better performance\n// Better for HDR than Catmull-Rom as it reduces ringing artifacts\n// while maintaining good sharpness\nfloat bicubic_weight(float x) {\n    x = abs(x);\n    \n    // Precomputed coefficients for B=1/3, C=1/3 (Mitchell-Netravali)\n    // For x < 1.0: (7/6)*x^3 - 2*x^2 + (8/9)\n    // For 1.0 <= x < 2.0: (-7/18)*x^3 + 2*x^2 - (10/3)*x + (16/9)\n    // For x >= 2.0: 0\n    \n    // Use step() and lerp() to avoid branches\n    // step(a, x) returns 1.0 if x >= a, else 0.0\n    float in_range_1 = 1.0 - step(1.0, x);  // 1.0 if x < 1.0, else 0.0\n    float in_range_2 = step(1.0, x) * (1.0 - step(2.0, x));  // 1.0 if 1.0 <= x < 2.0, else 0.0\n    \n    // Calculate both polynomial branches\n    float x2 = x * x;\n    float x3 = x2 * x;\n    \n    // Branch 1: x < 1.0\n    float weight1 = (7.0 / 6.0) * x3 - 2.0 * x2 + (8.0 / 9.0);\n    \n    // Branch 2: 1.0 <= x < 2.0\n    float weight2 = (-7.0 / 18.0) * x3 + 2.0 * x2 - (10.0 / 3.0) * x + (16.0 / 9.0);\n    \n    // Select result based on range (branchless)\n    return in_range_1 * weight1 + in_range_2 * weight2;\n}\n\n// Separable bicubic interpolation (optimized: weight calculations reduced from 32 to 8)\n// First applies horizontal filtering (4 samples per row), then vertical (combines 4 rows)\n// This reduces weight calculations by 75% (8 vs 32) while maintaining identical quality\n// Texture fetches remain 16 (4x4 grid), but weight computation is optimized\nfloat3 bicubic_sample(Texture2D tex, float2 uv, float2 texel_size) {\n    // Get the base pixel coordinate\n    float2 pixel_coord = uv / texel_size;\n    float2 base_coord = floor(pixel_coord - 0.5) + 0.5;\n    float2 frac_part = pixel_coord - base_coord;\n    \n    // Precompute horizontal weights (used for all 4 rows)\n    float w_h0 = bicubic_weight(frac_part.x - (-1));\n    float w_h1 = bicubic_weight(frac_part.x - 0);\n    float w_h2 = bicubic_weight(frac_part.x - 1);\n    float w_h3 = bicubic_weight(frac_part.x - 2);\n    \n    // Precompute vertical weights\n    float w_v0 = bicubic_weight(frac_part.y - (-1));\n    float w_v1 = bicubic_weight(frac_part.y - 0);\n    float w_v2 = bicubic_weight(frac_part.y - 1);\n    float w_v3 = bicubic_weight(frac_part.y - 2);\n    \n    // Step 1: Horizontal filtering - sample 4 rows, each with 4 horizontal samples\n    // Row -1\n    float2 coord_y1 = (base_coord + float2(0, -1)) * texel_size;\n    float3 row_m1 = tex.Sample(point_sampler, coord_y1 + float2(-1, 0) * texel_size).rgb * w_h0\n                   + tex.Sample(point_sampler, coord_y1).rgb * w_h1\n                   + tex.Sample(point_sampler, coord_y1 + float2(1, 0) * texel_size).rgb * w_h2\n                   + tex.Sample(point_sampler, coord_y1 + float2(2, 0) * texel_size).rgb * w_h3;\n    \n    // Row 0\n    float2 coord_y0 = base_coord * texel_size;\n    float3 row_0 = tex.Sample(point_sampler, coord_y0 + float2(-1, 0) * texel_size).rgb * w_h0\n                 + tex.Sample(point_sampler, coord_y0).rgb * w_h1\n                 + tex.Sample(point_sampler, coord_y0 + float2(1, 0) * texel_size).rgb * w_h2\n                 + tex.Sample(point_sampler, coord_y0 + float2(2, 0) * texel_size).rgb * w_h3;\n    \n    // Row 1\n    float2 coord_y1_pos = (base_coord + float2(0, 1)) * texel_size;\n    float3 row_1 = tex.Sample(point_sampler, coord_y1_pos + float2(-1, 0) * texel_size).rgb * w_h0\n                 + tex.Sample(point_sampler, coord_y1_pos).rgb * w_h1\n                 + tex.Sample(point_sampler, coord_y1_pos + float2(1, 0) * texel_size).rgb * w_h2\n                 + tex.Sample(point_sampler, coord_y1_pos + float2(2, 0) * texel_size).rgb * w_h3;\n    \n    // Row 2\n    float2 coord_y2 = (base_coord + float2(0, 2)) * texel_size;\n    float3 row_2 = tex.Sample(point_sampler, coord_y2 + float2(-1, 0) * texel_size).rgb * w_h0\n                 + tex.Sample(point_sampler, coord_y2).rgb * w_h1\n                 + tex.Sample(point_sampler, coord_y2 + float2(1, 0) * texel_size).rgb * w_h2\n                 + tex.Sample(point_sampler, coord_y2 + float2(2, 0) * texel_size).rgb * w_h3;\n    \n    // Step 2: Vertical filtering - combine the 4 horizontally-filtered rows\n    float3 result = row_m1 * w_v0 + row_0 * w_v1 + row_1 * w_v2 + row_2 * w_v3;\n    \n    // Anti-ringing clamp: find min/max from the 4 rows to prevent overshoot/undershoot\n    float3 min_rgb = min(min(row_m1, row_0), min(row_1, row_2));\n    float3 max_rgb = max(max(row_m1, row_0), max(row_1, row_2));\n    \n    // Clamp result to prevent ringing artifacts (especially visible in HDR UI/text)\n    return clamp(result, min_rgb, max_rgb);\n}\n\nstruct bicubic_vertex_t {\n    float4 viewpoint_pos : SV_Position;\n    float2 tex_coord : TEXCOORD;\n};\n\nfloat main_ps(bicubic_vertex_t input) : SV_Target\n{\n    // Get texture dimensions for texel size calculation\n    uint width, height;\n    image.GetDimensions(width, height);\n    float2 texel_size = float2(1.0 / width, 1.0 / height);\n    \n    // Use bicubic interpolation\n    float3 rgb = bicubic_sample(image, input.tex_coord, texel_size);\n\n    rgb = CONVERT_FUNCTION(rgb);\n\n    float y = dot(color_vec_y.xyz, rgb) + color_vec_y.w;\n\n    return y * range_y.x + range_y.y;\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/convert_yuv420_planar_y_ps_base.hlsl",
    "content": "Texture2D image : register(t0);\nSamplerState def_sampler : register(s0);\n\ncbuffer color_matrix_cbuffer : register(b0) {\n    float4 color_vec_y;\n    float4 color_vec_u;\n    float4 color_vec_v;\n    float2 range_y;\n    float2 range_uv;\n};\n\n#include \"include/base_vs_types.hlsl\"\n\nfloat main_ps(vertex_t input) : SV_Target\n{\n    float3 rgb = CONVERT_FUNCTION(image.Sample(def_sampler, input.tex_coord, 0).rgb);\n\n    float y = dot(color_vec_y.xyz, rgb) + color_vec_y.w;\n\n    return y * range_y.x + range_y.y;\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/include/convert_yuv444_ps_base.hlsl",
    "content": "Texture2D image : register(t0);\nSamplerState def_sampler : register(s0);\n\n#ifndef PLANAR_VIEWPORTS\ncbuffer color_matrix_cbuffer : register(b0) {\n    float4 color_vec_y;\n    float4 color_vec_u;\n    float4 color_vec_v;\n    float2 range_y;\n    float2 range_uv;\n};\n#endif\n\n#include \"include/base_vs_types.hlsl\"\n\n#ifdef PLANAR_VIEWPORTS\nuint main_ps(vertex_t input) : SV_Target\n#else\nuint4 main_ps(vertex_t input) : SV_Target\n#endif\n{\n    float3 rgb = CONVERT_FUNCTION(image.Sample(def_sampler, input.tex_coord, 0).rgb);\n\n#ifdef PLANAR_VIEWPORTS\n    // Planar R16, 10 most significant bits store the value\n    return uint(dot(input.color_vec.xyz, rgb) + input.color_vec.w) << 6;\n#else\n    float y = dot(color_vec_y.xyz, rgb) + color_vec_y.w;\n    float u = dot(color_vec_u.xyz, rgb) + color_vec_u.w;\n    float v = dot(color_vec_v.xyz, rgb) + color_vec_v.w;\n\n#ifdef Y410\n    return uint4(u, y, v, 0);\n#else\n    // AYUV\n    return uint4(v, u, y, 0);\n#endif\n#endif\n}\n"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/simple_cursor_ps.hlsl",
    "content": "struct PS_INPUT\n{\n    float4 Pos : SV_POSITION;\n};\n\nfloat4 main_ps(PS_INPUT input) : SV_Target\n{\n    return float4(0.9, 0.9, 0.9, 1.0);\n}"
  },
  {
    "path": "src_assets/windows/assets/shaders/directx/simple_cursor_vs.hlsl",
    "content": "cbuffer TransformBuffer : register(b0) {\n    float2 position; // x, y\n    float2 screenSize; // w, h\n};\n\nstruct PS_INPUT\n{\n    float4 Pos : SV_POSITION;\n};\n\n\nPS_INPUT main_vs(uint vertexID: SV_VERTEXID) {\n    PS_INPUT output;\n    float xOff = position.x / screenSize.x * 2 - 1;\n    float yOff = position.y / screenSize.y * 2 - 1;\n\n    if (vertexID == 0) {\n        output.Pos = float4(xOff, yOff, 0.0, 1.0);\n    } else if (vertexID == 1) {\n        output.Pos = float4(xOff + 22 / screenSize.x, yOff + 22 / screenSize.y, 0.0, 1.0);\n    } else {\n        output.Pos = float4(xOff, yOff + 32 / screenSize.y, 0.0, 1.0);\n    }\n\n    return output;\n}"
  },
  {
    "path": "src_assets/windows/misc/autostart/autostart-service.bat",
    "content": "@echo off\n\nrem Set the service to auto-start\nsc config SunshineService start= auto\n"
  },
  {
    "path": "src_assets/windows/misc/firewall/add-firewall-rule.bat",
    "content": "@echo off\r\n\r\nrem Get sunshine root directory\r\nfor %%I in (\"%~dp0\\..\") do set \"ROOT_DIR=%%~fI\"\r\n\r\nset RULE_NAME=Sunshine\r\nset PROGRAM_BIN=\"%ROOT_DIR%\\sunshine.exe\"\r\n\r\nrem Add the rule\r\nnetsh advfirewall firewall add rule name=%RULE_NAME% dir=in action=allow protocol=tcp program=%PROGRAM_BIN% enable=yes\r\nnetsh advfirewall firewall add rule name=%RULE_NAME% dir=in action=allow protocol=udp program=%PROGRAM_BIN% enable=yes\r\n"
  },
  {
    "path": "src_assets/windows/misc/firewall/delete-firewall-rule.bat",
    "content": "@echo off\r\n\r\nset RULE_NAME=Sunshine\r\n\r\nrem Delete the rule\r\nnetsh advfirewall firewall delete rule name=%RULE_NAME%\r\n"
  },
  {
    "path": "src_assets/windows/misc/gamepad/install-gamepad.bat",
    "content": "@echo off\nsetlocal enabledelayedexpansion\n\nrem Check if a compatible version of ViGEmBus is already installed (1.17 or later)\nrem\nrem Note: We use exit code 2 to indicate success because either 0 or 1 may be returned\nrem based on the PowerShell version if an exception occurs.\npowershell -c Exit $(if ((Get-Item \"$env:SystemRoot\\System32\\drivers\\ViGEmBus.sys\").VersionInfo.FileVersion -ge [System.Version]\"1.17\") { 2 } Else { 1 })\nif %ERRORLEVEL% EQU 2 (\n    goto skip\n)\ngoto continue\n\n:skip\necho \"The installed version is 1.17 or later, no update needed. Exiting.\"\nexit /b 0\n\n:continue\nrem Get temp directory\nset temp_dir=%temp%/Sunshine\n\nrem Create temp directory if it doesn't exist\nif not exist \"%temp_dir%\" mkdir \"%temp_dir%\"\n\nrem Get system proxy setting\nset proxy= \nfor /f \"tokens=3\" %%a in ('reg query \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" ^| find /i \"ProxyEnable\"') do (\n  set ProxyEnable=%%a\n    \n  if !ProxyEnable! equ 0x1 (\n  for /f \"tokens=3\" %%a in ('reg query \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" ^| find /i \"ProxyServer\"') do (\n      set proxy=%%a\n      echo Using system proxy !proxy! to download Virtual Gamepad\n      set proxy=-x !proxy!\n    )\n  ) else (\n    rem Proxy is not enabled.\n  )\n)\n\nrem get browser_download_url from asset 0 of https://api.github.com/repos/nefarius/vigembus/releases/latest\nset latest_release_url=https://api.github.com/repos/nefarius/vigembus/releases/latest\n\nrem Use curl to get the api response, and find the browser_download_url\nfor /F \"tokens=* USEBACKQ\" %%F in (`curl -s !proxy! -L %latest_release_url% ^| findstr browser_download_url`) do (\n  set browser_download_url=%%F\n)\n\nrem Strip quotes\nset browser_download_url=%browser_download_url:\"=%\n\nrem Remove the browser_download_url key\nset browser_download_url=%browser_download_url:browser_download_url: =%\n\necho %browser_download_url%\n\nrem Download the exe\ncurl -s -L !proxy! -o \"%temp_dir%\\virtual_gamepad.exe\" https://mirror.ghproxy.com/%browser_download_url%\n\nrem Install Virtual Gamepad\n%temp_dir%\\virtual_gamepad.exe /passive /promptrestart\n\nrem Delete temp directory\nrmdir /S /Q \"%temp_dir%\"\n"
  },
  {
    "path": "src_assets/windows/misc/gamepad/uninstall-gamepad.bat",
    "content": "@echo off\nsetlocal enabledelayedexpansion\n\nrem Uninstall ViGEm Bus Driver via registry UninstallString\nrem (Replaces slow \"wmic product\" which can hang for minutes)\n\nset \"FOUND=0\"\n\nrem Search Uninstall registry keys for ViGEmBus\nfor %%R in (\n  \"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\"\n  \"HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\"\n) do (\n  for /f \"tokens=*\" %%K in ('reg query %%R /s /f \"ViGEm Bus Driver\" /d 2^>nul ^| findstr /i \"HKEY_\"') do (\n    for /f \"tokens=2*\" %%A in ('reg query \"%%K\" /v UninstallString 2^>nul ^| findstr /i \"UninstallString\"') do (\n      set \"UNINSTALL_CMD=%%B\"\n      set \"FOUND=1\"\n    )\n  )\n)\n\nif \"!FOUND!\"==\"0\" (\n  rem Try finding by DisplayName containing \"ViGEm\"\n  for %%R in (\n    \"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\"\n    \"HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\"\n  ) do (\n    for /f \"tokens=*\" %%K in ('reg query %%R /s /f \"ViGEm\" /d 2^>nul ^| findstr /i \"HKEY_\"') do (\n      for /f \"tokens=2*\" %%A in ('reg query \"%%K\" /v UninstallString 2^>nul ^| findstr /i \"UninstallString\"') do (\n        set \"UNINSTALL_CMD=%%B\"\n        set \"FOUND=1\"\n      )\n    )\n  )\n)\n\nif \"!FOUND!\"==\"0\" (\n  echo ViGEm Bus Driver not found in registry, nothing to uninstall.\n  exit /b 0\n)\n\necho Uninstalling ViGEm Bus Driver...\necho Command: !UNINSTALL_CMD!\n\nrem MSI uninstall: replace /I with /X and add /qn for silent\necho !UNINSTALL_CMD! | findstr /i \"msiexec\" >nul\nif !ERRORLEVEL!==0 (\n  set \"SILENT_CMD=!UNINSTALL_CMD:/I=/X!\"\n  !SILENT_CMD! /qn\n) else (\n  rem Non-MSI installer: try running with /S or /silent flag\n  !UNINSTALL_CMD! /passive /norestart\n)\n\necho ViGEm Bus Driver uninstall completed.\n"
  },
  {
    "path": "src_assets/windows/misc/install_portable.bat",
    "content": "@echo off\nchcp 65001 >nul\nsetlocal enabledelayedexpansion\ncolor 0F\n\nREM ================================================\nREM Initialize environment variables\nREM ================================================\n\nREM Set Sunshine root directory path (current directory)\nset \"SUNSHINE_ROOT=%~dp0\"\nREM Get script directory path (scripts folder)\nset \"SCRIPT_DIR=%~dp0scripts\"\nREM Set language file directory path\nset \"LANG_DIR=%SCRIPT_DIR%\\languages\"\n\nREM ================================================\nREM Multi-language support initialization\nREM ================================================\n\nREM Detect system language and select corresponding language file\ncall :DetectLanguage\nREM Load selected language file\ncall :LoadLanguageFile \"%SELECTED_LANG%\"\n\nREM ================================================\nREM Display program title\nREM ================================================\n\necho.\necho ================================================\necho           %TITLE%\necho ================================================\necho.\n\nREM ================================================\nREM Permission check and elevation\nREM ================================================\n\nREM Check if running with administrator privileges\nnet session >nul 2>&1\nif !errorLevel! neq 0 (\n    call :LogError \"%ERROR_ADMIN%\"\n    echo %ERROR_ADMIN_DESC%\n    echo.\n    echo %ADMIN_ELEVATE_PROMPT%\n    set /p elevate_choice=\"%ADMIN_ELEVATE_CONFIRM% \"\n    if /i \"!elevate_choice!\"==\"Y\" (\n        echo %ADMIN_ELEVATING%...\n        REM Use PowerShell to elevate and restart script\n        powershell -NoProfile -Command \"Start-Process -FilePath $env:ComSpec -ArgumentList '/k','\\\"%~f0\\\"' -Verb RunAs -WorkingDirectory '%~dp0'\"\n        pause\n        exit /b 0\n    ) else (\n        echo %ADMIN_ELEVATE_CANCEL%\n        echo.\n        pause\n        exit /b 1\n    )\n)\n\ncall :LogInfo \"%INFO_ADMIN%\"\necho.\n\nREM ================================================\nREM Environment validation\nREM ================================================\n\nREM Verify Sunshine executable file exists\nif not exist \"!SUNSHINE_ROOT!sunshine.exe\" (\n    call :LogError \"%ERROR_EXE%\"\n    echo %ERROR_EXE_DESC%\n    echo Expected path: !SUNSHINE_ROOT!sunshine.exe\n    echo.\n    pause\n    exit /b 1\n)\n\ncall :LogInfo \"%INFO_EXE%: !SUNSHINE_ROOT!sunshine.exe\"\necho.\n\nREM ================================================\nREM User confirmation\nREM ================================================\n\nREM Display list of operations to be performed and wait for user confirmation\ncall :ShowScriptConfirmation\nif \"!CONFIRM_RESULT!\" neq \"1\" (\n    echo %SCRIPT_CONFIRM_CANCEL%\n    echo.\n    goto :SkipInstallation\n)\n\necho %SCRIPT_CONFIRM_PROCEEDING%\necho.\n\necho.\necho ================================================\necho           %TITLE_INSTALL%\necho ================================================\necho.\n\nREM ================================================\nREM Component installation process\nREM ================================================\n\nREM Step 1 - Install virtual display driver (VDD)\ncall :LogStep \"1/4\" \"%STEP_VDD%...\"\nREM Check if virtual display is already installed\nreg query \"HKLM\\SOFTWARE\\ZakoTech\\ZakoDisplayAdapter\" >nul 2>&1\nif !errorLevel! neq 0 (\n    set \"vdd_choice=Y\"\n    set /p vdd_choice=\"%VDD_PROMPT% \"\n    if /i \"!vdd_choice!\"==\"Y\" (\n        call :LogInfo \"%INFO_VDD_INSTALL%...\"\n        REM Call virtual display installation script\n        call \"%SCRIPT_DIR%\\install-vdd.bat\"\n        if !errorLevel! equ 0 (\n            call :LogSuccess \"%SUCCESS_VDD%\"\n        ) else (\n            call :LogWarning \"%WARNING_VDD%\"\n        )\n    ) else (\n        call :LogInfo \"%INFO_VDD_SKIP%\"\n    )\n) else (\n    call :LogInfo \"%INFO_VDD_EXISTS%\"\n)\necho.\n\nREM Step 2 - Install virtual microphone (VB-Cable)\ncall :LogStep \"2/4\" \"%STEP_VSINK%...\"\nREM Check if virtual microphone is already installed\nreg query \"HKLM\\SOFTWARE\\VB-Audio\" >nul 2>&1\nif !errorLevel! neq 0 (\n    set \"vsink_choice=Y\"\n    set /p vsink_choice=\"%VSINK_PROMPT% \"\n    if /i \"!vsink_choice!\"==\"Y\" (\n        call :LogInfo \"%INFO_VSINK_INSTALL%...\"\n        REM Call virtual microphone installation script\n        call \"%SCRIPT_DIR%\\install-vsink.bat\"\n        if !errorLevel! equ 0 (\n            call :LogSuccess \"%SUCCESS_VSINK%\"\n        ) else (\n            call :LogWarning \"%WARNING_VSINK%\"\n        )\n    ) else (\n        call :LogInfo \"%INFO_VSINK_SKIP%\"\n    )\n) else (\n    call :LogInfo \"%INFO_VSINK_EXISTS%\"\n)\necho.\n\nREM Step 3 - Configure firewall rules\ncall :LogStep \"3/4\" \"%STEP_FIREWALL%...\"\nset \"firewall_choice=Y\"\nset /p firewall_choice=\"%FIREWALL_PROMPT% \"\nif /i \"!firewall_choice!\"==\"Y\" (\n    call :LogInfo \"%INFO_FIREWALL_INSTALL%\"\n    REM Call firewall configuration script\n    call \"%SCRIPT_DIR%\\add-firewall-rule.bat\"\n) else (\n    call :LogInfo \"%INFO_FIREWALL_SKIP%\"\n)\necho.\n\nREM Step 4 - Install gamepad support\ncall :LogStep \"4/5\" \"%STEP_GAMEPAD%...\"\nset \"gamepad_choice=Y\"\nset /p gamepad_choice=\"%GAMEPAD_PROMPT% \"\nif /i \"!gamepad_choice!\"==\"Y\" (\n    call \"%SCRIPT_DIR%\\install-gamepad.bat\"\n) else (\n    call :LogInfo \"%INFO_GAMEPAD_SKIP%\"\n)\necho.\n\nREM ================================================\nREM Create startup script (always create)\nREM ================================================\n\ncall :LogInfo \"%INFO_STARTING%\"\nREM Create Sunshine startup script\necho Set WshShell = CreateObject^(^\"WScript.Shell^\"^) > \"!SUNSHINE_ROOT!start_sunshine.vbs\"\necho WshShell.Run ^\"!SUNSHINE_ROOT!sunshine.exe^\", 0, False >> \"!SUNSHINE_ROOT!start_sunshine.vbs\"\ncall :LogSuccess \"%SUCCESS_STARTUP_SCRIPT%\"\necho.\n\nREM ================================================\nREM Autostart configuration (optional)\nREM ================================================\n\nREM Ask user if they want to configure autostart\nset \"autostart_choice=Y\"\nset /p autostart_choice=\"%AUTOSTART_PROMPT% \"\nif /i \"!autostart_choice!\"==\"Y\" (\n    call :LogInfo \"%INFO_AUTOSTART_INSTALL%...\"\n    REM Create Sunshine shortcut\n    set \"SHORTCUT_PATH=%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\Sunshine_portable.lnk\"\n    echo !SHORTCUT_PATH!\n    REM Use PowerShell to create shortcut\n    powershell -Command \"try { $WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('!SHORTCUT_PATH!'); $Shortcut.TargetPath = '!SUNSHINE_ROOT!start_sunshine.vbs'; $Shortcut.WorkingDirectory = '!SUNSHINE_ROOT!'; $Shortcut.Description = 'Sunshine Portable'; $Shortcut.Save(); exit 0 } catch { exit 1 }\"\n    \n    if !errorLevel! equ 0 (\n        call :LogSuccess \"%SUCCESS_AUTOSTART%\"\n    ) else (\n        call :LogWarning \"%WARNING_AUTOSTART%\"\n    )\n) else (\n    call :LogInfo \"%INFO_AUTOSTART_SKIP%\"\n)\necho.\n\n:SkipInstallation\nREM ================================================\nREM Completion message\nREM ================================================\n\necho ================================================\necho           %TITLE_SUCCESS%\necho ================================================\necho.\ncall :LogSuccess \"%SUCCESS_MAIN%\"\necho.\necho %USAGE_1%\necho %NOTE_FIRST%\necho.\n\necho %SCRIPT_COMPLETE_PROMPT%\npause\nexit /b 0\n\nREM ================================================\nREM Function definition area\nREM ================================================\n\n:LogInfo\nREM Function - Output info level log (white)\necho [信息] %~1\ngoto :eof\n\n:LogSuccess\nREM Function - Output success level log (green)\npowershell -c \"Write-Host '[成功] %~1' -ForegroundColor Green\"\ngoto :eof\n\n:LogWarning\nREM Function - Output warning level log (yellow)\npowershell -c \"Write-Host '[警告] %~1' -ForegroundColor Yellow\"\ngoto :eof\n\n:LogError\nREM Function - Output error level log (red)\npowershell -c \"Write-Host '[错误] %~1' -ForegroundColor Red\"\ngoto :eof\n\n:LogStep\nREM Function - Output step level log (cyan)\npowershell -c \"Write-Host '[%~1] %~2' -ForegroundColor Cyan\"\ngoto :eof\n\n:DetectLanguage\nREM Function - Detect system language and select corresponding language file\nREM Parameters - None\nREM Returns - Set SELECTED_LANG variable\n\nREM Get system language code\nREM Use PowerShell to get system language for better compatibility\nfor /f \"tokens=*\" %%i in ('powershell -c \"[System.Globalization.CultureInfo]::CurrentCulture.Name\" 2^>nul') do set \"LOCALE=%%i\"\n\nREM If PowerShell fails, use default language\nif \"%LOCALE%\"==\"\" set \"LOCALE=en-US\"\n\nREM Select corresponding language file based on system language code\nif \"%LOCALE:~0,2%\"==\"zh\" (\n    set \"SELECTED_LANG=zh-Simple\"\n    set \"SELECTED_LANG_DISPLAY=简体中文\"\n) else if \"%LOCALE:~0,2%\"==\"de\" (\n    set \"SELECTED_LANG=de-DE\"\n    set \"SELECTED_LANG_DISPLAY=Deutsch\"\n) else if \"%LOCALE:~0,2%\"==\"fr\" (\n    set \"SELECTED_LANG=fr-FR\"\n    set \"SELECTED_LANG_DISPLAY=Français\"\n) else if \"%LOCALE:~0,2%\"==\"ja\" (\n    set \"SELECTED_LANG=ja-JP\"\n    set \"SELECTED_LANG_DISPLAY=日本語\"\n) else (\n    REM Default to English\n    set \"SELECTED_LANG=en-US\"\n    set \"SELECTED_LANG_DISPLAY=English\"\n)\ngoto :eof\n\n:LoadLanguageFile\nREM Function - Load specified language file\nREM Parameters - %%1 = Language code (e.g. zh-CN, en-US)\nREM Returns - Set all language variables\n\nREM Build language file path\nset \"LANG_FILE=%LANG_DIR%\\%~1.lang\"\n\nREM Check if language file exists\nif not exist \"%LANG_FILE%\" (\n    echo [ERROR] Language file not found: %LANG_FILE%\n    echo [ERROR] Language file does not exist: %LANG_FILE%\n    pause\n    exit /b 1\n)\n\nREM Parse language file and set variables\nfor /f \"usebackq tokens=1* delims==\" %%a in (\"%LANG_FILE%\") do (\n    if not \"%%a\"==\"\" if not \"%%a:~0,1%\"==\"#\" (\n        if not \"%%b\"==\"\" (\n            set \"%%a=%%b\" 2>nul\n        )\n    )\n)\n\nREM Check if this is the first time loading language (show confirmation)\nif not defined LANG_CONFIRMED (\n    call :LogInfo \"%LANG_DETECTED%: !SELECTED_LANG_DISPLAY!\"\n    call :LogInfo \"%LANG_CONFIRM%\"\n\n    set \"lang_confirm_input=Y\"\n    set /p lang_confirm_input=\"%LANG_CONFIRM_PROMPT% \"\n\n    if /i \"!lang_confirm_input!\"==\"N\" (\n        call :ShowLanguageSelection\n    ) else (\n        set \"LANG_CONFIRMED=1\"\n    )\n)\ngoto :eof\n\n:ShowLanguageSelection\nREM Function - Display language selection menu\nREM Parameters - None\nREM Returns - Reload selected language file\n\necho.\necho %LANG_SELECT%:\necho 1. %LANG_ZH_SIMPLE% (简体中文)\necho 2. %LANG_EN% (English)\necho 3. %LANG_DE% (Deutsch)\necho 4. %LANG_FR% (Français)\necho 5. %LANG_JA% (日本語)\necho 6. %LANG_RU% (Русский)\necho.\nset \"lang_choice=\"\nset /p lang_choice=\"%LANG_SELECT_PROMPT% \"\n\nREM Set language code based on user selection\nif \"!lang_choice!\"==\"1\" (\n    set \"SELECTED_LANG=zh-Simple\"\n    set \"SELECTED_LANG_DISPLAY=简体中文\"\n) else if \"!lang_choice!\"==\"2\" (\n    set \"SELECTED_LANG=en-US\"\n    set \"SELECTED_LANG_DISPLAY=English\"\n) else if \"!lang_choice!\"==\"3\" (\n    set \"SELECTED_LANG=de-DE\"\n    set \"SELECTED_LANG_DISPLAY=Deutsch\"\n) else if \"!lang_choice!\"==\"4\" (\n    set \"SELECTED_LANG=fr-FR\"\n    set \"SELECTED_LANG_DISPLAY=Français\"\n) else if \"!lang_choice!\"==\"5\" (\n    set \"SELECTED_LANG=ja-JP\"\n    set \"SELECTED_LANG_DISPLAY=日本語\"\n) else if \"!lang_choice!\"==\"6\" (\n    set \"SELECTED_LANG=ru-RU\"\n    set \"SELECTED_LANG_DISPLAY=Русский\"\n) else (\n    call :LogError \"%ERROR_INVALID_SELECTION%\"\n    set \"SELECTED_LANG=en-US\"\n    set \"SELECTED_LANG_DISPLAY=English\"\n)\n\nREM Reload selected language file\ncall :LoadLanguageFile \"%SELECTED_LANG%\"\ngoto :eof\n\n:ShowScriptConfirmation\nREM Function - Display script execution confirmation interface\nREM Parameters - None\nREM Returns - Set CONFIRM_RESULT variable (1=confirm, 0=cancel)\n\necho ================================================\necho           %SCRIPT_CONFIRM_TITLE%\necho ================================================\necho.\necho %SCRIPT_CONFIRM_DESC%:\necho.\necho 1. %SCRIPT_CONFIRM_VDD%\necho 2. %SCRIPT_CONFIRM_VSINK%\necho 3. %SCRIPT_CONFIRM_FIREWALL%\necho 4. %SCRIPT_CONFIRM_GAMEPAD%\necho 5. %SCRIPT_CONFIRM_AUTOSTART%\necho.\nset \"confirm_choice=Y\"\nset /p confirm_choice=\"%SCRIPT_CONFIRM_PROCEED% \"\nif /i \"!confirm_choice!\"==\"Y\" (\n    set \"CONFIRM_RESULT=1\"\n) else (\n    set \"CONFIRM_RESULT=0\"\n)\ngoto :eof\n"
  },
  {
    "path": "src_assets/windows/misc/languages/de-DE.lang",
    "content": "TITLE=Sunshine Portable Initialisierungstool\nERROR_ADMIN=Administratorrechte erforderlich, um dieses Skript auszuführen\nERROR_ADMIN_DESC=Bitte rechtsklicken Sie auf diese Datei und wählen Sie \"Als Administrator ausführen\"\nINFO_ADMIN=Administratorrechte erkannt, Initialisierung wird gestartet...\nERROR_EXE=sunshine.exe Datei nicht gefunden\nERROR_EXE_DESC=Bitte stellen Sie sicher, dass sich dieses Skript im tragbaren Verzeichnis befindet\nINFO_EXE=Sunshine ausführbare Datei gefunden\nINFO_CONFIG=Konfigurationsverzeichnis erstellen\nINFO_LOG=Protokollverzeichnis erstellen\nTITLE_INSTALL=Erforderliche Komponenten installieren\nSTEP_VDD=Virtuellen Bildschirm überprüfen\nINFO_VDD_INSTALL=Virtuellen Bildschirm installieren\nSUCCESS_VDD=Virtueller Bildschirm erfolgreich installiert\nWARNING_VDD=Virtueller Bildschirm Installation fehlgeschlagen, aber fortfahren\nINFO_VDD_EXISTS=Virtueller Bildschirm bereits installiert\nSTEP_VSINK=Virtuelles Mikrofon überprüfen\nINFO_VSINK_INSTALL=Virtuelles Mikrofon installieren\nSUCCESS_VSINK=Virtuelles Mikrofon erfolgreich installiert\nWARNING_VSINK=Virtuelles Mikrofon Installation fehlgeschlagen, aber fortfahren\nINFO_VSINK_EXISTS=Virtuelles Mikrofon bereits installiert\nSTEP_FIREWALL=Firewall-Regeln konfigurieren\nINFO_FIREWALL_INSTALL=Firewall-Regeln konfigurieren...\nSUCCESS_FIREWALL=Firewall-Regeln erfolgreich konfiguriert\nWARNING_FIREWALL=Firewall-Konfiguration fehlgeschlagen, aber fortfahren\nINFO_FIREWALL_SKIP=Firewall-Konfiguration überspringen\nSTEP_GAMEPAD=Gamepad-Unterstützung installieren\nINFO_GAMEPAD_INSTALL=Gamepad-Unterstützung installieren\nSUCCESS_GAMEPAD=Gamepad-Unterstützung erfolgreich installiert\nWARNING_GAMEPAD=Gamepad-Unterstützung Installation fehlgeschlagen, aber fortfahren\nINFO_GAMEPAD_EXISTS=Gamepad-Unterstützung bereits installiert\nINFO_GAMEPAD_SKIP=Gamepad-Unterstützung Installation überspringen\nGAMEPAD_PROMPT=Gamepad-Unterstützung installieren? (Y/N, Standard: Y)\nSTEP_AUTOSTART=Startup-Autostart konfigurieren\nAUTOSTART_PROMPT=Startup-Autostart konfigurieren? (Y/N, Standard: Y)\nINFO_AUTOSTART_INSTALL=Startup-Verknüpfung erstellen\nSUCCESS_AUTOSTART=Startup-Autostart-Konfiguration abgeschlossen\nWARNING_AUTOSTART=Startup-Autostart-Konfiguration fehlgeschlagen, aber fortfahren\nINFO_AUTOSTART_SKIP=Startup-Autostart-Konfiguration überspringen\nTITLE_SUCCESS=Initialisierung abgeschlossen!\nSUCCESS_MAIN=Sunshine tragbare Initialisierung abgeschlossen!\nUSAGE_1=Starten Sie sunshine.exe, um das Programm zu starten\nUSAGE_2=Verwenden Sie den Browser, um http://localhost:47989 für die Konfiguration aufzurufen\nCONFIG_LOCATION=Konfigurationsdatei Speicherort\nLOG_LOCATION=Protokolldatei Speicherort\nNOTE_FIRST=Nach dem ersten Start bitte mit der rechten Maustaste auf das Sunshine-Symbol im System-Tray klicken und \"Open Sunshine\" auswählen, dann Benutzername und Passwort setzen\n\n# Start bezogen\nPROMPT_START=Sunshine jetzt starten? (Y/N, Standard: Y)\nINFO_STARTING=Erstelle Startskript...\nSUCCESS_STARTUP_SCRIPT=Startskript erfolgreich erstellt. Sie können später start_sunshine.vbs ausführen, um Sunshine zu starten\nINFO_LATER=Sie können später sunshine.exe direkt ausführen, um Sunshine zu starten\n\n# Sprachauswahl bezogen\nLANG_DETECTED=Systemsprache erkannt\nLANG_CONFIRM=Bitte bestätigen Sie, ob Sie diese Sprache verwenden möchten\nLANG_YES=Ja (Y)\nLANG_NO=Nein (N)\nLANG_SELECT=Bitte Sprache wählen\nLANG_SELECT_PROMPT=Bitte Sprache wählen (1-6)\nLANG_CONFIRM_PROMPT=Bitte wählen (Y/N, Standard: Y)\nLANG_ZH_SIMPLE=简体中文\nLANG_EN=English\nLANG_DE=Deutsch\nLANG_FR=Français\nLANG_JA=日本語\nLANG_RU=Русский\n\n# Skript-Ausführungsbestätigung\nSCRIPT_CONFIRM_TITLE=Skript-Ausführungsbestätigung\nSCRIPT_CONFIRM_DESC=Die folgenden Operationen werden ausgeführt\nSCRIPT_CONFIRM_VDD=Virtuellen Bildschirmtreiber installieren\nSCRIPT_CONFIRM_VSINK=Virtuelles Mikrofon installieren\nSCRIPT_CONFIRM_FIREWALL=Firewall-Regeln konfigurieren\nSCRIPT_CONFIRM_GAMEPAD=Gamepad-Unterstützung installieren\nSCRIPT_CONFIRM_AUTOSTART=Startup-Autostart konfigurieren\nSCRIPT_CONFIRM_PROCEED=Möchten Sie fortfahren? (Y/N, Standard: Y)\nSCRIPT_CONFIRM_CANCEL=Operation abgebrochen\nSCRIPT_CONFIRM_PROCEEDING=Initialisierung wird gestartet...\n\n# Fehlermeldungen\nERROR_INVALID_SELECTION=Ungültige Auswahl, verwende Standardsprache\nERROR_LANG_FILE_NOT_FOUND=Sprachdatei nicht gefunden\nERROR_LANG_FILE_NOT_FOUND_EN=Language file not found\n\n# Admin-Eskalation\nADMIN_ELEVATE_PROMPT=Möchten Sie dieses Skript mit Administratorrechten neu starten?\nADMIN_ELEVATE_CONFIRM=Bitte wählen (Y/N, Standard: Y)\nADMIN_ELEVATING=Starte Skript mit Administratorrechten neu...\nADMIN_ELEVATE_CANCEL=Eskalation abgebrochen, Skript beendet\n\n# Deinstallationsbezogen\nTITLE_UNINSTALL=Sunshine Portable Deinstallations-Tool\nTITLE_UNINSTALL_PROCESS=Komponenten-Deinstallation starten\nSTEP_UNINSTALL_VDD=Virtuellen Bildschirm deinstallieren\nSTEP_UNINSTALL_VSINK=Virtuelles Mikrofon deinstallieren\nSTEP_UNINSTALL_FIREWALL=Firewall-Regeln entfernen\nSTEP_UNINSTALL_GAMEPAD=Gamepad-Unterstützung deinstallieren\nSTEP_UNINSTALL_AUTOSTART=Startup-Autostart entfernen\nVDD_UNINSTALL_PROMPT=Virtuellen Bildschirm deinstallieren? (Y/N, Standard: Y)\nVSINK_UNINSTALL_PROMPT=Virtuelles Mikrofon deinstallieren? (Y/N, Standard: Y)\nFIREWALL_UNINSTALL_PROMPT=Firewall-Regeln entfernen? (Y/N, Standard: Y)\nGAMEPAD_UNINSTALL_PROMPT=Gamepad-Unterstützung deinstallieren? (Y/N, Standard: Y)\nAUTOSTART_UNINSTALL_PROMPT=Startup-Autostart entfernen? (Y/N, Standard: Y)\nINFO_VDD_UNINSTALL=Virtuellen Bildschirm deinstallieren...\nINFO_VSINK_UNINSTALL=Virtuelles Mikrofon deinstallieren...\nINFO_FIREWALL_UNINSTALL=Firewall-Regeln entfernen...\nINFO_GAMEPAD_UNINSTALL=Gamepad-Unterstützung deinstallieren...\nINFO_AUTOSTART_UNINSTALL=Startup-Autostart entfernen...\nSUCCESS_VDD_UNINSTALL=Virtueller Bildschirm Deinstallation abgeschlossen\nSUCCESS_VSINK_UNINSTALL=Virtuelles Mikrofon Deinstallation abgeschlossen\nSUCCESS_FIREWALL_UNINSTALL=Firewall-Regeln Entfernung abgeschlossen\nSUCCESS_GAMEPAD_UNINSTALL=Gamepad-Unterstützung Deinstallation abgeschlossen\nSUCCESS_AUTOSTART_UNINSTALL=Startup-Autostart Entfernung abgeschlossen\nWARNING_VDD_UNINSTALL=Virtueller Bildschirm Deinstallation fehlgeschlagen, aber fortfahren\nWARNING_VSINK_UNINSTALL=Virtuelles Mikrofon Deinstallation fehlgeschlagen, aber fortfahren\nWARNING_FIREWALL_UNINSTALL=Firewall-Regeln Entfernung fehlgeschlagen, aber fortfahren\nWARNING_GAMEPAD_UNINSTALL=Gamepad-Unterstützung Deinstallation fehlgeschlagen, aber fortfahren\nWARNING_AUTOSTART_UNINSTALL=Startup-Autostart Entfernung fehlgeschlagen, aber fortfahren\nINFO_VDD_SKIP=Virtueller Bildschirm Deinstallation überspringen\nINFO_VSINK_SKIP=Virtuelles Mikrofon Deinstallation überspringen\nINFO_FIREWALL_SKIP=Firewall-Regeln Entfernung überspringen\nINFO_GAMEPAD_SKIP=Gamepad-Unterstützung Deinstallation überspringen\nINFO_AUTOSTART_SKIP=Startup-Autostart Entfernung überspringen\nINFO_AUTOSTART_NOT_FOUND=Startup-Autostart Verknüpfung nicht gefunden\nUNINSTALL_CONFIRM_TITLE=Deinstallationsbestätigung\nUNINSTALL_CONFIRM_DESC=Die folgenden Deinstallationsoperationen werden ausgeführt\nUNINSTALL_CONFIRM_VDD=Virtuellen Bildschirmtreiber deinstallieren\nUNINSTALL_CONFIRM_VSINK=Virtuelles Mikrofon deinstallieren\nUNINSTALL_CONFIRM_FIREWALL=Firewall-Regeln entfernen\nUNINSTALL_CONFIRM_GAMEPAD=Gamepad-Unterstützung deinstallieren\nUNINSTALL_CONFIRM_AUTOSTART=Startup-Autostart entfernen\nUNINSTALL_CONFIRM_PROCEED=Mit Deinstallation fortfahren? (Y/N, Standard: Y)\nUNINSTALL_CANCEL=Deinstallationsoperation abgebrochen\nUNINSTALL_PROCEEDING=Deinstallation wird gestartet...\nNOTE_UNINSTALL=Hinweis: Diese Operation wird keine tragbaren Dateien löschen. Bitte löschen Sie den gesamten Ordner manuell, wenn Sie ihn vollständig entfernen möchten.\n\n# Skript-Abschluss\nSCRIPT_COMPLETE_PROMPT=Skript-Ausführung abgeschlossen, beliebige Taste zum Beenden drücken\n"
  },
  {
    "path": "src_assets/windows/misc/languages/en-US.lang",
    "content": "TITLE=Sunshine Portable Initialization Tool\nERROR_ADMIN=Administrator privileges required to run this script\nERROR_ADMIN_DESC=Please right-click this file and select \"Run as administrator\"\nINFO_ADMIN=Administrator privileges detected, starting initialization...\nERROR_EXE=sunshine.exe file not found\nERROR_EXE_DESC=Please ensure this script is in the portable directory\nINFO_EXE=Found Sunshine executable\nINFO_CONFIG=Creating config directory\nINFO_LOG=Creating log directory\nTITLE_INSTALL=Installing Required Components\nSTEP_VDD=Checking virtual display\nINFO_VDD_INSTALL=Installing virtual display\nSUCCESS_VDD=Virtual display installation completed\nWARNING_VDD=Virtual display installation failed, but continuing\nINFO_VDD_EXISTS=Virtual display already installed\nINFO_VDD_SKIP=Skipping virtual display installation\nVDD_PROMPT=Install virtual display? (Y/N, Default: Y)\nSTEP_VSINK=Checking virtual microphone\nINFO_VSINK_INSTALL=Installing virtual microphone\nSUCCESS_VSINK=Virtual microphone installation completed\nWARNING_VSINK=Virtual microphone installation failed, but continuing\nINFO_VSINK_EXISTS=Virtual microphone already installed\nINFO_VSINK_SKIP=Skipping virtual microphone installation\nVSINK_PROMPT=Install virtual microphone? (Y/N, Default: Y)\nSTEP_FIREWALL=Configuring firewall rules\nINFO_FIREWALL_INSTALL=Configuring firewall rules...\nSUCCESS_FIREWALL=Firewall rules configured\nWARNING_FIREWALL=Firewall configuration failed, but continuing\nINFO_FIREWALL_SKIP=Skipping firewall configuration\nFIREWALL_PROMPT=Configure firewall rules? (Y/N, Default: Y)\nSTEP_GAMEPAD=Installing gamepad support\nINFO_GAMEPAD_INSTALL=Installing gamepad support\nSUCCESS_GAMEPAD=Gamepad support installation completed\nWARNING_GAMEPAD=Gamepad support installation failed, but continuing\nINFO_GAMEPAD_EXISTS=Gamepad support already installed\nINFO_GAMEPAD_SKIP=Skipping gamepad support installation\nGAMEPAD_PROMPT=Install gamepad support? (Y/N, Default: Y)\nSTEP_AUTOSTART=Configuring startup autostart\nAUTOSTART_PROMPT=Configure startup autostart? (Y/N, Default: Y)\nINFO_AUTOSTART_INSTALL=Creating startup shortcut\nSUCCESS_AUTOSTART=Startup autostart configuration completed\nWARNING_AUTOSTART=Startup autostart configuration failed, but continuing\nINFO_AUTOSTART_SKIP=Skipping startup autostart configuration\nTITLE_SUCCESS=Initialization Complete!\nSUCCESS_MAIN=Sunshine portable initialization completed!\nUSAGE_1=Run sunshine.exe to start the program\nUSAGE_2=Use browser to access http://localhost:47989 for configuration\nCONFIG_LOCATION=Configuration file location\nLOG_LOCATION=Log file location\nNOTE_FIRST=After first startup, please right-click the Sunshine icon in system tray and select \"Open Sunshine\" to launch, then set username and password\n\n# Launch related\nPROMPT_START=Start Sunshine now? (Y/N, Default: Y)\nINFO_STARTING=Creating startup script...\nSUCCESS_STARTUP_SCRIPT=Startup script created successfully. You can later run start_sunshine.vbs to start Sunshine\nINFO_LATER=You can later run sunshine.exe directly to start Sunshine\n\n# Language selection related\nLANG_DETECTED=Detected system language\nLANG_CONFIRM=Please confirm if you want to use this language\nLANG_YES=Yes (Y)\nLANG_NO=No (N)\nLANG_SELECT=Please select language\nLANG_SELECT_PROMPT=Please select language (1-6)\nLANG_CONFIRM_PROMPT=Please select (Y/N, Default: Y)\nLANG_ZH_SIMPLE=简体中文\nLANG_EN=English\nLANG_DE=Deutsch\nLANG_FR=Français\nLANG_JA=日本語\nLANG_RU=Русский\n\n# Script execution confirmation\nSCRIPT_CONFIRM_TITLE=Script Execution Confirmation\nSCRIPT_CONFIRM_DESC=The following operations will be performed\nSCRIPT_CONFIRM_VDD=Install virtual display driver\nSCRIPT_CONFIRM_VSINK=Install virtual microphone\nSCRIPT_CONFIRM_FIREWALL=Configure firewall rules\nSCRIPT_CONFIRM_GAMEPAD=Install gamepad support\nSCRIPT_CONFIRM_AUTOSTART=Configure startup autostart\nSCRIPT_CONFIRM_PROCEED=Do you want to continue? (Y/N, Default: Y)\nSCRIPT_CONFIRM_CANCEL=Operation cancelled\nSCRIPT_CONFIRM_PROCEEDING=Starting initialization...\n\n# Error messages\nERROR_INVALID_SELECTION=Invalid selection, using default language\nERROR_LANG_FILE_NOT_FOUND=Language file not found\nERROR_LANG_FILE_NOT_FOUND_EN=Language file not found\n\n# Uninstall related\nTITLE_UNINSTALL=Sunshine Portable Uninstall Tool\nTITLE_UNINSTALL_PROCESS=Starting component uninstallation\nSTEP_UNINSTALL_VDD=Uninstalling virtual display\nSTEP_UNINSTALL_VSINK=Uninstalling virtual microphone\nSTEP_UNINSTALL_FIREWALL=Removing firewall rules\nSTEP_UNINSTALL_GAMEPAD=Uninstalling gamepad support\nSTEP_UNINSTALL_AUTOSTART=Removing startup autostart\nVDD_UNINSTALL_PROMPT=Uninstall virtual display? (Y/N, Default: Y)\nVSINK_UNINSTALL_PROMPT=Uninstall virtual microphone? (Y/N, Default: Y)\nFIREWALL_UNINSTALL_PROMPT=Remove firewall rules? (Y/N, Default: Y)\nGAMEPAD_UNINSTALL_PROMPT=Uninstall gamepad support? (Y/N, Default: Y)\nAUTOSTART_UNINSTALL_PROMPT=Remove startup autostart? (Y/N, Default: Y)\nINFO_VDD_UNINSTALL=Uninstalling virtual display...\nINFO_VSINK_UNINSTALL=Uninstalling virtual microphone...\nINFO_FIREWALL_UNINSTALL=Removing firewall rules...\nINFO_GAMEPAD_UNINSTALL=Uninstalling gamepad support...\nINFO_AUTOSTART_UNINSTALL=Removing startup autostart...\nSUCCESS_VDD_UNINSTALL=Virtual display uninstallation completed\nSUCCESS_VSINK_UNINSTALL=Virtual microphone uninstallation completed\nSUCCESS_FIREWALL_UNINSTALL=Firewall rules removal completed\nSUCCESS_GAMEPAD_UNINSTALL=Gamepad support uninstallation completed\nSUCCESS_AUTOSTART_UNINSTALL=Startup autostart removal completed\nWARNING_VDD_UNINSTALL=Virtual display uninstallation failed, but continuing\nWARNING_VSINK_UNINSTALL=Virtual microphone uninstallation failed, but continuing\nWARNING_FIREWALL_UNINSTALL=Firewall rules removal failed, but continuing\nWARNING_GAMEPAD_UNINSTALL=Gamepad support uninstallation failed, but continuing\nWARNING_AUTOSTART_UNINSTALL=Startup autostart removal failed, but continuing\nINFO_VDD_SKIP=Skipping virtual display uninstallation\nINFO_VSINK_SKIP=Skipping virtual microphone uninstallation\nINFO_FIREWALL_SKIP=Skipping firewall rules removal\nINFO_GAMEPAD_SKIP=Skipping gamepad support uninstallation\nINFO_AUTOSTART_SKIP=Skipping startup autostart removal\nINFO_AUTOSTART_NOT_FOUND=Startup autostart shortcut not found\nUNINSTALL_CONFIRM_TITLE=Uninstall Confirmation\nUNINSTALL_CONFIRM_DESC=The following uninstall operations will be performed\nUNINSTALL_CONFIRM_VDD=Uninstall virtual display driver\nUNINSTALL_CONFIRM_VSINK=Uninstall virtual microphone\nUNINSTALL_CONFIRM_FIREWALL=Remove firewall rules\nUNINSTALL_CONFIRM_GAMEPAD=Uninstall gamepad support\nUNINSTALL_CONFIRM_AUTOSTART=Remove startup autostart\nUNINSTALL_CONFIRM_PROCEED=Continue with uninstallation? (Y/N, Default: Y)\nUNINSTALL_CANCEL=Uninstall operation cancelled\nUNINSTALL_PROCEEDING=Starting uninstallation...\nNOTE_UNINSTALL=Note: This operation will not delete portable files. Please manually delete the entire folder if you want to completely remove it.\n\n# Admin elevation related\nADMIN_ELEVATE_PROMPT=Do you want to restart this script with administrator privileges?\nADMIN_ELEVATE_CONFIRM=Please select (Y/N, Default: Y)\nADMIN_ELEVATING=Restarting script with administrator privileges...\nADMIN_ELEVATE_CANCEL=Elevation cancelled, script exiting\n\n# Script completion prompt\nSCRIPT_COMPLETE_PROMPT=Script execution completed, press any key to exit\n"
  },
  {
    "path": "src_assets/windows/misc/languages/fr-FR.lang",
    "content": "TITLE=Outil d'initialisation portable Sunshine\nERROR_ADMIN=Privilèges administrateur requis pour exécuter ce script\nERROR_ADMIN_DESC=Veuillez cliquer avec le bouton droit sur ce fichier et sélectionner \"Exécuter en tant qu'administrateur\"\nINFO_ADMIN=Privilèges administrateur détectés, démarrage de l'initialisation...\nERROR_EXE=Fichier sunshine.exe introuvable\nERROR_EXE_DESC=Veuillez vous assurer que ce script se trouve dans le répertoire portable\nINFO_EXE=Exécutable Sunshine trouvé\nINFO_CONFIG=Création du répertoire de configuration\nINFO_LOG=Création du répertoire de journaux\nTITLE_INSTALL=Installation des composants requis\nSTEP_VDD=Vérification de l'écran virtuel\nINFO_VDD_INSTALL=Installation de l'écran virtuel\nSUCCESS_VDD=Installation de l'écran virtuel terminée\nWARNING_VDD=Échec de l'installation de l'écran virtuel, mais continuation\nINFO_VDD_EXISTS=Écran virtuel déjà installé\nSTEP_VSINK=Vérification du microphone virtuel\nINFO_VSINK_INSTALL=Installation du microphone virtuel\nSUCCESS_VSINK=Installation du microphone virtuel terminée\nWARNING_VSINK=Échec de l'installation du microphone virtuel, mais continuation\nINFO_VSINK_EXISTS=Microphone virtuel déjà installé\nSTEP_FIREWALL=Configuration des règles de pare-feu\nINFO_FIREWALL_INSTALL=Configuration des règles de pare-feu...\nSUCCESS_FIREWALL=Règles de pare-feu configurées\nWARNING_FIREWALL=Échec de la configuration du pare-feu, mais continuation\nINFO_FIREWALL_SKIP=Ignorer la configuration du pare-feu\nSTEP_GAMEPAD=Installation du support de manette de jeu\nINFO_GAMEPAD_INSTALL=Installation du support de manette de jeu\nSUCCESS_GAMEPAD=Installation du support de manette de jeu terminée\nWARNING_GAMEPAD=Échec de l'installation du support de manette de jeu, mais continuation\nINFO_GAMEPAD_EXISTS=Support de manette de jeu déjà installé\nINFO_GAMEPAD_SKIP=Ignorer l'installation du support de manette de jeu\nGAMEPAD_PROMPT=Installer le support de manette de jeu ? (Y/N, Par défaut: Y)\nSTEP_AUTOSTART=Configuration du démarrage automatique au démarrage\nAUTOSTART_PROMPT=Configurer le démarrage automatique au démarrage ? (Y/N, Par défaut: Y)\nINFO_AUTOSTART_INSTALL=Création du raccourci de démarrage\nSUCCESS_AUTOSTART=Configuration du démarrage automatique au démarrage terminée\nWARNING_AUTOSTART=Échec de la configuration du démarrage automatique au démarrage, mais continuation\nINFO_AUTOSTART_SKIP=Ignorer la configuration du démarrage automatique au démarrage\nTITLE_SUCCESS=Initialisation terminée !\nSUCCESS_MAIN=Initialisation portable Sunshine terminée !\nUSAGE_1=Lancez sunshine.exe pour démarrer le programme\nUSAGE_2=Utilisez le navigateur pour accéder à http://localhost:47989 pour la configuration\nCONFIG_LOCATION=Emplacement du fichier de configuration\nLOG_LOCATION=Emplacement du fichier de journal\nNOTE_FIRST=Après le premier démarrage, veuillez cliquer avec le bouton droit sur l'icône Sunshine dans la barre système et sélectionner \"Open Sunshine\" pour lancer, puis définir le nom d'utilisateur et le mot de passe\n\n# Lancement\nPROMPT_START=Démarrer Sunshine maintenant ? (Y/N, Par défaut: Y)\nINFO_STARTING=Création du script de démarrage...\nSUCCESS_STARTUP_SCRIPT=Script de démarrage créé avec succès. Vous pouvez plus tard exécuter start_sunshine.vbs pour démarrer Sunshine\nINFO_LATER=Vous pouvez plus tard exécuter sunshine.exe directement pour démarrer Sunshine\n\n# Sélection de langue\nLANG_DETECTED=Langue du système détectée\nLANG_CONFIRM=Veuillez confirmer si vous voulez utiliser cette langue\nLANG_YES=Oui (Y)\nLANG_NO=Non (N)\nLANG_SELECT=Veuillez sélectionner la langue\nLANG_SELECT_PROMPT=Veuillez sélectionner la langue (1-6)\nLANG_CONFIRM_PROMPT=Veuillez sélectionner (Y/N, Par défaut: Y)\nLANG_ZH_SIMPLE=简体中文\nLANG_EN=English\nLANG_DE=Deutsch\nLANG_FR=Français\nLANG_JA=日本語\nLANG_RU=Русский\n\n# Confirmation d'exécution du script\nSCRIPT_CONFIRM_TITLE=Confirmation d'exécution du script\nSCRIPT_CONFIRM_DESC=Les opérations suivantes seront effectuées\nSCRIPT_CONFIRM_VDD=Installer le pilote d'écran virtuel\nSCRIPT_CONFIRM_VSINK=Installer le microphone virtuel\nSCRIPT_CONFIRM_FIREWALL=Configurer les règles de pare-feu\nSCRIPT_CONFIRM_GAMEPAD=Installer le support de manette de jeu\nSCRIPT_CONFIRM_AUTOSTART=Configurer le démarrage automatique au démarrage\nSCRIPT_CONFIRM_PROCEED=Voulez-vous continuer ? (Y/N, Par défaut: Y)\nSCRIPT_CONFIRM_CANCEL=Opération annulée\nSCRIPT_CONFIRM_PROCEEDING=Démarrage de l'initialisation...\n\n# Messages d'erreur\nERROR_INVALID_SELECTION=Sélection invalide, utilisation de la langue par défaut\nERROR_LANG_FILE_NOT_FOUND=Fichier de langue introuvable\nERROR_LANG_FILE_NOT_FOUND_EN=Language file not found\n\n# Élévation d'administrateur\nADMIN_ELEVATE_PROMPT=Voulez-vous redémarrer ce script avec les privilèges d'administrateur ?\nADMIN_ELEVATE_CONFIRM=Veuillez sélectionner (Y/N, Par défaut: Y)\nADMIN_ELEVATING=Redémarrage du script avec les privilèges d'administrateur...\nADMIN_ELEVATE_CANCEL=Élévation annulée, sortie du script\n\n# Désinstallation\nTITLE_UNINSTALL=Outil de désinstallation Sunshine Portable\nTITLE_UNINSTALL_PROCESS=Commencer la désinstallation des composants\nSTEP_UNINSTALL_VDD=Désinstaller l'écran virtuel\nSTEP_UNINSTALL_VSINK=Désinstaller le microphone virtuel\nSTEP_UNINSTALL_FIREWALL=Supprimer les règles de pare-feu\nSTEP_UNINSTALL_GAMEPAD=Désinstaller le support de manette de jeu\nSTEP_UNINSTALL_AUTOSTART=Supprimer le démarrage automatique au démarrage\nVDD_UNINSTALL_PROMPT=Désinstaller l'écran virtuel ? (Y/N, Par défaut: Y)\nVSINK_UNINSTALL_PROMPT=Désinstaller le microphone virtuel ? (Y/N, Par défaut: Y)\nFIREWALL_UNINSTALL_PROMPT=Supprimer les règles de pare-feu ? (Y/N, Par défaut: Y)\nGAMEPAD_UNINSTALL_PROMPT=Désinstaller le support de manette de jeu ? (Y/N, Par défaut: Y)\nAUTOSTART_UNINSTALL_PROMPT=Supprimer le démarrage automatique au démarrage ? (Y/N, Par défaut: Y)\nINFO_VDD_UNINSTALL=Désinstallation de l'écran virtuel...\nINFO_VSINK_UNINSTALL=Désinstallation du microphone virtuel...\nINFO_FIREWALL_UNINSTALL=Suppression des règles de pare-feu...\nINFO_GAMEPAD_UNINSTALL=Désinstallation du support de manette de jeu...\nINFO_AUTOSTART_UNINSTALL=Suppression du démarrage automatique au démarrage...\nSUCCESS_VDD_UNINSTALL=Désinstallation de l'écran virtuel terminée\nSUCCESS_VSINK_UNINSTALL=Désinstallation du microphone virtuel terminée\nSUCCESS_FIREWALL_UNINSTALL=Suppression des règles de pare-feu terminée\nSUCCESS_GAMEPAD_UNINSTALL=Désinstallation du support de manette de jeu terminée\nSUCCESS_AUTOSTART_UNINSTALL=Suppression du démarrage automatique au démarrage terminée\nWARNING_VDD_UNINSTALL=Échec de la désinstallation de l'écran virtuel, mais continuation\nWARNING_VSINK_UNINSTALL=Échec de la désinstallation du microphone virtuel, mais continuation\nWARNING_FIREWALL_UNINSTALL=Échec de la suppression des règles de pare-feu, mais continuation\nWARNING_GAMEPAD_UNINSTALL=Échec de la désinstallation du support de manette de jeu, mais continuation\nWARNING_AUTOSTART_UNINSTALL=Échec de la suppression du démarrage automatique au démarrage, mais continuation\nINFO_VDD_SKIP=Ignorer la désinstallation de l'écran virtuel\nINFO_VSINK_SKIP=Ignorer la désinstallation du microphone virtuel\nINFO_FIREWALL_SKIP=Ignorer la suppression des règles de pare-feu\nINFO_GAMEPAD_SKIP=Ignorer la désinstallation du support de manette de jeu\nINFO_AUTOSTART_SKIP=Ignorer la suppression du démarrage automatique au démarrage\nINFO_AUTOSTART_NOT_FOUND=Raccourci de démarrage automatique au démarrage introuvable\nUNINSTALL_CONFIRM_TITLE=Confirmation de désinstallation\nUNINSTALL_CONFIRM_DESC=Les opérations de désinstallation suivantes seront effectuées\nUNINSTALL_CONFIRM_VDD=Désinstaller le pilote d'écran virtuel\nUNINSTALL_CONFIRM_VSINK=Désinstaller le microphone virtuel\nUNINSTALL_CONFIRM_FIREWALL=Supprimer les règles de pare-feu\nUNINSTALL_CONFIRM_GAMEPAD=Désinstaller le support de manette de jeu\nUNINSTALL_CONFIRM_AUTOSTART=Supprimer le démarrage automatique au démarrage\nUNINSTALL_CONFIRM_PROCEED=Continuer avec la désinstallation ? (Y/N, Par défaut: Y)\nUNINSTALL_CANCEL=Opération de désinstallation annulée\nUNINSTALL_PROCEEDING=Démarrage de la désinstallation...\nNOTE_UNINSTALL=Note : Cette opération ne supprimera pas les fichiers portables. Veuillez supprimer manuellement le dossier entier si vous voulez le supprimer complètement.\n\n# Fin du script\nSCRIPT_COMPLETE_PROMPT=Exécution du script terminée, appuyez sur une touche pour quitter\n"
  },
  {
    "path": "src_assets/windows/misc/languages/ja-JP.lang",
    "content": "TITLE=Sunshine ポータブル初期化ツール\nERROR_ADMIN=このスクリプトを実行するには管理者権限が必要です\nERROR_ADMIN_DESC=このファイルを右クリックして「管理者として実行」を選択してください\nINFO_ADMIN=管理者権限が検出されました。初期化を開始します...\nERROR_EXE=sunshine.exe ファイルが見つかりません\nERROR_EXE_DESC=このスクリプトがポータブルディレクトリにあることを確認してください\nINFO_EXE=Sunshine 実行ファイルが見つかりました\nINFO_CONFIG=設定ディレクトリを作成中\nINFO_LOG=ログディレクトリを作成中\nTITLE_INSTALL=必要なコンポーネントをインストール中\nSTEP_VDD=仮想ディスプレイをチェック中\nINFO_VDD_INSTALL=仮想ディスプレイをインストール中\nSUCCESS_VDD=仮想ディスプレイのインストールが完了しました\nWARNING_VDD=仮想ディスプレイのインストールに失敗しましたが、続行します\nINFO_VDD_EXISTS=仮想ディスプレイは既にインストールされています\nSTEP_VSINK=仮想マイクをチェック中\nINFO_VSINK_INSTALL=仮想マイクをインストール中\nSUCCESS_VSINK=仮想マイクのインストールが完了しました\nWARNING_VSINK=仮想マイクのインストールに失敗しましたが、続行します\nINFO_VSINK_EXISTS=仮想マイクは既にインストールされています\nSTEP_FIREWALL=ファイアウォールルールを設定中\nINFO_FIREWALL_INSTALL=ファイアウォールルールを設定中...\nSUCCESS_FIREWALL=ファイアウォールルールの設定が完了しました\nWARNING_FIREWALL=ファイアウォールの設定に失敗しましたが、続行します\nINFO_FIREWALL_SKIP=ファイアウォールの設定をスキップします\nSTEP_GAMEPAD=ゲームパッドサポートをインストール中\nINFO_GAMEPAD_INSTALL=ゲームパッドサポートをインストール中\nSUCCESS_GAMEPAD=ゲームパッドサポートのインストールが完了しました\nWARNING_GAMEPAD=ゲームパッドサポートのインストールに失敗しましたが、続行します\nINFO_GAMEPAD_EXISTS=ゲームパッドサポートは既にインストールされています\nINFO_GAMEPAD_SKIP=ゲームパッドサポートのインストールをスキップします\nGAMEPAD_PROMPT=ゲームパッドサポートをインストールしますか？(Y/N, デフォルト: Y)\nSTEP_AUTOSTART=起動時自動起動を設定中\nAUTOSTART_PROMPT=起動時自動起動を設定しますか？(Y/N, デフォルト: Y)\nINFO_AUTOSTART_INSTALL=起動ショートカットを作成中\nSUCCESS_AUTOSTART=起動時自動起動の設定が完了しました\nWARNING_AUTOSTART=起動時自動起動の設定に失敗しましたが、続行します\nINFO_AUTOSTART_SKIP=起動時自動起動の設定をスキップします\nTITLE_SUCCESS=初期化完了！\nSUCCESS_MAIN=Sunshine ポータブルの初期化が完了しました！\nUSAGE_1=sunshine.exe を実行してプログラムを起動\nUSAGE_2=ブラウザで http://localhost:47989 にアクセスして設定\nCONFIG_LOCATION=設定ファイルの場所\nLOG_LOCATION=ログファイルの場所\nNOTE_FIRST=初回起動後、システムトレイのSunshineアイコンを右クリックして「Open Sunshine」を選択して起動し、ユーザー名とパスワードを設定してください\n\n# 起動関連\nPROMPT_START=今すぐSunshineを起動しますか？(Y/N, デフォルト: Y)\nINFO_STARTING=起動スクリプトを作成中...\nSUCCESS_STARTUP_SCRIPT=起動スクリプトが正常に作成されました。後でstart_sunshine.vbsを実行してSunshineを起動できます\nINFO_LATER=後でsunshine.exeを直接実行してSunshineを起動できます\n\n# 言語選択関連\nLANG_DETECTED=システム言語が検出されました\nLANG_CONFIRM=この言語を使用しますか？\nLANG_YES=はい (Y)\nLANG_NO=いいえ (N)\nLANG_SELECT=言語を選択してください\nLANG_SELECT_PROMPT=言語を選択してください (1-6)\nLANG_CONFIRM_PROMPT=選択してください (Y/N, デフォルト: Y)\nLANG_ZH_SIMPLE=简体中文\nLANG_EN=English\nLANG_DE=Deutsch\nLANG_FR=Français\nLANG_JA=日本語\nLANG_RU=Русский\n\n# スクリプト実行確認\nSCRIPT_CONFIRM_TITLE=スクリプト実行確認\nSCRIPT_CONFIRM_DESC=以下の操作が実行されます\nSCRIPT_CONFIRM_VDD=仮想ディスプレイドライバーをインストール\nSCRIPT_CONFIRM_VSINK=仮想マイクをインストール\nSCRIPT_CONFIRM_FIREWALL=ファイアウォールルールを設定\nSCRIPT_CONFIRM_GAMEPAD=ゲームパッドサポートをインストール\nSCRIPT_CONFIRM_AUTOSTART=起動時自動起動を設定\nSCRIPT_CONFIRM_PROCEED=続行しますか？(Y/N, デフォルト: Y)\nSCRIPT_CONFIRM_CANCEL=操作がキャンセルされました\nSCRIPT_CONFIRM_PROCEEDING=初期化を開始しています...\n\n# エラーメッセージ\nERROR_INVALID_SELECTION=無効な選択、デフォルト言語を使用\nERROR_LANG_FILE_NOT_FOUND=言語ファイルが見つかりません\nERROR_LANG_FILE_NOT_FOUND_EN=Language file not found\n\n# 管理者権限の昇格\nADMIN_ELEVATE_PROMPT=管理者権限でこのスクリプトを再起動しますか？\nADMIN_ELEVATE_CONFIRM=選択してください (Y/N, デフォルト: Y)\nADMIN_ELEVATING=管理者権限でスクリプトを再起動中...\nADMIN_ELEVATE_CANCEL=昇格がキャンセルされました、スクリプトを終了します\n\n# アンインストール関連\nTITLE_UNINSTALL=Sunshine ポータブルアンインストールツール\nTITLE_UNINSTALL_PROCESS=コンポーネントのアンインストールを開始\nSTEP_UNINSTALL_VDD=仮想ディスプレイをアンインストール\nSTEP_UNINSTALL_VSINK=仮想マイクをアンインストール\nSTEP_UNINSTALL_FIREWALL=ファイアウォールルールを削除\nSTEP_UNINSTALL_GAMEPAD=ゲームパッドサポートをアンインストール\nSTEP_UNINSTALL_AUTOSTART=起動時自動起動を削除\nVDD_UNINSTALL_PROMPT=仮想ディスプレイをアンインストールしますか？(Y/N, デフォルト: Y)\nVSINK_UNINSTALL_PROMPT=仮想マイクをアンインストールしますか？(Y/N, デフォルト: Y)\nFIREWALL_UNINSTALL_PROMPT=ファイアウォールルールを削除しますか？(Y/N, デフォルト: Y)\nGAMEPAD_UNINSTALL_PROMPT=ゲームパッドサポートをアンインストールしますか？(Y/N, デフォルト: Y)\nAUTOSTART_UNINSTALL_PROMPT=起動時自動起動を削除しますか？(Y/N, デフォルト: Y)\nINFO_VDD_UNINSTALL=仮想ディスプレイをアンインストール中...\nINFO_VSINK_UNINSTALL=仮想マイクをアンインストール中...\nINFO_FIREWALL_UNINSTALL=ファイアウォールルールを削除中...\nINFO_GAMEPAD_UNINSTALL=ゲームパッドサポートをアンインストール中...\nINFO_AUTOSTART_UNINSTALL=起動時自動起動を削除中...\nSUCCESS_VDD_UNINSTALL=仮想ディスプレイのアンインストールが完了しました\nSUCCESS_VSINK_UNINSTALL=仮想マイクのアンインストールが完了しました\nSUCCESS_FIREWALL_UNINSTALL=ファイアウォールルールの削除が完了しました\nSUCCESS_GAMEPAD_UNINSTALL=ゲームパッドサポートのアンインストールが完了しました\nSUCCESS_AUTOSTART_UNINSTALL=起動時自動起動の削除が完了しました\nWARNING_VDD_UNINSTALL=仮想ディスプレイのアンインストールに失敗しましたが、続行します\nWARNING_VSINK_UNINSTALL=仮想マイクのアンインストールに失敗しましたが、続行します\nWARNING_FIREWALL_UNINSTALL=ファイアウォールルールの削除に失敗しましたが、続行します\nWARNING_GAMEPAD_UNINSTALL=ゲームパッドサポートのアンインストールに失敗しましたが、続行します\nWARNING_AUTOSTART_UNINSTALL=起動時自動起動の削除に失敗しましたが、続行します\nINFO_VDD_SKIP=仮想ディスプレイのアンインストールをスキップします\nINFO_VSINK_SKIP=仮想マイクのアンインストールをスキップします\nINFO_FIREWALL_SKIP=ファイアウォールルールの削除をスキップします\nINFO_GAMEPAD_SKIP=ゲームパッドサポートのアンインストールをスキップします\nINFO_AUTOSTART_SKIP=起動時自動起動の削除をスキップします\nINFO_AUTOSTART_NOT_FOUND=起動時自動起動のショートカットが見つかりません\nUNINSTALL_CONFIRM_TITLE=アンインストール確認\nUNINSTALL_CONFIRM_DESC=以下のアンインストール操作が実行されます\nUNINSTALL_CONFIRM_VDD=仮想ディスプレイドライバーをアンインストール\nUNINSTALL_CONFIRM_VSINK=仮想マイクをアンインストール\nUNINSTALL_CONFIRM_FIREWALL=ファイアウォールルールを削除\nUNINSTALL_CONFIRM_GAMEPAD=ゲームパッドサポートをアンインストール\nUNINSTALL_CONFIRM_AUTOSTART=起動時自動起動を削除\nUNINSTALL_CONFIRM_PROCEED=アンインストールを続行しますか？(Y/N, デフォルト: Y)\nUNINSTALL_CANCEL=アンインストール操作がキャンセルされました\nUNINSTALL_PROCEEDING=アンインストールを開始しています...\nNOTE_UNINSTALL=注意：この操作はポータブルファイルを削除しません。完全に削除したい場合は、フォルダ全体を手動で削除してください\n\n# スクリプト完了\nSCRIPT_COMPLETE_PROMPT=スクリプトの実行が完了しました、任意のキーを押して終了してください\n"
  },
  {
    "path": "src_assets/windows/misc/languages/ru-RU.lang",
    "content": "TITLE=Инструмент инициализации Sunshine Portable\nERROR_ADMIN=Требуются права администратора для запуска этого скрипта\nERROR_ADMIN_DESC=Пожалуйста, щелкните правой кнопкой мыши по этому файлу и выберите \"Запуск от имени администратора\"\nINFO_ADMIN=Обнаружены права администратора, начинаем инициализацию...\nERROR_EXE=Файл sunshine.exe не найден\nERROR_EXE_DESC=Убедитесь, что этот скрипт находится в папке портативной версии\nINFO_EXE=Найден исполняемый файл Sunshine\nINFO_CONFIG=Создание папки конфигурации\nINFO_LOG=Создание папки логов\nTITLE_INSTALL=Начало установки необходимых компонентов\nSTEP_VDD=Проверка виртуального дисплея\nINFO_VDD_INSTALL=Установка виртуального дисплея\nSUCCESS_VDD=Установка виртуального дисплея завершена\nWARNING_VDD=Установка виртуального дисплея не удалась, но можно продолжить\nINFO_VDD_EXISTS=Виртуальный дисплей уже установлен\nINFO_VDD_SKIP=Пропуск установки виртуального дисплея\nVDD_PROMPT=Установить виртуальный дисплей? (Y/N, По умолчанию: Y)\nSTEP_VSINK=Проверка виртуального микрофона\nINFO_VSINK_INSTALL=Установка виртуального микрофона\nSUCCESS_VSINK=Установка виртуального микрофона завершена\nWARNING_VSINK=Установка виртуального микрофона не удалась, но можно продолжить\nINFO_VSINK_EXISTS=Виртуальный микрофон уже установлен\nINFO_VSINK_SKIP=Пропуск установки виртуального микрофона\nVSINK_PROMPT=Установить виртуальный микрофон? (Y/N, По умолчанию: Y)\nSTEP_FIREWALL=Настройка правил брандмауэра\nINFO_FIREWALL_INSTALL=Настройка правил брандмауэра...\nSUCCESS_FIREWALL=Настройка правил брандмауэра завершена\nWARNING_FIREWALL=Настройка правил брандмауэра не удалась, но можно продолжить\nINFO_FIREWALL_SKIP=Пропуск настройки правил брандмауэра\nFIREWALL_PROMPT=Настроить правила брандмауэра? (Y/N, По умолчанию: Y)\nSTEP_GAMEPAD=Установка поддержки геймпада\nINFO_GAMEPAD_INSTALL=Установка поддержки геймпада\nSUCCESS_GAMEPAD=Установка поддержки геймпада завершена\nWARNING_GAMEPAD=Установка поддержки геймпада не удалась, но можно продолжить\nINFO_GAMEPAD_EXISTS=Поддержка геймпада уже установлена\nINFO_GAMEPAD_SKIP=Пропуск установки поддержки геймпада\nGAMEPAD_PROMPT=Установить поддержку геймпада? (Y/N, По умолчанию: Y)\nSTEP_AUTOSTART=Настройка автозапуска при загрузке\nAUTOSTART_PROMPT=Настроить автозапуск при загрузке? (Y/N, По умолчанию: Y)\nINFO_AUTOSTART_INSTALL=Создание ярлыка автозапуска\nSUCCESS_AUTOSTART=Настройка автозапуска при загрузке завершена\nWARNING_AUTOSTART=Настройка автозапуска при загрузке не удалась, но можно продолжить\nINFO_AUTOSTART_SKIP=Пропуск настройки автозапуска при загрузке\nTITLE_SUCCESS=Инициализация завершена!\nSUCCESS_MAIN=Инициализация Sunshine Portable завершена!\nUSAGE_1=Запустите sunshine.exe для запуска программы\nUSAGE_2=Используйте браузер для доступа к http://localhost:47989 для настройки\nCONFIG_LOCATION=Расположение файла конфигурации\nLOG_LOCATION=Расположение файла логов\nNOTE_FIRST=После первого запуска щелкните правой кнопкой мыши по иконке Sunshine в системном трее и выберите \"Open Sunshine\" для запуска, затем настройте имя пользователя и пароль\n\n# Запуск\nPROMPT_START=Запустить Sunshine сейчас? (Y/N, По умолчанию: Y)\nINFO_STARTING=Создание скрипта запуска...\nSUCCESS_STARTUP_SCRIPT=Скрипт запуска успешно создан. Вы можете позже запустить start_sunshine.vbs для запуска Sunshine\nINFO_LATER=Вы можете позже запустить sunshine.exe напрямую для запуска Sunshine\n\n# Language selection related\nLANG_DETECTED=Обнаружен язык системы\nLANG_CONFIRM=Пожалуйста, подтвердите, хотите ли вы использовать этот язык\nLANG_YES=Да (Y)\nLANG_NO=Нет (N)\nLANG_SELECT=Пожалуйста, выберите язык\nLANG_SELECT_PROMPT=Пожалуйста, выберите язык (1-6)\nLANG_CONFIRM_PROMPT=Пожалуйста, выберите (Y/N, По умолчанию: Y)\nLANG_ZH_SIMPLE=简体中文\nLANG_EN=English\nLANG_DE=Deutsch\nLANG_FR=Français\nLANG_JA=日本語\nLANG_RU=Русский\n\n# Script execution confirmation\nSCRIPT_CONFIRM_TITLE=Подтверждение выполнения скрипта\nSCRIPT_CONFIRM_DESC=Будут выполнены следующие операции\nSCRIPT_CONFIRM_VDD=Установка драйвера виртуального дисплея\nSCRIPT_CONFIRM_VSINK=Установка виртуального микрофона\nSCRIPT_CONFIRM_FIREWALL=Настройка правил брандмауэра\nSCRIPT_CONFIRM_GAMEPAD=Установка поддержки геймпада\nSCRIPT_CONFIRM_AUTOSTART=Настройка автозапуска при загрузке\nSCRIPT_CONFIRM_PROCEED=Продолжить выполнение? (Y/N, По умолчанию: Y)\nSCRIPT_CONFIRM_CANCEL=Операция отменена\nSCRIPT_CONFIRM_PROCEEDING=Начинаем выполнение инициализации...\n\n# Error messages\nERROR_INVALID_SELECTION=Неверный выбор, используется язык по умолчанию\nERROR_LANG_FILE_NOT_FOUND=Файл языка не найден\nERROR_LANG_FILE_NOT_FOUND_EN=Language file not found\n\n# Uninstall related\nTITLE_UNINSTALL=Инструмент удаления Sunshine Portable\nTITLE_UNINSTALL_PROCESS=Начало удаления компонентов\nSTEP_UNINSTALL_VDD=Удаление виртуального дисплея\nSTEP_UNINSTALL_VSINK=Удаление виртуального микрофона\nSTEP_UNINSTALL_FIREWALL=Удаление правил брандмауэра\nSTEP_UNINSTALL_GAMEPAD=Удаление поддержки геймпада\nSTEP_UNINSTALL_AUTOSTART=Удаление автозапуска при загрузке\nVDD_UNINSTALL_PROMPT=Удалить виртуальный дисплей? (Y/N, По умолчанию: Y)\nVSINK_UNINSTALL_PROMPT=Удалить виртуальный микрофон? (Y/N, По умолчанию: Y)\nFIREWALL_UNINSTALL_PROMPT=Удалить правила брандмауэра? (Y/N, По умолчанию: Y)\nGAMEPAD_UNINSTALL_PROMPT=Удалить поддержку геймпада? (Y/N, По умолчанию: Y)\nAUTOSTART_UNINSTALL_PROMPT=Удалить автозапуск при загрузке? (Y/N, По умолчанию: Y)\nINFO_VDD_UNINSTALL=Удаление виртуального дисплея...\nINFO_VSINK_UNINSTALL=Удаление виртуального микрофона...\nINFO_FIREWALL_UNINSTALL=Удаление правил брандмауэра...\nINFO_GAMEPAD_UNINSTALL=Удаление поддержки геймпада...\nINFO_AUTOSTART_UNINSTALL=Удаление автозапуска при загрузке...\nSUCCESS_VDD_UNINSTALL=Удаление виртуального дисплея завершено\nSUCCESS_VSINK_UNINSTALL=Удаление виртуального микрофона завершено\nSUCCESS_FIREWALL_UNINSTALL=Удаление правил брандмауэра завершено\nSUCCESS_GAMEPAD_UNINSTALL=Удаление поддержки геймпада завершено\nSUCCESS_AUTOSTART_UNINSTALL=Удаление автозапуска при загрузке завершено\nWARNING_VDD_UNINSTALL=Удаление виртуального дисплея не удалось, но можно продолжить\nWARNING_VSINK_UNINSTALL=Удаление виртуального микрофона не удалось, но можно продолжить\nWARNING_FIREWALL_UNINSTALL=Удаление правил брандмауэра не удалось, но можно продолжить\nWARNING_GAMEPAD_UNINSTALL=Удаление поддержки геймпада не удалось, но можно продолжить\nWARNING_AUTOSTART_UNINSTALL=Удаление автозапуска при загрузке не удалось, но можно продолжить\nINFO_VDD_SKIP=Пропуск удаления виртуального дисплея\nINFO_VSINK_SKIP=Пропуск удаления виртуального микрофона\nINFO_FIREWALL_SKIP=Пропуск удаления правил брандмауэра\nINFO_GAMEPAD_SKIP=Пропуск удаления поддержки геймпада\nINFO_AUTOSTART_SKIP=Пропуск удаления автозапуска при загрузке\nINFO_AUTOSTART_NOT_FOUND=Ярлык автозапуска при загрузке не найден\nUNINSTALL_CONFIRM_TITLE=Подтверждение удаления\nUNINSTALL_CONFIRM_DESC=Будут выполнены следующие операции удаления\nUNINSTALL_CONFIRM_VDD=Удаление драйвера виртуального дисплея\nUNINSTALL_CONFIRM_VSINK=Удаление виртуального микрофона\nUNINSTALL_CONFIRM_FIREWALL=Удаление правил брандмауэра\nUNINSTALL_CONFIRM_GAMEPAD=Удаление поддержки геймпада\nUNINSTALL_CONFIRM_AUTOSTART=Удаление автозапуска при загрузке\nUNINSTALL_CONFIRM_PROCEED=Продолжить удаление? (Y/N, По умолчанию: Y)\nUNINSTALL_CANCEL=Операция удаления отменена\nUNINSTALL_PROCEEDING=Начинаем выполнение удаления...\nNOTE_UNINSTALL=Примечание: эта операция не удалит файлы портативной версии. Если вы хотите полностью удалить, пожалуйста, вручную удалите всю папку\n\n# Admin elevation related\nADMIN_ELEVATE_PROMPT=Хотите перезапустить этот скрипт с правами администратора?\nADMIN_ELEVATE_CONFIRM=Пожалуйста, выберите (Y/N, По умолчанию: Y)\nADMIN_ELEVATING=Перезапуск скрипта с правами администратора...\nADMIN_ELEVATE_CANCEL=Повышение прав отменено, скрипт завершается\n\n# Script completion prompt\nSCRIPT_COMPLETE_PROMPT=Выполнение скрипта завершено, нажмите любую клавишу для выхода\n"
  },
  {
    "path": "src_assets/windows/misc/languages/zh-Simple.lang",
    "content": "TITLE=Sunshine 便携版初始化工具\nERROR_ADMIN=需要管理员权限才能运行此脚本\nERROR_ADMIN_DESC=请右键点击此文件，选择\"以管理员身份运行\"\nINFO_ADMIN=检测到管理员权限，开始初始化...\nERROR_EXE=未找到 sunshine.exe 文件\nERROR_EXE_DESC=请确保此脚本位于便携版目录中\nINFO_EXE=找到 Sunshine 可执行文件\nINFO_CONFIG=创建配置目录\nINFO_LOG=创建日志目录\nTITLE_INSTALL=开始安装必要组件\nSTEP_VDD=检查虚拟显示器\nINFO_VDD_INSTALL=安装虚拟显示器\nSUCCESS_VDD=虚拟显示器安装完成\nWARNING_VDD=虚拟显示器安装失败，但可以继续\nINFO_VDD_EXISTS=虚拟显示器已安装\nINFO_VDD_SKIP=跳过虚拟显示器安装\nVDD_PROMPT=是否安装虚拟显示器？(Y/N, 默认: Y)\nSTEP_VSINK=检查虚拟麦克风\nINFO_VSINK_INSTALL=安装虚拟麦克风\nSUCCESS_VSINK=虚拟麦克风安装完成\nWARNING_VSINK=虚拟麦克风安装失败，但可以继续\nINFO_VSINK_EXISTS=虚拟麦克风已安装\nINFO_VSINK_SKIP=跳过虚拟麦克风安装\nVSINK_PROMPT=是否安装虚拟麦克风？(Y/N, 默认: Y)\nSTEP_FIREWALL=配置防火墙规则\nINFO_FIREWALL_INSTALL=配置防火墙规则...\nSUCCESS_FIREWALL=防火墙规则配置完成\nWARNING_FIREWALL=防火墙规则配置失败，但可以继续\nINFO_FIREWALL_SKIP=跳过防火墙规则配置\nFIREWALL_PROMPT=是否配置防火墙规则？(Y/N, 默认: Y)\nSTEP_GAMEPAD=安装游戏手柄支持\nINFO_GAMEPAD_INSTALL=安装游戏手柄支持\nSUCCESS_GAMEPAD=游戏手柄支持安装完成\nWARNING_GAMEPAD=游戏手柄支持安装失败，但可以继续\nINFO_GAMEPAD_EXISTS=游戏手柄支持已安装\nINFO_GAMEPAD_SKIP=跳过游戏手柄支持安装\nGAMEPAD_PROMPT=是否安装游戏手柄支持？(Y/N, 默认: Y)\nSTEP_AUTOSTART=配置开机自启动\nAUTOSTART_PROMPT=是否配置开机自启动？(Y/N, 默认: Y)\nINFO_AUTOSTART_INSTALL=创建开机自启动快捷方式\nSUCCESS_AUTOSTART=开机自启动配置完成\nWARNING_AUTOSTART=开机自启动配置失败，但可以继续\nINFO_AUTOSTART_SKIP=跳过开机自启动配置\nTITLE_SUCCESS=初始化完成！\nSUCCESS_MAIN=Sunshine 便携版初始化完成！\nUSAGE_1=运行 sunshine.exe 启动程序\nUSAGE_2=使用浏览器访问 http://localhost:47989 进行配置\nCONFIG_LOCATION=配置文件位置\nLOG_LOCATION=日志文件位置\nNOTE_FIRST=首次启动后请通过托盘的Sunshine图标，右键点击Open Sunshine启动，并设置用户名和密码\n\n# 启动相关\nPROMPT_START=是否现在启动Sunshine？(Y/N, 默认: Y)\nINFO_STARTING=正在创建启动脚本...\nSUCCESS_STARTUP_SCRIPT=启动脚本创建完成，稍后可以直接运行start_sunshine.vbs来启动Sunshine\nINFO_LATER=请可以稍后直接运行sunshine.exe来启动Sunshine\n\n# 语言选择相关\nLANG_DETECTED=检测到系统语言\nLANG_CONFIRM=请确认是否使用此语言\nLANG_YES=是 (Y)\nLANG_NO=否 (N)\nLANG_SELECT=请选择语言\nLANG_SELECT_PROMPT=请选择语言 (1-7)\nLANG_CONFIRM_PROMPT=请选择 (Y/N, 默认: Y)\nLANG_ZH_SIMPLE=简体中文\nLANG_ZH_TRADITIONAL=繁體中文\nLANG_EN=English\nLANG_DE=Deutsch\nLANG_FR=Français\nLANG_JA=日本語\nLANG_RU=Русский\n\n# 脚本执行确认\nSCRIPT_CONFIRM_TITLE=脚本执行确认\nSCRIPT_CONFIRM_DESC=即将执行以下操作\nSCRIPT_CONFIRM_VDD=安装虚拟显示器驱动\nSCRIPT_CONFIRM_VSINK=安装虚拟麦克风\nSCRIPT_CONFIRM_FIREWALL=配置防火墙规则\nSCRIPT_CONFIRM_GAMEPAD=安装游戏手柄支持\nSCRIPT_CONFIRM_AUTOSTART=配置开机自启动\nSCRIPT_CONFIRM_PROCEED=是否继续执行？(Y/N, 默认: Y)\nSCRIPT_CONFIRM_CANCEL=操作已取消\nSCRIPT_CONFIRM_PROCEEDING=开始执行初始化...\n\n# 错误消息\nERROR_INVALID_SELECTION=无效选择，使用默认语言\nERROR_LANG_FILE_NOT_FOUND=语言文件不存在\nERROR_LANG_FILE_NOT_FOUND_EN=Language file not found\n\n# 卸载相关\nTITLE_UNINSTALL=Sunshine 便携版卸载工具\nTITLE_UNINSTALL_PROCESS=开始卸载组件\nSTEP_UNINSTALL_VDD=卸载虚拟显示器\nSTEP_UNINSTALL_VSINK=卸载虚拟麦克风\nSTEP_UNINSTALL_FIREWALL=删除防火墙规则\nSTEP_UNINSTALL_GAMEPAD=卸载游戏手柄支持\nSTEP_UNINSTALL_AUTOSTART=删除开机自启动\nVDD_UNINSTALL_PROMPT=是否卸载虚拟显示器？(Y/N, 默认: Y)\nVSINK_UNINSTALL_PROMPT=是否卸载虚拟麦克风？(Y/N, 默认: Y)\nFIREWALL_UNINSTALL_PROMPT=是否删除防火墙规则？(Y/N, 默认: Y)\nGAMEPAD_UNINSTALL_PROMPT=是否卸载游戏手柄支持？(Y/N, 默认: Y)\nAUTOSTART_UNINSTALL_PROMPT=是否删除开机自启动？(Y/N, 默认: Y)\nINFO_VDD_UNINSTALL=卸载虚拟显示器...\nINFO_VSINK_UNINSTALL=卸载虚拟麦克风...\nINFO_FIREWALL_UNINSTALL=删除防火墙规则...\nINFO_GAMEPAD_UNINSTALL=卸载游戏手柄支持...\nINFO_AUTOSTART_UNINSTALL=删除开机自启动...\nSUCCESS_VDD_UNINSTALL=虚拟显示器卸载完成\nSUCCESS_VSINK_UNINSTALL=虚拟麦克风卸载完成\nSUCCESS_FIREWALL_UNINSTALL=防火墙规则删除完成\nSUCCESS_GAMEPAD_UNINSTALL=游戏手柄支持卸载完成\nSUCCESS_AUTOSTART_UNINSTALL=开机自启动删除完成\nWARNING_VDD_UNINSTALL=虚拟显示器卸载失败，但可以继续\nWARNING_VSINK_UNINSTALL=虚拟麦克风卸载失败，但可以继续\nWARNING_FIREWALL_UNINSTALL=防火墙规则删除失败，但可以继续\nWARNING_GAMEPAD_UNINSTALL=游戏手柄支持卸载失败，但可以继续\nWARNING_AUTOSTART_UNINSTALL=开机自启动删除失败，但可以继续\nINFO_VDD_SKIP=跳过虚拟显示器卸载\nINFO_VSINK_SKIP=跳过虚拟麦克风卸载\nINFO_FIREWALL_SKIP=跳过防火墙规则删除\nINFO_GAMEPAD_SKIP=跳过游戏手柄支持卸载\nINFO_AUTOSTART_SKIP=跳过开机自启动删除\nINFO_AUTOSTART_NOT_FOUND=开机自启动快捷方式不存在\nUNINSTALL_CONFIRM_TITLE=卸载确认\nUNINSTALL_CONFIRM_DESC=即将执行以下卸载操作\nUNINSTALL_CONFIRM_VDD=卸载虚拟显示器驱动\nUNINSTALL_CONFIRM_VSINK=卸载虚拟麦克风\nUNINSTALL_CONFIRM_FIREWALL=删除防火墙规则\nUNINSTALL_CONFIRM_GAMEPAD=卸载游戏手柄支持\nUNINSTALL_CONFIRM_AUTOSTART=删除开机自启动\nUNINSTALL_CONFIRM_PROCEED=是否继续执行卸载？(Y/N, 默认: Y)\nUNINSTALL_CANCEL=卸载操作已取消\nUNINSTALL_PROCEEDING=开始执行卸载...\nNOTE_UNINSTALL=注意：此操作不会删除便携版文件，如需完全删除请手动删除整个文件夹\n\n# 权限提权相关\nADMIN_ELEVATE_PROMPT=是否以管理员权限重新运行此脚本？\nADMIN_ELEVATE_CONFIRM=请选择 (Y/N, 默认: Y)\nADMIN_ELEVATING=正在以管理员权限重新启动脚本...\nADMIN_ELEVATE_CANCEL=已取消提权，脚本退出\n\n# 脚本完成提示\nSCRIPT_COMPLETE_PROMPT=脚本执行完成，按任意键退出\n"
  },
  {
    "path": "src_assets/windows/misc/migration/IMAGE_MIGRATION.md",
    "content": "# Image Path Migration Guide\n\n## 概述\n\n此迁移脚本会自动将存量的本地文件路径图片迁移到新的 `covers/` 目录，并更新 `apps.json` 中的引用。\n\n## 迁移逻辑\n\n### 1️⃣ 识别需要迁移的图片路径\n\n脚本会检查 `apps.json` 中每个应用的 `image-path` 字段：\n\n**跳过（无需迁移）：**\n- `desktop` - 使用桌面图片\n- `app_name_123.png` - 已经是 boxart 格式（仅文件名，无路径分隔符）\n\n**需要迁移：**\n- `C:\\Users\\...\\picture.png` - 绝对路径\n- `./images/game.jpg` - 相对路径\n- `covers/steam.png` - 旧的 covers 路径（已被第58行处理）\n\n### 2️⃣ 查找源文件\n\n脚本会按以下顺序查找图片文件：\n\n1. **绝对路径** - 直接检查路径是否存在\n2. **相对于 config 目录** - `config\\<image-path>`\n3. **相对于旧的 Sunshine 根目录** - `<parent>\\<image-path>`\n\n### 3️⃣ 复制到 covers 目录\n\n找到源文件后：\n\n```\n源文件: C:\\Users\\mohaha\\Pictures\\game.png\n应用名: Steam\n↓\n生成新文件名: app_Steam_1234567890.png\n↓\n复制到: config\\covers\\app_Steam_1234567890.png\n```\n\n### 4️⃣ 更新 apps.json\n\n```json\n// 迁移前\n{\n  \"name\": \"Steam\",\n  \"image-path\": \"C:\\\\Users\\\\mohaha\\\\Pictures\\\\game.png\"\n}\n\n// 迁移后\n{\n  \"name\": \"Steam\",\n  \"image-path\": \"app_Steam_1234567890.png\"\n}\n```\n\n## 工作流程\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│ 1. migrate-config.bat 执行                                   │\n│    - 迁移 covers/ 目录                                       │\n│    - 更新旧的 ./covers/ 路径为 ./config/covers/              │\n└─────────────────────────────────────────────────────────────┘\n                           ↓\n┌─────────────────────────────────────────────────────────────┐\n│ 2. 调用 migrate-images.ps1                                   │\n│    - 读取 apps.json                                         │\n│    - 查找每个应用的图片文件                                   │\n└─────────────────────────────────────────────────────────────┘\n                           ↓\n┌─────────────────────────────────────────────────────────────┐\n│ 3. 对每个图片文件：                                           │\n│    ✅ 复制到 config/covers/app_<name>_<timestamp>.png       │\n│    ✅ 更新 apps.json 中的 image-path                        │\n└─────────────────────────────────────────────────────────────┘\n                           ↓\n┌─────────────────────────────────────────────────────────────┐\n│ 4. Sunshine 服务启动                                         │\n│    ✅ getBoxArt 从 covers/ 目录加载图片                      │\n│    ✅ Web UI 通过 /boxart/<filename> 访问                   │\n└─────────────────────────────────────────────────────────────┘\n```\n\n## 示例场景\n\n### 场景 1: 绝对路径\n\n**迁移前：**\n```json\n{\n  \"apps\": [\n    {\n      \"name\": \"游戏A\",\n      \"image-path\": \"C:\\\\Users\\\\mohaha\\\\Pictures\\\\game1.jpg\"\n    }\n  ]\n}\n```\n\n**迁移后：**\n```json\n{\n  \"apps\": [\n    {\n      \"name\": \"游戏A\",\n      \"image-path\": \"app_游戏A_1234567890.jpg\"\n    }\n  ]\n}\n```\n\n**文件位置：**\n- 源: `C:\\Users\\mohaha\\Pictures\\game1.jpg`\n- 目标: `config\\covers\\app_游戏A_1234567890.jpg`\n\n### 场景 2: 相对路径\n\n**迁移前：**\n```json\n{\n  \"apps\": [\n    {\n      \"name\": \"Steam\",\n      \"image-path\": \"./assets/steam-icon.png\"\n    }\n  ]\n}\n```\n\n**迁移后：**\n```json\n{\n  \"apps\": [\n    {\n      \"name\": \"Steam\",\n      \"image-path\": \"app_Steam_1234567891.png\"\n    }\n  ]\n}\n```\n\n**查找顺序：**\n1. ❌ `./assets/steam-icon.png` (绝对路径)\n2. ✅ `config\\assets\\steam-icon.png` (相对于 config)\n3. (找到，停止查找)\n\n### 场景 3: 已经是新格式（跳过）\n\n**apps.json：**\n```json\n{\n  \"apps\": [\n    {\n      \"name\": \"Desktop\",\n      \"image-path\": \"desktop\"\n    },\n    {\n      \"name\": \"Chrome\",\n      \"image-path\": \"app_Chrome_1234567892.png\"\n    }\n  ]\n}\n```\n\n**结果：** 两个应用都跳过，不进行迁移\n\n## 错误处理\n\n### 文件不存在\n```\n⚠️  Image file not found for 游戏B: C:\\old\\path\\missing.png\n```\n- **行为**: 保留原始路径，不修改 apps.json\n- **原因**: 可能是网络路径或将来会可用\n\n### 复制失败\n```\n❌ Failed to copy image for 游戏C: Access denied\n```\n- **行为**: 保留原始路径，继续处理其他应用\n- **原因**: 权限问题或磁盘空间不足\n\n## 兼容性\n\n### 与新的 getBoxArt 配合\n\n```cpp\n// src/confighttp.cpp - getBoxArt 函数\nstd::string imagePath = SUNSHINE_ASSETS_DIR \"boxart/\" + path;\n\n// 如果在 boxart/ 未找到，尝试 covers/\nif (!fs::exists(imagePath)) {\n  std::string coversPath = platf::appdata().string() + \"/covers/\" + path;\n  if (fs::exists(coversPath)) {\n    imagePath = coversPath;  // ✅ 迁移的图片在这里\n  }\n}\n```\n\n### 与 Web UI 配合\n\n```javascript\n// getImagePreviewUrl 函数\nif (imagePath.includes('/') || imagePath.includes('\\\\')) {\n  return imagePath;  // 旧的路径格式（迁移前）\n} else {\n  return `/boxart/${imagePath}`;  // ✅ 新格式（迁移后）\n}\n```\n\n## 测试迁移\n\n### 手动测试\n\n1. 创建测试 apps.json：\n```json\n{\n  \"apps\": [\n    {\n      \"name\": \"Test\",\n      \"image-path\": \"C:\\\\test\\\\image.png\"\n    }\n  ]\n}\n```\n\n2. 运行迁移：\n```cmd\npowershell -ExecutionPolicy Bypass -File migrate-images.ps1 \"C:\\path\\to\\config\"\n```\n\n3. 验证结果：\n- ✅ 文件已复制到 `config\\covers\\app_Test_*.png`\n- ✅ apps.json 已更新为新路径\n- ✅ Sunshine Web UI 能正常显示图片\n\n## 日志输出示例\n\n```\nReading apps.json from: C:\\Sunshine\\config\\apps.json\nCreated covers directory: C:\\Sunshine\\config\\covers\nFound image file: C:\\Users\\mohaha\\Pictures\\game.png\n✅ Migrated: Steam\n   From: C:\\Users\\mohaha\\Pictures\\game.png\n   To:   C:\\Sunshine\\config\\covers\\app_Steam_1234567890.png\n   New path: app_Steam_1234567890.png\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n✅ Successfully migrated 1 image(s)\n   Updated: C:\\Sunshine\\config\\apps.json\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n```\n\n## 注意事项\n\n1. **备份**: 迁移前会自动备份（通过 Copy-Item，原文件不会删除）\n2. **权限**: 需要对 config 目录有写入权限\n3. **文件名**: 特殊字符会被替换为下划线（`[^a-zA-Z0-9_-]` → `_`）\n4. **时间戳**: 使用 Unix 时间戳确保文件名唯一\n5. **编码**: apps.json 使用 UTF-8 编码保存\n\n\n\n\n"
  },
  {
    "path": "src_assets/windows/misc/migration/migrate-config.bat",
    "content": "@echo off\n\nrem Get sunshine root directory\nfor %%I in (\"%~dp0\\..\") do set \"OLD_DIR=%%~fI\"\n\nrem Create the config directory if it didn't already exist\nset \"NEW_DIR=%OLD_DIR%\\config\"\nif not exist \"%NEW_DIR%\\\" mkdir \"%NEW_DIR%\"\nicacls \"%NEW_DIR%\" /reset\n\nrem Migrate all files that aren't already present in the config dir\nif exist \"%OLD_DIR%\\apps.json\" (\n    if not exist \"%NEW_DIR%\\apps.json\" (\n        move \"%OLD_DIR%\\apps.json\" \"%NEW_DIR%\\apps.json\"\n        icacls \"%NEW_DIR%\\apps.json\" /reset\n    )\n)\nif exist \"%OLD_DIR%\\sunshine.conf\" (\n    if not exist \"%NEW_DIR%\\sunshine.conf\" (\n        move \"%OLD_DIR%\\sunshine.conf\" \"%NEW_DIR%\\sunshine.conf\"\n        icacls \"%NEW_DIR%\\sunshine.conf\" /reset\n    )\n)\nif exist \"%OLD_DIR%\\sunshine_state.json\" (\n    if not exist \"%NEW_DIR%\\sunshine_state.json\" (\n        move \"%OLD_DIR%\\sunshine_state.json\" \"%NEW_DIR%\\sunshine_state.json\"\n        icacls \"%NEW_DIR%\\sunshine_state.json\" /reset\n    )\n)\n\nrem remove the original_display_settings.json file\nif exist \"%NEW_DIR%\\original_display_settings.json\" (\n    del \"%NEW_DIR%\\original_display_settings.json\"\n)\n\nrem Migrate the credentials directory\nif exist \"%OLD_DIR%\\credentials\\\" (\n    if not exist \"%NEW_DIR%\\credentials\\\" (\n        move \"%OLD_DIR%\\credentials\" \"%NEW_DIR%\\\"\n    )\n)\n\nrem Create the credentials directory if it wasn't migrated or already existing\nif not exist \"%NEW_DIR%\\credentials\\\" mkdir \"%NEW_DIR%\\credentials\"\n\nrem Disallow read access to the credentials directory contents for normal users\nrem Note: We must use the SIDs directly because \"Users\" and \"Administrators\" are localized\nicacls \"%NEW_DIR%\\credentials\" /inheritance:r\nicacls \"%NEW_DIR%\\credentials\" /grant:r *S-1-5-32-544:(OI)(CI)(F)\nicacls \"%NEW_DIR%\\credentials\" /grant:r *S-1-5-32-545:(R)\n\nrem Migrate the covers directory\nif exist \"%OLD_DIR%\\covers\\\" (\n    if not exist \"%NEW_DIR%\\covers\\\" (\n        move \"%OLD_DIR%\\covers\" \"%NEW_DIR%\\\"\n\n        rem Fix apps.json image path values that point at the old covers directory\n        powershell -c \"(Get-Content '%NEW_DIR%\\apps.json').replace('.\\/covers\\/', '.\\/config\\/covers\\/') | Set-Content '%NEW_DIR%\\apps.json'\"\n    )\n)\n\nrem Create covers directory if it doesn't exist\nif not exist \"%NEW_DIR%\\covers\\\" mkdir \"%NEW_DIR%\\covers\"\n\nrem Migrate local image files to covers directory and update apps.json\nif exist \"%NEW_DIR%\\apps.json\" (\n    echo Migrating local image files to covers directory...\n    powershell -ExecutionPolicy Bypass -File \"%~dp0migrate-images.ps1\" \"%NEW_DIR%\"\n)\n\nrem Remove log files\ndel \"%OLD_DIR%\\*.txt\"\ndel \"%OLD_DIR%\\*.log\"\n"
  },
  {
    "path": "src_assets/windows/misc/migration/migrate-images.ps1",
    "content": "# Migrate local image files to covers directory\n# Usage: migrate-images.ps1 <config_dir>\n\nparam(\n    [Parameter(Mandatory=$true)]\n    [string]$ConfigDir\n)\n\n$appsJsonPath = Join-Path $ConfigDir \"apps.json\"\n$coversDir = Join-Path $ConfigDir \"covers\"\n\n# Check if apps.json exists\nif (-not (Test-Path $appsJsonPath)) {\n    Write-Host \"apps.json not found at: $appsJsonPath\"\n    exit 0\n}\n\nWrite-Host \"Reading apps.json from: $appsJsonPath\"\n\n# Read and parse apps.json\ntry {\n    $appsJson = Get-Content $appsJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json\n} catch {\n    Write-Host \"Error parsing apps.json: $_\"\n    exit 1\n}\n\n# Ensure covers directory exists\nif (-not (Test-Path $coversDir)) {\n    New-Item -ItemType Directory -Path $coversDir -Force | Out-Null\n    Write-Host \"Created covers directory: $coversDir\"\n}\n\n$modified = $false\n$migratedCount = 0\n\n# Process each app\nif ($appsJson.apps) {\n    foreach ($app in $appsJson.apps) {\n        if (-not $app.'image-path') {\n            continue\n        }\n\n        $imagePath = $app.'image-path'\n        \n        # Skip if already using desktop or boxart path\n        if ($imagePath -eq 'desktop' -or $imagePath -match '^[^/\\\\]+\\.png$') {\n            Write-Host \"Skipping already migrated: $($app.name) -> $imagePath\"\n            continue\n        }\n\n        # Check if it's an absolute path or relative path pointing to a real file\n        $sourceFile = $null\n        \n        # Try as absolute path\n        if (Test-Path $imagePath) {\n            $sourceFile = $imagePath\n        }\n        # Try relative to config dir\n        elseif (Test-Path (Join-Path $ConfigDir $imagePath)) {\n            $sourceFile = Join-Path $ConfigDir $imagePath\n        }\n        # Try relative to old Sunshine root (parent of config)\n        elseif (Test-Path (Join-Path (Split-Path $ConfigDir -Parent) $imagePath)) {\n            $sourceFile = Join-Path (Split-Path $ConfigDir -Parent) $imagePath\n        }\n\n        if ($sourceFile -and (Test-Path $sourceFile)) {\n            Write-Host \"Found image file: $sourceFile\"\n            \n            # Generate new filename with hash to avoid name conflicts\n            # Use MD5 hash of the original filename to ensure uniqueness\n            $originalFileName = [System.IO.Path]::GetFileNameWithoutExtension($sourceFile)\n            $md5 = [System.Security.Cryptography.MD5]::Create()\n            $hashBytes = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($originalFileName))\n            $hash = [System.BitConverter]::ToString($hashBytes).Replace(\"-\", \"\").Substring(0, 8).ToLower()\n            \n            $timestamp = [DateTimeOffset]::Now.ToUnixTimeSeconds()\n            $extension = [System.IO.Path]::GetExtension($sourceFile)\n            if (-not $extension) {\n                $extension = \".png\"\n            }\n            \n            # Use hash instead of sanitized name to preserve uniqueness\n            $newFileName = \"app_${hash}_${timestamp}${extension}\"\n            $destinationPath = Join-Path $coversDir $newFileName\n            \n            # Copy file to covers directory\n            try {\n                Copy-Item -Path $sourceFile -Destination $destinationPath -Force\n                Write-Host \"✅ Migrated: $($app.name)\"\n                Write-Host \"   From: $sourceFile\"\n                Write-Host \"   To:   $destinationPath\"\n                Write-Host \"   New path: $newFileName\"\n                \n                # Update apps.json with new path (just the filename, no directory)\n                $app.'image-path' = $newFileName\n                $modified = $true\n                $migratedCount++\n            } catch {\n                Write-Host \"❌ Failed to copy image for $($app.name): $_\"\n            }\n        } else {\n            Write-Host \"⚠️  Image file not found for $($app.name): $imagePath\"\n            # Keep the original path in case it's a network path or will be available later\n        }\n    }\n}\n\n# Save updated apps.json if modified\nif ($modified) {\n    try {\n        $jsonOutput = $appsJson | ConvertTo-Json -Depth 10 -Compress:$false\n        $jsonOutput | Set-Content -Path $appsJsonPath -Encoding UTF8 -NoNewline\n        Write-Host \"\"\n        Write-Host \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n        Write-Host \"✅ Successfully migrated $migratedCount image(s)\"\n        Write-Host \"   Updated: $appsJsonPath\"\n        Write-Host \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n    } catch {\n        Write-Host \"❌ Error saving apps.json: $_\"\n        exit 1\n    }\n} else {\n    Write-Host \"No images to migrate.\"\n}\n\nexit 0\n"
  },
  {
    "path": "src_assets/windows/misc/path/update-path.bat",
    "content": "@echo off\nsetlocal EnableDelayedExpansion\n\nrem Check if parameter is provided\nif \"%~1\"==\"\" (\n    echo Usage: %0 [add^|remove]\n    echo   add    - Adds Sunshine directories to system PATH\n    echo   remove - Removes Sunshine directories from system PATH\n    exit /b 1\n)\n\nrem Get sunshine root directory\nfor %%I in (\"%~dp0\\..\") do set \"ROOT_DIR=%%~fI\"\necho Sunshine root directory: !ROOT_DIR!\n\nrem Define directories to add to path\nset \"PATHS_TO_MANAGE[0]=!ROOT_DIR!\"\nset \"PATHS_TO_MANAGE[1]=!ROOT_DIR!\\tools\"\n\nrem System path registry location\nset \"KEY_NAME=HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\"\nset \"VALUE_NAME=Path\"\n\nrem Get the current path\nfor /f \"tokens=2*\" %%A in ('reg query \"%KEY_NAME%\" /v \"%VALUE_NAME%\"') do set \"CURRENT_PATH=%%B\"\necho Current path: !CURRENT_PATH!\n\nrem Check if adding to path\nif /i \"%~1\"==\"add\" (\n    set \"NEW_PATH=!CURRENT_PATH!\"\n\n    rem Process each directory to add\n    for /L %%i in (0,1,1) do (\n        set \"DIR_TO_ADD=!PATHS_TO_MANAGE[%%i]!\"\n\n        rem Check if path already contains this directory\n        echo \"!CURRENT_PATH!\" | findstr /i /c:\"!DIR_TO_ADD!\" > nul\n        if !ERRORLEVEL!==0 (\n            echo !DIR_TO_ADD! already in path\n        ) else (\n            echo Adding to path: !DIR_TO_ADD!\n            set \"NEW_PATH=!NEW_PATH!;!DIR_TO_ADD!\"\n        )\n    )\n\n    rem Only update if path was changed\n    if \"!NEW_PATH!\" neq \"!CURRENT_PATH!\" (\n        rem Set the new path in the registry\n        reg add \"%KEY_NAME%\" /v \"%VALUE_NAME%\" /t REG_EXPAND_SZ /d \"!NEW_PATH!\" /f\n        if !ERRORLEVEL!==0 (\n            echo Successfully added Sunshine directories to PATH\n        ) else (\n            echo Failed to add Sunshine directories to PATH\n        )\n    ) else (\n        echo No changes needed to PATH\n    )\n    exit /b !ERRORLEVEL!\n)\n\nrem Check if removing from path\nif /i \"%~1\"==\"remove\" (\n    set \"CHANGES_MADE=0\"\n\n    rem Process each directory to remove\n    for /L %%i in (0,1,1) do (\n        set \"DIR_TO_REMOVE=!PATHS_TO_MANAGE[%%i]!\"\n\n        rem Check if path contains this directory\n        echo \"!CURRENT_PATH!\" | findstr /i /c:\"!DIR_TO_REMOVE!\" > nul\n        if !ERRORLEVEL!==0 (\n            echo Removing from path: !DIR_TO_REMOVE!\n\n            rem Build a new path by parsing and filtering the current path\n            set \"NEW_PATH=\"\n            for %%p in (\"!CURRENT_PATH:;=\" \"!\") do (\n                set \"PART=%%~p\"\n                if /i \"!PART!\" NEQ \"!DIR_TO_REMOVE!\" (\n                    if defined NEW_PATH (\n                        set \"NEW_PATH=!NEW_PATH!;!PART!\"\n                    ) else (\n                        set \"NEW_PATH=!PART!\"\n                    )\n                )\n            )\n\n            set \"CURRENT_PATH=!NEW_PATH!\"\n            set \"CHANGES_MADE=1\"\n        ) else (\n            echo !DIR_TO_REMOVE! not found in path\n        )\n    )\n\n    rem Only update if path was changed\n    if \"!CHANGES_MADE!\"==\"1\" (\n        rem Set the new path in the registry\n        reg add \"%KEY_NAME%\" /v \"%VALUE_NAME%\" /t REG_EXPAND_SZ /d \"!CURRENT_PATH!\" /f\n        if !ERRORLEVEL!==0 (\n            echo Successfully removed Sunshine directories from PATH\n        ) else (\n            echo Failed to remove Sunshine directories from PATH\n        )\n    ) else (\n        echo No changes needed to PATH\n    )\n    exit /b !ERRORLEVEL!\n)\n\necho Unknown parameter: %~1\necho Usage: %0 [add^|remove]\nexit /b 1"
  },
  {
    "path": "src_assets/windows/misc/service/install-service.bat",
    "content": "@echo off\nsetlocal enabledelayedexpansion\n\nrem Get sunshine root directory\nfor %%I in (\"%~dp0\\..\") do set \"ROOT_DIR=%%~fI\"\n\nset SERVICE_NAME=SunshineService\nset \"SERVICE_BIN=%ROOT_DIR%\\tools\\sunshinesvc.exe\"\nset \"SERVICE_CONFIG_DIR=%LOCALAPPDATA%\\LizardByte\\Sunshine\"\nset \"SERVICE_CONFIG_FILE=%SERVICE_CONFIG_DIR%\\service_start_type.txt\"\n\nrem Set service to demand start. It will be changed to auto later if the user selected that option.\nset SERVICE_START_TYPE=demand\n\nrem Remove the legacy SunshineSvc service\nnet stop sunshinesvc\nsc delete sunshinesvc\n\nrem Check if SunshineService already exists\nsc qc %SERVICE_NAME% > nul 2>&1\nif %ERRORLEVEL%==0 (\n    rem Stop the existing service if running\n    net stop %SERVICE_NAME%\n\n    rem Reconfigure the existing service\n    set SC_CMD=config\n) else (\n    rem Create a new service\n    set SC_CMD=create\n)\n\nrem Check if we have a saved start type from previous installation\nif exist \"%SERVICE_CONFIG_FILE%\" (\n    rem Debug output file content\n    type \"%SERVICE_CONFIG_FILE%\"\n\n    rem Read the saved start type\n    for /f \"usebackq delims=\" %%a in (\"%SERVICE_CONFIG_FILE%\") do (\n        set \"SAVED_START_TYPE=%%a\"\n    )\n\n    echo Raw saved start type: [!SAVED_START_TYPE!]\n\n    rem Check start type\n    if \"!SAVED_START_TYPE!\"==\"2-delayed\" (\n        set SERVICE_START_TYPE=delayed-auto\n    ) else if \"!SAVED_START_TYPE!\"==\"2\" (\n        set SERVICE_START_TYPE=auto\n    ) else if \"!SAVED_START_TYPE!\"==\"3\" (\n        set SERVICE_START_TYPE=demand\n    ) else if \"!SAVED_START_TYPE!\"==\"4\" (\n        set SERVICE_START_TYPE=disabled\n    )\n\n    del \"%SERVICE_CONFIG_FILE%\"\n)\n\necho Setting service start type set to: [!SERVICE_START_TYPE!]\n\nrem Run the sc command to create/reconfigure the service\nsc %SC_CMD% %SERVICE_NAME% binPath= \"%SERVICE_BIN%\" start= %SERVICE_START_TYPE% DisplayName= \"Sunshine Service\"\n\nrem Set the description of the service\nsc description %SERVICE_NAME% \"Sunshine is a self-hosted game stream host for Moonlight.\"\n\nrem Start the new service\nnet start %SERVICE_NAME%\n\nrem Determine the Web UI port from config (default base port 47989 + 1 = 47990)\nset /a WEB_PORT=47990\nset \"SUNSHINE_CONF=%ROOT_DIR%\\config\\sunshine.conf\"\nif exist \"%SUNSHINE_CONF%\" (\n    for /f \"usebackq tokens=1,* delims==\" %%a in (\"%SUNSHINE_CONF%\") do (\n        set \"KEY=%%a\"\n        set \"VAL=%%b\"\n        rem Trim spaces from key\n        for /f \"tokens=* delims= \" %%k in (\"!KEY!\") do set \"KEY=%%k\"\n        if \"!KEY!\"==\"port\" (\n            for /f \"tokens=* delims= \" %%v in (\"!VAL!\") do set \"VAL=%%v\"\n            set /a WEB_PORT=!VAL!+1\n        )\n    )\n)\n\nrem Wait for Sunshine API to be ready\necho Waiting for Sunshine API on port !WEB_PORT!...\nset /a WAIT_COUNT=0\nset /a WAIT_MAX=15\n:wait_loop\nif !WAIT_COUNT! GEQ !WAIT_MAX! (\n    echo Sunshine API did not become ready within %WAIT_MAX% seconds, continuing anyway...\n    goto :wait_done\n)\npowershell -NoProfile -Command \"try { $c = [System.Net.Sockets.TcpClient]::new(); $c.Connect('127.0.0.1', !WEB_PORT!); $c.Close(); exit 0 } catch { exit 1 }\" >nul 2>&1\nif !ERRORLEVEL!==0 (\n    echo Sunshine API is ready on port !WEB_PORT!.\n    goto :wait_done\n)\nset /a WAIT_COUNT+=1\ntimeout /t 1 /nobreak >nul\ngoto :wait_loop\n:wait_done\n"
  },
  {
    "path": "src_assets/windows/misc/service/sleep.bat",
    "content": "rundll32.exe powrprof.dll,SetSuspendState 0,1,0"
  },
  {
    "path": "src_assets/windows/misc/service/uninstall-service.bat",
    "content": "@echo off\nsetlocal enabledelayedexpansion\n\nset \"SERVICE_CONFIG_DIR=%LOCALAPPDATA%\\LizardByte\\Sunshine\"\nset \"SERVICE_CONFIG_FILE=%SERVICE_CONFIG_DIR%\\service_start_type.txt\"\n\nrem Save the current service start type to a file if the service exists\nsc qc SunshineService >nul 2>&1\nif %ERRORLEVEL%==0 (\n    if not exist \"%SERVICE_CONFIG_DIR%\\\" mkdir \"%SERVICE_CONFIG_DIR%\\\"\n\n    rem Get the start type\n    for /f \"tokens=3\" %%i in ('sc qc SunshineService ^| findstr /C:\"START_TYPE\"') do (\n        set \"CURRENT_START_TYPE=%%i\"\n    )\n\n    rem Set the content to write\n    if \"!CURRENT_START_TYPE!\"==\"2\" (\n        sc qc SunshineService | findstr /C:\"(DELAYED)\" >nul\n        if !ERRORLEVEL!==0 (\n            set \"CONTENT=2-delayed\"\n        ) else (\n            set \"CONTENT=2\"\n        )\n    ) else if \"!CURRENT_START_TYPE!\" NEQ \"\" (\n        set \"CONTENT=!CURRENT_START_TYPE!\"\n    ) else (\n        set \"CONTENT=unknown\"\n    )\n\n    rem Write content to file\n    echo !CONTENT!> \"%SERVICE_CONFIG_FILE%\"\n)\n\nrem Stop and delete the legacy SunshineSvc service\nnet stop sunshinesvc\nsc delete sunshinesvc\n\nrem Stop and delete the new SunshineService service\nnet stop SunshineService\nsc delete SunshineService\n"
  },
  {
    "path": "src_assets/windows/misc/uninstall_portable.bat",
    "content": "@echo off\nchcp 65001 >nul\nsetlocal enabledelayedexpansion\ncolor 0F\n\nREM ================================================\nREM Initialize environment variables\nREM ================================================\n\nREM Get script directory path (scripts folder)\nset \"SCRIPT_DIR=%~dp0scripts\"\nREM Set language file directory path\nset \"LANG_DIR=%SCRIPT_DIR%\\languages\"\n\nREM ================================================\nREM Multi-language support initialization\nREM ================================================\n\nREM Detect system language and select corresponding language file\ncall :DetectLanguage\nREM Load selected language file\ncall :LoadLanguageFile \"%SELECTED_LANG%\"\n\necho.\necho \"================================================\"\necho \"           %TITLE%\"\necho \"================================================\"\necho.\n\nREM Check if running with administrator privileges\nnet session >nul 2>&1\nif !errorLevel! neq 0 (\n    call :LogError \"%ERROR_ADMIN%\"\n    echo %ERROR_ADMIN_DESC%\n    echo.\n    echo %ADMIN_ELEVATE_PROMPT%\n    set \"elevate_choice=Y\"\n    set /p elevate_choice=\"%ADMIN_ELEVATE_CONFIRM% \"\n    if /i \"!elevate_choice!\"==\"Y\" (\n        echo %ADMIN_ELEVATING%...\n        REM Use PowerShell to elevate and restart script\n        powershell -Command \"Start-Process '%~f0' -Verb RunAs\"\n        pause\n        exit /b 0\n    ) else (\n        echo %ADMIN_ELEVATE_CANCEL%\n        echo.\n        pause\n        exit /b 1\n    )\n)\n\ncall :LogInfo \"%INFO_ADMIN%\"\necho.\n\nREM ================================================\nREM Environment settings\nREM ================================================\n\nREM Set Sunshine root directory path (current directory)\nset \"SUNSHINE_ROOT=%~dp0\"\n\nREM ================================================\nREM User confirmation\nREM ================================================\n\nREM Display list of operations to be performed and wait for user confirmation\ncall :ShowUninstallConfirmation\nif \"%CONFIRM_RESULT%\" neq \"1\" (\n    echo %UNINSTALL_CANCEL%\n    echo.\n    pause\n    exit /b 0\n)\n\necho %UNINSTALL_PROCEEDING%\necho.\n\nREM ================================================\nREM Uninstall process\nREM ================================================\n\necho \"================================================\"\necho \"           %TITLE_UNINSTALL%\"\necho \"================================================\"\necho.\n\nREM Step 1 - Uninstall virtual display driver (VDD)\ncall :LogStep \"1/5\" \"%STEP_UNINSTALL_VDD%...\"\nset \"vdd_choice=Y\"\nset /p vdd_choice=\"%VDD_UNINSTALL_PROMPT% \"\nif /i \"!vdd_choice!\"==\"Y\" (\n    call :LogInfo \"%INFO_VDD_UNINSTALL%...\"\n    REM Call virtual display uninstall script\n    call \"%SCRIPT_DIR%\\uninstall-vdd.bat\"\n) else (\n    call :LogInfo \"%INFO_VDD_SKIP%\"\n)\necho.\n\nREM Step 2 - Uninstall virtual microphone (VB-Cable)\ncall :LogStep \"2/5\" \"%STEP_UNINSTALL_VSINK%...\"\nset \"vsink_choice=Y\"\nset /p vsink_choice=\"%VSINK_UNINSTALL_PROMPT% \"\nif /i \"!vsink_choice!\"==\"Y\" (\n    call :LogInfo \"%INFO_VSINK_UNINSTALL%...\"\n    REM Call virtual microphone uninstall script\n    call \"%SCRIPT_DIR%\\uninstall-vsink.bat\"\n) else (\n    call :LogInfo \"%INFO_VSINK_SKIP%\"\n)\necho.\n\nREM Step 3 - Delete firewall rules\ncall :LogStep \"3/5\" \"%STEP_UNINSTALL_FIREWALL%...\"\nset \"firewall_choice=Y\"\nset /p firewall_choice=\"%FIREWALL_UNINSTALL_PROMPT% \"\nif /i \"!firewall_choice!\"==\"Y\" (\n    call :LogInfo \"%INFO_FIREWALL_UNINSTALL%...\"\n    call \"%SCRIPT_DIR%\\delete-firewall-rule.bat\"\n) else (\n    call :LogInfo \"%INFO_FIREWALL_SKIP%\"\n)\necho.\n\nREM Step 4 - Uninstall gamepad support\ncall :LogStep \"4/5\" \"%STEP_UNINSTALL_GAMEPAD%...\"\nset \"gamepad_choice=Y\"\nset /p gamepad_choice=\"%GAMEPAD_UNINSTALL_PROMPT% \"\nif /i \"!gamepad_choice!\"==\"Y\" (\n    call :LogInfo \"%INFO_GAMEPAD_UNINSTALL%...\"\n    REM Call gamepad uninstallation script\n    call \"%SCRIPT_DIR%\\uninstall-gamepad.bat\"\n    if !errorLevel! equ 0 (\n        call :LogSuccess \"%SUCCESS_GAMEPAD_UNINSTALL%\"\n    ) else (\n        call :LogWarning \"%WARNING_GAMEPAD_UNINSTALL%\"\n    )\n) else (\n    call :LogInfo \"%INFO_GAMEPAD_SKIP%\"\n)\necho.\n\nREM Step 5 - Delete autostart\ncall :LogStep \"5/5\" \"%STEP_UNINSTALL_AUTOSTART%...\"\nset \"autostart_choice=Y\"\nset /p autostart_choice=\"%AUTOSTART_UNINSTALL_PROMPT% \"\nif /i \"!autostart_choice!\"==\"Y\" (\n    call :LogInfo \"%INFO_AUTOSTART_UNINSTALL%...\"\n    set \"SHORTCUT_PATH=%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\Sunshine_portable.lnk\"\n    if exist \"!SHORTCUT_PATH!\" (\n        del \"!SHORTCUT_PATH!\"\n        if !errorLevel! equ 0 (\n            call :LogSuccess \"%SUCCESS_AUTOSTART_UNINSTALL%\"\n        ) else (\n            call :LogWarning \"%WARNING_AUTOSTART_UNINSTALL%\"\n        )\n    ) else (\n        call :LogInfo \"%INFO_AUTOSTART_NOT_FOUND%\"\n    )\n) else (\n    call :LogInfo \"%INFO_AUTOSTART_SKIP%\"\n)\necho.\n\nREM ================================================\nREM Completion message\nREM ================================================\n\necho ================================================\necho           %TITLE_SUCCESS%\necho ================================================\necho.\ncall :LogSuccess \"%SUCCESS_MAIN%\"\necho.\necho %NOTE_UNINSTALL%\necho.\necho %SCRIPT_COMPLETE_PROMPT%\npause\nexit /b 0\n\nREM ================================================\nREM Function definition area\nREM ================================================\n\n:LogInfo\nREM Function - Output info level log (white)\ncls\necho [信息] %~1\ngoto :eof\n\n:LogSuccess\nREM Function - Output success level log (green)\ncls\npowershell -c \"Write-Host '[成功] %~1' -ForegroundColor Green\"\ngoto :eof\n\n:LogWarning\nREM Function - Output warning level log (yellow)\ncls\npowershell -c \"Write-Host '[警告] %~1' -ForegroundColor Yellow\"\ngoto :eof\n\n:LogError\nREM Function - Output error level log (red)\ncls\npowershell -c \"Write-Host '[错误] %~1' -ForegroundColor Red\"\ngoto :eof\n\n:LogStep\nREM Function - Output step level log (cyan)\nREM Parameters - %%1 - Step number, %%2 - Step description\nREM Returns - None\ncls\npowershell -c \"Write-Host '[%~1] %~2' -ForegroundColor Cyan\"\ngoto :eof\n\n\n:DetectLanguage\nREM Function - Detect system language and select corresponding language file\nREM Parameters - None\nREM Returns - Set SELECTED_LANG variable\ncls\n\nREM Get system language code\nREM Use PowerShell to get system language for better compatibility\nfor /f \"tokens=*\" %%i in ('powershell -c \"[System.Globalization.CultureInfo]::CurrentCulture.Name\" 2^>nul') do set \"LOCALE=%%i\"\n\nREM If PowerShell fails, use default language\nif \"%LOCALE%\"==\"\" set \"LOCALE=en-US\"\n\nREM Select corresponding language file based on system language code\nif \"%LOCALE:~0,2%\"==\"zh\" (\n    set \"SELECTED_LANG=zh-Simple\"\n    set \"SELECTED_LANG_DISPLAY=简体中文\"\n) else if \"%LOCALE:~0,2%\"==\"de\" (\n    set \"SELECTED_LANG=de-DE\"\n    set \"SELECTED_LANG_DISPLAY=Deutsch\"\n) else if \"%LOCALE:~0,2%\"==\"fr\" (\n    set \"SELECTED_LANG=fr-FR\"\n    set \"SELECTED_LANG_DISPLAY=Français\"\n) else if \"%LOCALE:~0,2%\"==\"ja\" (\n    set \"SELECTED_LANG=ja-JP\"\n    set \"SELECTED_LANG_DISPLAY=日本語\"\n) else (\n    REM Default to English\n    set \"SELECTED_LANG=en-US\"\n    set \"SELECTED_LANG_DISPLAY=English\"\n)\ngoto :eof\n\n:LoadLanguageFile\nREM Function - Load specified language file\nREM Parameters - %%1 = Language code (e.g. zh-CN, en-US)\nREM Returns - Set all language variables\ncls\n\nREM Build language file path\nset \"LANG_FILE=%LANG_DIR%\\%~1.lang\"\n\nREM Check if language file exists\nif not exist \"%LANG_FILE%\" (\n    echo [ERROR] Language file not found: %LANG_FILE%\n    echo [ERROR] Language file does not exist: %LANG_FILE%\n    pause\n    exit /b 1\n)\n\nREM Parse language file and set variables\nfor /f \"usebackq tokens=1* delims==\" %%a in (\"%LANG_FILE%\") do (\n    if not \"%%a\"==\"\" if not \"%%a:~0,1%\"==\"#\" if not \"%%a:~0,2%\"==\"::\" (\n        if not \"%%b\"==\"\" (\n            set \"%%a=%%b\"\n        )\n    )\n) 2>nul\n\nREM Check if this is the first time loading language (show confirmation)\nif not defined LANG_CONFIRMED (\n    call :LogInfo \"%LANG_DETECTED%: !SELECTED_LANG_DISPLAY!\"\n    call :LogInfo \"%LANG_CONFIRM%\"\n\n    set \"lang_confirm_input=Y\"\n    set /p lang_confirm_input=\"%LANG_CONFIRM_PROMPT% \"\n    if /i \"!lang_confirm_input!\"==\"N\" (\n        call :ShowLanguageSelection\n    ) else (\n        set \"LANG_CONFIRMED=1\"\n    )\n)\ngoto :eof\n\n:ShowLanguageSelection\nREM Function - Display language selection menu\nREM Parameters - None\nREM Returns - Reload selected language file\ncls\n\necho.\necho %LANG_SELECT%:\necho 1. %LANG_ZH_SIMPLE% (简体中文)\necho 2. %LANG_EN% (English)\necho 3. %LANG_DE% (Deutsch)\necho 4. %LANG_FR% (Français)\necho 5. %LANG_JA% (日本語)\necho 6. %LANG_RU% (Русский)\necho.\nset \"lang_choice=\"\nset /p lang_choice=\"%LANG_SELECT_PROMPT% \"\n\nREM Set language code based on user selection\nif \"!lang_choice!\"==\"1\" (\n    set \"SELECTED_LANG=zh-Simple\"\n    set \"SELECTED_LANG_DISPLAY=简体中文\"\n) else if \"!lang_choice!\"==\"2\" (\n    set \"SELECTED_LANG=en-US\"\n    set \"SELECTED_LANG_DISPLAY=English\"\n) else if \"!lang_choice!\"==\"3\" (\n    set \"SELECTED_LANG=de-DE\"\n    set \"SELECTED_LANG_DISPLAY=Deutsch\"\n) else if \"!lang_choice!\"==\"4\" (\n    set \"SELECTED_LANG=fr-FR\"\n    set \"SELECTED_LANG_DISPLAY=Français\"\n) else if \"!lang_choice!\"==\"5\" (\n    set \"SELECTED_LANG=ja-JP\"\n    set \"SELECTED_LANG_DISPLAY=日本語\"\n) else if \"!lang_choice!\"==\"6\" (\n    set \"SELECTED_LANG=ru-RU\"\n    set \"SELECTED_LANG_DISPLAY=Русский\"\n) else (\n    call :LogError \"%ERROR_INVALID_SELECTION%\"\n    set \"SELECTED_LANG=en-US\"\n    set \"SELECTED_LANG_DISPLAY=English\"\n)\n\nREM Reload selected language file\ncall :LoadLanguageFile \"%SELECTED_LANG%\"\ngoto :eof\n\n:ShowUninstallConfirmation\nREM Function - Display uninstall confirmation interface\nREM Parameters - None\nREM Returns - Set CONFIRM_RESULT variable (1=confirm, 0=cancel)\ncls\n\necho ================================================\necho           %UNINSTALL_CONFIRM_TITLE%\necho ================================================\necho.\necho %UNINSTALL_CONFIRM_DESC%:\necho.\necho 1. %UNINSTALL_CONFIRM_VDD%\necho 2. %UNINSTALL_CONFIRM_VSINK%\necho 3. %UNINSTALL_CONFIRM_FIREWALL%\necho 4. %UNINSTALL_CONFIRM_GAMEPAD%\necho 5. %UNINSTALL_CONFIRM_AUTOSTART%\necho.\nset \"confirm_choice=Y\"\nset /p confirm_choice=\"%UNINSTALL_CONFIRM_PROCEED% \"\nif /i \"!confirm_choice!\"==\"Y\" (\n    set \"CONFIRM_RESULT=1\"\n) else (\n    set \"CONFIRM_RESULT=0\"\n)\ngoto :eof\n"
  },
  {
    "path": "src_assets/windows/misc/vdd/driver/vdd_settings.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<vdd_settings>\n    <monitors>\n        <count>1</count>\n    </monitors>\n    <gpu>\n        <friendlyname>default</friendlyname>\n    </gpu>\n\t<global>\n\t\t<!--These are global refreshrates, any you add in here, will be replicated to all resolutions-->\n\t\t<g_refresh_rate>60</g_refresh_rate>\n\t\t<g_refresh_rate>90</g_refresh_rate>\n\t\t<g_refresh_rate>120</g_refresh_rate>\n\t\t<g_refresh_rate>144</g_refresh_rate>\n\t\t<g_refresh_rate>165</g_refresh_rate>\n\t\t<g_refresh_rate>240</g_refresh_rate>\n\t</global>\n    <resolutions>\n        <resolution>\n            <width>800</width>\n            <height>600</height>\n            <refresh_rate>30</refresh_rate>\n        </resolution>\n        <resolution>\n            <width>1366</width>\n            <height>768</height>\n            <refresh_rate>30</refresh_rate>\n        </resolution>\n        <resolution>\n            <width>1920</width>\n            <height>1080</height>\n            <refresh_rate>30</refresh_rate>\n        </resolution>\n        <resolution>\n            <width>2560</width>\n            <height>1440</height>\n            <refresh_rate>30</refresh_rate>\n        </resolution>\n        <resolution>\n            <width>3840</width>\n            <height>2160</height>\n            <refresh_rate>30</refresh_rate>\n        </resolution>\n    \n        \n    </resolutions>\n\t<logging>\n\t\t<SendLogsThroughPipe>true</SendLogsThroughPipe> <!-- Can only send logs through pipe if logging is enabled-->\n\t\t<logging>false</logging>\n\t\t<!-- DEBUG LOGGING FOR EXPERTS ONLY!-->\n\t\t<debuglogging>false</debuglogging>\n\t\t<!-- DEBUG LOGS CAN GENERATE 1000+ LINES-->\n\t\t<!-- Warning: Leaving logging on too long can lead to excessive filesize. Especially DebugLogging, which should only be used for short periods to log errors. -->\n\t\t<!-- Logging: Useful to troubleshoot and determine which GPUs are being used, and if displays are working as intended.-->\n\t\t<!-- Debug Logging: Logs local system information with every driver function/event/process. Useful for GitHub Help Tickets.-->\n\t</logging>\n\t<colour>\n\t\t<SDR10bit>false</SDR10bit>\n\t\t<HDRPlus>false</HDRPlus> <!-- If you have SDR10 bit enabled, HDRPlus wont work - there’s a conflict because the display system cannot simultaneously handle both high dynamic range 12-bit and standard dynamic range 10-bit settings. -->\n\t\t<ColourFormat>RGB</ColourFormat>\n\t\t<!--\n\t\tSupported colour formats:\n\t\tRGB\n\t\tYCbCr444\n\t\tYCbCr422\n\t\tYCbCr420\n\t\t\n\t\tAny invalid colour formats which are used will default to use RGB\n\t\t-->\n\t</colour>\n\t<cursor>\n\t\t<HardwareCursor>true</HardwareCursor>\n\t\t<!--Whether to display a hardware cursor in the buffer (If disabled streaming apps will use client cursor)-->\n\t\t<CursorMaxY>128</CursorMaxY>\n\t\t<!--The maximum height support for all cursor types. Older intel cpus may be limited to 64x64 -->\n\t\t<CursorMaxX>128</CursorMaxX>\n\t\t<!--The maximum width supported for all supported cursor types.-->\n\t\t<AlphaCursorSupport>true</AlphaCursorSupport>\n\t\t<!--Indicates if the adapter supports the 32-bit alpha cursor format. Most cursors are alpha format.-->\n\t\t<XorCursorSupportLevel>2</XorCursorSupportLevel>\n\t\t<!-- Do not change if you don't know what you're doing -->\n\t\t<!--\n\t\t0 = IDDCX_XOR_CURSOR_SUPPORT_UNINITIALIZED\n\t\t1 = IDDCX_XOR_CURSOR_SUPPORT_NONE\n\t\t2 = IDDCX_XOR_CURSOR_SUPPORT_FULL\n\t\t3 = IDDCX_XOR_CURSOR_SUPPORT_EMULATION\n\t\t-->\n\t</cursor>\n\t<edid>\n\t\t<CustomEdid>false</CustomEdid>\n\t\t<!-- Custom Edid should be named \"user_edid.bin\"! This does not support emulating resolutions!-->\n\t\t<PreventSpoof>false</PreventSpoof>\n\t\t<!--Enable this to prevent manufacturer spoofing when using custom edid. Please only do so if you need to!-->\n\t\t<EdidCeaOverride>false</EdidCeaOverride>\n\t\t<!--Enable this to override or add hard coded cea-extension block to custom Edid support allowing you to enable HDR-->\n\t</edid>\n</vdd_settings>"
  },
  {
    "path": "src_assets/windows/misc/vdd/install-vdd.bat",
    "content": "@echo off\nchcp 65001 >nul\nsetlocal enabledelayedexpansion\n\nrem install\nset \"DRIVER_DIR=%~dp0\\driver\"\necho %DRIVER_DIR%\n\nrem Get sunshine root directory\nfor %%I in (\"%~dp0\\..\") do set \"ROOT_DIR=%%~fI\"\n\nset \"DIST_DIR=%ROOT_DIR%\\tools\\vdd\"\nset \"CONFIG_DIR=%ROOT_DIR%\\config\"\nset \"NEFCON=%ROOT_DIR%\\tools\\nefconw.exe\"\nif not exist \"%NEFCON%\" set \"NEFCON=%DIST_DIR%\\nefconw.exe\"\nset \"VDD_CONFIG=%CONFIG_DIR%\\vdd_settings.xml\"\n\nrem First, copy files to target directory so nefconw.exe can be used\nif exist \"%DIST_DIR%\" (\n    rmdir /s /q \"%DIST_DIR%\"\n)\nmkdir \"%DIST_DIR%\"\ncopy \"%DRIVER_DIR%\\*.*\" \"%DIST_DIR%\"\n\nrem Now we can use nefconw.exe to thoroughly clean up existing VDD adapters\necho Thoroughly cleaning up existing VDD adapters...\n\nrem Remove all device nodes with the same hardware ID (multiple instances)\necho Removing all existing device nodes...\n\"%NEFCON%\" --remove-device-node --hardware-id Root\\ZakoVDD --class-guid 4d36e968-e325-11ce-bfc1-08002be10318\nif %ERRORLEVEL% EQU 0 (\n    echo Successfully removed device node\n) else (\n    echo Device node removal failed or not found\n)\n\nrem Wait to ensure device is completely removed\ntimeout /t 3 /nobreak 1>nul\n\nrem Try to uninstall driver completely\necho Uninstalling VDD driver...\n\"%NEFCON%\" --uninstall-driver --inf-path \"%DIST_DIR%\\ZakoVDD.inf\"\nif %ERRORLEVEL% EQU 0 (\n    echo Successfully uninstalled driver\n) else (\n    echo Driver uninstall failed or not found\n)\n\nrem Wait to ensure driver is completely uninstalled\ntimeout /t 3 /nobreak 1>nul\n\nrem Clean up registry entries\necho Cleaning registry...\nreg delete \"HKLM\\SOFTWARE\\ZakoTech\\ZakoDisplayAdapter\" /f 2>nul\nif %ERRORLEVEL% EQU 0 (\n    echo Successfully cleaned registry\n) else (\n    echo Registry cleanup failed or not found\n)\n\nrem Additional cleanup - remove any remaining device instances\necho Performing additional cleanup...\n\"%NEFCON%\" --remove-device-node --hardware-id Root\\ZakoVDD --class-guid 4d36e968-e325-11ce-bfc1-08002be10318 2>nul\ntimeout /t 2 /nobreak 1>nul\n\nrem Wait a bit more to ensure everything is cleaned up\ntimeout /t 5 /nobreak 1>nul\n\nif not exist \"%VDD_CONFIG%\" (\n    copy \"%DRIVER_DIR%\\vdd_settings.xml\" \"%VDD_CONFIG%\"\n)\n\n@REM write registry\nreg add \"HKLM\\SOFTWARE\\ZakoTech\\ZakoDisplayAdapter\" /v VDDPATH /t REG_SZ /d \"%CONFIG_DIR%\" /f\n\n@REM rem install cet\nset \"CERTIFICATE=%DIST_DIR%\\ZakoVDD.cer\"\ncertutil -addstore -f root \"%CERTIFICATE%\"\n@REM certutil -addstore -f TrustedPublisher %CERTIFICATE%\n\n@REM install inf\necho Installing VDD adapter...\n\"%NEFCON%\" --create-device-node --hardware-id Root\\ZakoVDD --service-name ZAKO_HDR_FOR_SUNSHINE --class-name Display --class-guid 4D36E968-E325-11CE-BFC1-08002BE10318\n\"%NEFCON%\" --install-driver --inf-path \"%DIST_DIR%\\ZakoVDD.inf\"\n\necho VDD installation completed!"
  },
  {
    "path": "src_assets/windows/misc/vdd/uninstall-vdd.bat",
    "content": "@echo off\n\nrem Get sunshine root directory\nfor %%I in (\"%~dp0\\..\") do set \"ROOT_DIR=%%~fI\"\n\nrem uninstall\nset \"DIST_DIR=%ROOT_DIR%\\tools\\vdd\"\nset \"NEFCON=%ROOT_DIR%\\tools\\nefconw.exe\"\nif not exist \"%NEFCON%\" set \"NEFCON=%DIST_DIR%\\nefconw.exe\"\nif not exist \"%NEFCON%\" (\n    echo WARNING: nefconw.exe not found, skipping device node removal.\n    goto :cleanup\n)\nif exist \"%DIST_DIR%\" (\n    \"%NEFCON%\" --remove-device-node --hardware-id ROOT\\ZakoVDD --class-guid 4d36e968-e325-11ce-bfc1-08002be10318\n)\n:cleanup\nreg delete \"HKLM\\SOFTWARE\\ZakoTech\" /f\nrmdir /S /Q \"%DIST_DIR%\"\n"
  },
  {
    "path": "src_assets/windows/misc/vmouse/driver/README.md",
    "content": "This directory holds the built virtual mouse driver files for packaging.\n\nRequired files (from the WDK build output):\n- ZakoVirtualMouse.dll  - The UMDF 2.x HID minidriver\n- ZakoVirtualMouse.inf  - Driver installation information\n- ZakoVirtualMouse.cer  - Test signing certificate\n- ZakoVirtualMouse.cat  - Driver catalog (optional, for production signing)\n\nBuild the driver:\n  Open drivers/virtual_mouse/ZakoVirtualMouse.sln in Visual Studio\n  Build Release|x64\n  Copy output from drivers/virtual_mouse/x64/Release/ to this directory\n"
  },
  {
    "path": "src_assets/windows/misc/vmouse/install-vmouse.bat",
    "content": "@echo off\nchcp 65001 >nul\nsetlocal enabledelayedexpansion\n\nrem ============================================================================\nrem  Zako Virtual Mouse Driver - Installation Script\nrem  UMDF 2.x HID Minidriver for hardware-level virtual mouse\nrem ============================================================================\n\nset \"DRIVER_DIR=%~dp0driver\"\n\nrem Get sunshine root directory\nfor %%I in (\"%~dp0..\\..\") do set \"ROOT_DIR=%%~fI\"\n\nset \"DIST_DIR=%ROOT_DIR%\\tools\\vmouse\"\nset \"NEFCON=%ROOT_DIR%\\tools\\nefconw.exe\"\n\nrem Check if nefconw.exe exists\nif not exist \"%NEFCON%\" (\n    rem Fallback: try VDD component location\n    set \"NEFCON=%ROOT_DIR%\\tools\\vdd\\nefconw.exe\"\n)\nif not exist \"%NEFCON%\" (\n    echo ERROR: nefconw.exe not found.\n    echo        Expected at: %ROOT_DIR%\\tools\\nefconw.exe\n    exit /b 1\n)\n\nrem Copy driver files to target directory\nif exist \"%DIST_DIR%\" (\n    rmdir /s /q \"%DIST_DIR%\"\n)\nmkdir \"%DIST_DIR%\"\ncopy \"%DRIVER_DIR%\\*.*\" \"%DIST_DIR%\"\n\nrem ============================================================================\nrem  Stop Sunshine service to release HID device handle\nrem ============================================================================\n\necho Stopping Sunshine service...\nset \"SERVICE_WAS_RUNNING=0\"\nnet stop SunshineService >nul 2>&1\nif not errorlevel 1 (\n    set \"SERVICE_WAS_RUNNING=1\"\n    echo Sunshine service stopped.\n    timeout /t 2 /nobreak 1>nul\n) else (\n    echo Sunshine service not running, OK.\n)\n\nrem ============================================================================\nrem  Cleanup existing installation\nrem ============================================================================\n\necho Cleaning up existing Virtual Mouse driver...\n\nrem Remove ALL existing device nodes (loop until none remain)\nset \"CLEANUP_COUNT=0\"\n:remove_loop\n\"%NEFCON%\" --remove-device-node --hardware-id Root\\ZakoVirtualMouse --class-guid 745a17a0-74d3-11d0-b6fe-00a0c90f57da\nif not errorlevel 1 (\n    set /a CLEANUP_COUNT+=1\n    echo Removed a device node, checking for more...\n    timeout /t 1 /nobreak >nul\n    goto remove_loop\n)\necho Removed !CLEANUP_COUNT! device node(s) via nefcon.\n\nrem Fallback: use pnputil to remove any remaining device instances\nfor /f \"tokens=*\" %%d in ('powershell -NoProfile -Command ^\n    \"Get-PnpDevice -InstanceId 'ROOT\\ZAKOVIRTUALMOUSE\\*' -ErrorAction SilentlyContinue | ForEach-Object { $_.InstanceId }\"') do (\n    echo Removing remaining device: %%d\n    pnputil /remove-device \"%%d\" >nul 2>&1\n)\necho All existing device nodes removed.\n\ntimeout /t 2 /nobreak 1>nul\n\nrem Uninstall previous driver\n\"%NEFCON%\" --uninstall-driver --inf-path \"%DIST_DIR%\\ZakoVirtualMouse.inf\"\nif not errorlevel 1 (\n    echo Successfully uninstalled previous driver\n) else (\n    echo No previous driver found, OK.\n)\n\ntimeout /t 3 /nobreak 1>nul\n\nrem ============================================================================\nrem  Install Certificate and Driver\nrem ============================================================================\n\nrem Install certificate to Trusted Root and Trusted Publisher stores\nset \"CERTIFICATE=%DIST_DIR%\\ZakoVirtualMouse.cer\"\nif exist \"%CERTIFICATE%\" (\n    echo Installing driver certificate...\n    certutil -addstore -f root \"%CERTIFICATE%\"\n    certutil -addstore -f TrustedPublisher \"%CERTIFICATE%\"\n)\n\nrem Create device node and install driver\necho Installing Virtual Mouse driver...\n\"%NEFCON%\" --create-device-node --hardware-id Root\\ZakoVirtualMouse --class-name HIDClass --class-guid 745a17a0-74d3-11d0-b6fe-00a0c90f57da\n\"%NEFCON%\" --install-driver --inf-path \"%DIST_DIR%\\ZakoVirtualMouse.inf\"\n\nif not errorlevel 1 (\n    echo Virtual Mouse driver installation completed successfully!\n) else (\n    echo Virtual Mouse driver installation failed with error !ERRORLEVEL!\n    echo Rolling back: removing device node...\n    \"%NEFCON%\" --remove-device-node --hardware-id Root\\ZakoVirtualMouse --class-guid 745a17a0-74d3-11d0-b6fe-00a0c90f57da\n)\n\nrem ============================================================================\nrem  Restart Sunshine service if it was running before\nrem ============================================================================\n\nif \"!SERVICE_WAS_RUNNING!\"==\"1\" (\n    echo Restarting Sunshine service...\n    net start SunshineService >nul 2>&1\n    if not errorlevel 1 (\n        echo Sunshine service restarted.\n    ) else (\n        echo WARNING: Could not restart Sunshine service.\n    )\n)\n"
  },
  {
    "path": "src_assets/windows/misc/vmouse/uninstall-vmouse.bat",
    "content": "@echo off\nchcp 65001 >nul\nsetlocal enabledelayedexpansion\n\nrem ============================================================================\nrem  Zako Virtual Mouse Driver - Uninstallation Script\nrem ============================================================================\n\nrem Get sunshine root directory\nfor %%I in (\"%~dp0..\\..\") do set \"ROOT_DIR=%%~fI\"\n\nset \"DIST_DIR=%ROOT_DIR%\\tools\\vmouse\"\nset \"NEFCON=%ROOT_DIR%\\tools\\nefconw.exe\"\n\nrem Check if nefconw.exe exists\nif not exist \"%NEFCON%\" (\n    set \"NEFCON=%ROOT_DIR%\\tools\\vdd\\nefconw.exe\"\n)\n\nrem Stop Sunshine service to release HID device handle\necho Stopping Sunshine service...\nset \"SERVICE_WAS_RUNNING=0\"\nnet stop SunshineService >nul 2>&1\nif not errorlevel 1 (\n    set \"SERVICE_WAS_RUNNING=1\"\n    echo Sunshine service stopped.\n    timeout /t 2 /nobreak 1>nul\n) else (\n    echo Sunshine service not running, OK.\n)\n\nif not exist \"%NEFCON%\" goto skip_nefcon_uninstall\n\necho Removing all Virtual Mouse devices via nefcon...\nset \"NEFCON_REMOVED=0\"\n:uninstall_remove_loop\n\"%NEFCON%\" --remove-device-node --hardware-id Root\\ZakoVirtualMouse --class-guid 745a17a0-74d3-11d0-b6fe-00a0c90f57da\nif not errorlevel 1 (\n    set /a NEFCON_REMOVED+=1\n    timeout /t 1 /nobreak >nul\n    goto uninstall_remove_loop\n)\necho Removed !NEFCON_REMOVED! device node(s) via nefcon.\n\necho Uninstalling Virtual Mouse driver...\n\"%NEFCON%\" --uninstall-driver --inf-path \"%DIST_DIR%\\ZakoVirtualMouse.inf\"\n:skip_nefcon_uninstall\n\nrem Fallback: use pnputil to remove any remaining device instances\nrem This catches ghost devices that nefcon may fail to handle\necho Checking for remaining Virtual Mouse devices...\nset \"PNPUTIL_REMOVED=0\"\nfor /f \"tokens=*\" %%d in ('powershell -NoProfile -Command ^\n    \"Get-PnpDevice -InstanceId 'ROOT\\ZAKOVIRTUALMOUSE\\*' -ErrorAction SilentlyContinue | ForEach-Object { $_.InstanceId }\"') do (\n    echo Removing remaining device: %%d\n    pnputil /remove-device \"%%d\" >nul 2>&1\n    set /a PNPUTIL_REMOVED+=1\n)\nif !PNPUTIL_REMOVED! GTR 0 (\n    echo Removed !PNPUTIL_REMOVED! remaining device(s) via pnputil.\n) else (\n    echo No remaining devices found.\n)\n\nrem Clean up driver package from DriverStore (locale-independent)\nfor /f \"tokens=*\" %%p in ('powershell -NoProfile -Command ^\n    \"Get-ChildItem \\\"$env:SystemRoot\\INF\\oem*.inf\\\" -ErrorAction SilentlyContinue | Where-Object { Select-String -Path $_.FullName -Pattern 'ZakoVirtualMouse' -Quiet } | ForEach-Object { $_.Name }\"') do (\n    echo Removing driver package: %%p\n    pnputil /delete-driver \"%%p\" /force >nul 2>&1\n)\n\nrem Clean up files\nif exist \"%DIST_DIR%\" (\n    rmdir /S /Q \"%DIST_DIR%\"\n)\n\necho Virtual Mouse driver uninstalled.\n\nrem Restart Sunshine service if it was running before\nif \"!SERVICE_WAS_RUNNING!\"==\"1\" (\n    echo Restarting Sunshine service...\n    net start SunshineService >nul 2>&1\n)\n"
  },
  {
    "path": "src_assets/windows/misc/vsink/install-vsink.bat",
    "content": "@echo off\n:: Check for administrator privileges\nnet session >nul 2>&1\nif %errorLevel% neq 0 (\n    echo Please run this script as administrator\n    pause\n    exit /b\n)\n\n:: Check if VB-Cable driver is already installed\n:: Use PowerShell to check registry (more reliable, doesn't depend on PATH)\npowershell -Command \"try { $key = Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\VB-Audio\\Cable' -ErrorAction SilentlyContinue; if ($key) { exit 0 } else { exit 1 } } catch { exit 1 }\" >nul 2>&1\nif %errorLevel% equ 0 (\n    echo VB-Cable driver is already installed (detected via registry)\n    pause\n    exit /b\n)\n\n:: Method 2: Check if VB-Cable audio device exists in system (fallback method)\n:: This method is more reliable as it checks actual hardware devices\npowershell -Command \"try { $devices = Get-PnpDevice -Class AudioEndpoint -ErrorAction SilentlyContinue | Where-Object {$_.FriendlyName -like '*VB-Cable*' -or $_.FriendlyName -like '*CABLE*' -or $_.FriendlyName -like '*VB-Audio Virtual Cable*'}; if ($devices) { exit 0 } else { exit 1 } } catch { exit 1 }\" >nul 2>&1\nif %errorLevel% equ 0 (\n    echo VB-Cable driver is already installed (detected via audio device)\n    pause\n    exit /b\n)\n\n:: Method 3: Check audio devices via registry using PowerShell (additional fallback)\npowershell -Command \"try { $devices = Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Enum\\SWD\\MMDEVAPI\\*' -ErrorAction SilentlyContinue | Where-Object {$_.FriendlyName -like '*CABLE*' -or $_.FriendlyName -like '*VB-Cable*'}; if ($devices) { exit 0 } else { exit 1 } } catch { exit 1 }\" >nul 2>&1\nif %errorLevel% equ 0 (\n    echo VB-Cable driver is already installed (detected via device registry)\n    pause\n    exit /b\n)\n\n:: Set variables\nset \"installer=VBCABLE_Driver_Pack43.zip\"\nset \"download_url=https://download.vb-audio.com/Download_CABLE/VBCABLE_Driver_Pack43.zip\"\nset \"temp_dir=%TEMP%\\vb_cable_install\"\n\n:: Create temp directory\nif not exist \"%temp_dir%\" mkdir \"%temp_dir%\"\n\n:: Download installer\necho Downloading VB-Cable driver...\npowershell -Command \"Invoke-WebRequest -Uri '%download_url%' -OutFile '%temp_dir%\\%installer%'\"\n\n:: Extract files\necho Extracting files...\npowershell -Command \"Expand-Archive -Path '%temp_dir%\\%installer%' -DestinationPath '%temp_dir%' -Force\"\n\n:: Install driver\necho Installing VB-Cable driver...\nstart /wait \"\" \"%temp_dir%\\VBCABLE_Setup_x64.exe\" /S\n\n:: Clean up temp files\necho Cleaning up temporary files...\nrd /s /q \"%temp_dir%\"\n\necho VB-Cable driver installation completed!\npause\n"
  },
  {
    "path": "src_assets/windows/misc/vsink/uninstall-vsink.bat",
    "content": "@echo off\n:: Uninstall VB-Cable virtual audio device\n:: Requires administrator privileges\n\n:: Check if running as administrator\nnet session >nul 2>&1\nif %errorLevel% neq 0 (\n    echo Please run this script as administrator\n    pause\n    exit /b\n)\n\n:: Stop and remove VB-Cable service\necho Stopping VB-Cable service...\nnet stop \"VB-Audio Virtual Cable\" >nul 2>&1\n\necho Removing VB-Cable driver...\npnputil /delete-driver oem*.inf /uninstall /force >nul 2>&1\n\n:: Clean registry entries\necho Cleaning registry...\nreg delete \"HKLM\\SYSTEM\\CurrentControlSet\\Services\\VB-Audio Virtual Cable\" /f >nul 2>&1\nreg delete \"HKLM\\SOFTWARE\\VB-Audio\" /f >nul 2>&1\n\n:: Remove program files\necho Removing program files...\nrd /s /q \"%ProgramFiles%\\VB\\Cable\" >nul 2>&1\nrd /s /q \"%ProgramFiles(x86)%\\VB\\Cable\" >nul 2>&1\n\necho Uninstallation complete!\necho Please restart your computer for changes to take effect\npause\n"
  },
  {
    "path": "tests/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.13)\n# https://github.com/google/oss-policies-info/blob/main/foundational-cxx-support-matrix.md#foundational-c-support\n\nproject(test_sunshine)\n\ninclude_directories(\"${CMAKE_SOURCE_DIR}\")\n\nenable_testing()\n\n# Add GoogleTest directory to the project\nset(GTEST_SOURCE_DIR \"${CMAKE_SOURCE_DIR}/third-party/googletest\")\nset(INSTALL_GTEST OFF)\nset(INSTALL_GMOCK OFF)\nadd_subdirectory(\"${GTEST_SOURCE_DIR}\" \"${CMAKE_CURRENT_BINARY_DIR}/googletest\")\ninclude_directories(\"${GTEST_SOURCE_DIR}/googletest/include\" \"${GTEST_SOURCE_DIR}\")\n\n# coverage\n# https://gcovr.com/en/stable/guide/compiling.html#compiler-options\noption(SUNSHINE_TESTS_ENABLE_COVERAGE \"Enable gcov coverage instrumentation for tests\" OFF)\nif (SUNSHINE_TESTS_ENABLE_COVERAGE)\n    set(CMAKE_CXX_FLAGS \"${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage -ggdb -O0\")\n    set(CMAKE_C_FLAGS \"${CMAKE_C_FLAGS} -fprofile-arcs -ftest-coverage -ggdb -O0\")\nendif ()\n\n# if windows\nif (WIN32)\n    # For Windows: Prevent overriding the parent project's compiler/linker settings\n    set(gtest_force_shared_crt ON CACHE BOOL \"\" FORCE)  # cmake-lint: disable=C0103\nendif ()\n\n# modify SUNSHINE_DEFINITIONS\nif (WIN32)\n    list(APPEND\n            SUNSHINE_DEFINITIONS SUNSHINE_SHADERS_DIR=\"${CMAKE_SOURCE_DIR}/src_assets/windows/assets/shaders/directx\")\nelseif (NOT APPLE)\n    list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_SHADERS_DIR=\"${CMAKE_SOURCE_DIR}/src_assets/linux/assets/shaders/opengl\")\nendif ()\n\nset(TEST_DEFINITIONS)  # list will be appended as needed\n\n# this indicates we're building tests in case sunshine needs to adjust some code or add private tests\nlist(APPEND TEST_DEFINITIONS SUNSHINE_TESTS)\n\nfile(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS\n        ${CMAKE_SOURCE_DIR}/tests/*.h\n        ${CMAKE_SOURCE_DIR}/tests/*.cpp)\n\nset(SUNSHINE_SOURCES\n        ${SUNSHINE_TARGET_FILES})\n\n# remove main.cpp from the list of sources\nlist(REMOVE_ITEM SUNSHINE_SOURCES ${CMAKE_SOURCE_DIR}/src/main.cpp)\n\nadd_executable(${PROJECT_NAME}\n        ${TEST_SOURCES}\n        ${SUNSHINE_SOURCES})\n\nforeach(dep ${SUNSHINE_TARGET_DEPENDENCIES})\n    add_dependencies(${PROJECT_NAME} ${dep})  # compile these before sunshine\nendforeach()\n\nset_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 23)\ntarget_link_libraries(${PROJECT_NAME}\n        ${SUNSHINE_EXTERNAL_LIBRARIES}\n        gtest\n        ${PLATFORM_LIBRARIES})\ntarget_compile_definitions(${PROJECT_NAME} PUBLIC ${SUNSHINE_DEFINITIONS} ${TEST_DEFINITIONS})\ntarget_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${SUNSHINE_COMPILE_OPTIONS}>;$<$<COMPILE_LANGUAGE:CUDA>:${SUNSHINE_COMPILE_OPTIONS_CUDA};-std=c++17>)  # cmake-lint: disable=C0301\ntarget_link_options(${PROJECT_NAME} PRIVATE)\n\nif (WIN32)\n    # prefer static libraries since we're linking statically\n    # this fixes libcurl linking errors when using non MSYS2 version of CMake\n    set_target_properties(${PROJECT_NAME} PROPERTIES LINK_SEARCH_START_STATIC 1)\nendif ()\n\nif (WIN32)\n    add_executable(vmouse_unit_tests\n            ${CMAKE_SOURCE_DIR}/tests/unit/platform/windows/test_virtual_mouse.cpp\n            ${CMAKE_SOURCE_DIR}/src/platform/windows/virtual_mouse.cpp)\n    target_include_directories(vmouse_unit_tests PRIVATE \"${CMAKE_SOURCE_DIR}\")\n    target_link_libraries(vmouse_unit_tests\n            ${SUNSHINE_EXTERNAL_LIBRARIES}\n            gtest_main\n            ${PLATFORM_LIBRARIES})\n    target_compile_definitions(vmouse_unit_tests PUBLIC ${SUNSHINE_DEFINITIONS} ${TEST_DEFINITIONS} SUNSHINE_VIRTUAL_MOUSE_STANDALONE_TEST)\n    target_compile_options(vmouse_unit_tests PRIVATE\n            $<$<COMPILE_LANGUAGE:CXX>:${SUNSHINE_COMPILE_OPTIONS}>\n            -fno-profile-arcs\n            -fno-test-coverage)\n    target_link_options(vmouse_unit_tests PRIVATE\n            -fno-profile-arcs\n            -fno-test-coverage)\n    set_target_properties(vmouse_unit_tests PROPERTIES\n            CXX_STANDARD 23\n            LINK_SEARCH_START_STATIC 1)\n\n    add_executable(vmouse_probe\n            ${CMAKE_SOURCE_DIR}/tests/tools/vmouse_probe.cpp\n            ${CMAKE_SOURCE_DIR}/src/platform/windows/virtual_mouse.cpp)\n    target_include_directories(vmouse_probe PRIVATE \"${CMAKE_SOURCE_DIR}\")\n    target_link_libraries(vmouse_probe\n            ${SUNSHINE_EXTERNAL_LIBRARIES}\n            ${PLATFORM_LIBRARIES})\n    target_compile_definitions(vmouse_probe PUBLIC ${SUNSHINE_DEFINITIONS} ${TEST_DEFINITIONS} SUNSHINE_VIRTUAL_MOUSE_STANDALONE_TEST)\n    target_compile_options(vmouse_probe PRIVATE\n            $<$<COMPILE_LANGUAGE:CXX>:${SUNSHINE_COMPILE_OPTIONS}>\n            -fno-profile-arcs\n            -fno-test-coverage)\n    target_link_options(vmouse_probe PRIVATE\n            -fno-profile-arcs\n            -fno-test-coverage)\n    set_target_properties(vmouse_probe PROPERTIES\n            CXX_STANDARD 23\n            LINK_SEARCH_START_STATIC 1)\n\n    add_executable(vmouse_send_diag\n            ${CMAKE_SOURCE_DIR}/tests/tools/vmouse_send_diag.cpp)\n    target_include_directories(vmouse_send_diag PRIVATE \"${CMAKE_SOURCE_DIR}\")\n    target_link_libraries(vmouse_send_diag\n            ${PLATFORM_LIBRARIES})\n    target_compile_definitions(vmouse_send_diag PUBLIC ${SUNSHINE_DEFINITIONS} ${TEST_DEFINITIONS})\n    set_target_properties(vmouse_send_diag PROPERTIES\n            CXX_STANDARD 23\n            LINK_SEARCH_START_STATIC 1)\nendif ()\n\n"
  },
  {
    "path": "tests/tests_common.h",
    "content": "/**\n * @file tests/tests_common.h\n * @brief Common declarations.\n */\n#pragma once\n#include <gtest/gtest.h>\n\n#include <src/globals.h>\n#include <src/logging.h>\n#include <src/platform/common.h>\n\nstruct PlatformTestSuite: testing::Test {\n  static void\n  SetUpTestSuite() {\n    ASSERT_FALSE(platf_deinit);\n    BOOST_LOG(tests) << \"Setting up platform test suite\";\n    platf_deinit = platf::init();\n    ASSERT_TRUE(platf_deinit);\n  }\n\n  static void\n  TearDownTestSuite() {\n    ASSERT_TRUE(platf_deinit);\n    platf_deinit = {};\n    BOOST_LOG(tests) << \"Tore down platform test suite\";\n  }\n\nprivate:\n  inline static std::unique_ptr<platf::deinit_t> platf_deinit;\n};\n"
  },
  {
    "path": "tests/tests_environment.h",
    "content": "/**\n * @file tests/tests_environment.h\n * @brief Declarations for SunshineEnvironment.\n */\n#pragma once\n#include \"tests_common.h\"\n\nstruct SunshineEnvironment: testing::Environment {\n  void\n  SetUp() override {\n    mail::man = std::make_shared<safe::mail_raw_t>();\n    deinit_log = logging::init(0, \"test_sunshine.log\", false);\n  }\n\n  void\n  TearDown() override {\n    deinit_log = {};\n    mail::man = {};\n  }\n\n  std::unique_ptr<logging::deinit_t> deinit_log;\n};\n"
  },
  {
    "path": "tests/tests_events.h",
    "content": "/**\n * @file tests/tests_events.h\n * @brief Declarations for SunshineEventListener.\n */\n#pragma once\n#include \"tests_common.h\"\n\nstruct SunshineEventListener: testing::EmptyTestEventListener {\n  SunshineEventListener() {\n    sink = boost::make_shared<sink_t>();\n    sink_buffer = boost::make_shared<std::stringstream>();\n    sink->locked_backend()->add_stream(sink_buffer);\n    sink->set_formatter(&logging::formatter);\n  }\n\n  void\n  OnTestProgramStart(const testing::UnitTest &unit_test) override {\n    boost::log::core::get()->add_sink(sink);\n  }\n\n  void\n  OnTestProgramEnd(const testing::UnitTest &unit_test) override {\n    boost::log::core::get()->remove_sink(sink);\n  }\n\n  void\n  OnTestStart(const testing::TestInfo &test_info) override {\n    BOOST_LOG(tests) << \"From \" << test_info.file() << \":\" << test_info.line();\n    BOOST_LOG(tests) << \"  \" << test_info.test_suite_name() << \"/\" << test_info.name() << \" started\";\n  }\n\n  void\n  OnTestPartResult(const testing::TestPartResult &test_part_result) override {\n    std::string file = test_part_result.file_name();\n    BOOST_LOG(tests) << \"At \" << file << \":\" << test_part_result.line_number();\n\n    auto result_text = test_part_result.passed()            ? \"Success\" :\n                       test_part_result.nonfatally_failed() ? \"Non-fatal failure\" :\n                       test_part_result.fatally_failed()    ? \"Failure\" :\n                                                              \"Skip\";\n\n    std::string summary = test_part_result.summary();\n    std::string message = test_part_result.message();\n    BOOST_LOG(tests) << \"  \" << result_text << \": \" << summary;\n    if (message != summary) {\n      BOOST_LOG(tests) << \"  \" << message;\n    }\n  }\n\n  void\n  OnTestEnd(const testing::TestInfo &test_info) override {\n    auto &result = *test_info.result();\n\n    auto result_text = result.Passed()  ? \"passed\" :\n                       result.Skipped() ? \"skipped\" :\n                                          \"failed\";\n    BOOST_LOG(tests) << test_info.test_suite_name() << \"/\" << test_info.name() << \" \" << result_text;\n\n    if (result.Failed()) {\n      std::cout << sink_buffer->str();\n    }\n\n    sink_buffer->str(\"\");\n    sink_buffer->clear();\n  }\n\n  using sink_t = boost::log::sinks::synchronous_sink<boost::log::sinks::text_ostream_backend>;\n  boost::shared_ptr<sink_t> sink;\n  boost::shared_ptr<std::stringstream> sink_buffer;\n};\n"
  },
  {
    "path": "tests/tests_log_checker.h",
    "content": "/**\n * @file tests/tests_log_checker.h\n * @brief Utility functions to check log file contents.\n */\n#pragma once\n\n#include <algorithm>\n#include <fstream>\n#include <regex>\n#include <string>\n\n#include <src/logging.h>\n\nnamespace log_checker {\n\n  /**\n   * @brief Remove the timestamp prefix from a log line.\n   * @param line The log line.\n   * @return The log line without the timestamp prefix.\n   */\n  inline std::string\n  remove_timestamp_prefix(const std::string &line) {\n    static const std::regex timestamp_regex(R\"(\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\]: )\");\n    return std::regex_replace(line, timestamp_regex, \"\");\n  }\n\n  /**\n   * @brief Check if a log file contains a line that starts with the given string.\n   * @param log_file Path to the log file.\n   * @param start_str The string that the line should start with.\n   * @return True if such a line is found, false otherwise.\n   */\n  inline bool\n  line_starts_with(const std::string &log_file, const std::string_view &start_str) {\n    logging::log_flush();\n\n    std::ifstream input(log_file);\n    if (!input.is_open()) {\n      return false;\n    }\n\n    for (std::string line; std::getline(input, line);) {\n      line = remove_timestamp_prefix(line);\n      if (line.rfind(start_str, 0) == 0) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * @brief Check if a log file contains a line that ends with the given string.\n   * @param log_file Path to the log file.\n   * @param end_str The string that the line should end with.\n   * @return True if such a line is found, false otherwise.\n   */\n  inline bool\n  line_ends_with(const std::string &log_file, const std::string_view &end_str) {\n    logging::log_flush();\n\n    std::ifstream input(log_file);\n    if (!input.is_open()) {\n      return false;\n    }\n\n    for (std::string line; std::getline(input, line);) {\n      line = remove_timestamp_prefix(line);\n      if (line.size() >= end_str.size() &&\n          line.compare(line.size() - end_str.size(), end_str.size(), end_str) == 0) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * @brief Check if a log file contains a line that equals the given string.\n   * @param log_file Path to the log file.\n   * @param str The string that the line should equal.\n   * @return True if such a line is found, false otherwise.\n   */\n  inline bool\n  line_equals(const std::string &log_file, const std::string_view &str) {\n    logging::log_flush();\n\n    std::ifstream input(log_file);\n    if (!input.is_open()) {\n      return false;\n    }\n\n    for (std::string line; std::getline(input, line);) {\n      line = remove_timestamp_prefix(line);\n      if (line == str) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * @brief Check if a log file contains a line that contains the given substring.\n   * @param log_file Path to the log file.\n   * @param substr The substring to search for.\n   * @param case_insensitive Whether the search should be case-insensitive.\n   * @return True if such a line is found, false otherwise.\n   */\n  inline bool\n  line_contains(const std::string &log_file, const std::string_view &substr, bool case_insensitive = false) {\n    logging::log_flush();\n\n    std::ifstream input(log_file);\n    if (!input.is_open()) {\n      return false;\n    }\n\n    std::string search_str(substr);\n    if (case_insensitive) {\n      // sonarcloud complains about this, but the solution doesn't work for macOS-12\n      std::transform(search_str.begin(), search_str.end(), search_str.begin(), ::tolower);\n    }\n\n    for (std::string line; std::getline(input, line);) {\n      line = remove_timestamp_prefix(line);\n      if (case_insensitive) {\n        // sonarcloud complains about this, but the solution doesn't work for macOS-12\n        std::transform(line.begin(), line.end(), line.begin(), ::tolower);\n      }\n      if (line.find(search_str) != std::string::npos) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n}  // namespace log_checker\n"
  },
  {
    "path": "tests/tests_main.cpp",
    "content": "/**\n * @file tests/tests_main.cpp\n * @brief Entry point definition.\n */\n#include \"tests_common.h\"\n#include \"tests_environment.h\"\n#include \"tests_events.h\"\n\nint\nmain(int argc, char **argv) {\n  testing::InitGoogleTest(&argc, argv);\n  testing::AddGlobalTestEnvironment(new SunshineEnvironment);\n  testing::UnitTest::GetInstance()->listeners().Append(new SunshineEventListener);\n  return RUN_ALL_TESTS();\n}\n"
  },
  {
    "path": "tests/tools/vmouse_logging_stubs.cpp",
    "content": "#include <boost/log/sources/severity_logger.hpp>\n\nboost::log::sources::severity_logger<int> verbose(0);\nboost::log::sources::severity_logger<int> debug(1);\nboost::log::sources::severity_logger<int> info(2);\nboost::log::sources::severity_logger<int> warning(3);\nboost::log::sources::severity_logger<int> error(4);\nboost::log::sources::severity_logger<int> fatal(5);\n#ifdef SUNSHINE_TESTS\nboost::log::sources::severity_logger<int> tests(10);\n#endif\n"
  },
  {
    "path": "tests/tools/vmouse_probe.cpp",
    "content": "#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include <windows.h>\n\n#include <algorithm>\n#include <atomic>\n#include <chrono>\n#include <cstddef>\n#include <cctype>\n#include <cstdint>\n#include <iostream>\n#include <optional>\n#include <string>\n#include <string_view>\n#include <unordered_set>\n#include <utility>\n#include <vector>\n\n#include <src/platform/windows/virtual_mouse.h>\n\nnamespace {\n  struct options_t {\n    bool list_only = false;\n    bool require_device = false;\n    bool require_events = false;\n    bool send_test_sequence = false;\n    bool quiet = false;\n    DWORD timeout_ms = 5000;\n    std::string match_substring = \"VID_1ACE&PID_0002\";\n  };\n\n  struct raw_device_t {\n    HANDLE handle {};\n    std::string name;\n  };\n\n  struct probe_result_t {\n    bool matched_device_present = false;\n    std::size_t matched_device_count = 0;\n    std::size_t total_event_count = 0;\n    std::size_t matched_event_count = 0;\n    bool send_sequence_ok = false;\n  };\n\n  std::string\n  to_lower(std::string value) {\n    std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {\n      return static_cast<char>(std::tolower(ch));\n    });\n    return value;\n  }\n\n  std::string\n  wide_to_utf8(const std::wstring &wstr) {\n    if (wstr.empty()) {\n      return {};\n    }\n\n    const int size = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), static_cast<int>(wstr.size()), nullptr, 0, nullptr, nullptr);\n    if (size <= 0) {\n      return {};\n    }\n\n    std::string result(size, '\\0');\n    WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), static_cast<int>(wstr.size()), result.data(), size, nullptr, nullptr);\n    return result;\n  }\n\n  std::vector<raw_device_t>\n  enumerate_raw_mouse_devices() {\n    UINT device_count = 0;\n    if (GetRawInputDeviceList(nullptr, &device_count, sizeof(RAWINPUTDEVICELIST)) != 0) {\n      return {};\n    }\n\n    std::vector<RAWINPUTDEVICELIST> raw_devices(device_count);\n    if (GetRawInputDeviceList(raw_devices.data(), &device_count, sizeof(RAWINPUTDEVICELIST)) == static_cast<UINT>(-1)) {\n      return {};\n    }\n\n    std::vector<raw_device_t> result;\n    for (const auto &device: raw_devices) {\n      if (device.dwType != RIM_TYPEMOUSE) {\n        continue;\n      }\n\n      UINT name_length = 0;\n      if (GetRawInputDeviceInfoW(device.hDevice, RIDI_DEVICENAME, nullptr, &name_length) != 0 || name_length == 0) {\n        continue;\n      }\n\n      std::wstring device_name(name_length, L'\\0');\n      if (GetRawInputDeviceInfoW(device.hDevice, RIDI_DEVICENAME, device_name.data(), &name_length) == static_cast<UINT>(-1)) {\n        continue;\n      }\n\n      if (!device_name.empty() && device_name.back() == L'\\0') {\n        device_name.pop_back();\n      }\n\n      result.push_back(raw_device_t {\n        device.hDevice,\n        wide_to_utf8(device_name),\n      });\n    }\n\n    return result;\n  }\n\n  bool\n  contains_case_insensitive(std::string_view haystack, std::string_view needle) {\n    return to_lower(std::string(haystack)).find(to_lower(std::string(needle))) != std::string::npos;\n  }\n\n  void\n  sleep_ms(DWORD duration_ms) {\n    ::Sleep(duration_ms);\n  }\n\n  class raw_input_probe_t {\n  public:\n    raw_input_probe_t(std::string match_substring, bool quiet):\n        _match_substring(std::move(match_substring)),\n        _quiet(quiet) {}\n\n    bool\n    initialize() {\n      _devices = enumerate_raw_mouse_devices();\n      for (const auto &device: _devices) {\n        if (contains_case_insensitive(device.name, _match_substring)) {\n          _matched_devices.insert(device.handle);\n        }\n      }\n\n      WNDCLASSW wc {};\n      wc.lpfnWndProc = &raw_input_probe_t::wnd_proc_setup;\n      wc.hInstance = GetModuleHandleW(nullptr);\n      wc.lpszClassName = L\"SunshineVMouseProbeWindow\";\n\n      if (!RegisterClassW(&wc) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {\n        return false;\n      }\n\n      _window = CreateWindowExW(\n        0,\n        wc.lpszClassName,\n        L\"Sunshine VMouse Probe\",\n        WS_OVERLAPPED,\n        0,\n        0,\n        1,\n        1,\n        nullptr,\n        nullptr,\n        wc.hInstance,\n        this);\n\n      if (_window == nullptr) {\n        return false;\n      }\n\n      RAWINPUTDEVICE rid {};\n      rid.usUsagePage = 0x01;\n      rid.usUsage = 0x02;\n      rid.dwFlags = RIDEV_INPUTSINK;\n      rid.hwndTarget = _window;\n\n      return RegisterRawInputDevices(&rid, 1, sizeof(rid)) == TRUE;\n    }\n\n    ~raw_input_probe_t() {\n      if (_window != nullptr) {\n        DestroyWindow(_window);\n      }\n    }\n\n    probe_result_t\n    run(DWORD timeout_ms) {\n      const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);\n      MSG msg {};\n\n      while (std::chrono::steady_clock::now() < deadline) {\n        while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {\n          TranslateMessage(&msg);\n          DispatchMessageW(&msg);\n        }\n        sleep_ms(10);\n      }\n\n      return probe_result_t {\n        !_matched_devices.empty(),\n        _matched_devices.size(),\n        _total_event_count,\n        _matched_event_count,\n        false,\n      };\n    }\n\n  private:\n    static LRESULT CALLBACK\n    wnd_proc_setup(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {\n      if (msg == WM_NCCREATE) {\n        auto *create = reinterpret_cast<CREATESTRUCTW *>(lparam);\n        auto *self = static_cast<raw_input_probe_t *>(create->lpCreateParams);\n        SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self));\n        return TRUE;\n      }\n\n      auto *self = reinterpret_cast<raw_input_probe_t *>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));\n      if (self == nullptr) {\n        return DefWindowProcW(hwnd, msg, wparam, lparam);\n      }\n\n      return self->wnd_proc(hwnd, msg, wparam, lparam);\n    }\n\n    LRESULT\n    wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {\n      if (msg != WM_INPUT) {\n        return DefWindowProcW(hwnd, msg, wparam, lparam);\n      }\n\n      UINT size = 0;\n      if (GetRawInputData(reinterpret_cast<HRAWINPUT>(lparam), RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER)) != 0 || size == 0) {\n        return 0;\n      }\n\n      std::vector<std::byte> buffer(size);\n      if (GetRawInputData(reinterpret_cast<HRAWINPUT>(lparam), RID_INPUT, buffer.data(), &size, sizeof(RAWINPUTHEADER)) == static_cast<UINT>(-1)) {\n        return 0;\n      }\n\n      const auto *raw = reinterpret_cast<const RAWINPUT *>(buffer.data());\n      if (raw->header.dwType != RIM_TYPEMOUSE) {\n        return 0;\n      }\n\n      ++_total_event_count;\n\n      const bool matched = _matched_devices.contains(raw->header.hDevice);\n      if (matched) {\n        ++_matched_event_count;\n      }\n\n      if (!_quiet) {\n        std::cout\n          << \"EVENT device=\" << reinterpret_cast<std::uintptr_t>(raw->header.hDevice)\n          << \" matched=\" << (matched ? 1 : 0)\n          << \" dx=\" << raw->data.mouse.lLastX\n          << \" dy=\" << raw->data.mouse.lLastY\n          << \" flags=\" << raw->data.mouse.usButtonFlags\n          << \" wheel=\" << static_cast<SHORT>(raw->data.mouse.usButtonData)\n          << '\\n';\n      }\n\n      return 0;\n    }\n\n    std::string _match_substring;\n    bool _quiet;\n    HWND _window = nullptr;\n    std::vector<raw_device_t> _devices;\n    std::unordered_set<HANDLE> _matched_devices;\n    std::size_t _total_event_count = 0;\n    std::size_t _matched_event_count = 0;\n  };\n\n  void\n  print_usage() {\n    std::cout\n      << \"Usage: vmouse_probe [--list-only] [--timeout-ms N] [--match-substring TEXT]\\n\"\n      << \"                    [--require-device] [--require-events] [--send-test-sequence] [--quiet]\\n\";\n  }\n\n  std::optional<options_t>\n  parse_args(int argc, char **argv) {\n    options_t options;\n\n    for (int i = 1; i < argc; ++i) {\n      const std::string arg = argv[i];\n      if (arg == \"--list-only\") {\n        options.list_only = true;\n      }\n      else if (arg == \"--require-device\") {\n        options.require_device = true;\n      }\n      else if (arg == \"--require-events\") {\n        options.require_events = true;\n      }\n      else if (arg == \"--send-test-sequence\") {\n        options.send_test_sequence = true;\n      }\n      else if (arg == \"--quiet\") {\n        options.quiet = true;\n      }\n      else if (arg == \"--timeout-ms\" && i + 1 < argc) {\n        options.timeout_ms = static_cast<DWORD>(std::stoul(argv[++i]));\n      }\n      else if (arg == \"--match-substring\" && i + 1 < argc) {\n        options.match_substring = argv[++i];\n      }\n      else if (arg == \"--help\" || arg == \"-h\") {\n        print_usage();\n        return std::nullopt;\n      }\n      else {\n        std::cerr << \"Unknown argument: \" << arg << '\\n';\n        print_usage();\n        return std::nullopt;\n      }\n    }\n\n    return options;\n  }\n\n  bool\n  send_test_sequence() {\n    auto device = platf::vmouse::create();\n    if (!device.is_available()) {\n      return false;\n    }\n\n    bool ok = true;\n    ok = ok && device.move(48, 24);\n    sleep_ms(30);\n    ok = ok && device.button(platf::vmouse::BTN_LEFT, false);\n    sleep_ms(30);\n    ok = ok && device.button(platf::vmouse::BTN_LEFT, true);\n    sleep_ms(30);\n    ok = ok && device.scroll(120);\n    return ok;\n  }\n\n  void\n  print_enumeration(const std::string &match_substring) {\n    const auto devices = enumerate_raw_mouse_devices();\n    std::size_t matched = 0;\n\n    for (const auto &device: devices) {\n      const bool is_match = contains_case_insensitive(device.name, match_substring);\n      matched += is_match ? 1U : 0U;\n      std::cout << \"DEVICE matched=\" << (is_match ? 1 : 0)\n                << \" handle=\" << reinterpret_cast<std::uintptr_t>(device.handle)\n                << \" name=\" << device.name << '\\n';\n    }\n\n    std::cout << \"MATCHED_DEVICE_PRESENT=\" << (matched > 0 ? 1 : 0) << '\\n';\n    std::cout << \"MATCHED_DEVICE_COUNT=\" << matched << '\\n';\n  }\n}  // namespace\n\nint\nmain(int argc, char **argv) {\n  const auto parsed = parse_args(argc, argv);\n  if (!parsed.has_value()) {\n    return 1;\n  }\n\n  const auto &options = *parsed;\n\n  if (options.list_only) {\n    print_enumeration(options.match_substring);\n    return 0;\n  }\n\n  raw_input_probe_t probe(options.match_substring, options.quiet);\n  if (!probe.initialize()) {\n    std::cerr << \"Failed to initialize raw input probe\\n\";\n    return 2;\n  }\n\n  std::atomic_bool send_sequence_ok = false;\n  if (options.send_test_sequence) {\n    sleep_ms(250);\n    send_sequence_ok = send_test_sequence();\n  }\n\n  auto result = probe.run(options.timeout_ms);\n  result.send_sequence_ok = send_sequence_ok.load();\n\n  std::cout << \"MATCHED_DEVICE_PRESENT=\" << (result.matched_device_present ? 1 : 0) << '\\n';\n  std::cout << \"MATCHED_DEVICE_COUNT=\" << result.matched_device_count << '\\n';\n  std::cout << \"TOTAL_EVENT_COUNT=\" << result.total_event_count << '\\n';\n  std::cout << \"MATCHED_EVENT_COUNT=\" << result.matched_event_count << '\\n';\n  std::cout << \"SEND_SEQUENCE_OK=\" << (result.send_sequence_ok ? 1 : 0) << '\\n';\n\n  if (options.require_device && !result.matched_device_present) {\n    return 3;\n  }\n\n  if (options.require_events && result.matched_event_count == 0) {\n    return 4;\n  }\n\n  if (options.send_test_sequence && !result.send_sequence_ok) {\n    return 5;\n  }\n\n  return 0;\n}\n"
  },
  {
    "path": "tests/tools/vmouse_send_diag.cpp",
    "content": "#define WIN32_LEAN_AND_MEAN\n#define NOMINMAX\n#include <windows.h>\n\n#include <hidsdi.h>\n#include <hidpi.h>\n#include <setupapi.h>\n\n#include <array>\n#include <cstdint>\n#include <iostream>\n#include <memory>\n#include <string>\n\nnamespace {\n  constexpr uint16_t VMOUSE_VID = 0x1ACE;\n  constexpr uint16_t VMOUSE_PID = 0x0002;\n\n  std::string\n  wide_to_utf8(const wchar_t *wstr) {\n    if (!wstr) {\n      return {};\n    }\n\n    const int len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr);\n    if (len <= 0) {\n      return {};\n    }\n\n    std::string result(len - 1, '\\0');\n    WideCharToMultiByte(CP_UTF8, 0, wstr, -1, result.data(), len, nullptr, nullptr);\n    return result;\n  }\n\n  std::array<uint8_t, 8>\n  build_report(uint8_t buttons, int16_t dx, int16_t dy, int8_t wheel, int8_t hwheel) {\n    return {\n      0x02,\n      buttons,\n      static_cast<uint8_t>(dx & 0xFF),\n      static_cast<uint8_t>((dx >> 8) & 0xFF),\n      static_cast<uint8_t>(dy & 0xFF),\n      static_cast<uint8_t>((dy >> 8) & 0xFF),\n      static_cast<uint8_t>(wheel),\n      static_cast<uint8_t>(hwheel),\n    };\n  }\n}  // namespace\n\nint\nmain() {\n  GUID hid_guid;\n  HidD_GetHidGuid(&hid_guid);\n\n  HDEVINFO dev_info = SetupDiGetClassDevsW(&hid_guid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);\n  if (dev_info == INVALID_HANDLE_VALUE) {\n    std::cerr << \"SetupDiGetClassDevs failed err=\" << GetLastError() << '\\n';\n    return 1;\n  }\n\n  SP_DEVICE_INTERFACE_DATA iface {};\n  iface.cbSize = sizeof(iface);\n\n  for (DWORD i = 0; SetupDiEnumDeviceInterfaces(dev_info, nullptr, &hid_guid, i, &iface); ++i) {\n    DWORD required_size = 0;\n    SetupDiGetDeviceInterfaceDetailW(dev_info, &iface, nullptr, 0, &required_size, nullptr);\n    if (required_size == 0) {\n      continue;\n    }\n\n    auto detail_buf = std::make_unique<BYTE[]>(required_size);\n    auto *detail = reinterpret_cast<PSP_DEVICE_INTERFACE_DETAIL_DATA_W>(detail_buf.get());\n    detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);\n    if (!SetupDiGetDeviceInterfaceDetailW(dev_info, &iface, detail, required_size, nullptr, nullptr)) {\n      continue;\n    }\n\n    HANDLE attr_handle = CreateFileW(detail->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);\n    if (attr_handle == INVALID_HANDLE_VALUE) {\n      continue;\n    }\n\n    HIDD_ATTRIBUTES attrs {};\n    attrs.Size = sizeof(attrs);\n    const bool ok_attrs = HidD_GetAttributes(attr_handle, &attrs) == TRUE;\n    CloseHandle(attr_handle);\n    if (!ok_attrs || attrs.VendorID != VMOUSE_VID || attrs.ProductID != VMOUSE_PID) {\n      continue;\n    }\n\n    std::cout << \"TARGET path=\" << wide_to_utf8(detail->DevicePath) << '\\n';\n\n    HANDLE caps_handle = CreateFileW(detail->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);\n    if (caps_handle != INVALID_HANDLE_VALUE) {\n      PHIDP_PREPARSED_DATA preparsed_data = nullptr;\n      if (HidD_GetPreparsedData(caps_handle, &preparsed_data)) {\n        HIDP_CAPS caps {};\n        if (HidP_GetCaps(preparsed_data, &caps) == HIDP_STATUS_SUCCESS) {\n          std::cout << \"CAPS usage_page=\" << caps.UsagePage\n                    << \" usage=\" << caps.Usage\n                    << \" input_len=\" << caps.InputReportByteLength\n                    << \" output_len=\" << caps.OutputReportByteLength\n                    << \" feature_len=\" << caps.FeatureReportByteLength << '\\n';\n        }\n        HidD_FreePreparsedData(preparsed_data);\n      }\n      CloseHandle(caps_handle);\n    }\n\n    HANDLE write_handle = CreateFileW(detail->DevicePath, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);\n    if (write_handle == INVALID_HANDLE_VALUE) {\n      std::cout << \"OPEN_WRITE_FAIL err=\" << GetLastError() << '\\n';\n      SetupDiDestroyDeviceInfoList(dev_info);\n      return 2;\n    }\n\n    // Test HidD_SetFeature (what Sunshine actually uses)\n    const auto move_report = build_report(0, 48, 24, 0, 0);\n    SetLastError(0);\n    const BOOL feat_move_ok = HidD_SetFeature(write_handle, const_cast<uint8_t *>(move_report.data()), static_cast<ULONG>(move_report.size()));\n    std::cout << \"MOVE_SETFEATURE ok=\" << (feat_move_ok ? 1 : 0) << \" err=\" << GetLastError() << '\\n';\n\n    Sleep(100);\n\n    const auto click_report = build_report(1, 0, 0, 0, 0);\n    SetLastError(0);\n    const BOOL feat_click_ok = HidD_SetFeature(write_handle, const_cast<uint8_t *>(click_report.data()), static_cast<ULONG>(click_report.size()));\n    std::cout << \"CLICK_SETFEATURE ok=\" << (feat_click_ok ? 1 : 0) << \" err=\" << GetLastError() << '\\n';\n\n    Sleep(100);\n\n    const auto release_report = build_report(0, 0, 0, 0, 0);\n    SetLastError(0);\n    const BOOL feat_release_ok = HidD_SetFeature(write_handle, const_cast<uint8_t *>(release_report.data()), static_cast<ULONG>(release_report.size()));\n    std::cout << \"RELEASE_SETFEATURE ok=\" << (feat_release_ok ? 1 : 0) << \" err=\" << GetLastError() << '\\n';\n\n    // Also test HidD_SetOutputReport for comparison (expected to fail since driver has no Output Report)\n    SetLastError(0);\n    const BOOL out_ok = HidD_SetOutputReport(write_handle, const_cast<uint8_t *>(move_report.data()), static_cast<ULONG>(move_report.size()));\n    std::cout << \"MOVE_SETOUTPUTREPORT ok=\" << (out_ok ? 1 : 0) << \" err=\" << GetLastError() << \" (expected fail, no output report)\\n\";\n\n    CloseHandle(write_handle);\n    SetupDiDestroyDeviceInfoList(dev_info);\n    return 0;\n  }\n\n  SetupDiDestroyDeviceInfoList(dev_info);\n  std::cout << \"TARGET_NOT_FOUND\\n\";\n  return 3;\n}\n"
  },
  {
    "path": "tests/unit/platform/test_common.cpp",
    "content": "/**\n * @file tests/unit/platform/test_common.cpp\n * @brief Test src/platform/common.*.\n */\n#include <src/platform/common.h>\n\n#include <boost/asio/ip/host_name.hpp>\n\n#include \"../../tests_common.h\"\n\nstruct SetEnvTest: ::testing::TestWithParam<std::tuple<std::string, std::string, int>> {\nprotected:\n  void\n  TearDown() override {\n    // Clean up environment variable after each test\n    const auto &[name, value, expected] = GetParam();\n    platf::unset_env(name);\n  }\n};\n\nTEST_P(SetEnvTest, SetEnvironmentVariableTests) {\n  const auto &[name, value, expected] = GetParam();\n  platf::set_env(name, value);\n\n  const char *env_value = std::getenv(name.c_str());\n  if (expected == 0 && !value.empty()) {\n    ASSERT_NE(env_value, nullptr);\n    ASSERT_EQ(std::string(env_value), value);\n  }\n  else {\n    ASSERT_EQ(env_value, nullptr);\n  }\n}\n\nTEST_P(SetEnvTest, UnsetEnvironmentVariableTests) {\n  const auto &[name, value, expected] = GetParam();\n  platf::unset_env(name);\n\n  const char *env_value = std::getenv(name.c_str());\n  if (expected == 0) {\n    ASSERT_EQ(env_value, nullptr);\n  }\n}\n\nINSTANTIATE_TEST_SUITE_P(\n  SetEnvTests,\n  SetEnvTest,\n  ::testing::Values(\n    std::make_tuple(\"SUNSHINE_UNIT_TEST_ENV_VAR\", \"test_value_0\", 0),\n    std::make_tuple(\"SUNSHINE_UNIT_TEST_ENV_VAR\", \"test_value_1\", 0),\n    std::make_tuple(\"\", \"test_value\", -1)));\n\nTEST(HostnameTests, TestAsioEquality) {\n  // These should be equivalent on all platforms for ASCII hostnames\n  ASSERT_EQ(platf::get_host_name(), boost::asio::ip::host_name());\n}\n"
  },
  {
    "path": "tests/unit/platform/windows/test_virtual_mouse.cpp",
    "content": "/**\n * @file tests/unit/platform/windows/test_virtual_mouse.cpp\n * @brief Test virtual mouse HID report helpers.\n */\n\n#ifdef _WIN32\n\n  #define WIN32_LEAN_AND_MEAN\n  #include <windows.h>\n\n  #include <src/platform/windows/virtual_mouse.h>\n\n  #include \"../../../tests_common.h\"\n\nTEST(VMouseReportTests, BuildOutputReportSerializesLittleEndianSignedDeltas) {\n  const auto report = platf::vmouse::detail::build_output_report(\n    platf::vmouse::BTN_LEFT | platf::vmouse::BTN_SIDE,\n    0x1234,\n    static_cast<int16_t>(-200),\n    12,\n    static_cast<int8_t>(-7));\n\n  const platf::vmouse::detail::output_report_t expected {\n    0x02,\n    static_cast<uint8_t>(platf::vmouse::BTN_LEFT | platf::vmouse::BTN_SIDE),\n    0x34,\n    0x12,\n    0x38,\n    0xFF,\n    0x0C,\n    0xF9,\n  };\n\n  EXPECT_EQ(report, expected);\n}\n\nTEST(VMouseReportTests, ApplyButtonTransitionMaintainsAggregateState) {\n  uint8_t buttons = 0;\n\n  buttons = platf::vmouse::detail::apply_button_transition(buttons, platf::vmouse::BTN_LEFT, false);\n  EXPECT_EQ(buttons, platf::vmouse::BTN_LEFT);\n\n  buttons = platf::vmouse::detail::apply_button_transition(buttons, platf::vmouse::BTN_RIGHT, false);\n  EXPECT_EQ(buttons, platf::vmouse::BTN_LEFT | platf::vmouse::BTN_RIGHT);\n\n  buttons = platf::vmouse::detail::apply_button_transition(buttons, platf::vmouse::BTN_LEFT, true);\n  EXPECT_EQ(buttons, platf::vmouse::BTN_RIGHT);\n}\n\nTEST(VMouseReportTests, BuildOutputReportKeepsScrollBytesIndependent) {\n  const auto report = platf::vmouse::detail::build_output_report(\n    0,\n    0,\n    0,\n    static_cast<int8_t>(-127),\n    127);\n\n  EXPECT_EQ(report[6], 0x81);\n  EXPECT_EQ(report[7], 0x7F);\n}\n\nTEST(VMouseReportTests, DisconnectErrorsTriggerHandleClose) {\n  EXPECT_TRUE(platf::vmouse::detail::should_close_on_write_error(ERROR_DEVICE_NOT_CONNECTED));\n  EXPECT_TRUE(platf::vmouse::detail::should_close_on_write_error(ERROR_GEN_FAILURE));\n  EXPECT_FALSE(platf::vmouse::detail::should_close_on_write_error(ERROR_INVALID_PARAMETER));\n}\n\n#endif\n"
  },
  {
    "path": "tests/unit/test_audio.cpp",
    "content": "/**\n * @file tests/unit/test_audio.cpp\n * @brief Test src/audio.*.\n */\n#include <src/audio.h>\n\n#include \"../tests_common.h\"\n\nusing namespace audio;\n\nstruct AudioTest: PlatformTestSuite, testing::WithParamInterface<std::tuple<std::basic_string_view<char>, config_t>> {\n  void\n  SetUp() override {\n    m_config = std::get<1>(GetParam());\n    m_mail = std::make_shared<safe::mail_raw_t>();\n  }\n\n  config_t m_config;\n  safe::mail_t m_mail;\n};\n\nconstexpr std::bitset<config_t::MAX_FLAGS> config_flags(const int flag = -1) {\n  std::bitset<3> result = std::bitset<config_t::MAX_FLAGS>();\n  if (flag >= 0) {\n    result.set(flag);\n  }\n  return result;\n}\n\nINSTANTIATE_TEST_SUITE_P(\n  Configurations,\n  AudioTest,\n  testing::Values(\n    std::make_tuple(\"HIGH_STEREO\", config_t { 5, 2, 0x3, { 0 }, config_flags(config_t::HIGH_QUALITY) }),\n    std::make_tuple(\"SURROUND51\", config_t { 5, 6, 0x3F, { 0 }, config_flags() }),\n    std::make_tuple(\"SURROUND71\", config_t { 5, 8, 0x63F, { 0 }, config_flags() }),\n    std::make_tuple(\"SURROUND51_CUSTOM\", config_t { 5, 6, 0x3F, { 6, 4, 2, { 0, 1, 4, 5, 2, 3 } }, config_flags(config_t::CUSTOM_SURROUND_PARAMS) })),\n  [](const auto &info) { return std::string(std::get<0>(info.param)); });\n\nTEST_P(AudioTest, TestEncode) {\n  std::thread timer([&] {\n    // Terminate the audio capture after 100 ms\n    std::this_thread::sleep_for(100ms);\n    const auto shutdown_event = m_mail->event<bool>(mail::shutdown);\n    const auto audio_packets = m_mail->queue<packet_t>(mail::audio_packets);\n    shutdown_event->raise(true);\n    audio_packets->stop();\n  });\n  std::thread capture([&] {\n    const auto packets = m_mail->queue<packet_t>(mail::audio_packets);\n    const auto shutdown_event = m_mail->event<bool>(mail::shutdown);\n    while (const auto packet = packets->pop()) {\n      if (shutdown_event->peek()) {\n        break;\n      }\n      if (auto packet_data = packet->second; packet_data.size() == 0) {\n        FAIL() << \"Empty packet data\";\n      }\n    }\n  });\n  audio::capture(m_mail, m_config, nullptr);\n\n  timer.join();\n  capture.join();\n}\n"
  },
  {
    "path": "tests/unit/test_entry_handler.cpp",
    "content": "/**\n * @file tests/unit/test_entry_handler.cpp\n * @brief Test src/entry_handler.*.\n */\n#include <src/entry_handler.h>\n\n#include \"../tests_common.h\"\n#include \"../tests_log_checker.h\"\n\nTEST(EntryHandlerTests, LogPublisherDataTest) {\n  // call log_publisher_data\n  log_publisher_data();\n\n  // check if specific log messages exist\n  ASSERT_TRUE(log_checker::line_starts_with(\"test_sunshine.log\", \"Info: Package Publisher: \"));\n  ASSERT_TRUE(log_checker::line_starts_with(\"test_sunshine.log\", \"Info: Publisher Website: \"));\n  ASSERT_TRUE(log_checker::line_starts_with(\"test_sunshine.log\", \"Info: Get support: \"));\n}\n"
  },
  {
    "path": "tests/unit/test_file_handler.cpp",
    "content": "/**\n * @file tests/unit/test_file_handler.cpp\n * @brief Test src/file_handler.*.\n */\n#include <src/file_handler.h>\n\n#include \"../tests_common.h\"\n\nstruct FileHandlerParentDirectoryTest: testing::TestWithParam<std::tuple<std::string, std::string>> {};\n\nTEST_P(FileHandlerParentDirectoryTest, Run) {\n  auto [input, expected] = GetParam();\n  EXPECT_EQ(file_handler::get_parent_directory(input), expected);\n}\n\nINSTANTIATE_TEST_SUITE_P(\n  FileHandlerTests,\n  FileHandlerParentDirectoryTest,\n  testing::Values(\n    std::make_tuple(\"/path/to/file.txt\", \"/path/to\"),\n    std::make_tuple(\"/path/to/directory\", \"/path/to\"),\n    std::make_tuple(\"/path/to/directory/\", \"/path/to\")));\n\nstruct FileHandlerMakeDirectoryTest: testing::TestWithParam<std::tuple<std::string, bool, bool>> {};\n\nTEST_P(FileHandlerMakeDirectoryTest, Run) {\n  auto [input, expected, remove] = GetParam();\n  const std::string test_dir = platf::appdata().string() + \"/tests/path/\";\n  input = test_dir + input;\n\n  EXPECT_EQ(file_handler::make_directory(input), expected);\n  EXPECT_TRUE(std::filesystem::exists(input));\n\n  // remove test directory\n  if (remove) {\n    std::filesystem::remove_all(test_dir);\n    EXPECT_FALSE(std::filesystem::exists(test_dir));\n  }\n}\n\nINSTANTIATE_TEST_SUITE_P(\n  FileHandlerTests,\n  FileHandlerMakeDirectoryTest,\n  testing::Values(\n    std::make_tuple(\"dir_123\", true, false),\n    std::make_tuple(\"dir_123\", true, true),\n    std::make_tuple(\"dir_123/abc\", true, false),\n    std::make_tuple(\"dir_123/abc\", true, true)));\n\nstruct FileHandlerTests: testing::TestWithParam<std::tuple<int, std::string>> {};\n\nINSTANTIATE_TEST_SUITE_P(\n  TestFiles,\n  FileHandlerTests,\n  testing::Values(\n    std::make_tuple(0, \"\"),  // empty file\n    std::make_tuple(1, \"a\"),  // single character\n    std::make_tuple(2, \"Mr. Blue Sky - Electric Light Orchestra\"),  // single line\n    std::make_tuple(3, R\"(\nMorning! Today's forecast calls for blue skies\nThe sun is shining in the sky\nThere ain't a cloud in sight\nIt's stopped raining\nEverybody's in the play\nAnd don't you know, it's a beautiful new day\nHey, hey, hey!\nRunning down the avenue\nSee how the sun shines brightly in the city\nAll the streets where once was pity\nMr. Blue Sky is living here today!\nHey, hey, hey!\n    )\")  // multi-line\n    ));\n\nTEST_P(FileHandlerTests, WriteFileTest) {\n  auto [fileNum, content] = GetParam();\n  std::string fileName = \"write_file_test_\" + std::to_string(fileNum) + \".txt\";\n  EXPECT_EQ(file_handler::write_file(fileName.c_str(), content), 0);\n}\n\nTEST_P(FileHandlerTests, ReadFileTest) {\n  auto [fileNum, content] = GetParam();\n  std::string fileName = \"write_file_test_\" + std::to_string(fileNum) + \".txt\";\n  EXPECT_EQ(file_handler::read_file(fileName.c_str()), content);\n}\n\nTEST(FileHandlerTests, ReadMissingFileTest) {\n  // read missing file\n  EXPECT_EQ(file_handler::read_file(\"non-existing-file.txt\"), \"\");\n}\n"
  },
  {
    "path": "tests/unit/test_httpcommon.cpp",
    "content": "/**\n * @file tests/unit/test_httpcommon.cpp\n * @brief Test src/httpcommon.*.\n */\n// test imports\n#include \"../tests_common.h\"\n\n// lib imports\n#include <curl/curl.h>\n\n// local imports\n#include <src/httpcommon.h>\n\n#include \"../tests_common.h\"\n\nstruct UrlEscapeTest: testing::TestWithParam<std::tuple<std::string, std::string>> {};\n\nTEST_P(UrlEscapeTest, Run) {\n  const auto &[input, expected] = GetParam();\n  ASSERT_EQ(http::url_escape(input), expected);\n}\n\nINSTANTIATE_TEST_SUITE_P(\n  UrlEscapeTests,\n  UrlEscapeTest,\n  testing::Values(\n    std::make_tuple(\"igdb_0123456789\", \"igdb_0123456789\"),\n    std::make_tuple(\"../../../\", \"..%2F..%2F..%2F\"),\n    std::make_tuple(\"..*\\\\\", \"..%2A%5C\")));\n\nstruct UrlGetHostTest: testing::TestWithParam<std::tuple<std::string, std::string>> {};\n\nTEST_P(UrlGetHostTest, Run) {\n  const auto &[input, expected] = GetParam();\n  ASSERT_EQ(http::url_get_host(input), expected);\n}\n\nINSTANTIATE_TEST_SUITE_P(\n  UrlGetHostTests,\n  UrlGetHostTest,\n  testing::Values(\n    std::make_tuple(\"https://images.igdb.com/example.txt\", \"images.igdb.com\"),\n    std::make_tuple(\"http://localhost:8080\", \"localhost\"),\n    std::make_tuple(\"nonsense!!}{::\", \"\")));\n\nstruct DownloadFileTest: testing::TestWithParam<std::tuple<std::string, std::string>> {};\n\nTEST_P(DownloadFileTest, Run) {\n  const auto &[url, filename] = GetParam();\n  const std::string test_dir = platf::appdata().string() + \"/tests/\";\n  std::string path = test_dir + filename;\n  ASSERT_TRUE(http::download_file(url, path, CURL_SSLVERSION_TLSv1_0));\n}\n\nINSTANTIATE_TEST_SUITE_P(\n  DownloadFileTests,\n  DownloadFileTest,\n  testing::Values(\n    std::make_tuple(\"https://httpbin.org/base64/aGVsbG8h\", \"hello.txt\"),\n    std::make_tuple(\"https://httpbin.org/redirect-to?url=/base64/aGVsbG8h\", \"hello-redirect.txt\")));\n"
  },
  {
    "path": "tests/unit/test_logging.cpp",
    "content": "/**\n * @file tests/unit/test_logging.cpp\n * @brief Test src/logging.*.\n */\n#include <src/logging.h>\n\n#include \"../tests_common.h\"\n#include \"../tests_log_checker.h\"\n\n#include <random>\n\nnamespace {\n  std::array log_levels = {\n    std::tuple(\"verbose\", &verbose),\n    std::tuple(\"debug\", &debug),\n    std::tuple(\"info\", &info),\n    std::tuple(\"warning\", &warning),\n    std::tuple(\"error\", &error),\n    std::tuple(\"fatal\", &fatal),\n  };\n\n  constexpr auto log_file = \"test_sunshine.log\";\n}  // namespace\n\nstruct LogLevelsTest: testing::TestWithParam<decltype(log_levels)::value_type> {};\n\nINSTANTIATE_TEST_SUITE_P(\n  Logging,\n  LogLevelsTest,\n  testing::ValuesIn(log_levels),\n  [](const auto &info) { return std::string(std::get<0>(info.param)); });\n\nTEST_P(LogLevelsTest, PutMessage) {\n  auto [label, plogger] = GetParam();\n  ASSERT_TRUE(plogger);\n  auto &logger = *plogger;\n\n  std::random_device rand_dev;\n  std::mt19937_64 rand_gen(rand_dev());\n  auto test_message = std::to_string(rand_gen()) + std::to_string(rand_gen());\n  BOOST_LOG(logger) << test_message;\n\n  ASSERT_TRUE(log_checker::line_contains(log_file, test_message));\n}\n"
  },
  {
    "path": "tests/unit/test_mouse.cpp",
    "content": "/**\n * @file tests/unit/test_mouse.cpp\n * @brief Test src/input.*.\n */\n#include <src/input.h>\n\n#include \"../tests_common.h\"\n\nstruct MouseHIDTest: PlatformTestSuite, testing::WithParamInterface<util::point_t> {\n  void\n  SetUp() override {\n#ifdef _WIN32\n    // TODO: Windows tests are failing, `get_mouse_loc` seems broken and `platf::abs_mouse` too\n    //       the alternative `platf::abs_mouse` method seem to work better during tests,\n    //       but I'm not sure about real work\n    GTEST_SKIP() << \"TODO Windows\";\n#elif __linux__\n    // TODO: Inputtino waiting https://github.com/games-on-whales/inputtino/issues/6 is resolved.\n    GTEST_SKIP() << \"TODO Inputtino\";\n#endif\n  }\n\n  void\n  TearDown() override {\n    std::this_thread::sleep_for(std::chrono::milliseconds(200));\n  }\n};\n\nINSTANTIATE_TEST_SUITE_P(\n  MouseInputs,\n  MouseHIDTest,\n  testing::Values(\n    util::point_t { 40, 40 },\n    util::point_t { 70, 150 }));\n// todo: add tests for hitting screen edges\n\nTEST_P(MouseHIDTest, MoveInputTest) {\n  util::point_t mouse_delta = GetParam();\n\n  BOOST_LOG(tests) << \"MoveInputTest:: got param: \" << mouse_delta;\n  platf::input_t input = platf::input();\n  BOOST_LOG(tests) << \"MoveInputTest:: init input\";\n\n  BOOST_LOG(tests) << \"MoveInputTest:: get current mouse loc\";\n  auto old_loc = platf::get_mouse_loc(input);\n  BOOST_LOG(tests) << \"MoveInputTest:: got current mouse loc: \" << old_loc;\n\n  BOOST_LOG(tests) << \"MoveInputTest:: move: \" << mouse_delta;\n  platf::move_mouse(input, mouse_delta.x, mouse_delta.y);\n  std::this_thread::sleep_for(std::chrono::milliseconds(200));\n  BOOST_LOG(tests) << \"MoveInputTest:: moved: \" << mouse_delta;\n\n  BOOST_LOG(tests) << \"MoveInputTest:: get updated mouse loc\";\n  auto new_loc = platf::get_mouse_loc(input);\n  BOOST_LOG(tests) << \"MoveInputTest:: got updated mouse loc: \" << new_loc;\n\n  bool has_input_moved = old_loc.x != new_loc.x && old_loc.y != new_loc.y;\n\n  if (!has_input_moved) {\n    BOOST_LOG(tests) << \"MoveInputTest:: haven't moved\";\n  }\n  else {\n    BOOST_LOG(tests) << \"MoveInputTest:: moved\";\n  }\n\n  EXPECT_TRUE(has_input_moved);\n\n  // Verify we moved as much as we requested\n  EXPECT_EQ(new_loc.x - old_loc.x, mouse_delta.x);\n  EXPECT_EQ(new_loc.y - old_loc.y, mouse_delta.y);\n}\n\nTEST_P(MouseHIDTest, AbsMoveInputTest) {\n  util::point_t mouse_pos = GetParam();\n  BOOST_LOG(tests) << \"AbsMoveInputTest:: got param: \" << mouse_pos;\n\n  platf::input_t input = platf::input();\n  BOOST_LOG(tests) << \"AbsMoveInputTest:: init input\";\n\n  BOOST_LOG(tests) << \"AbsMoveInputTest:: get current mouse loc\";\n  auto old_loc = platf::get_mouse_loc(input);\n  BOOST_LOG(tests) << \"AbsMoveInputTest:: got current mouse loc: \" << old_loc;\n\n#ifdef _WIN32\n  platf::touch_port_t abs_port {\n    0, 0,\n    65535, 65535\n  };\n#elif __linux__\n  platf::touch_port_t abs_port {\n    0, 0,\n    19200, 12000\n  };\n#else\n  platf::touch_port_t abs_port {};\n#endif\n  BOOST_LOG(tests) << \"AbsMoveInputTest:: move: \" << mouse_pos;\n  platf::abs_mouse(input, abs_port, mouse_pos.x, mouse_pos.y);\n  std::this_thread::sleep_for(std::chrono::milliseconds(200));\n  BOOST_LOG(tests) << \"AbsMoveInputTest:: moved: \" << mouse_pos;\n\n  BOOST_LOG(tests) << \"AbsMoveInputTest:: get updated mouse loc\";\n  auto new_loc = platf::get_mouse_loc(input);\n  BOOST_LOG(tests) << \"AbsMoveInputTest:: got updated mouse loc: \" << new_loc;\n\n  bool has_input_moved = old_loc.x != new_loc.x || old_loc.y != new_loc.y;\n\n  if (!has_input_moved) {\n    BOOST_LOG(tests) << \"AbsMoveInputTest:: haven't moved\";\n  }\n  else {\n    BOOST_LOG(tests) << \"AbsMoveInputTest:: moved\";\n  }\n\n  EXPECT_TRUE(has_input_moved);\n\n  // Verify we moved to the absolute coordinate\n  EXPECT_EQ(new_loc.x, mouse_pos.x);\n  EXPECT_EQ(new_loc.y, mouse_pos.y);\n}\n"
  },
  {
    "path": "tests/unit/test_network.cpp",
    "content": "/**\n * @file tests/unit/test_network.cpp\n * @brief Test src/network.*\n */\n#include <src/network.h>\n\n#include \"../tests_common.h\"\n\nstruct MdnsInstanceNameTest: testing::TestWithParam<std::tuple<std::string, std::string>> {};\n\nTEST_P(MdnsInstanceNameTest, Run) {\n  auto [input, expected] = GetParam();\n  ASSERT_EQ(net::mdns_instance_name(input), expected);\n}\n\nINSTANTIATE_TEST_SUITE_P(\n  MdnsInstanceNameTests,\n  MdnsInstanceNameTest,\n  testing::Values(\n    std::make_tuple(\"shortname-123\", \"shortname-123\"),\n    std::make_tuple(\"space 123\", \"space-123\"),\n    std::make_tuple(\"hostname.domain.test\", \"hostname\"),\n    std::make_tuple(\"&\", \"Sunshine\"),\n    std::make_tuple(\"\", \"Sunshine\"),\n    std::make_tuple(\"😁\", \"Sunshine\"),\n    std::make_tuple(std::string(128, 'a'), std::string(63, 'a'))\n  )\n);\n\n/**\n * @brief Test fixture for bind_address tests with setup/teardown\n */\nclass BindAddressTest: public ::testing::Test {\nprotected:\n  std::string original_bind_address;\n\n  void SetUp() override {\n    // Save the original bind_address config\n    original_bind_address = config::sunshine.bind_address;\n  }\n\n  void TearDown() override {\n    // Restore the original bind_address config\n    config::sunshine.bind_address = original_bind_address;\n  }\n};\n\n/**\n * @brief Test that get_bind_address returns wildcard when bind_address is not configured\n */\nTEST_F(BindAddressTest, DefaultBehaviorIPv4) {\n  // Clear bind_address to test the default behavior\n  config::sunshine.bind_address = \"\";\n\n  const auto bind_addr = net::get_bind_address(net::af_e::IPV4);\n  ASSERT_EQ(bind_addr, \"0.0.0.0\");\n}\n\n/**\n * @brief Test that get_bind_address returns wildcard when bind_address is not configured (IPv6)\n */\nTEST_F(BindAddressTest, DefaultBehaviorIPv6) {\n  // Clear bind_address to test the default behavior\n  config::sunshine.bind_address = \"\";\n\n  const auto bind_addr = net::get_bind_address(net::af_e::BOTH);\n  ASSERT_EQ(bind_addr, \"::\");\n}\n\n/**\n * @brief Test that get_bind_address returns configured IPv4 address\n */\nTEST_F(BindAddressTest, ConfiguredIPv4Address) {\n  // Set a specific IPv4 address\n  config::sunshine.bind_address = \"192.168.1.100\";\n\n  const auto bind_addr = net::get_bind_address(net::af_e::IPV4);\n  ASSERT_EQ(bind_addr, \"192.168.1.100\");\n}\n\n/**\n * @brief Test that get_bind_address returns configured IPv6 address\n */\nTEST_F(BindAddressTest, ConfiguredIPv6Address) {\n  // Set a specific IPv6 address\n  config::sunshine.bind_address = \"::1\";\n\n  const auto bind_addr = net::get_bind_address(net::af_e::BOTH);\n  ASSERT_EQ(bind_addr, \"::1\");\n}\n\n/**\n * @brief Test that get_bind_address returns configured address regardless of address family\n */\nTEST_F(BindAddressTest, ConfiguredAddressOverridesFamily) {\n  // Set a specific IPv6 address but request IPv4 family\n  // The configured address should still be returned\n  config::sunshine.bind_address = \"2001:db8::1\";\n\n  const auto bind_addr = net::get_bind_address(net::af_e::IPV4);\n  ASSERT_EQ(bind_addr, \"2001:db8::1\");\n}\n\n/**\n * @brief Test with loopback addresses\n */\nTEST_F(BindAddressTest, LoopbackAddresses) {\n  // Test IPv4 loopback\n  config::sunshine.bind_address = \"127.0.0.1\";\n  const auto bind_addr_v4 = net::get_bind_address(net::af_e::IPV4);\n  ASSERT_EQ(bind_addr_v4, \"127.0.0.1\");\n\n  // Test IPv6 loopback\n  config::sunshine.bind_address = \"::1\";\n  const auto bind_addr_v6 = net::get_bind_address(net::af_e::BOTH);\n  ASSERT_EQ(bind_addr_v6, \"::1\");\n}\n\n/**\n * @brief Test with link-local addresses\n */\nTEST_F(BindAddressTest, LinkLocalAddresses) {\n  // Test IPv4 link-local\n  config::sunshine.bind_address = \"169.254.1.1\";\n  const auto bind_addr_v4 = net::get_bind_address(net::af_e::IPV4);\n  ASSERT_EQ(bind_addr_v4, \"169.254.1.1\");\n\n  // Test IPv6 link-local\n  config::sunshine.bind_address = \"fe80::1\";\n  const auto bind_addr_v6 = net::get_bind_address(net::af_e::BOTH);\n  ASSERT_EQ(bind_addr_v6, \"fe80::1\");\n}\n\n/**\n * @brief Test that af_to_any_address_string still works correctly\n */\nTEST_F(BindAddressTest, WildcardAddressFunction) {\n  ASSERT_EQ(net::af_to_any_address_string(net::af_e::IPV4), \"0.0.0.0\");\n  ASSERT_EQ(net::af_to_any_address_string(net::af_e::BOTH), \"::\");\n}\n"
  },
  {
    "path": "tests/unit/test_rswrapper.cpp",
    "content": "/**\n * @file tests/unit/test_rswrapper.cpp\n * @brief Test src/rswrapper.*\n */\nextern \"C\" {\n#include <src/rswrapper.h>\n}\n\n#include \"../tests_common.h\"\n\nTEST(ReedSolomonWrapperTests, InitTest) {\n  reed_solomon_init();\n\n  // Ensure all function pointers were populated\n  ASSERT_NE(reed_solomon_new, nullptr);\n  ASSERT_NE(reed_solomon_release, nullptr);\n  ASSERT_NE(reed_solomon_encode, nullptr);\n  ASSERT_NE(reed_solomon_decode, nullptr);\n}\n\nTEST(ReedSolomonWrapperTests, EncodeTest) {\n  reed_solomon_init();\n\n  auto rs = reed_solomon_new(1, 1);\n  ASSERT_NE(rs, nullptr);\n\n  uint8_t dataShard[16] = {};\n  uint8_t fecShard[16] = {};\n\n  // If we picked the incorrect ISA in our wrapper, we should crash here\n  uint8_t *shardPtrs[2] = { dataShard, fecShard };\n  auto ret = reed_solomon_encode(rs, shardPtrs, 2, sizeof(dataShard));\n  ASSERT_EQ(ret, 0);\n\n  reed_solomon_release(rs);\n}\n"
  },
  {
    "path": "tests/unit/test_stream.cpp",
    "content": "/**\n * @file tests/unit/test_stream.cpp\n * @brief Test src/stream.*\n */\n\n#include <cstdint>\n#include <functional>\n#include <string>\n#include <vector>\n\nnamespace stream {\n  std::vector<uint8_t>\n  concat_and_insert(uint64_t insert_size, uint64_t slice_size, const std::string_view &data1, const std::string_view &data2);\n}\n\n#include \"../tests_common.h\"\n\nTEST(ConcatAndInsertTests, ConcatNoInsertionTest) {\n  char b1[] = { 'a', 'b' };\n  char b2[] = { 'c', 'd', 'e' };\n  auto res = stream::concat_and_insert(0, 2, std::string_view { b1, sizeof(b1) }, std::string_view { b2, sizeof(b2) });\n  auto expected = std::vector<uint8_t> { 'a', 'b', 'c', 'd', 'e' };\n  ASSERT_EQ(res, expected);\n}\n\nTEST(ConcatAndInsertTests, ConcatLargeStrideTest) {\n  char b1[] = { 'a', 'b' };\n  char b2[] = { 'c', 'd', 'e' };\n  auto res = stream::concat_and_insert(1, sizeof(b1) + sizeof(b2) + 1, std::string_view { b1, sizeof(b1) }, std::string_view { b2, sizeof(b2) });\n  auto expected = std::vector<uint8_t> { 0, 'a', 'b', 'c', 'd', 'e' };\n  ASSERT_EQ(res, expected);\n}\n\nTEST(ConcatAndInsertTests, ConcatSmallStrideTest) {\n  char b1[] = { 'a', 'b' };\n  char b2[] = { 'c', 'd', 'e' };\n  auto res = stream::concat_and_insert(1, 1, std::string_view { b1, sizeof(b1) }, std::string_view { b2, sizeof(b2) });\n  auto expected = std::vector<uint8_t> { 0, 'a', 0, 'b', 0, 'c', 0, 'd', 0, 'e' };\n  ASSERT_EQ(res, expected);\n}\n"
  },
  {
    "path": "tests/unit/test_video.cpp",
    "content": "/**\n * @file tests/unit/test_video.cpp\n * @brief Test src/video.*.\n */\n#include <src/video.h>\n\n#include \"../tests_common.h\"\n\nstruct EncoderTest: PlatformTestSuite, testing::WithParamInterface<video::encoder_t *> {\n  void\n  SetUp() override {\n    auto &encoder = *GetParam();\n    if (!video::validate_encoder(encoder, false)) {\n      // Encoder failed validation,\n      // if it's software - fail, otherwise skip\n      if (encoder.name == \"software\") {\n        FAIL() << \"Software encoder not available\";\n      }\n      else {\n        GTEST_SKIP() << \"Encoder not available\";\n      }\n    }\n  }\n};\n\nINSTANTIATE_TEST_SUITE_P(\n  EncoderVariants,\n  EncoderTest,\n  testing::Values(\n#if !defined(__APPLE__)\n    &video::nvenc,\n#endif\n#ifdef _WIN32\n    &video::amdvce,\n    &video::quicksync,\n#endif\n#ifdef __linux__\n    &video::vaapi,\n#endif\n#ifdef __APPLE__\n    &video::videotoolbox,\n#endif\n    &video::software),\n  [](const auto &info) { return std::string(info.param->name); });\n\nTEST_P(EncoderTest, ValidateEncoder) {\n  // todo:: test something besides fixture setup\n}\n"
  },
  {
    "path": "tests/unit/test_webhook.cpp",
    "content": "/**\n * @file tests/unit/test_webhook.cpp\n * @brief Test webhook functionality.\n */\n\n#include <string>\n#include <unordered_map>\n\n#include \"../tests_common.h\"\n#include \"webhook.h\"\n#include \"config.h\"\n\nusing namespace std::literals;\n\nstruct WebhookTest : testing::Test {\n  void SetUp() override {\n    // Reset webhook config to defaults\n    config::webhook.enabled = false;\n    config::webhook.url = \"\";\n    config::webhook.skip_ssl_verify = false;\n    config::webhook.timeout = std::chrono::milliseconds(1000);\n  }\n};\n\nTEST_F(WebhookTest, IsEnabledCheck) {\n  // Test when disabled\n  EXPECT_FALSE(webhook::is_enabled());\n  \n  // Test when enabled but no URL\n  config::webhook.enabled = true;\n  EXPECT_FALSE(webhook::is_enabled());\n  \n  // Test when enabled with URL\n  config::webhook.url = \"https://example.com/webhook\";\n  EXPECT_TRUE(webhook::is_enabled());\n}\n\nTEST_F(WebhookTest, AlertMessageLocalization) {\n  // Test Chinese messages\n  config::sunshine.locale = \"zh\";\n  bool is_chinese = true;\n  \n  EXPECT_EQ(webhook::get_alert_message(webhook::event_type_t::CONFIG_PIN_SUCCESS, is_chinese), \"🔗 配置配对成功\");\n  EXPECT_EQ(webhook::get_alert_message(webhook::event_type_t::NV_APP_LAUNCH, is_chinese), \"🚀 应用启动\");\n  EXPECT_EQ(webhook::get_alert_message(webhook::event_type_t::NV_APP_RESUME, is_chinese), \"▶️ 应用恢复\");\n  \n  // Test English messages\n  is_chinese = false;\n  \n  EXPECT_EQ(webhook::get_alert_message(webhook::event_type_t::CONFIG_PIN_SUCCESS, is_chinese), \"🔗 Config pairing successful\");\n  EXPECT_EQ(webhook::get_alert_message(webhook::event_type_t::NV_APP_LAUNCH, is_chinese), \"🚀 application launched\");\n  EXPECT_EQ(webhook::get_alert_message(webhook::event_type_t::NV_APP_RESUME, is_chinese), \"▶️ application resumed\");\n}\n\nTEST_F(WebhookTest, JsonStringSanitization) {\n  // Test basic escaping\n  EXPECT_EQ(webhook::sanitize_json_string(\"Hello \\\"World\\\"\"), \"Hello \\\\\\\"World\\\\\\\"\");\n  EXPECT_EQ(webhook::sanitize_json_string(\"Line1\\nLine2\"), \"Line1\\\\nLine2\");\n  EXPECT_EQ(webhook::sanitize_json_string(\"Tab\\tHere\"), \"Tab\\\\tHere\");\n  EXPECT_EQ(webhook::sanitize_json_string(\"Back\\\\slash\"), \"Back\\\\\\\\slash\");\n  \n  // Test control characters\n  EXPECT_EQ(webhook::sanitize_json_string(\"Text\\x01\\x02\\x03\"), \"Text\");\n  \n  // Test empty string\n  EXPECT_EQ(webhook::sanitize_json_string(\"\"), \"\");\n  \n  // Test special characters that should be preserved\n  EXPECT_EQ(webhook::sanitize_json_string(\"Hello 世界\"), \"Hello 世界\");\n  EXPECT_EQ(webhook::sanitize_json_string(\"Emoji 🚀\"), \"Emoji 🚀\");\n}\n\nTEST_F(WebhookTest, TimestampFormat) {\n  std::string timestamp = webhook::get_current_timestamp();\n  \n  // Check format: YYYY-MM-DDTHH:MM:SS.sss\n  EXPECT_EQ(timestamp.length(), 23);\n  EXPECT_EQ(timestamp[4], '-');\n  EXPECT_EQ(timestamp[7], '-');\n  EXPECT_EQ(timestamp[10], 'T');\n  EXPECT_EQ(timestamp[13], ':');\n  EXPECT_EQ(timestamp[16], ':');\n  EXPECT_EQ(timestamp[19], '.');\n}\n\nTEST_F(WebhookTest, EventStructure) {\n  webhook::event_t event;\n  event.type = webhook::event_type_t::NV_APP_LAUNCH;\n  event.alert_type = \"nv_app_launch\";\n  event.timestamp = \"2024-01-01T12:00:00.000Z\";\n  event.client_name = \"Test Client\";\n  event.client_ip = \"192.168.1.100\";\n  event.app_name = \"Test App\";\n  event.app_id = 123;\n  event.session_id = \"session123\";\n  event.extra_data = {{\"resolution\", \"1920x1080\"}, {\"fps\", \"60\"}};\n  \n  EXPECT_EQ(event.type, webhook::event_type_t::NV_APP_LAUNCH);\n  EXPECT_EQ(event.alert_type, \"nv_app_launch\");\n  EXPECT_EQ(event.client_name, \"Test Client\");\n  EXPECT_EQ(event.app_id, 123);\n  EXPECT_EQ(event.extra_data.size(), 2);\n  EXPECT_EQ(event.extra_data[\"resolution\"], \"1920x1080\");\n  EXPECT_EQ(event.extra_data[\"fps\"], \"60\");\n}\n\nTEST_F(WebhookTest, GenerateWebhookJson) {\n  // Test Chinese JSON generation\n  config::sunshine.locale = \"zh\";\n  bool is_chinese = true;\n  \n  webhook::event_t event;\n  event.type = webhook::event_type_t::NV_APP_LAUNCH;\n  event.alert_type = \"nv_app_launch\";\n  event.timestamp = \"2024-01-01T12:00:00.000Z\";\n  event.client_name = \"Test Client\";\n  event.client_ip = \"192.168.1.100\";\n  event.app_name = \"Test App\";\n  event.app_id = 123;\n  event.session_id = \"session123\";\n  event.extra_data = {{\"resolution\", \"1920x1080\"}, {\"fps\", \"60\"}, {\"host_audio\", \"true\"}};\n  \n  std::string json = webhook::generate_webhook_json(event, is_chinese);\n  \n  // Check that JSON contains expected Chinese content\n  EXPECT_TRUE(json.find(\"🚀 应用启动\") != std::string::npos);\n  EXPECT_TRUE(json.find(\"应用: Test App\") != std::string::npos);\n  EXPECT_TRUE(json.find(\"客户端: Test Client\") != std::string::npos);\n  EXPECT_TRUE(json.find(\"IP地址: 192.168.1.100\") != std::string::npos);\n  EXPECT_TRUE(json.find(\"分辨率: 1920x1080\") != std::string::npos);\n  EXPECT_TRUE(json.find(\"帧率: 60\") != std::string::npos);\n  EXPECT_TRUE(json.find(\"音频: 启用\") != std::string::npos);\n  EXPECT_TRUE(json.find(\"时间: 2024-01-01T12:00:00.000Z\") != std::string::npos);\n  \n  // Test English JSON generation\n  is_chinese = false;\n  std::string json_en = webhook::generate_webhook_json(event, is_chinese);\n  \n  // Check that JSON contains expected English content\n  EXPECT_TRUE(json_en.find(\"🚀 application launched\") != std::string::npos);\n  EXPECT_TRUE(json_en.find(\"App: Test App\") != std::string::npos);\n  EXPECT_TRUE(json_en.find(\"Client: Test Client\") != std::string::npos);\n  EXPECT_TRUE(json_en.find(\"IP: 192.168.1.100\") != std::string::npos);\n  EXPECT_TRUE(json_en.find(\"Resolution: 1920x1080\") != std::string::npos);\n  EXPECT_TRUE(json_en.find(\"FPS: 60\") != std::string::npos);\n  EXPECT_TRUE(json_en.find(\"Audio: Enabled\") != std::string::npos);\n  EXPECT_TRUE(json_en.find(\"Time: 2024-01-01T12:00:00.000Z\") != std::string::npos);\n}\n\nTEST_F(WebhookTest, GenerateWebhookJsonPairing) {\n  // Test pairing event JSON generation\n  webhook::event_t event;\n  event.type = webhook::event_type_t::CONFIG_PIN_SUCCESS;\n  event.alert_type = \"config_pair_success\";\n  event.timestamp = \"2024-01-01T12:00:00.000Z\";\n  event.client_name = \"My Phone\";\n  event.client_ip = \"192.168.1.50\";\n  event.extra_data = {};\n  \n  std::string json = webhook::generate_webhook_json(event, true);\n  \n  // Check that JSON contains pairing information\n  EXPECT_TRUE(json.find(\"🔗 配置配对成功\") != std::string::npos);\n  EXPECT_TRUE(json.find(\"设备名称: My Phone\") != std::string::npos);\n  EXPECT_TRUE(json.find(\"IP地址: 192.168.1.50\") != std::string::npos);\n}\n\nTEST_F(WebhookTest, RateLimiting) {\n  // Test rate limiting functionality\n  config::webhook.enabled = true;\n  config::webhook.url = \"http://example.com/webhook\";\n  \n  // Initially should not be rate limited\n  EXPECT_FALSE(webhook::is_rate_limited());\n  \n  // Record some successful sends (simulate)\n  for (int i = 0; i < 10; ++i) {\n    webhook::record_successful_send();\n  }\n  \n  // Should now be rate limited\n  EXPECT_TRUE(webhook::is_rate_limited());\n}\n\nTEST_F(WebhookTest, ThreadManagement) {\n  // Test thread management functionality\n  config::webhook.enabled = true;\n  config::webhook.url = \"http://example.com/webhook\";\n  \n  // Initially should be able to create threads\n  EXPECT_TRUE(webhook::can_create_thread());\n  \n  // Register some threads (simulate)\n  for (int i = 0; i < 10; ++i) {\n    webhook::register_thread();\n  }\n  \n  // Should now be at thread limit\n  EXPECT_FALSE(webhook::can_create_thread());\n  \n  // Unregister some threads\n  for (int i = 0; i < 5; ++i) {\n    webhook::unregister_thread();\n  }\n  \n  // Should now be able to create threads again\n  EXPECT_TRUE(webhook::can_create_thread());\n}\n\nTEST_F(WebhookTest, ThreadLimitEnforcement) {\n  // Test that thread limit is actually enforced\n  config::webhook.enabled = true;\n  config::webhook.url = \"http://example.com/webhook\";\n  \n  // Create a simple event\n  webhook::event_t event;\n  event.type = webhook::event_type_t::CONFIG_PIN_SUCCESS;\n  event.alert_type = \"test\";\n  event.timestamp = \"2024-01-01T12:00:00.000Z\";\n  \n  // Fill up to thread limit\n  for (int i = 0; i < 10; ++i) {\n    webhook::register_thread();\n  }\n  \n  // Now try to send an event - should be blocked by thread limit\n  // This is a bit tricky to test since send_event_async is async,\n  // but we can at least verify the can_create_thread check\n  EXPECT_FALSE(webhook::can_create_thread());\n  \n  // Clean up\n  for (int i = 0; i < 10; ++i) {\n    webhook::unregister_thread();\n  }\n}\n\nTEST_F(WebhookTest, ThreadRegistrationOrder) {\n  // Test that thread registration happens before thread creation\n  config::webhook.enabled = true;\n  config::webhook.url = \"http://example.com/webhook\";\n  \n  // Initially should be able to create threads\n  EXPECT_TRUE(webhook::can_create_thread());\n  \n  // Register 9 threads\n  for (int i = 0; i < 9; ++i) {\n    webhook::register_thread();\n  }\n  \n  // Should still be able to create one more thread\n  EXPECT_TRUE(webhook::can_create_thread());\n  \n  // Register the 10th thread\n  webhook::register_thread();\n  \n  // Now should be at limit\n  EXPECT_FALSE(webhook::can_create_thread());\n  \n  // Clean up\n  for (int i = 0; i < 10; ++i) {\n    webhook::unregister_thread();\n  }\n}\n\nTEST_F(WebhookTest, RateLimitNotificationBypass) {\n  // Test that rate limit notification bypasses thread limit\n  config::webhook.enabled = true;\n  config::webhook.url = \"http://example.com/webhook\";\n  \n  // Fill up to thread limit\n  for (int i = 0; i < 10; ++i) {\n    webhook::register_thread();\n  }\n  \n  // Should be at thread limit\n  EXPECT_FALSE(webhook::can_create_thread());\n  \n  // Rate limit notification should still be able to send\n  // (This is tested by the fact that send_rate_limit_notification doesn't check can_create_thread)\n  // We can't easily test the actual sending, but we can verify the function exists and doesn't crash\n  webhook::send_rate_limit_notification();\n  \n  // Clean up\n  for (int i = 0; i < 10; ++i) {\n    webhook::unregister_thread();\n  }\n}\n"
  },
  {
    "path": "tests/unit/test_webhook_config.cpp",
    "content": "/**\n * @file tests/unit/test_webhook_config.cpp\n * @brief Test webhook configuration parsing.\n */\n\n#include <string>\n#include <unordered_map>\n\n#include \"../tests_common.h\"\n#include \"config.h\"\n\nusing namespace std::literals;\n\nstruct WebhookConfigTest : testing::Test {\n  void SetUp() override {\n    // Reset webhook config to defaults\n    config::webhook.enabled = false;\n    config::webhook.url = \"\";\n    config::webhook.skip_ssl_verify = false;\n    config::webhook.timeout = std::chrono::milliseconds(1000);\n  }\n};\n\nTEST_F(WebhookConfigTest, DefaultValues) {\n  EXPECT_FALSE(config::webhook.enabled);\n  EXPECT_EQ(config::webhook.url, \"\");\n  EXPECT_FALSE(config::webhook.skip_ssl_verify);\n  EXPECT_EQ(config::webhook.timeout.count(), 1000);\n}\n\nTEST_F(WebhookConfigTest, ParseWebhookEnabled) {\n  std::unordered_map<std::string, std::string> vars;\n  vars[\"webhook_enabled\"] = \"true\";\n  \n  config::apply_config(std::move(vars));\n  \n  EXPECT_TRUE(config::webhook.enabled);\n}\n\nTEST_F(WebhookConfigTest, ParseWebhookUrl) {\n  std::unordered_map<std::string, std::string> vars;\n  vars[\"webhook_url\"] = \"https://example.com/webhook\";\n  \n  config::apply_config(std::move(vars));\n  \n  EXPECT_EQ(config::webhook.url, \"https://example.com/webhook\");\n}\n\nTEST_F(WebhookConfigTest, ParseWebhookSkipSslVerify) {\n  std::unordered_map<std::string, std::string> vars;\n  vars[\"webhook_skip_ssl_verify\"] = \"true\";\n  \n  config::apply_config(std::move(vars));\n  \n  EXPECT_TRUE(config::webhook.skip_ssl_verify);\n}\n\nTEST_F(WebhookConfigTest, ParseWebhookTimeout) {\n  std::unordered_map<std::string, std::string> vars;\n  vars[\"webhook_timeout\"] = \"2000\";\n  \n  config::apply_config(std::move(vars));\n  \n  EXPECT_EQ(config::webhook.timeout.count(), 2000);\n}\n\nTEST_F(WebhookConfigTest, ParseWebhookTimeoutOutOfRange) {\n  std::unordered_map<std::string, std::string> vars;\n  vars[\"webhook_timeout\"] = \"10000\";  // Out of range (100-5000)\n  \n  config::apply_config(std::move(vars));\n  \n  // Should remain at default value\n  EXPECT_EQ(config::webhook.timeout.count(), 1000);\n}\n\nTEST_F(WebhookConfigTest, ParseWebhookTimeoutTooLow) {\n  std::unordered_map<std::string, std::string> vars;\n  vars[\"webhook_timeout\"] = \"50\";  // Too low (100-5000)\n  \n  config::apply_config(std::move(vars));\n  \n  // Should remain at default value\n  EXPECT_EQ(config::webhook.timeout.count(), 1000);\n}\n\nTEST_F(WebhookConfigTest, ParseAllWebhookSettings) {\n  std::unordered_map<std::string, std::string> vars;\n  vars[\"webhook_enabled\"] = \"true\";\n  vars[\"webhook_url\"] = \"https://test.com/hook\";\n  vars[\"webhook_skip_ssl_verify\"] = \"true\";\n  vars[\"webhook_timeout\"] = \"3000\";\n  \n  config::apply_config(std::move(vars));\n  \n  EXPECT_TRUE(config::webhook.enabled);\n  EXPECT_EQ(config::webhook.url, \"https://test.com/hook\");\n  EXPECT_TRUE(config::webhook.skip_ssl_verify);\n  EXPECT_EQ(config::webhook.timeout.count(), 3000);\n}\n\nTEST_F(WebhookConfigTest, ParseWebhookTimeoutBoundaryValues) {\n  // Test minimum valid value\n  {\n    std::unordered_map<std::string, std::string> vars;\n    vars[\"webhook_timeout\"] = \"100\";  // Minimum valid value\n    \n    config::apply_config(std::move(vars));\n    \n    EXPECT_EQ(config::webhook.timeout.count(), 100);\n  }\n  \n  // Test maximum valid value\n  {\n    std::unordered_map<std::string, std::string> vars;\n    vars[\"webhook_timeout\"] = \"5000\";  // Maximum valid value\n    \n    config::apply_config(std::move(vars));\n    \n    EXPECT_EQ(config::webhook.timeout.count(), 5000);\n  }\n}\n\nTEST_F(WebhookConfigTest, ParseWebhookBooleanVariations) {\n  // Test various boolean representations for enabled\n  std::vector<std::string> true_values = {\"true\", \"True\", \"TRUE\", \"1\", \"yes\", \"Yes\", \"YES\", \"enable\", \"enabled\", \"on\"};\n  std::vector<std::string> false_values = {\"false\", \"False\", \"FALSE\", \"0\", \"no\", \"No\", \"NO\", \"disable\", \"disabled\", \"off\"};\n  \n  for (const auto& value : true_values) {\n    std::unordered_map<std::string, std::string> vars;\n    vars[\"webhook_enabled\"] = value;\n    \n    config::apply_config(std::move(vars));\n    \n    EXPECT_TRUE(config::webhook.enabled) << \"Failed for value: \" << value;\n  }\n  \n  for (const auto& value : false_values) {\n    std::unordered_map<std::string, std::string> vars;\n    vars[\"webhook_enabled\"] = value;\n    \n    config::apply_config(std::move(vars));\n    \n    EXPECT_FALSE(config::webhook.enabled) << \"Failed for value: \" << value;\n  }\n}\n"
  },
  {
    "path": "third-party/.clang-format-ignore",
    "content": ""
  },
  {
    "path": "third-party/glad/include/EGL/eglplatform.h",
    "content": "#ifndef __eglplatform_h_\n#define __eglplatform_h_\n\n/*\n** Copyright 2007-2020 The Khronos Group Inc.\n** SPDX-License-Identifier: Apache-2.0\n*/\n\n/* Platform-specific types and definitions for egl.h\n *\n * Adopters may modify khrplatform.h and this file to suit their platform.\n * You are encouraged to submit all modifications to the Khronos group so that\n * they can be included in future versions of this file.  Please submit changes\n * by filing an issue or pull request on the public Khronos EGL Registry, at\n * https://www.github.com/KhronosGroup/EGL-Registry/\n */\n\n#include <KHR/khrplatform.h>\n\n/* Macros used in EGL function prototype declarations.\n *\n * EGL functions should be prototyped as:\n *\n * EGLAPI return-type EGLAPIENTRY eglFunction(arguments);\n * typedef return-type (EXPAPIENTRYP PFNEGLFUNCTIONPROC) (arguments);\n *\n * KHRONOS_APICALL and KHRONOS_APIENTRY are defined in KHR/khrplatform.h\n */\n\n#ifndef EGLAPI\n  #define EGLAPI KHRONOS_APICALL\n#endif\n\n#ifndef EGLAPIENTRY\n  #define EGLAPIENTRY KHRONOS_APIENTRY\n#endif\n#define EGLAPIENTRYP EGLAPIENTRY *\n\n/* The types NativeDisplayType, NativeWindowType, and NativePixmapType\n * are aliases of window-system-dependent types, such as X Display * or\n * Windows Device Context. They must be defined in platform-specific\n * code below. The EGL-prefixed versions of Native*Type are the same\n * types, renamed in EGL 1.3 so all types in the API start with \"EGL\".\n *\n * Khronos STRONGLY RECOMMENDS that you use the default definitions\n * provided below, since these changes affect both binary and source\n * portability of applications using EGL running on different EGL\n * implementations.\n */\n\n#if defined(EGL_NO_PLATFORM_SPECIFIC_TYPES)\n\ntypedef void *EGLNativeDisplayType;\ntypedef void *EGLNativePixmapType;\ntypedef void *EGLNativeWindowType;\n\n#elif defined(_WIN32) || defined(__VC32__) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__) /* Win32 and WinCE */\n  #ifndef WIN32_LEAN_AND_MEAN\n    #define WIN32_LEAN_AND_MEAN 1\n  #endif\n  #include <windows.h>\n\ntypedef HDC EGLNativeDisplayType;\ntypedef HBITMAP EGLNativePixmapType;\ntypedef HWND EGLNativeWindowType;\n\n#elif defined(__EMSCRIPTEN__)\n\ntypedef int EGLNativeDisplayType;\ntypedef int EGLNativePixmapType;\ntypedef int EGLNativeWindowType;\n\n#elif defined(__WINSCW__) || defined(__SYMBIAN32__) /* Symbian */\n\ntypedef int EGLNativeDisplayType;\ntypedef void *EGLNativePixmapType;\ntypedef void *EGLNativeWindowType;\n\n#elif defined(WL_EGL_PLATFORM)\n\ntypedef struct wl_display *EGLNativeDisplayType;\ntypedef struct wl_egl_pixmap *EGLNativePixmapType;\ntypedef struct wl_egl_window *EGLNativeWindowType;\n\n#elif defined(__GBM__)\n\ntypedef struct gbm_device *EGLNativeDisplayType;\ntypedef struct gbm_bo *EGLNativePixmapType;\ntypedef void *EGLNativeWindowType;\n\n#elif defined(__ANDROID__) || defined(ANDROID)\n\nstruct ANativeWindow;\nstruct egl_native_pixmap_t;\n\ntypedef void *EGLNativeDisplayType;\ntypedef struct egl_native_pixmap_t *EGLNativePixmapType;\ntypedef struct ANativeWindow *EGLNativeWindowType;\n\n#elif defined(USE_OZONE)\n\ntypedef intptr_t EGLNativeDisplayType;\ntypedef intptr_t EGLNativePixmapType;\ntypedef intptr_t EGLNativeWindowType;\n\n#elif defined(__unix__) && defined(EGL_NO_X11)\n\ntypedef void *EGLNativeDisplayType;\ntypedef khronos_uintptr_t EGLNativePixmapType;\ntypedef khronos_uintptr_t EGLNativeWindowType;\n\n#elif defined(__unix__) || defined(USE_X11)\n\n  /* X11 (tentative)  */\n  #include <X11/Xlib.h>\n  #include <X11/Xutil.h>\n\ntypedef Display *EGLNativeDisplayType;\ntypedef Pixmap EGLNativePixmapType;\ntypedef Window EGLNativeWindowType;\n\n#elif defined(__APPLE__)\n\ntypedef int EGLNativeDisplayType;\ntypedef void *EGLNativePixmapType;\ntypedef void *EGLNativeWindowType;\n\n#elif defined(__HAIKU__)\n\n  #include <kernel/image.h>\n\ntypedef void *EGLNativeDisplayType;\ntypedef khronos_uintptr_t EGLNativePixmapType;\ntypedef khronos_uintptr_t EGLNativeWindowType;\n\n#elif defined(__Fuchsia__)\n\ntypedef void *EGLNativeDisplayType;\ntypedef khronos_uintptr_t EGLNativePixmapType;\ntypedef khronos_uintptr_t EGLNativeWindowType;\n\n#else\n  #error \"Platform not recognized\"\n#endif\n\n/* EGL 1.2 types, renamed for consistency in EGL 1.3 */\ntypedef EGLNativeDisplayType NativeDisplayType;\ntypedef EGLNativePixmapType NativePixmapType;\ntypedef EGLNativeWindowType NativeWindowType;\n\n/* Define EGLint. This must be a signed integral type large enough to contain\n * all legal attribute names and values passed into and out of EGL, whether\n * their type is boolean, bitmask, enumerant (symbolic constant), integer,\n * handle, or other.  While in general a 32-bit integer will suffice, if\n * handles are 64 bit types, then EGLint should be defined as a signed 64-bit\n * integer type.\n */\ntypedef khronos_int32_t EGLint;\n\n/* C++ / C typecast macros for special EGL handle values */\n#if defined(__cplusplus)\n  #define EGL_CAST(type, value) (static_cast<type>(value))\n#else\n  #define EGL_CAST(type, value) ((type) (value))\n#endif\n\n#endif /* __eglplatform_h */"
  },
  {
    "path": "third-party/glad/include/KHR/khrplatform.h",
    "content": "#ifndef __khrplatform_h_\n#define __khrplatform_h_\n\n/*\n** Copyright (c) 2008-2018 The Khronos Group Inc.\n**\n** Permission is hereby granted, free of charge, to any person obtaining a\n** copy of this software and/or associated documentation files (the\n** \"Materials\"), to deal in the Materials without restriction, including\n** without limitation the rights to use, copy, modify, merge, publish,\n** distribute, sublicense, and/or sell copies of the Materials, and to\n** permit persons to whom the Materials are furnished to do so, subject to\n** the following conditions:\n**\n** The above copyright notice and this permission notice shall be included\n** in all copies or substantial portions of the Materials.\n**\n** THE MATERIALS ARE PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.\n*/\n\n/* Khronos platform-specific types and definitions.\n *\n * The master copy of khrplatform.h is maintained in the Khronos EGL\n * Registry repository at https://github.com/KhronosGroup/EGL-Registry\n * The last semantic modification to khrplatform.h was at commit ID:\n *      67a3e0864c2d75ea5287b9f3d2eb74a745936692\n *\n * Adopters may modify this file to suit their platform. Adopters are\n * encouraged to submit platform specific modifications to the Khronos\n * group so that they can be included in future versions of this file.\n * Please submit changes by filing pull requests or issues on\n * the EGL Registry repository linked above.\n *\n *\n * See the Implementer's Guidelines for information about where this file\n * should be located on your system and for more details of its use:\n *    http://www.khronos.org/registry/implementers_guide.pdf\n *\n * This file should be included as\n *        #include <KHR/khrplatform.h>\n * by Khronos client API header files that use its types and defines.\n *\n * The types in khrplatform.h should only be used to define API-specific types.\n *\n * Types defined in khrplatform.h:\n *    khronos_int8_t              signed   8  bit\n *    khronos_uint8_t             unsigned 8  bit\n *    khronos_int16_t             signed   16 bit\n *    khronos_uint16_t            unsigned 16 bit\n *    khronos_int32_t             signed   32 bit\n *    khronos_uint32_t            unsigned 32 bit\n *    khronos_int64_t             signed   64 bit\n *    khronos_uint64_t            unsigned 64 bit\n *    khronos_intptr_t            signed   same number of bits as a pointer\n *    khronos_uintptr_t           unsigned same number of bits as a pointer\n *    khronos_ssize_t             signed   size\n *    khronos_usize_t             unsigned size\n *    khronos_float_t             signed   32 bit floating point\n *    khronos_time_ns_t           unsigned 64 bit time in nanoseconds\n *    khronos_utime_nanoseconds_t unsigned time interval or absolute time in\n *                                         nanoseconds\n *    khronos_stime_nanoseconds_t signed time interval in nanoseconds\n *    khronos_boolean_enum_t      enumerated boolean type. This should\n *      only be used as a base type when a client API's boolean type is\n *      an enum. Client APIs which use an integer or other type for\n *      booleans cannot use this as the base type for their boolean.\n *\n * Tokens defined in khrplatform.h:\n *\n *    KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.\n *\n *    KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.\n *    KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.\n *\n * Calling convention macros defined in this file:\n *    KHRONOS_APICALL\n *    KHRONOS_APIENTRY\n *    KHRONOS_APIATTRIBUTES\n *\n * These may be used in function prototypes as:\n *\n *      KHRONOS_APICALL void KHRONOS_APIENTRY funcname(\n *                                  int arg1,\n *                                  int arg2) KHRONOS_APIATTRIBUTES;\n */\n\n#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)\n  #define KHRONOS_STATIC 1\n#endif\n\n/*-------------------------------------------------------------------------\n * Definition of KHRONOS_APICALL\n *-------------------------------------------------------------------------\n * This precedes the return type of the function in the function prototype.\n */\n#if defined(KHRONOS_STATIC)\n  /* If the preprocessor constant KHRONOS_STATIC is defined, make the\n   * header compatible with static linking. */\n  #define KHRONOS_APICALL\n#elif defined(_WIN32)\n  #define KHRONOS_APICALL __declspec(dllimport)\n#elif defined(__SYMBIAN32__)\n  #define KHRONOS_APICALL IMPORT_C\n#elif defined(__ANDROID__)\n  #define KHRONOS_APICALL __attribute__((visibility(\"default\")))\n#else\n  #define KHRONOS_APICALL\n#endif\n\n/*-------------------------------------------------------------------------\n * Definition of KHRONOS_APIENTRY\n *-------------------------------------------------------------------------\n * This follows the return type of the function  and precedes the function\n * name in the function prototype.\n */\n#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)\n  /* Win32 but not WinCE */\n  #define KHRONOS_APIENTRY __stdcall\n#else\n  #define KHRONOS_APIENTRY\n#endif\n\n/*-------------------------------------------------------------------------\n * Definition of KHRONOS_APIATTRIBUTES\n *-------------------------------------------------------------------------\n * This follows the closing parenthesis of the function prototype arguments.\n */\n#if defined(__ARMCC_2__)\n  #define KHRONOS_APIATTRIBUTES __softfp\n#else\n  #define KHRONOS_APIATTRIBUTES\n#endif\n\n/*-------------------------------------------------------------------------\n * basic type definitions\n *-----------------------------------------------------------------------*/\n#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)\n\n  /*\n   * Using <stdint.h>\n   */\n  #include <stdint.h>\ntypedef int32_t khronos_int32_t;\ntypedef uint32_t khronos_uint32_t;\ntypedef int64_t khronos_int64_t;\ntypedef uint64_t khronos_uint64_t;\n  #define KHRONOS_SUPPORT_INT64 1\n  #define KHRONOS_SUPPORT_FLOAT 1\n\n#elif defined(__VMS) || defined(__sgi)\n\n  /*\n   * Using <inttypes.h>\n   */\n  #include <inttypes.h>\ntypedef int32_t khronos_int32_t;\ntypedef uint32_t khronos_uint32_t;\ntypedef int64_t khronos_int64_t;\ntypedef uint64_t khronos_uint64_t;\n  #define KHRONOS_SUPPORT_INT64 1\n  #define KHRONOS_SUPPORT_FLOAT 1\n\n#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)\n\n/*\n * Win32\n */\ntypedef __int32 khronos_int32_t;\ntypedef unsigned __int32 khronos_uint32_t;\ntypedef __int64 khronos_int64_t;\ntypedef unsigned __int64 khronos_uint64_t;\n  #define KHRONOS_SUPPORT_INT64 1\n  #define KHRONOS_SUPPORT_FLOAT 1\n\n#elif defined(__sun__) || defined(__digital__)\n\n/*\n * Sun or Digital\n */\ntypedef int khronos_int32_t;\ntypedef unsigned int khronos_uint32_t;\n  #if defined(__arch64__) || defined(_LP64)\ntypedef long int khronos_int64_t;\ntypedef unsigned long int khronos_uint64_t;\n  #else\ntypedef long long int khronos_int64_t;\ntypedef unsigned long long int khronos_uint64_t;\n  #endif /* __arch64__ */\n  #define KHRONOS_SUPPORT_INT64 1\n  #define KHRONOS_SUPPORT_FLOAT 1\n\n#elif 0\n\n/*\n * Hypothetical platform with no float or int64 support\n */\ntypedef int khronos_int32_t;\ntypedef unsigned int khronos_uint32_t;\n  #define KHRONOS_SUPPORT_INT64 0\n  #define KHRONOS_SUPPORT_FLOAT 0\n\n#else\n\n  /*\n   * Generic fallback\n   */\n  #include <stdint.h>\ntypedef int32_t khronos_int32_t;\ntypedef uint32_t khronos_uint32_t;\ntypedef int64_t khronos_int64_t;\ntypedef uint64_t khronos_uint64_t;\n  #define KHRONOS_SUPPORT_INT64 1\n  #define KHRONOS_SUPPORT_FLOAT 1\n\n#endif\n\n/*\n * Types that are (so far) the same on all platforms\n */\ntypedef signed char khronos_int8_t;\ntypedef unsigned char khronos_uint8_t;\ntypedef signed short int khronos_int16_t;\ntypedef unsigned short int khronos_uint16_t;\n\n/*\n * Types that differ between LLP64 and LP64 architectures - in LLP64,\n * pointers are 64 bits, but 'long' is still 32 bits. Win64 appears\n * to be the only LLP64 architecture in current use.\n */\n#ifdef _WIN64\ntypedef signed long long int khronos_intptr_t;\ntypedef unsigned long long int khronos_uintptr_t;\ntypedef signed long long int khronos_ssize_t;\ntypedef unsigned long long int khronos_usize_t;\n#else\ntypedef signed long int khronos_intptr_t;\ntypedef unsigned long int khronos_uintptr_t;\ntypedef signed long int khronos_ssize_t;\ntypedef unsigned long int khronos_usize_t;\n#endif\n\n#if KHRONOS_SUPPORT_FLOAT\n/*\n * Float type\n */\ntypedef float khronos_float_t;\n#endif\n\n#if KHRONOS_SUPPORT_INT64\n/* Time types\n *\n * These types can be used to represent a time interval in nanoseconds or\n * an absolute Unadjusted System Time.  Unadjusted System Time is the number\n * of nanoseconds since some arbitrary system event (e.g. since the last\n * time the system booted).  The Unadjusted System Time is an unsigned\n * 64 bit value that wraps back to 0 every 584 years.  Time intervals\n * may be either signed or unsigned.\n */\ntypedef khronos_uint64_t khronos_utime_nanoseconds_t;\ntypedef khronos_int64_t khronos_stime_nanoseconds_t;\n#endif\n\n/*\n * Dummy value used to pad enum types to 32 bits.\n */\n#ifndef KHRONOS_MAX_ENUM\n  #define KHRONOS_MAX_ENUM 0x7FFFFFFF\n#endif\n\n/*\n * Enumerated boolean type\n *\n * Values other than zero should be considered to be true.  Therefore\n * comparisons should not be made against KHRONOS_TRUE.\n */\ntypedef enum {\n  KHRONOS_FALSE = 0,\n  KHRONOS_TRUE = 1,\n  KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM\n} khronos_boolean_enum_t;\n\n#endif /* __khrplatform_h_ */"
  },
  {
    "path": "third-party/glad/include/glad/egl.h",
    "content": "/**\n * Loader generated by glad 2.0.0-beta on Tue Jun  1 10:22:05 2021\n *\n * Generator: C/C++\n * Specification: egl\n * Extensions: 0\n *\n * APIs:\n *  - egl=1.5\n *\n * Options:\n *  - ALIAS = False\n *  - DEBUG = False\n *  - HEADER_ONLY = False\n *  - LOADER = True\n *  - MX = True\n *  - MX_GLOBAL = False\n *  - ON_DEMAND = False\n *\n * Commandline:\n *    --api='egl=1.5' --extensions='' c --loader --mx\n *\n * Online:\n *    http://glad.sh/#api=egl%3D1.5&extensions=&generator=c&options=LOADER%2CMX\n *\n */\n\n#ifndef GLAD_EGL_H_\n#define GLAD_EGL_H_\n\n#define GLAD_EGL\n#define GLAD_OPTION_EGL_LOADER\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n#ifndef GLAD_PLATFORM_H_\n  #define GLAD_PLATFORM_H_\n\n  #ifndef GLAD_PLATFORM_WIN32\n    #if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__MINGW32__)\n      #define GLAD_PLATFORM_WIN32 1\n    #else\n      #define GLAD_PLATFORM_WIN32 0\n    #endif\n  #endif\n\n  #ifndef GLAD_PLATFORM_APPLE\n    #ifdef __APPLE__\n      #define GLAD_PLATFORM_APPLE 1\n    #else\n      #define GLAD_PLATFORM_APPLE 0\n    #endif\n  #endif\n\n  #ifndef GLAD_PLATFORM_EMSCRIPTEN\n    #ifdef __EMSCRIPTEN__\n      #define GLAD_PLATFORM_EMSCRIPTEN 1\n    #else\n      #define GLAD_PLATFORM_EMSCRIPTEN 0\n    #endif\n  #endif\n\n  #ifndef GLAD_PLATFORM_UWP\n    #if defined(_MSC_VER) && !defined(GLAD_INTERNAL_HAVE_WINAPIFAMILY)\n      #ifdef __has_include\n        #if __has_include(<winapifamily.h>)\n          #define GLAD_INTERNAL_HAVE_WINAPIFAMILY 1\n        #endif\n      #elif _MSC_VER >= 1700 && !_USING_V110_SDK71_\n        #define GLAD_INTERNAL_HAVE_WINAPIFAMILY 1\n      #endif\n    #endif\n\n    #ifdef GLAD_INTERNAL_HAVE_WINAPIFAMILY\n      #include <winapifamily.h>\n      #if !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) && WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP)\n        #define GLAD_PLATFORM_UWP 1\n      #endif\n    #endif\n\n    #ifndef GLAD_PLATFORM_UWP\n      #define GLAD_PLATFORM_UWP 0\n    #endif\n  #endif\n\n  #ifdef __GNUC__\n    #define GLAD_GNUC_EXTENSION __extension__\n  #else\n    #define GLAD_GNUC_EXTENSION\n  #endif\n\n  #ifndef GLAD_API_CALL\n    #if defined(GLAD_API_CALL_EXPORT)\n      #if GLAD_PLATFORM_WIN32 || defined(__CYGWIN__)\n        #if defined(GLAD_API_CALL_EXPORT_BUILD)\n          #if defined(__GNUC__)\n            #define GLAD_API_CALL __attribute__((dllexport)) extern\n          #else\n            #define GLAD_API_CALL __declspec(dllexport) extern\n          #endif\n        #else\n          #if defined(__GNUC__)\n            #define GLAD_API_CALL __attribute__((dllimport)) extern\n          #else\n            #define GLAD_API_CALL __declspec(dllimport) extern\n          #endif\n        #endif\n      #elif defined(__GNUC__) && defined(GLAD_API_CALL_EXPORT_BUILD)\n        #define GLAD_API_CALL __attribute__((visibility(\"default\"))) extern\n      #else\n        #define GLAD_API_CALL extern\n      #endif\n    #else\n      #define GLAD_API_CALL extern\n    #endif\n  #endif\n\n  #ifdef APIENTRY\n    #define GLAD_API_PTR APIENTRY\n  #elif GLAD_PLATFORM_WIN32\n    #define GLAD_API_PTR __stdcall\n  #else\n    #define GLAD_API_PTR\n  #endif\n\n  #ifndef GLAPI\n    #define GLAPI GLAD_API_CALL\n  #endif\n\n  #ifndef GLAPIENTRY\n    #define GLAPIENTRY GLAD_API_PTR\n  #endif\n\n  #define GLAD_MAKE_VERSION(major, minor) (major * 10000 + minor)\n  #define GLAD_VERSION_MAJOR(version) (version / 10000)\n  #define GLAD_VERSION_MINOR(version) (version % 10000)\n\n  #define GLAD_GENERATOR_VERSION \"2.0.0-beta\"\n\ntypedef void (*GLADapiproc)(void);\n\ntypedef GLADapiproc (*GLADloadfunc)(const char *name);\ntypedef GLADapiproc (*GLADuserptrloadfunc)(void *userptr, const char *name);\n\ntypedef void (*GLADprecallback)(const char *name, GLADapiproc apiproc, int len_args, ...);\ntypedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apiproc, int len_args, ...);\n\n#endif /* GLAD_PLATFORM_H_ */\n\n#define EGL_ALPHA_FORMAT 0x3088\n#define EGL_ALPHA_FORMAT_NONPRE 0x308B\n#define EGL_ALPHA_FORMAT_PRE 0x308C\n#define EGL_ALPHA_MASK_SIZE 0x303E\n#define EGL_ALPHA_SIZE 0x3021\n#define EGL_BACK_BUFFER 0x3084\n#define EGL_BAD_ACCESS 0x3002\n#define EGL_BAD_ALLOC 0x3003\n#define EGL_BAD_ATTRIBUTE 0x3004\n#define EGL_BAD_CONFIG 0x3005\n#define EGL_BAD_CONTEXT 0x3006\n#define EGL_BAD_CURRENT_SURFACE 0x3007\n#define EGL_BAD_DISPLAY 0x3008\n#define EGL_BAD_MATCH 0x3009\n#define EGL_BAD_NATIVE_PIXMAP 0x300A\n#define EGL_BAD_NATIVE_WINDOW 0x300B\n#define EGL_BAD_PARAMETER 0x300C\n#define EGL_BAD_SURFACE 0x300D\n#define EGL_BIND_TO_TEXTURE_RGB 0x3039\n#define EGL_BIND_TO_TEXTURE_RGBA 0x303A\n#define EGL_BLUE_SIZE 0x3022\n#define EGL_BUFFER_DESTROYED 0x3095\n#define EGL_BUFFER_PRESERVED 0x3094\n#define EGL_BUFFER_SIZE 0x3020\n#define EGL_CLIENT_APIS 0x308D\n#define EGL_CL_EVENT_HANDLE 0x309C\n#define EGL_COLORSPACE 0x3087\n#define EGL_COLORSPACE_LINEAR 0x308A\n#define EGL_COLORSPACE_sRGB 0x3089\n#define EGL_COLOR_BUFFER_TYPE 0x303F\n#define EGL_CONDITION_SATISFIED 0x30F6\n#define EGL_CONFIG_CAVEAT 0x3027\n#define EGL_CONFIG_ID 0x3028\n#define EGL_CONFORMANT 0x3042\n#define EGL_CONTEXT_CLIENT_TYPE 0x3097\n#define EGL_CONTEXT_CLIENT_VERSION 0x3098\n#define EGL_CONTEXT_LOST 0x300E\n#define EGL_CONTEXT_MAJOR_VERSION 0x3098\n#define EGL_CONTEXT_MINOR_VERSION 0x30FB\n#define EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT 0x00000002\n#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001\n#define EGL_CONTEXT_OPENGL_DEBUG 0x31B0\n#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE 0x31B1\n#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD\n#define EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY 0x31BD\n#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS 0x31B2\n#define EGL_CORE_NATIVE_ENGINE 0x305B\n#define EGL_DEFAULT_DISPLAY EGL_CAST(EGLNativeDisplayType, 0)\n#define EGL_DEPTH_SIZE 0x3025\n#define EGL_DISPLAY_SCALING 10000\n#define EGL_DONT_CARE EGL_CAST(EGLint, -1)\n#define EGL_DRAW 0x3059\n#define EGL_EXTENSIONS 0x3055\n#define EGL_FALSE 0\n#define EGL_FOREVER 0xFFFFFFFFFFFFFFFF\n#define EGL_GL_COLORSPACE 0x309D\n#define EGL_GL_COLORSPACE_LINEAR 0x308A\n#define EGL_GL_COLORSPACE_SRGB 0x3089\n#define EGL_GL_RENDERBUFFER 0x30B9\n#define EGL_GL_TEXTURE_2D 0x30B1\n#define EGL_GL_TEXTURE_3D 0x30B2\n#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x30B4\n#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x30B6\n#define EGL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x30B8\n#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x30B3\n#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x30B5\n#define EGL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x30B7\n#define EGL_GL_TEXTURE_LEVEL 0x30BC\n#define EGL_GL_TEXTURE_ZOFFSET 0x30BD\n#define EGL_GREEN_SIZE 0x3023\n#define EGL_HEIGHT 0x3056\n#define EGL_HORIZONTAL_RESOLUTION 0x3090\n#define EGL_IMAGE_PRESERVED 0x30D2\n#define EGL_LARGEST_PBUFFER 0x3058\n#define EGL_LEVEL 0x3029\n#define EGL_LOSE_CONTEXT_ON_RESET 0x31BF\n#define EGL_LUMINANCE_BUFFER 0x308F\n#define EGL_LUMINANCE_SIZE 0x303D\n#define EGL_MATCH_NATIVE_PIXMAP 0x3041\n#define EGL_MAX_PBUFFER_HEIGHT 0x302A\n#define EGL_MAX_PBUFFER_PIXELS 0x302B\n#define EGL_MAX_PBUFFER_WIDTH 0x302C\n#define EGL_MAX_SWAP_INTERVAL 0x303C\n#define EGL_MIN_SWAP_INTERVAL 0x303B\n#define EGL_MIPMAP_LEVEL 0x3083\n#define EGL_MIPMAP_TEXTURE 0x3082\n#define EGL_MULTISAMPLE_RESOLVE 0x3099\n#define EGL_MULTISAMPLE_RESOLVE_BOX 0x309B\n#define EGL_MULTISAMPLE_RESOLVE_BOX_BIT 0x0200\n#define EGL_MULTISAMPLE_RESOLVE_DEFAULT 0x309A\n#define EGL_NATIVE_RENDERABLE 0x302D\n#define EGL_NATIVE_VISUAL_ID 0x302E\n#define EGL_NATIVE_VISUAL_TYPE 0x302F\n#define EGL_NONE 0x3038\n#define EGL_NON_CONFORMANT_CONFIG 0x3051\n#define EGL_NOT_INITIALIZED 0x3001\n#define EGL_NO_CONTEXT EGL_CAST(EGLContext, 0)\n#define EGL_NO_DISPLAY EGL_CAST(EGLDisplay, 0)\n#define EGL_NO_IMAGE EGL_CAST(EGLImage, 0)\n#define EGL_NO_RESET_NOTIFICATION 0x31BE\n#define EGL_NO_SURFACE EGL_CAST(EGLSurface, 0)\n#define EGL_NO_SYNC EGL_CAST(EGLSync, 0)\n#define EGL_NO_TEXTURE 0x305C\n#define EGL_OPENGL_API 0x30A2\n#define EGL_OPENGL_BIT 0x0008\n#define EGL_OPENGL_ES2_BIT 0x0004\n#define EGL_OPENGL_ES3_BIT 0x00000040\n#define EGL_OPENGL_ES_API 0x30A0\n#define EGL_OPENGL_ES_BIT 0x0001\n#define EGL_OPENVG_API 0x30A1\n#define EGL_OPENVG_BIT 0x0002\n#define EGL_OPENVG_IMAGE 0x3096\n#define EGL_PBUFFER_BIT 0x0001\n#define EGL_PIXEL_ASPECT_RATIO 0x3092\n#define EGL_PIXMAP_BIT 0x0002\n#define EGL_READ 0x305A\n#define EGL_RED_SIZE 0x3024\n#define EGL_RENDERABLE_TYPE 0x3040\n#define EGL_RENDER_BUFFER 0x3086\n#define EGL_RGB_BUFFER 0x308E\n#define EGL_SAMPLES 0x3031\n#define EGL_SAMPLE_BUFFERS 0x3032\n#define EGL_SIGNALED 0x30F2\n#define EGL_SINGLE_BUFFER 0x3085\n#define EGL_SLOW_CONFIG 0x3050\n#define EGL_STENCIL_SIZE 0x3026\n#define EGL_SUCCESS 0x3000\n#define EGL_SURFACE_TYPE 0x3033\n#define EGL_SWAP_BEHAVIOR 0x3093\n#define EGL_SWAP_BEHAVIOR_PRESERVED_BIT 0x0400\n#define EGL_SYNC_CL_EVENT 0x30FE\n#define EGL_SYNC_CL_EVENT_COMPLETE 0x30FF\n#define EGL_SYNC_CONDITION 0x30F8\n#define EGL_SYNC_FENCE 0x30F9\n#define EGL_SYNC_FLUSH_COMMANDS_BIT 0x0001\n#define EGL_SYNC_PRIOR_COMMANDS_COMPLETE 0x30F0\n#define EGL_SYNC_STATUS 0x30F1\n#define EGL_SYNC_TYPE 0x30F7\n#define EGL_TEXTURE_2D 0x305F\n#define EGL_TEXTURE_FORMAT 0x3080\n#define EGL_TEXTURE_RGB 0x305D\n#define EGL_TEXTURE_RGBA 0x305E\n#define EGL_TEXTURE_TARGET 0x3081\n#define EGL_TIMEOUT_EXPIRED 0x30F5\n#define EGL_TRANSPARENT_BLUE_VALUE 0x3035\n#define EGL_TRANSPARENT_GREEN_VALUE 0x3036\n#define EGL_TRANSPARENT_RED_VALUE 0x3037\n#define EGL_TRANSPARENT_RGB 0x3052\n#define EGL_TRANSPARENT_TYPE 0x3034\n#define EGL_TRUE 1\n#define EGL_UNKNOWN EGL_CAST(EGLint, -1)\n#define EGL_UNSIGNALED 0x30F3\n#define EGL_VENDOR 0x3053\n#define EGL_VERSION 0x3054\n#define EGL_VERTICAL_RESOLUTION 0x3091\n#define EGL_VG_ALPHA_FORMAT 0x3088\n#define EGL_VG_ALPHA_FORMAT_NONPRE 0x308B\n#define EGL_VG_ALPHA_FORMAT_PRE 0x308C\n#define EGL_VG_ALPHA_FORMAT_PRE_BIT 0x0040\n#define EGL_VG_COLORSPACE 0x3087\n#define EGL_VG_COLORSPACE_LINEAR 0x308A\n#define EGL_VG_COLORSPACE_LINEAR_BIT 0x0020\n#define EGL_VG_COLORSPACE_sRGB 0x3089\n#define EGL_WIDTH 0x3057\n#define EGL_WINDOW_BIT 0x0004\n\n#include <KHR/khrplatform.h>\n\n#include <EGL/eglplatform.h>\n\nstruct AHardwareBuffer;\n\nstruct wl_buffer;\n\nstruct wl_display;\n\nstruct wl_resource;\n\ntypedef unsigned int EGLBoolean;\n\ntypedef unsigned int EGLenum;\n\ntypedef intptr_t EGLAttribKHR;\n\ntypedef intptr_t EGLAttrib;\n\ntypedef void *EGLClientBuffer;\n\ntypedef void *EGLConfig;\n\ntypedef void *EGLContext;\n\ntypedef void *EGLDeviceEXT;\n\ntypedef void *EGLDisplay;\n\ntypedef void *EGLImage;\n\ntypedef void *EGLImageKHR;\n\ntypedef void *EGLLabelKHR;\n\ntypedef void *EGLObjectKHR;\n\ntypedef void *EGLOutputLayerEXT;\n\ntypedef void *EGLOutputPortEXT;\n\ntypedef void *EGLStreamKHR;\n\ntypedef void *EGLSurface;\n\ntypedef void *EGLSync;\n\ntypedef void *EGLSyncKHR;\n\ntypedef void *EGLSyncNV;\n\ntypedef void (*__eglMustCastToProperFunctionPointerType)(void);\n\ntypedef khronos_utime_nanoseconds_t EGLTimeKHR;\n\ntypedef khronos_utime_nanoseconds_t EGLTime;\n\ntypedef khronos_utime_nanoseconds_t EGLTimeNV;\n\ntypedef khronos_utime_nanoseconds_t EGLuint64NV;\n\ntypedef khronos_uint64_t EGLuint64KHR;\n\ntypedef khronos_stime_nanoseconds_t EGLnsecsANDROID;\n\ntypedef int EGLNativeFileDescriptorKHR;\n\ntypedef khronos_ssize_t EGLsizeiANDROID;\n\ntypedef void (*EGLSetBlobFuncANDROID)(const void *key, EGLsizeiANDROID keySize, const void *value, EGLsizeiANDROID valueSize);\n\ntypedef EGLsizeiANDROID (*EGLGetBlobFuncANDROID)(const void *key, EGLsizeiANDROID keySize, void *value, EGLsizeiANDROID valueSize);\n\nstruct EGLClientPixmapHI {\n  void *pData;\n  EGLint iWidth;\n  EGLint iHeight;\n  EGLint iStride;\n};\n\ntypedef void(GLAD_API_PTR *EGLDEBUGPROCKHR)(EGLenum error, const char *command, EGLint messageType, EGLLabelKHR threadLabel, EGLLabelKHR objectLabel, const char *message);\n\n#define PFNEGLBINDWAYLANDDISPLAYWL PFNEGLBINDWAYLANDDISPLAYWLPROC\n\n#define PFNEGLUNBINDWAYLANDDISPLAYWL PFNEGLUNBINDWAYLANDDISPLAYWLPROC\n\n#define PFNEGLQUERYWAYLANDBUFFERWL PFNEGLQUERYWAYLANDBUFFERWLPROC\n\n#define PFNEGLCREATEWAYLANDBUFFERFROMIMAGEWL PFNEGLCREATEWAYLANDBUFFERFROMIMAGEWLPROC\n\n#define EGL_VERSION_1_0 1\nGLAD_API_CALL int GLAD_EGL_VERSION_1_0;\n#define EGL_VERSION_1_1 1\nGLAD_API_CALL int GLAD_EGL_VERSION_1_1;\n#define EGL_VERSION_1_2 1\nGLAD_API_CALL int GLAD_EGL_VERSION_1_2;\n#define EGL_VERSION_1_3 1\nGLAD_API_CALL int GLAD_EGL_VERSION_1_3;\n#define EGL_VERSION_1_4 1\nGLAD_API_CALL int GLAD_EGL_VERSION_1_4;\n#define EGL_VERSION_1_5 1\nGLAD_API_CALL int GLAD_EGL_VERSION_1_5;\n\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLBINDAPIPROC)(EGLenum api);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLBINDTEXIMAGEPROC)(EGLDisplay dpy, EGLSurface surface, EGLint buffer);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLCHOOSECONFIGPROC)(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);\ntypedef EGLint(GLAD_API_PTR *PFNEGLCLIENTWAITSYNCPROC)(EGLDisplay dpy, EGLSync sync, EGLint flags, EGLTime timeout);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLCOPYBUFFERSPROC)(EGLDisplay dpy, EGLSurface surface, EGLNativePixmapType target);\ntypedef EGLContext(GLAD_API_PTR *PFNEGLCREATECONTEXTPROC)(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);\ntypedef EGLImage(GLAD_API_PTR *PFNEGLCREATEIMAGEPROC)(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib *attrib_list);\ntypedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC)(EGLDisplay dpy, EGLenum buftype, EGLClientBuffer buffer, EGLConfig config, const EGLint *attrib_list);\ntypedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEPBUFFERSURFACEPROC)(EGLDisplay dpy, EGLConfig config, const EGLint *attrib_list);\ntypedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEPIXMAPSURFACEPROC)(EGLDisplay dpy, EGLConfig config, EGLNativePixmapType pixmap, const EGLint *attrib_list);\ntypedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC)(EGLDisplay dpy, EGLConfig config, void *native_pixmap, const EGLAttrib *attrib_list);\ntypedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEPLATFORMWINDOWSURFACEPROC)(EGLDisplay dpy, EGLConfig config, void *native_window, const EGLAttrib *attrib_list);\ntypedef EGLSync(GLAD_API_PTR *PFNEGLCREATESYNCPROC)(EGLDisplay dpy, EGLenum type, const EGLAttrib *attrib_list);\ntypedef EGLSurface(GLAD_API_PTR *PFNEGLCREATEWINDOWSURFACEPROC)(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLDESTROYCONTEXTPROC)(EGLDisplay dpy, EGLContext ctx);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLDESTROYIMAGEPROC)(EGLDisplay dpy, EGLImage image);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLDESTROYSURFACEPROC)(EGLDisplay dpy, EGLSurface surface);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLDESTROYSYNCPROC)(EGLDisplay dpy, EGLSync sync);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLGETCONFIGATTRIBPROC)(EGLDisplay dpy, EGLConfig config, EGLint attribute, EGLint *value);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLGETCONFIGSPROC)(EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config);\ntypedef EGLContext(GLAD_API_PTR *PFNEGLGETCURRENTCONTEXTPROC)(void);\ntypedef EGLDisplay(GLAD_API_PTR *PFNEGLGETCURRENTDISPLAYPROC)(void);\ntypedef EGLSurface(GLAD_API_PTR *PFNEGLGETCURRENTSURFACEPROC)(EGLint readdraw);\ntypedef EGLDisplay(GLAD_API_PTR *PFNEGLGETDISPLAYPROC)(EGLNativeDisplayType display_id);\ntypedef EGLint(GLAD_API_PTR *PFNEGLGETERRORPROC)(void);\ntypedef EGLDisplay(GLAD_API_PTR *PFNEGLGETPLATFORMDISPLAYPROC)(EGLenum platform, void *native_display, const EGLAttrib *attrib_list);\ntypedef __eglMustCastToProperFunctionPointerType(GLAD_API_PTR *PFNEGLGETPROCADDRESSPROC)(const char *procname);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLGETSYNCATTRIBPROC)(EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib *value);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLINITIALIZEPROC)(EGLDisplay dpy, EGLint *major, EGLint *minor);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLMAKECURRENTPROC)(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);\ntypedef EGLenum(GLAD_API_PTR *PFNEGLQUERYAPIPROC)(void);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLQUERYCONTEXTPROC)(EGLDisplay dpy, EGLContext ctx, EGLint attribute, EGLint *value);\ntypedef const char *(GLAD_API_PTR *PFNEGLQUERYSTRINGPROC)(EGLDisplay dpy, EGLint name);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLQUERYSURFACEPROC)(EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint *value);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLRELEASETEXIMAGEPROC)(EGLDisplay dpy, EGLSurface surface, EGLint buffer);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLRELEASETHREADPROC)(void);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLSURFACEATTRIBPROC)(EGLDisplay dpy, EGLSurface surface, EGLint attribute, EGLint value);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLSWAPBUFFERSPROC)(EGLDisplay dpy, EGLSurface surface);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLSWAPINTERVALPROC)(EGLDisplay dpy, EGLint interval);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLTERMINATEPROC)(EGLDisplay dpy);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLWAITCLIENTPROC)(void);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLWAITGLPROC)(void);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLWAITNATIVEPROC)(EGLint engine);\ntypedef EGLBoolean(GLAD_API_PTR *PFNEGLWAITSYNCPROC)(EGLDisplay dpy, EGLSync sync, EGLint flags);\n\ntypedef EGLImageKHR(EGLAPIENTRYP PFNEGLCREATEIMAGEKHRPROC)(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list);\ntypedef EGLBoolean(EGLAPIENTRYP PFNEGLDESTROYIMAGEKHRPROC)(EGLDisplay dpy, EGLImageKHR image);\n\nGLAD_API_CALL PFNEGLCREATEIMAGEKHRPROC glad_eglCreateImageKHR;\n#define eglCreateImageKHR glad_eglCreateImageKHR\nGLAD_API_CALL PFNEGLDESTROYIMAGEKHRPROC glad_eglDestroyImageKHR;\n#define eglDestroyImageKHR glad_eglDestroyImageKHR\n\nGLAD_API_CALL PFNEGLBINDAPIPROC glad_eglBindAPI;\n#define eglBindAPI glad_eglBindAPI\nGLAD_API_CALL PFNEGLBINDTEXIMAGEPROC glad_eglBindTexImage;\n#define eglBindTexImage glad_eglBindTexImage\nGLAD_API_CALL PFNEGLCHOOSECONFIGPROC glad_eglChooseConfig;\n#define eglChooseConfig glad_eglChooseConfig\nGLAD_API_CALL PFNEGLCLIENTWAITSYNCPROC glad_eglClientWaitSync;\n#define eglClientWaitSync glad_eglClientWaitSync\nGLAD_API_CALL PFNEGLCOPYBUFFERSPROC glad_eglCopyBuffers;\n#define eglCopyBuffers glad_eglCopyBuffers\nGLAD_API_CALL PFNEGLCREATECONTEXTPROC glad_eglCreateContext;\n#define eglCreateContext glad_eglCreateContext\nGLAD_API_CALL PFNEGLCREATEIMAGEPROC glad_eglCreateImage;\n#define eglCreateImage glad_eglCreateImage\nGLAD_API_CALL PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC glad_eglCreatePbufferFromClientBuffer;\n#define eglCreatePbufferFromClientBuffer glad_eglCreatePbufferFromClientBuffer\nGLAD_API_CALL PFNEGLCREATEPBUFFERSURFACEPROC glad_eglCreatePbufferSurface;\n#define eglCreatePbufferSurface glad_eglCreatePbufferSurface\nGLAD_API_CALL PFNEGLCREATEPIXMAPSURFACEPROC glad_eglCreatePixmapSurface;\n#define eglCreatePixmapSurface glad_eglCreatePixmapSurface\nGLAD_API_CALL PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC glad_eglCreatePlatformPixmapSurface;\n#define eglCreatePlatformPixmapSurface glad_eglCreatePlatformPixmapSurface\nGLAD_API_CALL PFNEGLCREATEPLATFORMWINDOWSURFACEPROC glad_eglCreatePlatformWindowSurface;\n#define eglCreatePlatformWindowSurface glad_eglCreatePlatformWindowSurface\nGLAD_API_CALL PFNEGLCREATESYNCPROC glad_eglCreateSync;\n#define eglCreateSync glad_eglCreateSync\nGLAD_API_CALL PFNEGLCREATEWINDOWSURFACEPROC glad_eglCreateWindowSurface;\n#define eglCreateWindowSurface glad_eglCreateWindowSurface\nGLAD_API_CALL PFNEGLDESTROYCONTEXTPROC glad_eglDestroyContext;\n#define eglDestroyContext glad_eglDestroyContext\nGLAD_API_CALL PFNEGLDESTROYIMAGEPROC glad_eglDestroyImage;\n#define eglDestroyImage glad_eglDestroyImage\nGLAD_API_CALL PFNEGLDESTROYSURFACEPROC glad_eglDestroySurface;\n#define eglDestroySurface glad_eglDestroySurface\nGLAD_API_CALL PFNEGLDESTROYSYNCPROC glad_eglDestroySync;\n#define eglDestroySync glad_eglDestroySync\nGLAD_API_CALL PFNEGLGETCONFIGATTRIBPROC glad_eglGetConfigAttrib;\n#define eglGetConfigAttrib glad_eglGetConfigAttrib\nGLAD_API_CALL PFNEGLGETCONFIGSPROC glad_eglGetConfigs;\n#define eglGetConfigs glad_eglGetConfigs\nGLAD_API_CALL PFNEGLGETCURRENTCONTEXTPROC glad_eglGetCurrentContext;\n#define eglGetCurrentContext glad_eglGetCurrentContext\nGLAD_API_CALL PFNEGLGETCURRENTDISPLAYPROC glad_eglGetCurrentDisplay;\n#define eglGetCurrentDisplay glad_eglGetCurrentDisplay\nGLAD_API_CALL PFNEGLGETCURRENTSURFACEPROC glad_eglGetCurrentSurface;\n#define eglGetCurrentSurface glad_eglGetCurrentSurface\nGLAD_API_CALL PFNEGLGETDISPLAYPROC glad_eglGetDisplay;\n#define eglGetDisplay glad_eglGetDisplay\nGLAD_API_CALL PFNEGLGETERRORPROC glad_eglGetError;\n#define eglGetError glad_eglGetError\nGLAD_API_CALL PFNEGLGETPLATFORMDISPLAYPROC glad_eglGetPlatformDisplay;\n#define eglGetPlatformDisplay glad_eglGetPlatformDisplay\nGLAD_API_CALL PFNEGLGETPROCADDRESSPROC glad_eglGetProcAddress;\n#define eglGetProcAddress glad_eglGetProcAddress\nGLAD_API_CALL PFNEGLGETSYNCATTRIBPROC glad_eglGetSyncAttrib;\n#define eglGetSyncAttrib glad_eglGetSyncAttrib\nGLAD_API_CALL PFNEGLINITIALIZEPROC glad_eglInitialize;\n#define eglInitialize glad_eglInitialize\nGLAD_API_CALL PFNEGLMAKECURRENTPROC glad_eglMakeCurrent;\n#define eglMakeCurrent glad_eglMakeCurrent\nGLAD_API_CALL PFNEGLQUERYAPIPROC glad_eglQueryAPI;\n#define eglQueryAPI glad_eglQueryAPI\nGLAD_API_CALL PFNEGLQUERYCONTEXTPROC glad_eglQueryContext;\n#define eglQueryContext glad_eglQueryContext\nGLAD_API_CALL PFNEGLQUERYSTRINGPROC glad_eglQueryString;\n#define eglQueryString glad_eglQueryString\nGLAD_API_CALL PFNEGLQUERYSURFACEPROC glad_eglQuerySurface;\n#define eglQuerySurface glad_eglQuerySurface\nGLAD_API_CALL PFNEGLRELEASETEXIMAGEPROC glad_eglReleaseTexImage;\n#define eglReleaseTexImage glad_eglReleaseTexImage\nGLAD_API_CALL PFNEGLRELEASETHREADPROC glad_eglReleaseThread;\n#define eglReleaseThread glad_eglReleaseThread\nGLAD_API_CALL PFNEGLSURFACEATTRIBPROC glad_eglSurfaceAttrib;\n#define eglSurfaceAttrib glad_eglSurfaceAttrib\nGLAD_API_CALL PFNEGLSWAPBUFFERSPROC glad_eglSwapBuffers;\n#define eglSwapBuffers glad_eglSwapBuffers\nGLAD_API_CALL PFNEGLSWAPINTERVALPROC glad_eglSwapInterval;\n#define eglSwapInterval glad_eglSwapInterval\nGLAD_API_CALL PFNEGLTERMINATEPROC glad_eglTerminate;\n#define eglTerminate glad_eglTerminate\nGLAD_API_CALL PFNEGLWAITCLIENTPROC glad_eglWaitClient;\n#define eglWaitClient glad_eglWaitClient\nGLAD_API_CALL PFNEGLWAITGLPROC glad_eglWaitGL;\n#define eglWaitGL glad_eglWaitGL\nGLAD_API_CALL PFNEGLWAITNATIVEPROC glad_eglWaitNative;\n#define eglWaitNative glad_eglWaitNative\nGLAD_API_CALL PFNEGLWAITSYNCPROC glad_eglWaitSync;\n#define eglWaitSync glad_eglWaitSync\n\nGLAD_API_CALL int\ngladLoadEGLUserPtr(EGLDisplay display, GLADuserptrloadfunc load, void *userptr);\nGLAD_API_CALL int\ngladLoadEGL(EGLDisplay display, GLADloadfunc load);\n\n#ifdef GLAD_EGL\n\nGLAD_API_CALL int\ngladLoaderLoadEGL(EGLDisplay display);\n\nGLAD_API_CALL void\ngladLoaderUnloadEGL(void);\n\n#endif\n#ifdef __cplusplus\n}\n#endif\n#endif"
  },
  {
    "path": "third-party/glad/include/glad/gl.h",
    "content": "/**\n * Loader generated by glad 2.0.0-beta on Tue Jun  1 10:22:06 2021\n *\n * Generator: C/C++\n * Specification: gl\n * Extensions: 0\n *\n * APIs:\n *  - gl:compatibility=4.6\n *\n * Options:\n *  - ALIAS = False\n *  - DEBUG = False\n *  - HEADER_ONLY = False\n *  - LOADER = True\n *  - MX = True\n *  - MX_GLOBAL = False\n *  - ON_DEMAND = False\n *\n * Commandline:\n *    --api='gl:compatibility=4.6' --extensions='' c --loader --mx\n *\n * Online:\n *    http://glad.sh/#api=gl%3Acompatibility%3D4.6&extensions=&generator=c&options=LOADER%2CMX\n *\n */\n\n#ifndef GLAD_GL_H_\n#define GLAD_GL_H_\n\n#ifdef __clang__\n  #pragma clang diagnostic push\n  #pragma clang diagnostic ignored \"-Wreserved-id-macro\"\n#endif\n#ifdef __gl_h_\n  #error OpenGL (gl.h) header already included (API: gl), remove previous include!\n#endif\n#define __gl_h_ 1\n#ifdef __gl3_h_\n  #error OpenGL (gl3.h) header already included (API: gl), remove previous include!\n#endif\n#define __gl3_h_ 1\n#ifdef __glext_h_\n  #error OpenGL (glext.h) header already included (API: gl), remove previous include!\n#endif\n#define __glext_h_ 1\n#ifdef __gl3ext_h_\n  #error OpenGL (gl3ext.h) header already included (API: gl), remove previous include!\n#endif\n#define __gl3ext_h_ 1\n#ifdef __clang__\n  #pragma clang diagnostic pop\n#endif\n\n#define GLAD_GL\n#define GLAD_OPTION_GL_LOADER\n#define GLAD_OPTION_GL_MX\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n#ifndef GLAD_PLATFORM_H_\n  #define GLAD_PLATFORM_H_\n\n  #ifndef GLAD_PLATFORM_WIN32\n    #if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__MINGW32__)\n      #define GLAD_PLATFORM_WIN32 1\n    #else\n      #define GLAD_PLATFORM_WIN32 0\n    #endif\n  #endif\n\n  #ifndef GLAD_PLATFORM_APPLE\n    #ifdef __APPLE__\n      #define GLAD_PLATFORM_APPLE 1\n    #else\n      #define GLAD_PLATFORM_APPLE 0\n    #endif\n  #endif\n\n  #ifndef GLAD_PLATFORM_EMSCRIPTEN\n    #ifdef __EMSCRIPTEN__\n      #define GLAD_PLATFORM_EMSCRIPTEN 1\n    #else\n      #define GLAD_PLATFORM_EMSCRIPTEN 0\n    #endif\n  #endif\n\n  #ifndef GLAD_PLATFORM_UWP\n    #if defined(_MSC_VER) && !defined(GLAD_INTERNAL_HAVE_WINAPIFAMILY)\n      #ifdef __has_include\n        #if __has_include(<winapifamily.h>)\n          #define GLAD_INTERNAL_HAVE_WINAPIFAMILY 1\n        #endif\n      #elif _MSC_VER >= 1700 && !_USING_V110_SDK71_\n        #define GLAD_INTERNAL_HAVE_WINAPIFAMILY 1\n      #endif\n    #endif\n\n    #ifdef GLAD_INTERNAL_HAVE_WINAPIFAMILY\n      #include <winapifamily.h>\n      #if !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) && WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP)\n        #define GLAD_PLATFORM_UWP 1\n      #endif\n    #endif\n\n    #ifndef GLAD_PLATFORM_UWP\n      #define GLAD_PLATFORM_UWP 0\n    #endif\n  #endif\n\n  #ifdef __GNUC__\n    #define GLAD_GNUC_EXTENSION __extension__\n  #else\n    #define GLAD_GNUC_EXTENSION\n  #endif\n\n  #ifndef GLAD_API_CALL\n    #if defined(GLAD_API_CALL_EXPORT)\n      #if GLAD_PLATFORM_WIN32 || defined(__CYGWIN__)\n        #if defined(GLAD_API_CALL_EXPORT_BUILD)\n          #if defined(__GNUC__)\n            #define GLAD_API_CALL __attribute__((dllexport)) extern\n          #else\n            #define GLAD_API_CALL __declspec(dllexport) extern\n          #endif\n        #else\n          #if defined(__GNUC__)\n            #define GLAD_API_CALL __attribute__((dllimport)) extern\n          #else\n            #define GLAD_API_CALL __declspec(dllimport) extern\n          #endif\n        #endif\n      #elif defined(__GNUC__) && defined(GLAD_API_CALL_EXPORT_BUILD)\n        #define GLAD_API_CALL __attribute__((visibility(\"default\"))) extern\n      #else\n        #define GLAD_API_CALL extern\n      #endif\n    #else\n      #define GLAD_API_CALL extern\n    #endif\n  #endif\n\n  #ifdef APIENTRY\n    #define GLAD_API_PTR APIENTRY\n  #elif GLAD_PLATFORM_WIN32\n    #define GLAD_API_PTR __stdcall\n  #else\n    #define GLAD_API_PTR\n  #endif\n\n  #ifndef GLAPI\n    #define GLAPI GLAD_API_CALL\n  #endif\n\n  #ifndef GLAPIENTRY\n    #define GLAPIENTRY GLAD_API_PTR\n  #endif\n\n  #define GLAD_MAKE_VERSION(major, minor) (major * 10000 + minor)\n  #define GLAD_VERSION_MAJOR(version) (version / 10000)\n  #define GLAD_VERSION_MINOR(version) (version % 10000)\n\n  #define GLAD_GENERATOR_VERSION \"2.0.0-beta\"\n\ntypedef void (*GLADapiproc)(void);\n\ntypedef GLADapiproc (*GLADloadfunc)(const char *name);\ntypedef GLADapiproc (*GLADuserptrloadfunc)(void *userptr, const char *name);\n\ntypedef void (*GLADprecallback)(const char *name, GLADapiproc apiproc, int len_args, ...);\ntypedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apiproc, int len_args, ...);\n\n#endif /* GLAD_PLATFORM_H_ */\n\n#define GL_2D 0x0600\n#define GL_2_BYTES 0x1407\n#define GL_3D 0x0601\n#define GL_3D_COLOR 0x0602\n#define GL_3D_COLOR_TEXTURE 0x0603\n#define GL_3_BYTES 0x1408\n#define GL_4D_COLOR_TEXTURE 0x0604\n#define GL_4_BYTES 0x1409\n#define GL_ACCUM 0x0100\n#define GL_ACCUM_ALPHA_BITS 0x0D5B\n#define GL_ACCUM_BLUE_BITS 0x0D5A\n#define GL_ACCUM_BUFFER_BIT 0x00000200\n#define GL_ACCUM_CLEAR_VALUE 0x0B80\n#define GL_ACCUM_GREEN_BITS 0x0D59\n#define GL_ACCUM_RED_BITS 0x0D58\n#define GL_ACTIVE_ATOMIC_COUNTER_BUFFERS 0x92D9\n#define GL_ACTIVE_ATTRIBUTES 0x8B89\n#define GL_ACTIVE_ATTRIBUTE_MAX_LENGTH 0x8B8A\n#define GL_ACTIVE_PROGRAM 0x8259\n#define GL_ACTIVE_RESOURCES 0x92F5\n#define GL_ACTIVE_SUBROUTINES 0x8DE5\n#define GL_ACTIVE_SUBROUTINE_MAX_LENGTH 0x8E48\n#define GL_ACTIVE_SUBROUTINE_UNIFORMS 0x8DE6\n#define GL_ACTIVE_SUBROUTINE_UNIFORM_LOCATIONS 0x8E47\n#define GL_ACTIVE_SUBROUTINE_UNIFORM_MAX_LENGTH 0x8E49\n#define GL_ACTIVE_TEXTURE 0x84E0\n#define GL_ACTIVE_UNIFORMS 0x8B86\n#define GL_ACTIVE_UNIFORM_BLOCKS 0x8A36\n#define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35\n#define GL_ACTIVE_UNIFORM_MAX_LENGTH 0x8B87\n#define GL_ACTIVE_VARIABLES 0x9305\n#define GL_ADD 0x0104\n#define GL_ADD_SIGNED 0x8574\n#define GL_ALIASED_LINE_WIDTH_RANGE 0x846E\n#define GL_ALIASED_POINT_SIZE_RANGE 0x846D\n#define GL_ALL_ATTRIB_BITS 0xFFFFFFFF\n#define GL_ALL_BARRIER_BITS 0xFFFFFFFF\n#define GL_ALL_SHADER_BITS 0xFFFFFFFF\n#define GL_ALPHA 0x1906\n#define GL_ALPHA12 0x803D\n#define GL_ALPHA16 0x803E\n#define GL_ALPHA4 0x803B\n#define GL_ALPHA8 0x803C\n#define GL_ALPHA_BIAS 0x0D1D\n#define GL_ALPHA_BITS 0x0D55\n#define GL_ALPHA_INTEGER 0x8D97\n#define GL_ALPHA_SCALE 0x0D1C\n#define GL_ALPHA_TEST 0x0BC0\n#define GL_ALPHA_TEST_FUNC 0x0BC1\n#define GL_ALPHA_TEST_REF 0x0BC2\n#define GL_ALREADY_SIGNALED 0x911A\n#define GL_ALWAYS 0x0207\n#define GL_AMBIENT 0x1200\n#define GL_AMBIENT_AND_DIFFUSE 0x1602\n#define GL_AND 0x1501\n#define GL_AND_INVERTED 0x1504\n#define GL_AND_REVERSE 0x1502\n#define GL_ANY_SAMPLES_PASSED 0x8C2F\n#define GL_ANY_SAMPLES_PASSED_CONSERVATIVE 0x8D6A\n#define GL_ARRAY_BUFFER 0x8892\n#define GL_ARRAY_BUFFER_BINDING 0x8894\n#define GL_ARRAY_SIZE 0x92FB\n#define GL_ARRAY_STRIDE 0x92FE\n#define GL_ATOMIC_COUNTER_BARRIER_BIT 0x00001000\n#define GL_ATOMIC_COUNTER_BUFFER 0x92C0\n#define GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS 0x92C5\n#define GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES 0x92C6\n#define GL_ATOMIC_COUNTER_BUFFER_BINDING 0x92C1\n#define GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE 0x92C4\n#define GL_ATOMIC_COUNTER_BUFFER_INDEX 0x9301\n#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER 0x90ED\n#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER 0x92CB\n#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER 0x92CA\n#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER 0x92C8\n#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER 0x92C9\n#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER 0x92C7\n#define GL_ATOMIC_COUNTER_BUFFER_SIZE 0x92C3\n#define GL_ATOMIC_COUNTER_BUFFER_START 0x92C2\n#define GL_ATTACHED_SHADERS 0x8B85\n#define GL_ATTRIB_STACK_DEPTH 0x0BB0\n#define GL_AUTO_GENERATE_MIPMAP 0x8295\n#define GL_AUTO_NORMAL 0x0D80\n#define GL_AUX0 0x0409\n#define GL_AUX1 0x040A\n#define GL_AUX2 0x040B\n#define GL_AUX3 0x040C\n#define GL_AUX_BUFFERS 0x0C00\n#define GL_BACK 0x0405\n#define GL_BACK_LEFT 0x0402\n#define GL_BACK_RIGHT 0x0403\n#define GL_BGR 0x80E0\n#define GL_BGRA 0x80E1\n#define GL_BGRA_INTEGER 0x8D9B\n#define GL_BGR_INTEGER 0x8D9A\n#define GL_BITMAP 0x1A00\n#define GL_BITMAP_TOKEN 0x0704\n#define GL_BLEND 0x0BE2\n#define GL_BLEND_COLOR 0x8005\n#define GL_BLEND_DST 0x0BE0\n#define GL_BLEND_DST_ALPHA 0x80CA\n#define GL_BLEND_DST_RGB 0x80C8\n#define GL_BLEND_EQUATION 0x8009\n#define GL_BLEND_EQUATION_ALPHA 0x883D\n#define GL_BLEND_EQUATION_RGB 0x8009\n#define GL_BLEND_SRC 0x0BE1\n#define GL_BLEND_SRC_ALPHA 0x80CB\n#define GL_BLEND_SRC_RGB 0x80C9\n#define GL_BLOCK_INDEX 0x92FD\n#define GL_BLUE 0x1905\n#define GL_BLUE_BIAS 0x0D1B\n#define GL_BLUE_BITS 0x0D54\n#define GL_BLUE_INTEGER 0x8D96\n#define GL_BLUE_SCALE 0x0D1A\n#define GL_BOOL 0x8B56\n#define GL_BOOL_VEC2 0x8B57\n#define GL_BOOL_VEC3 0x8B58\n#define GL_BOOL_VEC4 0x8B59\n#define GL_BUFFER 0x82E0\n#define GL_BUFFER_ACCESS 0x88BB\n#define GL_BUFFER_ACCESS_FLAGS 0x911F\n#define GL_BUFFER_BINDING 0x9302\n#define GL_BUFFER_DATA_SIZE 0x9303\n#define GL_BUFFER_IMMUTABLE_STORAGE 0x821F\n#define GL_BUFFER_MAPPED 0x88BC\n#define GL_BUFFER_MAP_LENGTH 0x9120\n#define GL_BUFFER_MAP_OFFSET 0x9121\n#define GL_BUFFER_MAP_POINTER 0x88BD\n#define GL_BUFFER_SIZE 0x8764\n#define GL_BUFFER_STORAGE_FLAGS 0x8220\n#define GL_BUFFER_UPDATE_BARRIER_BIT 0x00000200\n#define GL_BUFFER_USAGE 0x8765\n#define GL_BUFFER_VARIABLE 0x92E5\n#define GL_BYTE 0x1400\n#define GL_C3F_V3F 0x2A24\n#define GL_C4F_N3F_V3F 0x2A26\n#define GL_C4UB_V2F 0x2A22\n#define GL_C4UB_V3F 0x2A23\n#define GL_CAVEAT_SUPPORT 0x82B8\n#define GL_CCW 0x0901\n#define GL_CLAMP 0x2900\n#define GL_CLAMP_FRAGMENT_COLOR 0x891B\n#define GL_CLAMP_READ_COLOR 0x891C\n#define GL_CLAMP_TO_BORDER 0x812D\n#define GL_CLAMP_TO_EDGE 0x812F\n#define GL_CLAMP_VERTEX_COLOR 0x891A\n#define GL_CLEAR 0x1500\n#define GL_CLEAR_BUFFER 0x82B4\n#define GL_CLEAR_TEXTURE 0x9365\n#define GL_CLIENT_ACTIVE_TEXTURE 0x84E1\n#define GL_CLIENT_ALL_ATTRIB_BITS 0xFFFFFFFF\n#define GL_CLIENT_ATTRIB_STACK_DEPTH 0x0BB1\n#define GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT 0x00004000\n#define GL_CLIENT_PIXEL_STORE_BIT 0x00000001\n#define GL_CLIENT_STORAGE_BIT 0x0200\n#define GL_CLIENT_VERTEX_ARRAY_BIT 0x00000002\n#define GL_CLIPPING_INPUT_PRIMITIVES 0x82F6\n#define GL_CLIPPING_OUTPUT_PRIMITIVES 0x82F7\n#define GL_CLIP_DEPTH_MODE 0x935D\n#define GL_CLIP_DISTANCE0 0x3000\n#define GL_CLIP_DISTANCE1 0x3001\n#define GL_CLIP_DISTANCE2 0x3002\n#define GL_CLIP_DISTANCE3 0x3003\n#define GL_CLIP_DISTANCE4 0x3004\n#define GL_CLIP_DISTANCE5 0x3005\n#define GL_CLIP_DISTANCE6 0x3006\n#define GL_CLIP_DISTANCE7 0x3007\n#define GL_CLIP_ORIGIN 0x935C\n#define GL_CLIP_PLANE0 0x3000\n#define GL_CLIP_PLANE1 0x3001\n#define GL_CLIP_PLANE2 0x3002\n#define GL_CLIP_PLANE3 0x3003\n#define GL_CLIP_PLANE4 0x3004\n#define GL_CLIP_PLANE5 0x3005\n#define GL_COEFF 0x0A00\n#define GL_COLOR 0x1800\n#define GL_COLOR_ARRAY 0x8076\n#define GL_COLOR_ARRAY_BUFFER_BINDING 0x8898\n#define GL_COLOR_ARRAY_POINTER 0x8090\n#define GL_COLOR_ARRAY_SIZE 0x8081\n#define GL_COLOR_ARRAY_STRIDE 0x8083\n#define GL_COLOR_ARRAY_TYPE 0x8082\n#define GL_COLOR_ATTACHMENT0 0x8CE0\n#define GL_COLOR_ATTACHMENT1 0x8CE1\n#define GL_COLOR_ATTACHMENT10 0x8CEA\n#define GL_COLOR_ATTACHMENT11 0x8CEB\n#define GL_COLOR_ATTACHMENT12 0x8CEC\n#define GL_COLOR_ATTACHMENT13 0x8CED\n#define GL_COLOR_ATTACHMENT14 0x8CEE\n#define GL_COLOR_ATTACHMENT15 0x8CEF\n#define GL_COLOR_ATTACHMENT16 0x8CF0\n#define GL_COLOR_ATTACHMENT17 0x8CF1\n#define GL_COLOR_ATTACHMENT18 0x8CF2\n#define GL_COLOR_ATTACHMENT19 0x8CF3\n#define GL_COLOR_ATTACHMENT2 0x8CE2\n#define GL_COLOR_ATTACHMENT20 0x8CF4\n#define GL_COLOR_ATTACHMENT21 0x8CF5\n#define GL_COLOR_ATTACHMENT22 0x8CF6\n#define GL_COLOR_ATTACHMENT23 0x8CF7\n#define GL_COLOR_ATTACHMENT24 0x8CF8\n#define GL_COLOR_ATTACHMENT25 0x8CF9\n#define GL_COLOR_ATTACHMENT26 0x8CFA\n#define GL_COLOR_ATTACHMENT27 0x8CFB\n#define GL_COLOR_ATTACHMENT28 0x8CFC\n#define GL_COLOR_ATTACHMENT29 0x8CFD\n#define GL_COLOR_ATTACHMENT3 0x8CE3\n#define GL_COLOR_ATTACHMENT30 0x8CFE\n#define GL_COLOR_ATTACHMENT31 0x8CFF\n#define GL_COLOR_ATTACHMENT4 0x8CE4\n#define GL_COLOR_ATTACHMENT5 0x8CE5\n#define GL_COLOR_ATTACHMENT6 0x8CE6\n#define GL_COLOR_ATTACHMENT7 0x8CE7\n#define GL_COLOR_ATTACHMENT8 0x8CE8\n#define GL_COLOR_ATTACHMENT9 0x8CE9\n#define GL_COLOR_BUFFER_BIT 0x00004000\n#define GL_COLOR_CLEAR_VALUE 0x0C22\n#define GL_COLOR_COMPONENTS 0x8283\n#define GL_COLOR_ENCODING 0x8296\n#define GL_COLOR_INDEX 0x1900\n#define GL_COLOR_INDEXES 0x1603\n#define GL_COLOR_LOGIC_OP 0x0BF2\n#define GL_COLOR_MATERIAL 0x0B57\n#define GL_COLOR_MATERIAL_FACE 0x0B55\n#define GL_COLOR_MATERIAL_PARAMETER 0x0B56\n#define GL_COLOR_RENDERABLE 0x8286\n#define GL_COLOR_SUM 0x8458\n#define GL_COLOR_TABLE 0x80D0\n#define GL_COLOR_WRITEMASK 0x0C23\n#define GL_COMBINE 0x8570\n#define GL_COMBINE_ALPHA 0x8572\n#define GL_COMBINE_RGB 0x8571\n#define GL_COMMAND_BARRIER_BIT 0x00000040\n#define GL_COMPARE_REF_TO_TEXTURE 0x884E\n#define GL_COMPARE_R_TO_TEXTURE 0x884E\n#define GL_COMPATIBLE_SUBROUTINES 0x8E4B\n#define GL_COMPILE 0x1300\n#define GL_COMPILE_AND_EXECUTE 0x1301\n#define GL_COMPILE_STATUS 0x8B81\n#define GL_COMPRESSED_ALPHA 0x84E9\n#define GL_COMPRESSED_INTENSITY 0x84EC\n#define GL_COMPRESSED_LUMINANCE 0x84EA\n#define GL_COMPRESSED_LUMINANCE_ALPHA 0x84EB\n#define GL_COMPRESSED_R11_EAC 0x9270\n#define GL_COMPRESSED_RED 0x8225\n#define GL_COMPRESSED_RED_RGTC1 0x8DBB\n#define GL_COMPRESSED_RG 0x8226\n#define GL_COMPRESSED_RG11_EAC 0x9272\n#define GL_COMPRESSED_RGB 0x84ED\n#define GL_COMPRESSED_RGB8_ETC2 0x9274\n#define GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9276\n#define GL_COMPRESSED_RGBA 0x84EE\n#define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278\n#define GL_COMPRESSED_RGBA_BPTC_UNORM 0x8E8C\n#define GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT 0x8E8E\n#define GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT 0x8E8F\n#define GL_COMPRESSED_RG_RGTC2 0x8DBD\n#define GL_COMPRESSED_SIGNED_R11_EAC 0x9271\n#define GL_COMPRESSED_SIGNED_RED_RGTC1 0x8DBC\n#define GL_COMPRESSED_SIGNED_RG11_EAC 0x9273\n#define GL_COMPRESSED_SIGNED_RG_RGTC2 0x8DBE\n#define GL_COMPRESSED_SLUMINANCE 0x8C4A\n#define GL_COMPRESSED_SLUMINANCE_ALPHA 0x8C4B\n#define GL_COMPRESSED_SRGB 0x8C48\n#define GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC 0x9279\n#define GL_COMPRESSED_SRGB8_ETC2 0x9275\n#define GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9277\n#define GL_COMPRESSED_SRGB_ALPHA 0x8C49\n#define GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM 0x8E8D\n#define GL_COMPRESSED_TEXTURE_FORMATS 0x86A3\n#define GL_COMPUTE_SHADER 0x91B9\n#define GL_COMPUTE_SHADER_BIT 0x00000020\n#define GL_COMPUTE_SHADER_INVOCATIONS 0x82F5\n#define GL_COMPUTE_SUBROUTINE 0x92ED\n#define GL_COMPUTE_SUBROUTINE_UNIFORM 0x92F3\n#define GL_COMPUTE_TEXTURE 0x82A0\n#define GL_COMPUTE_WORK_GROUP_SIZE 0x8267\n#define GL_CONDITION_SATISFIED 0x911C\n#define GL_CONSTANT 0x8576\n#define GL_CONSTANT_ALPHA 0x8003\n#define GL_CONSTANT_ATTENUATION 0x1207\n#define GL_CONSTANT_COLOR 0x8001\n#define GL_CONTEXT_COMPATIBILITY_PROFILE_BIT 0x00000002\n#define GL_CONTEXT_CORE_PROFILE_BIT 0x00000001\n#define GL_CONTEXT_FLAGS 0x821E\n#define GL_CONTEXT_FLAG_DEBUG_BIT 0x00000002\n#define GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT 0x00000001\n#define GL_CONTEXT_FLAG_NO_ERROR_BIT 0x00000008\n#define GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT 0x00000004\n#define GL_CONTEXT_LOST 0x0507\n#define GL_CONTEXT_PROFILE_MASK 0x9126\n#define GL_CONTEXT_RELEASE_BEHAVIOR 0x82FB\n#define GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH 0x82FC\n#define GL_CONVOLUTION_1D 0x8010\n#define GL_CONVOLUTION_2D 0x8011\n#define GL_COORD_REPLACE 0x8862\n#define GL_COPY 0x1503\n#define GL_COPY_INVERTED 0x150C\n#define GL_COPY_PIXEL_TOKEN 0x0706\n#define GL_COPY_READ_BUFFER 0x8F36\n#define GL_COPY_READ_BUFFER_BINDING 0x8F36\n#define GL_COPY_WRITE_BUFFER 0x8F37\n#define GL_COPY_WRITE_BUFFER_BINDING 0x8F37\n#define GL_CULL_FACE 0x0B44\n#define GL_CULL_FACE_MODE 0x0B45\n#define GL_CURRENT_BIT 0x00000001\n#define GL_CURRENT_COLOR 0x0B00\n#define GL_CURRENT_FOG_COORD 0x8453\n#define GL_CURRENT_FOG_COORDINATE 0x8453\n#define GL_CURRENT_INDEX 0x0B01\n#define GL_CURRENT_NORMAL 0x0B02\n#define GL_CURRENT_PROGRAM 0x8B8D\n#define GL_CURRENT_QUERY 0x8865\n#define GL_CURRENT_RASTER_COLOR 0x0B04\n#define GL_CURRENT_RASTER_DISTANCE 0x0B09\n#define GL_CURRENT_RASTER_INDEX 0x0B05\n#define GL_CURRENT_RASTER_POSITION 0x0B07\n#define GL_CURRENT_RASTER_POSITION_VALID 0x0B08\n#define GL_CURRENT_RASTER_SECONDARY_COLOR 0x845F\n#define GL_CURRENT_RASTER_TEXTURE_COORDS 0x0B06\n#define GL_CURRENT_SECONDARY_COLOR 0x8459\n#define GL_CURRENT_TEXTURE_COORDS 0x0B03\n#define GL_CURRENT_VERTEX_ATTRIB 0x8626\n#define GL_CW 0x0900\n#define GL_DEBUG_CALLBACK_FUNCTION 0x8244\n#define GL_DEBUG_CALLBACK_USER_PARAM 0x8245\n#define GL_DEBUG_GROUP_STACK_DEPTH 0x826D\n#define GL_DEBUG_LOGGED_MESSAGES 0x9145\n#define GL_DEBUG_NEXT_LOGGED_MESSAGE_LENGTH 0x8243\n#define GL_DEBUG_OUTPUT 0x92E0\n#define GL_DEBUG_OUTPUT_SYNCHRONOUS 0x8242\n#define GL_DEBUG_SEVERITY_HIGH 0x9146\n#define GL_DEBUG_SEVERITY_LOW 0x9148\n#define GL_DEBUG_SEVERITY_MEDIUM 0x9147\n#define GL_DEBUG_SEVERITY_NOTIFICATION 0x826B\n#define GL_DEBUG_SOURCE_API 0x8246\n#define GL_DEBUG_SOURCE_APPLICATION 0x824A\n#define GL_DEBUG_SOURCE_OTHER 0x824B\n#define GL_DEBUG_SOURCE_SHADER_COMPILER 0x8248\n#define GL_DEBUG_SOURCE_THIRD_PARTY 0x8249\n#define GL_DEBUG_SOURCE_WINDOW_SYSTEM 0x8247\n#define GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR 0x824D\n#define GL_DEBUG_TYPE_ERROR 0x824C\n#define GL_DEBUG_TYPE_MARKER 0x8268\n#define GL_DEBUG_TYPE_OTHER 0x8251\n#define GL_DEBUG_TYPE_PERFORMANCE 0x8250\n#define GL_DEBUG_TYPE_POP_GROUP 0x826A\n#define GL_DEBUG_TYPE_PORTABILITY 0x824F\n#define GL_DEBUG_TYPE_PUSH_GROUP 0x8269\n#define GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR 0x824E\n#define GL_DECAL 0x2101\n#define GL_DECR 0x1E03\n#define GL_DECR_WRAP 0x8508\n#define GL_DELETE_STATUS 0x8B80\n#define GL_DEPTH 0x1801\n#define GL_DEPTH24_STENCIL8 0x88F0\n#define GL_DEPTH32F_STENCIL8 0x8CAD\n#define GL_DEPTH_ATTACHMENT 0x8D00\n#define GL_DEPTH_BIAS 0x0D1F\n#define GL_DEPTH_BITS 0x0D56\n#define GL_DEPTH_BUFFER_BIT 0x00000100\n#define GL_DEPTH_CLAMP 0x864F\n#define GL_DEPTH_CLEAR_VALUE 0x0B73\n#define GL_DEPTH_COMPONENT 0x1902\n#define GL_DEPTH_COMPONENT16 0x81A5\n#define GL_DEPTH_COMPONENT24 0x81A6\n#define GL_DEPTH_COMPONENT32 0x81A7\n#define GL_DEPTH_COMPONENT32F 0x8CAC\n#define GL_DEPTH_COMPONENTS 0x8284\n#define GL_DEPTH_FUNC 0x0B74\n#define GL_DEPTH_RANGE 0x0B70\n#define GL_DEPTH_RENDERABLE 0x8287\n#define GL_DEPTH_SCALE 0x0D1E\n#define GL_DEPTH_STENCIL 0x84F9\n#define GL_DEPTH_STENCIL_ATTACHMENT 0x821A\n#define GL_DEPTH_STENCIL_TEXTURE_MODE 0x90EA\n#define GL_DEPTH_TEST 0x0B71\n#define GL_DEPTH_TEXTURE_MODE 0x884B\n#define GL_DEPTH_WRITEMASK 0x0B72\n#define GL_DIFFUSE 0x1201\n#define GL_DISPATCH_INDIRECT_BUFFER 0x90EE\n#define GL_DISPATCH_INDIRECT_BUFFER_BINDING 0x90EF\n#define GL_DISPLAY_LIST 0x82E7\n#define GL_DITHER 0x0BD0\n#define GL_DOMAIN 0x0A02\n#define GL_DONT_CARE 0x1100\n#define GL_DOT3_RGB 0x86AE\n#define GL_DOT3_RGBA 0x86AF\n#define GL_DOUBLE 0x140A\n#define GL_DOUBLEBUFFER 0x0C32\n#define GL_DOUBLE_MAT2 0x8F46\n#define GL_DOUBLE_MAT2x3 0x8F49\n#define GL_DOUBLE_MAT2x4 0x8F4A\n#define GL_DOUBLE_MAT3 0x8F47\n#define GL_DOUBLE_MAT3x2 0x8F4B\n#define GL_DOUBLE_MAT3x4 0x8F4C\n#define GL_DOUBLE_MAT4 0x8F48\n#define GL_DOUBLE_MAT4x2 0x8F4D\n#define GL_DOUBLE_MAT4x3 0x8F4E\n#define GL_DOUBLE_VEC2 0x8FFC\n#define GL_DOUBLE_VEC3 0x8FFD\n#define GL_DOUBLE_VEC4 0x8FFE\n#define GL_DRAW_BUFFER 0x0C01\n#define GL_DRAW_BUFFER0 0x8825\n#define GL_DRAW_BUFFER1 0x8826\n#define GL_DRAW_BUFFER10 0x882F\n#define GL_DRAW_BUFFER11 0x8830\n#define GL_DRAW_BUFFER12 0x8831\n#define GL_DRAW_BUFFER13 0x8832\n#define GL_DRAW_BUFFER14 0x8833\n#define GL_DRAW_BUFFER15 0x8834\n#define GL_DRAW_BUFFER2 0x8827\n#define GL_DRAW_BUFFER3 0x8828\n#define GL_DRAW_BUFFER4 0x8829\n#define GL_DRAW_BUFFER5 0x882A\n#define GL_DRAW_BUFFER6 0x882B\n#define GL_DRAW_BUFFER7 0x882C\n#define GL_DRAW_BUFFER8 0x882D\n#define GL_DRAW_BUFFER9 0x882E\n#define GL_DRAW_FRAMEBUFFER 0x8CA9\n#define GL_DRAW_FRAMEBUFFER_BINDING 0x8CA6\n#define GL_DRAW_INDIRECT_BUFFER 0x8F3F\n#define GL_DRAW_INDIRECT_BUFFER_BINDING 0x8F43\n#define GL_DRAW_PIXEL_TOKEN 0x0705\n#define GL_DST_ALPHA 0x0304\n#define GL_DST_COLOR 0x0306\n#define GL_DYNAMIC_COPY 0x88EA\n#define GL_DYNAMIC_DRAW 0x88E8\n#define GL_DYNAMIC_READ 0x88E9\n#define GL_DYNAMIC_STORAGE_BIT 0x0100\n#define GL_EDGE_FLAG 0x0B43\n#define GL_EDGE_FLAG_ARRAY 0x8079\n#define GL_EDGE_FLAG_ARRAY_BUFFER_BINDING 0x889B\n#define GL_EDGE_FLAG_ARRAY_POINTER 0x8093\n#define GL_EDGE_FLAG_ARRAY_STRIDE 0x808C\n#define GL_ELEMENT_ARRAY_BARRIER_BIT 0x00000002\n#define GL_ELEMENT_ARRAY_BUFFER 0x8893\n#define GL_ELEMENT_ARRAY_BUFFER_BINDING 0x8895\n#define GL_EMISSION 0x1600\n#define GL_ENABLE_BIT 0x00002000\n#define GL_EQUAL 0x0202\n#define GL_EQUIV 0x1509\n#define GL_EVAL_BIT 0x00010000\n#define GL_EXP 0x0800\n#define GL_EXP2 0x0801\n#define GL_EXTENSIONS 0x1F03\n#define GL_EYE_LINEAR 0x2400\n#define GL_EYE_PLANE 0x2502\n#define GL_FALSE 0\n#define GL_FASTEST 0x1101\n#define GL_FEEDBACK 0x1C01\n#define GL_FEEDBACK_BUFFER_POINTER 0x0DF0\n#define GL_FEEDBACK_BUFFER_SIZE 0x0DF1\n#define GL_FEEDBACK_BUFFER_TYPE 0x0DF2\n#define GL_FILL 0x1B02\n#define GL_FILTER 0x829A\n#define GL_FIRST_VERTEX_CONVENTION 0x8E4D\n#define GL_FIXED 0x140C\n#define GL_FIXED_ONLY 0x891D\n#define GL_FLAT 0x1D00\n#define GL_FLOAT 0x1406\n#define GL_FLOAT_32_UNSIGNED_INT_24_8_REV 0x8DAD\n#define GL_FLOAT_MAT2 0x8B5A\n#define GL_FLOAT_MAT2x3 0x8B65\n#define GL_FLOAT_MAT2x4 0x8B66\n#define GL_FLOAT_MAT3 0x8B5B\n#define GL_FLOAT_MAT3x2 0x8B67\n#define GL_FLOAT_MAT3x4 0x8B68\n#define GL_FLOAT_MAT4 0x8B5C\n#define GL_FLOAT_MAT4x2 0x8B69\n#define GL_FLOAT_MAT4x3 0x8B6A\n#define GL_FLOAT_VEC2 0x8B50\n#define GL_FLOAT_VEC3 0x8B51\n#define GL_FLOAT_VEC4 0x8B52\n#define GL_FOG 0x0B60\n#define GL_FOG_BIT 0x00000080\n#define GL_FOG_COLOR 0x0B66\n#define GL_FOG_COORD 0x8451\n#define GL_FOG_COORDINATE 0x8451\n#define GL_FOG_COORDINATE_ARRAY 0x8457\n#define GL_FOG_COORDINATE_ARRAY_BUFFER_BINDING 0x889D\n#define GL_FOG_COORDINATE_ARRAY_POINTER 0x8456\n#define GL_FOG_COORDINATE_ARRAY_STRIDE 0x8455\n#define GL_FOG_COORDINATE_ARRAY_TYPE 0x8454\n#define GL_FOG_COORDINATE_SOURCE 0x8450\n#define GL_FOG_COORD_ARRAY 0x8457\n#define GL_FOG_COORD_ARRAY_BUFFER_BINDING 0x889D\n#define GL_FOG_COORD_ARRAY_POINTER 0x8456\n#define GL_FOG_COORD_ARRAY_STRIDE 0x8455\n#define GL_FOG_COORD_ARRAY_TYPE 0x8454\n#define GL_FOG_COORD_SRC 0x8450\n#define GL_FOG_DENSITY 0x0B62\n#define GL_FOG_END 0x0B64\n#define GL_FOG_HINT 0x0C54\n#define GL_FOG_INDEX 0x0B61\n#define GL_FOG_MODE 0x0B65\n#define GL_FOG_START 0x0B63\n#define GL_FRACTIONAL_EVEN 0x8E7C\n#define GL_FRACTIONAL_ODD 0x8E7B\n#define GL_FRAGMENT_DEPTH 0x8452\n#define GL_FRAGMENT_INTERPOLATION_OFFSET_BITS 0x8E5D\n#define GL_FRAGMENT_SHADER 0x8B30\n#define GL_FRAGMENT_SHADER_BIT 0x00000002\n#define GL_FRAGMENT_SHADER_DERIVATIVE_HINT 0x8B8B\n#define GL_FRAGMENT_SHADER_INVOCATIONS 0x82F4\n#define GL_FRAGMENT_SUBROUTINE 0x92EC\n#define GL_FRAGMENT_SUBROUTINE_UNIFORM 0x92F2\n#define GL_FRAGMENT_TEXTURE 0x829F\n#define GL_FRAMEBUFFER 0x8D40\n#define GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE 0x8215\n#define GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE 0x8214\n#define GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING 0x8210\n#define GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE 0x8211\n#define GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE 0x8216\n#define GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE 0x8213\n#define GL_FRAMEBUFFER_ATTACHMENT_LAYERED 0x8DA7\n#define GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME 0x8CD1\n#define GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE 0x8CD0\n#define GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE 0x8212\n#define GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE 0x8217\n#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE 0x8CD3\n#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER 0x8CD4\n#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL 0x8CD2\n#define GL_FRAMEBUFFER_BARRIER_BIT 0x00000400\n#define GL_FRAMEBUFFER_BINDING 0x8CA6\n#define GL_FRAMEBUFFER_BLEND 0x828B\n#define GL_FRAMEBUFFER_COMPLETE 0x8CD5\n#define GL_FRAMEBUFFER_DEFAULT 0x8218\n#define GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS 0x9314\n#define GL_FRAMEBUFFER_DEFAULT_HEIGHT 0x9311\n#define GL_FRAMEBUFFER_DEFAULT_LAYERS 0x9312\n#define GL_FRAMEBUFFER_DEFAULT_SAMPLES 0x9313\n#define GL_FRAMEBUFFER_DEFAULT_WIDTH 0x9310\n#define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT 0x8CD6\n#define GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER 0x8CDB\n#define GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS 0x8DA8\n#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 0x8CD7\n#define GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE 0x8D56\n#define GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER 0x8CDC\n#define GL_FRAMEBUFFER_RENDERABLE 0x8289\n#define GL_FRAMEBUFFER_RENDERABLE_LAYERED 0x828A\n#define GL_FRAMEBUFFER_SRGB 0x8DB9\n#define GL_FRAMEBUFFER_UNDEFINED 0x8219\n#define GL_FRAMEBUFFER_UNSUPPORTED 0x8CDD\n#define GL_FRONT 0x0404\n#define GL_FRONT_AND_BACK 0x0408\n#define GL_FRONT_FACE 0x0B46\n#define GL_FRONT_LEFT 0x0400\n#define GL_FRONT_RIGHT 0x0401\n#define GL_FULL_SUPPORT 0x82B7\n#define GL_FUNC_ADD 0x8006\n#define GL_FUNC_REVERSE_SUBTRACT 0x800B\n#define GL_FUNC_SUBTRACT 0x800A\n#define GL_GENERATE_MIPMAP 0x8191\n#define GL_GENERATE_MIPMAP_HINT 0x8192\n#define GL_GEOMETRY_INPUT_TYPE 0x8917\n#define GL_GEOMETRY_OUTPUT_TYPE 0x8918\n#define GL_GEOMETRY_SHADER 0x8DD9\n#define GL_GEOMETRY_SHADER_BIT 0x00000004\n#define GL_GEOMETRY_SHADER_INVOCATIONS 0x887F\n#define GL_GEOMETRY_SHADER_PRIMITIVES_EMITTED 0x82F3\n#define GL_GEOMETRY_SUBROUTINE 0x92EB\n#define GL_GEOMETRY_SUBROUTINE_UNIFORM 0x92F1\n#define GL_GEOMETRY_TEXTURE 0x829E\n#define GL_GEOMETRY_VERTICES_OUT 0x8916\n#define GL_GEQUAL 0x0206\n#define GL_GET_TEXTURE_IMAGE_FORMAT 0x8291\n#define GL_GET_TEXTURE_IMAGE_TYPE 0x8292\n#define GL_GREATER 0x0204\n#define GL_GREEN 0x1904\n#define GL_GREEN_BIAS 0x0D19\n#define GL_GREEN_BITS 0x0D53\n#define GL_GREEN_INTEGER 0x8D95\n#define GL_GREEN_SCALE 0x0D18\n#define GL_GUILTY_CONTEXT_RESET 0x8253\n#define GL_HALF_FLOAT 0x140B\n#define GL_HIGH_FLOAT 0x8DF2\n#define GL_HIGH_INT 0x8DF5\n#define GL_HINT_BIT 0x00008000\n#define GL_HISTOGRAM 0x8024\n#define GL_IMAGE_1D 0x904C\n#define GL_IMAGE_1D_ARRAY 0x9052\n#define GL_IMAGE_2D 0x904D\n#define GL_IMAGE_2D_ARRAY 0x9053\n#define GL_IMAGE_2D_MULTISAMPLE 0x9055\n#define GL_IMAGE_2D_MULTISAMPLE_ARRAY 0x9056\n#define GL_IMAGE_2D_RECT 0x904F\n#define GL_IMAGE_3D 0x904E\n#define GL_IMAGE_BINDING_ACCESS 0x8F3E\n#define GL_IMAGE_BINDING_FORMAT 0x906E\n#define GL_IMAGE_BINDING_LAYER 0x8F3D\n#define GL_IMAGE_BINDING_LAYERED 0x8F3C\n#define GL_IMAGE_BINDING_LEVEL 0x8F3B\n#define GL_IMAGE_BINDING_NAME 0x8F3A\n#define GL_IMAGE_BUFFER 0x9051\n#define GL_IMAGE_CLASS_10_10_10_2 0x82C3\n#define GL_IMAGE_CLASS_11_11_10 0x82C2\n#define GL_IMAGE_CLASS_1_X_16 0x82BE\n#define GL_IMAGE_CLASS_1_X_32 0x82BB\n#define GL_IMAGE_CLASS_1_X_8 0x82C1\n#define GL_IMAGE_CLASS_2_X_16 0x82BD\n#define GL_IMAGE_CLASS_2_X_32 0x82BA\n#define GL_IMAGE_CLASS_2_X_8 0x82C0\n#define GL_IMAGE_CLASS_4_X_16 0x82BC\n#define GL_IMAGE_CLASS_4_X_32 0x82B9\n#define GL_IMAGE_CLASS_4_X_8 0x82BF\n#define GL_IMAGE_COMPATIBILITY_CLASS 0x82A8\n#define GL_IMAGE_CUBE 0x9050\n#define GL_IMAGE_CUBE_MAP_ARRAY 0x9054\n#define GL_IMAGE_FORMAT_COMPATIBILITY_BY_CLASS 0x90C9\n#define GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE 0x90C8\n#define GL_IMAGE_FORMAT_COMPATIBILITY_TYPE 0x90C7\n#define GL_IMAGE_PIXEL_FORMAT 0x82A9\n#define GL_IMAGE_PIXEL_TYPE 0x82AA\n#define GL_IMAGE_TEXEL_SIZE 0x82A7\n#define GL_IMPLEMENTATION_COLOR_READ_FORMAT 0x8B9B\n#define GL_IMPLEMENTATION_COLOR_READ_TYPE 0x8B9A\n#define GL_INCR 0x1E02\n#define GL_INCR_WRAP 0x8507\n#define GL_INDEX 0x8222\n#define GL_INDEX_ARRAY 0x8077\n#define GL_INDEX_ARRAY_BUFFER_BINDING 0x8899\n#define GL_INDEX_ARRAY_POINTER 0x8091\n#define GL_INDEX_ARRAY_STRIDE 0x8086\n#define GL_INDEX_ARRAY_TYPE 0x8085\n#define GL_INDEX_BITS 0x0D51\n#define GL_INDEX_CLEAR_VALUE 0x0C20\n#define GL_INDEX_LOGIC_OP 0x0BF1\n#define GL_INDEX_MODE 0x0C30\n#define GL_INDEX_OFFSET 0x0D13\n#define GL_INDEX_SHIFT 0x0D12\n#define GL_INDEX_WRITEMASK 0x0C21\n#define GL_INFO_LOG_LENGTH 0x8B84\n#define GL_INNOCENT_CONTEXT_RESET 0x8254\n#define GL_INT 0x1404\n#define GL_INTENSITY 0x8049\n#define GL_INTENSITY12 0x804C\n#define GL_INTENSITY16 0x804D\n#define GL_INTENSITY4 0x804A\n#define GL_INTENSITY8 0x804B\n#define GL_INTERLEAVED_ATTRIBS 0x8C8C\n#define GL_INTERNALFORMAT_ALPHA_SIZE 0x8274\n#define GL_INTERNALFORMAT_ALPHA_TYPE 0x827B\n#define GL_INTERNALFORMAT_BLUE_SIZE 0x8273\n#define GL_INTERNALFORMAT_BLUE_TYPE 0x827A\n#define GL_INTERNALFORMAT_DEPTH_SIZE 0x8275\n#define GL_INTERNALFORMAT_DEPTH_TYPE 0x827C\n#define GL_INTERNALFORMAT_GREEN_SIZE 0x8272\n#define GL_INTERNALFORMAT_GREEN_TYPE 0x8279\n#define GL_INTERNALFORMAT_PREFERRED 0x8270\n#define GL_INTERNALFORMAT_RED_SIZE 0x8271\n#define GL_INTERNALFORMAT_RED_TYPE 0x8278\n#define GL_INTERNALFORMAT_SHARED_SIZE 0x8277\n#define GL_INTERNALFORMAT_STENCIL_SIZE 0x8276\n#define GL_INTERNALFORMAT_STENCIL_TYPE 0x827D\n#define GL_INTERNALFORMAT_SUPPORTED 0x826F\n#define GL_INTERPOLATE 0x8575\n#define GL_INT_2_10_10_10_REV 0x8D9F\n#define GL_INT_IMAGE_1D 0x9057\n#define GL_INT_IMAGE_1D_ARRAY 0x905D\n#define GL_INT_IMAGE_2D 0x9058\n#define GL_INT_IMAGE_2D_ARRAY 0x905E\n#define GL_INT_IMAGE_2D_MULTISAMPLE 0x9060\n#define GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x9061\n#define GL_INT_IMAGE_2D_RECT 0x905A\n#define GL_INT_IMAGE_3D 0x9059\n#define GL_INT_IMAGE_BUFFER 0x905C\n#define GL_INT_IMAGE_CUBE 0x905B\n#define GL_INT_IMAGE_CUBE_MAP_ARRAY 0x905F\n#define GL_INT_SAMPLER_1D 0x8DC9\n#define GL_INT_SAMPLER_1D_ARRAY 0x8DCE\n#define GL_INT_SAMPLER_2D 0x8DCA\n#define GL_INT_SAMPLER_2D_ARRAY 0x8DCF\n#define GL_INT_SAMPLER_2D_MULTISAMPLE 0x9109\n#define GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY 0x910C\n#define GL_INT_SAMPLER_2D_RECT 0x8DCD\n#define GL_INT_SAMPLER_3D 0x8DCB\n#define GL_INT_SAMPLER_BUFFER 0x8DD0\n#define GL_INT_SAMPLER_CUBE 0x8DCC\n#define GL_INT_SAMPLER_CUBE_MAP_ARRAY 0x900E\n#define GL_INT_VEC2 0x8B53\n#define GL_INT_VEC3 0x8B54\n#define GL_INT_VEC4 0x8B55\n#define GL_INVALID_ENUM 0x0500\n#define GL_INVALID_FRAMEBUFFER_OPERATION 0x0506\n#define GL_INVALID_INDEX 0xFFFFFFFF\n#define GL_INVALID_OPERATION 0x0502\n#define GL_INVALID_VALUE 0x0501\n#define GL_INVERT 0x150A\n#define GL_ISOLINES 0x8E7A\n#define GL_IS_PER_PATCH 0x92E7\n#define GL_IS_ROW_MAJOR 0x9300\n#define GL_KEEP 0x1E00\n#define GL_LAST_VERTEX_CONVENTION 0x8E4E\n#define GL_LAYER_PROVOKING_VERTEX 0x825E\n#define GL_LEFT 0x0406\n#define GL_LEQUAL 0x0203\n#define GL_LESS 0x0201\n#define GL_LIGHT0 0x4000\n#define GL_LIGHT1 0x4001\n#define GL_LIGHT2 0x4002\n#define GL_LIGHT3 0x4003\n#define GL_LIGHT4 0x4004\n#define GL_LIGHT5 0x4005\n#define GL_LIGHT6 0x4006\n#define GL_LIGHT7 0x4007\n#define GL_LIGHTING 0x0B50\n#define GL_LIGHTING_BIT 0x00000040\n#define GL_LIGHT_MODEL_AMBIENT 0x0B53\n#define GL_LIGHT_MODEL_COLOR_CONTROL 0x81F8\n#define GL_LIGHT_MODEL_LOCAL_VIEWER 0x0B51\n#define GL_LIGHT_MODEL_TWO_SIDE 0x0B52\n#define GL_LINE 0x1B01\n#define GL_LINEAR 0x2601\n#define GL_LINEAR_ATTENUATION 0x1208\n#define GL_LINEAR_MIPMAP_LINEAR 0x2703\n#define GL_LINEAR_MIPMAP_NEAREST 0x2701\n#define GL_LINES 0x0001\n#define GL_LINES_ADJACENCY 0x000A\n#define GL_LINE_BIT 0x00000004\n#define GL_LINE_LOOP 0x0002\n#define GL_LINE_RESET_TOKEN 0x0707\n#define GL_LINE_SMOOTH 0x0B20\n#define GL_LINE_SMOOTH_HINT 0x0C52\n#define GL_LINE_STIPPLE 0x0B24\n#define GL_LINE_STIPPLE_PATTERN 0x0B25\n#define GL_LINE_STIPPLE_REPEAT 0x0B26\n#define GL_LINE_STRIP 0x0003\n#define GL_LINE_STRIP_ADJACENCY 0x000B\n#define GL_LINE_TOKEN 0x0702\n#define GL_LINE_WIDTH 0x0B21\n#define GL_LINE_WIDTH_GRANULARITY 0x0B23\n#define GL_LINE_WIDTH_RANGE 0x0B22\n#define GL_LINK_STATUS 0x8B82\n#define GL_LIST_BASE 0x0B32\n#define GL_LIST_BIT 0x00020000\n#define GL_LIST_INDEX 0x0B33\n#define GL_LIST_MODE 0x0B30\n#define GL_LOAD 0x0101\n#define GL_LOCATION 0x930E\n#define GL_LOCATION_COMPONENT 0x934A\n#define GL_LOCATION_INDEX 0x930F\n#define GL_LOGIC_OP 0x0BF1\n#define GL_LOGIC_OP_MODE 0x0BF0\n#define GL_LOSE_CONTEXT_ON_RESET 0x8252\n#define GL_LOWER_LEFT 0x8CA1\n#define GL_LOW_FLOAT 0x8DF0\n#define GL_LOW_INT 0x8DF3\n#define GL_LUMINANCE 0x1909\n#define GL_LUMINANCE12 0x8041\n#define GL_LUMINANCE12_ALPHA12 0x8047\n#define GL_LUMINANCE12_ALPHA4 0x8046\n#define GL_LUMINANCE16 0x8042\n#define GL_LUMINANCE16_ALPHA16 0x8048\n#define GL_LUMINANCE4 0x803F\n#define GL_LUMINANCE4_ALPHA4 0x8043\n#define GL_LUMINANCE6_ALPHA2 0x8044\n#define GL_LUMINANCE8 0x8040\n#define GL_LUMINANCE8_ALPHA8 0x8045\n#define GL_LUMINANCE_ALPHA 0x190A\n#define GL_MAJOR_VERSION 0x821B\n#define GL_MANUAL_GENERATE_MIPMAP 0x8294\n#define GL_MAP1_COLOR_4 0x0D90\n#define GL_MAP1_GRID_DOMAIN 0x0DD0\n#define GL_MAP1_GRID_SEGMENTS 0x0DD1\n#define GL_MAP1_INDEX 0x0D91\n#define GL_MAP1_NORMAL 0x0D92\n#define GL_MAP1_TEXTURE_COORD_1 0x0D93\n#define GL_MAP1_TEXTURE_COORD_2 0x0D94\n#define GL_MAP1_TEXTURE_COORD_3 0x0D95\n#define GL_MAP1_TEXTURE_COORD_4 0x0D96\n#define GL_MAP1_VERTEX_3 0x0D97\n#define GL_MAP1_VERTEX_4 0x0D98\n#define GL_MAP2_COLOR_4 0x0DB0\n#define GL_MAP2_GRID_DOMAIN 0x0DD2\n#define GL_MAP2_GRID_SEGMENTS 0x0DD3\n#define GL_MAP2_INDEX 0x0DB1\n#define GL_MAP2_NORMAL 0x0DB2\n#define GL_MAP2_TEXTURE_COORD_1 0x0DB3\n#define GL_MAP2_TEXTURE_COORD_2 0x0DB4\n#define GL_MAP2_TEXTURE_COORD_3 0x0DB5\n#define GL_MAP2_TEXTURE_COORD_4 0x0DB6\n#define GL_MAP2_VERTEX_3 0x0DB7\n#define GL_MAP2_VERTEX_4 0x0DB8\n#define GL_MAP_COHERENT_BIT 0x0080\n#define GL_MAP_COLOR 0x0D10\n#define GL_MAP_FLUSH_EXPLICIT_BIT 0x0010\n#define GL_MAP_INVALIDATE_BUFFER_BIT 0x0008\n#define GL_MAP_INVALIDATE_RANGE_BIT 0x0004\n#define GL_MAP_PERSISTENT_BIT 0x0040\n#define GL_MAP_READ_BIT 0x0001\n#define GL_MAP_STENCIL 0x0D11\n#define GL_MAP_UNSYNCHRONIZED_BIT 0x0020\n#define GL_MAP_WRITE_BIT 0x0002\n#define GL_MATRIX_MODE 0x0BA0\n#define GL_MATRIX_STRIDE 0x92FF\n#define GL_MAX 0x8008\n#define GL_MAX_3D_TEXTURE_SIZE 0x8073\n#define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF\n#define GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS 0x92DC\n#define GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE 0x92D8\n#define GL_MAX_ATTRIB_STACK_DEPTH 0x0D35\n#define GL_MAX_CLIENT_ATTRIB_STACK_DEPTH 0x0D3B\n#define GL_MAX_CLIP_DISTANCES 0x0D32\n#define GL_MAX_CLIP_PLANES 0x0D32\n#define GL_MAX_COLOR_ATTACHMENTS 0x8CDF\n#define GL_MAX_COLOR_TEXTURE_SAMPLES 0x910E\n#define GL_MAX_COMBINED_ATOMIC_COUNTERS 0x92D7\n#define GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS 0x92D1\n#define GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES 0x82FA\n#define GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS 0x8266\n#define GL_MAX_COMBINED_DIMENSIONS 0x8282\n#define GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS 0x8A33\n#define GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS 0x8A32\n#define GL_MAX_COMBINED_IMAGE_UNIFORMS 0x90CF\n#define GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS 0x8F39\n#define GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES 0x8F39\n#define GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS 0x90DC\n#define GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS 0x8E1E\n#define GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS 0x8E1F\n#define GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS 0x8B4D\n#define GL_MAX_COMBINED_UNIFORM_BLOCKS 0x8A2E\n#define GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS 0x8A31\n#define GL_MAX_COMPUTE_ATOMIC_COUNTERS 0x8265\n#define GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS 0x8264\n#define GL_MAX_COMPUTE_IMAGE_UNIFORMS 0x91BD\n#define GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS 0x90DB\n#define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262\n#define GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS 0x91BC\n#define GL_MAX_COMPUTE_UNIFORM_BLOCKS 0x91BB\n#define GL_MAX_COMPUTE_UNIFORM_COMPONENTS 0x8263\n#define GL_MAX_COMPUTE_WORK_GROUP_COUNT 0x91BE\n#define GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 0x90EB\n#define GL_MAX_COMPUTE_WORK_GROUP_SIZE 0x91BF\n#define GL_MAX_CUBE_MAP_TEXTURE_SIZE 0x851C\n#define GL_MAX_CULL_DISTANCES 0x82F9\n#define GL_MAX_DEBUG_GROUP_STACK_DEPTH 0x826C\n#define GL_MAX_DEBUG_LOGGED_MESSAGES 0x9144\n#define GL_MAX_DEBUG_MESSAGE_LENGTH 0x9143\n#define GL_MAX_DEPTH 0x8280\n#define GL_MAX_DEPTH_TEXTURE_SAMPLES 0x910F\n#define GL_MAX_DRAW_BUFFERS 0x8824\n#define GL_MAX_DUAL_SOURCE_DRAW_BUFFERS 0x88FC\n#define GL_MAX_ELEMENTS_INDICES 0x80E9\n#define GL_MAX_ELEMENTS_VERTICES 0x80E8\n#define GL_MAX_ELEMENT_INDEX 0x8D6B\n#define GL_MAX_EVAL_ORDER 0x0D30\n#define GL_MAX_FRAGMENT_ATOMIC_COUNTERS 0x92D6\n#define GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS 0x92D0\n#define GL_MAX_FRAGMENT_IMAGE_UNIFORMS 0x90CE\n#define GL_MAX_FRAGMENT_INPUT_COMPONENTS 0x9125\n#define GL_MAX_FRAGMENT_INTERPOLATION_OFFSET 0x8E5C\n#define GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS 0x90DA\n#define GL_MAX_FRAGMENT_UNIFORM_BLOCKS 0x8A2D\n#define GL_MAX_FRAGMENT_UNIFORM_COMPONENTS 0x8B49\n#define GL_MAX_FRAGMENT_UNIFORM_VECTORS 0x8DFD\n#define GL_MAX_FRAMEBUFFER_HEIGHT 0x9316\n#define GL_MAX_FRAMEBUFFER_LAYERS 0x9317\n#define GL_MAX_FRAMEBUFFER_SAMPLES 0x9318\n#define GL_MAX_FRAMEBUFFER_WIDTH 0x9315\n#define GL_MAX_GEOMETRY_ATOMIC_COUNTERS 0x92D5\n#define GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS 0x92CF\n#define GL_MAX_GEOMETRY_IMAGE_UNIFORMS 0x90CD\n#define GL_MAX_GEOMETRY_INPUT_COMPONENTS 0x9123\n#define GL_MAX_GEOMETRY_OUTPUT_COMPONENTS 0x9124\n#define GL_MAX_GEOMETRY_OUTPUT_VERTICES 0x8DE0\n#define GL_MAX_GEOMETRY_SHADER_INVOCATIONS 0x8E5A\n#define GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS 0x90D7\n#define GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS 0x8C29\n#define GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS 0x8DE1\n#define GL_MAX_GEOMETRY_UNIFORM_BLOCKS 0x8A2C\n#define GL_MAX_GEOMETRY_UNIFORM_COMPONENTS 0x8DDF\n#define GL_MAX_HEIGHT 0x827F\n#define GL_MAX_IMAGE_SAMPLES 0x906D\n#define GL_MAX_IMAGE_UNITS 0x8F38\n#define GL_MAX_INTEGER_SAMPLES 0x9110\n#define GL_MAX_LABEL_LENGTH 0x82E8\n#define GL_MAX_LAYERS 0x8281\n#define GL_MAX_LIGHTS 0x0D31\n#define GL_MAX_LIST_NESTING 0x0B31\n#define GL_MAX_MODELVIEW_STACK_DEPTH 0x0D36\n#define GL_MAX_NAME_LENGTH 0x92F6\n#define GL_MAX_NAME_STACK_DEPTH 0x0D37\n#define GL_MAX_NUM_ACTIVE_VARIABLES 0x92F7\n#define GL_MAX_NUM_COMPATIBLE_SUBROUTINES 0x92F8\n#define GL_MAX_PATCH_VERTICES 0x8E7D\n#define GL_MAX_PIXEL_MAP_TABLE 0x0D34\n#define GL_MAX_PROGRAM_TEXEL_OFFSET 0x8905\n#define GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET 0x8E5F\n#define GL_MAX_PROJECTION_STACK_DEPTH 0x0D38\n#define GL_MAX_RECTANGLE_TEXTURE_SIZE 0x84F8\n#define GL_MAX_RENDERBUFFER_SIZE 0x84E8\n#define GL_MAX_SAMPLES 0x8D57\n#define GL_MAX_SAMPLE_MASK_WORDS 0x8E59\n#define GL_MAX_SERVER_WAIT_TIMEOUT 0x9111\n#define GL_MAX_SHADER_STORAGE_BLOCK_SIZE 0x90DE\n#define GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 0x90DD\n#define GL_MAX_SUBROUTINES 0x8DE7\n#define GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS 0x8DE8\n#define GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS 0x92D3\n#define GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS 0x92CD\n#define GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS 0x90CB\n#define GL_MAX_TESS_CONTROL_INPUT_COMPONENTS 0x886C\n#define GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS 0x8E83\n#define GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS 0x90D8\n#define GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS 0x8E81\n#define GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS 0x8E85\n#define GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS 0x8E89\n#define GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS 0x8E7F\n#define GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS 0x92D4\n#define GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS 0x92CE\n#define GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS 0x90CC\n#define GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS 0x886D\n#define GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS 0x8E86\n#define GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS 0x90D9\n#define GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS 0x8E82\n#define GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS 0x8E8A\n#define GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS 0x8E80\n#define GL_MAX_TESS_GEN_LEVEL 0x8E7E\n#define GL_MAX_TESS_PATCH_COMPONENTS 0x8E84\n#define GL_MAX_TEXTURE_BUFFER_SIZE 0x8C2B\n#define GL_MAX_TEXTURE_COORDS 0x8871\n#define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872\n#define GL_MAX_TEXTURE_LOD_BIAS 0x84FD\n#define GL_MAX_TEXTURE_MAX_ANISOTROPY 0x84FF\n#define GL_MAX_TEXTURE_SIZE 0x0D33\n#define GL_MAX_TEXTURE_STACK_DEPTH 0x0D39\n#define GL_MAX_TEXTURE_UNITS 0x84E2\n#define GL_MAX_TRANSFORM_FEEDBACK_BUFFERS 0x8E70\n#define GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS 0x8C8A\n#define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS 0x8C8B\n#define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS 0x8C80\n#define GL_MAX_UNIFORM_BLOCK_SIZE 0x8A30\n#define GL_MAX_UNIFORM_BUFFER_BINDINGS 0x8A2F\n#define GL_MAX_UNIFORM_LOCATIONS 0x826E\n#define GL_MAX_VARYING_COMPONENTS 0x8B4B\n#define GL_MAX_VARYING_FLOATS 0x8B4B\n#define GL_MAX_VARYING_VECTORS 0x8DFC\n#define GL_MAX_VERTEX_ATOMIC_COUNTERS 0x92D2\n#define GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS 0x92CC\n#define GL_MAX_VERTEX_ATTRIBS 0x8869\n#define GL_MAX_VERTEX_ATTRIB_BINDINGS 0x82DA\n#define GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET 0x82D9\n#define GL_MAX_VERTEX_ATTRIB_STRIDE 0x82E5\n#define GL_MAX_VERTEX_IMAGE_UNIFORMS 0x90CA\n#define GL_MAX_VERTEX_OUTPUT_COMPONENTS 0x9122\n#define GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS 0x90D6\n#define GL_MAX_VERTEX_STREAMS 0x8E71\n#define GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS 0x8B4C\n#define GL_MAX_VERTEX_UNIFORM_BLOCKS 0x8A2B\n#define GL_MAX_VERTEX_UNIFORM_COMPONENTS 0x8B4A\n#define GL_MAX_VERTEX_UNIFORM_VECTORS 0x8DFB\n#define GL_MAX_VIEWPORTS 0x825B\n#define GL_MAX_VIEWPORT_DIMS 0x0D3A\n#define GL_MAX_WIDTH 0x827E\n#define GL_MEDIUM_FLOAT 0x8DF1\n#define GL_MEDIUM_INT 0x8DF4\n#define GL_MIN 0x8007\n#define GL_MINMAX 0x802E\n#define GL_MINOR_VERSION 0x821C\n#define GL_MIN_FRAGMENT_INTERPOLATION_OFFSET 0x8E5B\n#define GL_MIN_MAP_BUFFER_ALIGNMENT 0x90BC\n#define GL_MIN_PROGRAM_TEXEL_OFFSET 0x8904\n#define GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET 0x8E5E\n#define GL_MIN_SAMPLE_SHADING_VALUE 0x8C37\n#define GL_MIPMAP 0x8293\n#define GL_MIRRORED_REPEAT 0x8370\n#define GL_MIRROR_CLAMP_TO_EDGE 0x8743\n#define GL_MODELVIEW 0x1700\n#define GL_MODELVIEW_MATRIX 0x0BA6\n#define GL_MODELVIEW_STACK_DEPTH 0x0BA3\n#define GL_MODULATE 0x2100\n#define GL_MULT 0x0103\n#define GL_MULTISAMPLE 0x809D\n#define GL_MULTISAMPLE_BIT 0x20000000\n#define GL_N3F_V3F 0x2A25\n#define GL_NAME_LENGTH 0x92F9\n#define GL_NAME_STACK_DEPTH 0x0D70\n#define GL_NAND 0x150E\n#define GL_NEAREST 0x2600\n#define GL_NEAREST_MIPMAP_LINEAR 0x2702\n#define GL_NEAREST_MIPMAP_NEAREST 0x2700\n#define GL_NEGATIVE_ONE_TO_ONE 0x935E\n#define GL_NEVER 0x0200\n#define GL_NICEST 0x1102\n#define GL_NONE 0\n#define GL_NOOP 0x1505\n#define GL_NOR 0x1508\n#define GL_NORMALIZE 0x0BA1\n#define GL_NORMAL_ARRAY 0x8075\n#define GL_NORMAL_ARRAY_BUFFER_BINDING 0x8897\n#define GL_NORMAL_ARRAY_POINTER 0x808F\n#define GL_NORMAL_ARRAY_STRIDE 0x807F\n#define GL_NORMAL_ARRAY_TYPE 0x807E\n#define GL_NORMAL_MAP 0x8511\n#define GL_NOTEQUAL 0x0205\n#define GL_NO_ERROR 0\n#define GL_NO_RESET_NOTIFICATION 0x8261\n#define GL_NUM_ACTIVE_VARIABLES 0x9304\n#define GL_NUM_COMPATIBLE_SUBROUTINES 0x8E4A\n#define GL_NUM_COMPRESSED_TEXTURE_FORMATS 0x86A2\n#define GL_NUM_EXTENSIONS 0x821D\n#define GL_NUM_PROGRAM_BINARY_FORMATS 0x87FE\n#define GL_NUM_SAMPLE_COUNTS 0x9380\n#define GL_NUM_SHADER_BINARY_FORMATS 0x8DF9\n#define GL_NUM_SHADING_LANGUAGE_VERSIONS 0x82E9\n#define GL_NUM_SPIR_V_EXTENSIONS 0x9554\n#define GL_OBJECT_LINEAR 0x2401\n#define GL_OBJECT_PLANE 0x2501\n#define GL_OBJECT_TYPE 0x9112\n#define GL_OFFSET 0x92FC\n#define GL_ONE 1\n#define GL_ONE_MINUS_CONSTANT_ALPHA 0x8004\n#define GL_ONE_MINUS_CONSTANT_COLOR 0x8002\n#define GL_ONE_MINUS_DST_ALPHA 0x0305\n#define GL_ONE_MINUS_DST_COLOR 0x0307\n#define GL_ONE_MINUS_SRC1_ALPHA 0x88FB\n#define GL_ONE_MINUS_SRC1_COLOR 0x88FA\n#define GL_ONE_MINUS_SRC_ALPHA 0x0303\n#define GL_ONE_MINUS_SRC_COLOR 0x0301\n#define GL_OPERAND0_ALPHA 0x8598\n#define GL_OPERAND0_RGB 0x8590\n#define GL_OPERAND1_ALPHA 0x8599\n#define GL_OPERAND1_RGB 0x8591\n#define GL_OPERAND2_ALPHA 0x859A\n#define GL_OPERAND2_RGB 0x8592\n#define GL_OR 0x1507\n#define GL_ORDER 0x0A01\n#define GL_OR_INVERTED 0x150D\n#define GL_OR_REVERSE 0x150B\n#define GL_OUT_OF_MEMORY 0x0505\n#define GL_PACK_ALIGNMENT 0x0D05\n#define GL_PACK_COMPRESSED_BLOCK_DEPTH 0x912D\n#define GL_PACK_COMPRESSED_BLOCK_HEIGHT 0x912C\n#define GL_PACK_COMPRESSED_BLOCK_SIZE 0x912E\n#define GL_PACK_COMPRESSED_BLOCK_WIDTH 0x912B\n#define GL_PACK_IMAGE_HEIGHT 0x806C\n#define GL_PACK_LSB_FIRST 0x0D01\n#define GL_PACK_ROW_LENGTH 0x0D02\n#define GL_PACK_SKIP_IMAGES 0x806B\n#define GL_PACK_SKIP_PIXELS 0x0D04\n#define GL_PACK_SKIP_ROWS 0x0D03\n#define GL_PACK_SWAP_BYTES 0x0D00\n#define GL_PARAMETER_BUFFER 0x80EE\n#define GL_PARAMETER_BUFFER_BINDING 0x80EF\n#define GL_PASS_THROUGH_TOKEN 0x0700\n#define GL_PATCHES 0x000E\n#define GL_PATCH_DEFAULT_INNER_LEVEL 0x8E73\n#define GL_PATCH_DEFAULT_OUTER_LEVEL 0x8E74\n#define GL_PATCH_VERTICES 0x8E72\n#define GL_PERSPECTIVE_CORRECTION_HINT 0x0C50\n#define GL_PIXEL_BUFFER_BARRIER_BIT 0x00000080\n#define GL_PIXEL_MAP_A_TO_A 0x0C79\n#define GL_PIXEL_MAP_A_TO_A_SIZE 0x0CB9\n#define GL_PIXEL_MAP_B_TO_B 0x0C78\n#define GL_PIXEL_MAP_B_TO_B_SIZE 0x0CB8\n#define GL_PIXEL_MAP_G_TO_G 0x0C77\n#define GL_PIXEL_MAP_G_TO_G_SIZE 0x0CB7\n#define GL_PIXEL_MAP_I_TO_A 0x0C75\n#define GL_PIXEL_MAP_I_TO_A_SIZE 0x0CB5\n#define GL_PIXEL_MAP_I_TO_B 0x0C74\n#define GL_PIXEL_MAP_I_TO_B_SIZE 0x0CB4\n#define GL_PIXEL_MAP_I_TO_G 0x0C73\n#define GL_PIXEL_MAP_I_TO_G_SIZE 0x0CB3\n#define GL_PIXEL_MAP_I_TO_I 0x0C70\n#define GL_PIXEL_MAP_I_TO_I_SIZE 0x0CB0\n#define GL_PIXEL_MAP_I_TO_R 0x0C72\n#define GL_PIXEL_MAP_I_TO_R_SIZE 0x0CB2\n#define GL_PIXEL_MAP_R_TO_R 0x0C76\n#define GL_PIXEL_MAP_R_TO_R_SIZE 0x0CB6\n#define GL_PIXEL_MAP_S_TO_S 0x0C71\n#define GL_PIXEL_MAP_S_TO_S_SIZE 0x0CB1\n#define GL_PIXEL_MODE_BIT 0x00000020\n#define GL_PIXEL_PACK_BUFFER 0x88EB\n#define GL_PIXEL_PACK_BUFFER_BINDING 0x88ED\n#define GL_PIXEL_UNPACK_BUFFER 0x88EC\n#define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF\n#define GL_POINT 0x1B00\n#define GL_POINTS 0x0000\n#define GL_POINT_BIT 0x00000002\n#define GL_POINT_DISTANCE_ATTENUATION 0x8129\n#define GL_POINT_FADE_THRESHOLD_SIZE 0x8128\n#define GL_POINT_SIZE 0x0B11\n#define GL_POINT_SIZE_GRANULARITY 0x0B13\n#define GL_POINT_SIZE_MAX 0x8127\n#define GL_POINT_SIZE_MIN 0x8126\n#define GL_POINT_SIZE_RANGE 0x0B12\n#define GL_POINT_SMOOTH 0x0B10\n#define GL_POINT_SMOOTH_HINT 0x0C51\n#define GL_POINT_SPRITE 0x8861\n#define GL_POINT_SPRITE_COORD_ORIGIN 0x8CA0\n#define GL_POINT_TOKEN 0x0701\n#define GL_POLYGON 0x0009\n#define GL_POLYGON_BIT 0x00000008\n#define GL_POLYGON_MODE 0x0B40\n#define GL_POLYGON_OFFSET_CLAMP 0x8E1B\n#define GL_POLYGON_OFFSET_FACTOR 0x8038\n#define GL_POLYGON_OFFSET_FILL 0x8037\n#define GL_POLYGON_OFFSET_LINE 0x2A02\n#define GL_POLYGON_OFFSET_POINT 0x2A01\n#define GL_POLYGON_OFFSET_UNITS 0x2A00\n#define GL_POLYGON_SMOOTH 0x0B41\n#define GL_POLYGON_SMOOTH_HINT 0x0C53\n#define GL_POLYGON_STIPPLE 0x0B42\n#define GL_POLYGON_STIPPLE_BIT 0x00000010\n#define GL_POLYGON_TOKEN 0x0703\n#define GL_POSITION 0x1203\n#define GL_POST_COLOR_MATRIX_COLOR_TABLE 0x80D2\n#define GL_POST_CONVOLUTION_COLOR_TABLE 0x80D1\n#define GL_PREVIOUS 0x8578\n#define GL_PRIMARY_COLOR 0x8577\n#define GL_PRIMITIVES_GENERATED 0x8C87\n#define GL_PRIMITIVES_SUBMITTED 0x82EF\n#define GL_PRIMITIVE_RESTART 0x8F9D\n#define GL_PRIMITIVE_RESTART_FIXED_INDEX 0x8D69\n#define GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED 0x8221\n#define GL_PRIMITIVE_RESTART_INDEX 0x8F9E\n#define GL_PROGRAM 0x82E2\n#define GL_PROGRAM_BINARY_FORMATS 0x87FF\n#define GL_PROGRAM_BINARY_LENGTH 0x8741\n#define GL_PROGRAM_BINARY_RETRIEVABLE_HINT 0x8257\n#define GL_PROGRAM_INPUT 0x92E3\n#define GL_PROGRAM_OUTPUT 0x92E4\n#define GL_PROGRAM_PIPELINE 0x82E4\n#define GL_PROGRAM_PIPELINE_BINDING 0x825A\n#define GL_PROGRAM_POINT_SIZE 0x8642\n#define GL_PROGRAM_SEPARABLE 0x8258\n#define GL_PROJECTION 0x1701\n#define GL_PROJECTION_MATRIX 0x0BA7\n#define GL_PROJECTION_STACK_DEPTH 0x0BA4\n#define GL_PROVOKING_VERTEX 0x8E4F\n#define GL_PROXY_COLOR_TABLE 0x80D3\n#define GL_PROXY_HISTOGRAM 0x8025\n#define GL_PROXY_POST_COLOR_MATRIX_COLOR_TABLE 0x80D5\n#define GL_PROXY_POST_CONVOLUTION_COLOR_TABLE 0x80D4\n#define GL_PROXY_TEXTURE_1D 0x8063\n#define GL_PROXY_TEXTURE_1D_ARRAY 0x8C19\n#define GL_PROXY_TEXTURE_2D 0x8064\n#define GL_PROXY_TEXTURE_2D_ARRAY 0x8C1B\n#define GL_PROXY_TEXTURE_2D_MULTISAMPLE 0x9101\n#define GL_PROXY_TEXTURE_2D_MULTISAMPLE_ARRAY 0x9103\n#define GL_PROXY_TEXTURE_3D 0x8070\n#define GL_PROXY_TEXTURE_CUBE_MAP 0x851B\n#define GL_PROXY_TEXTURE_CUBE_MAP_ARRAY 0x900B\n#define GL_PROXY_TEXTURE_RECTANGLE 0x84F7\n#define GL_Q 0x2003\n#define GL_QUADRATIC_ATTENUATION 0x1209\n#define GL_QUADS 0x0007\n#define GL_QUADS_FOLLOW_PROVOKING_VERTEX_CONVENTION 0x8E4C\n#define GL_QUAD_STRIP 0x0008\n#define GL_QUERY 0x82E3\n#define GL_QUERY_BUFFER 0x9192\n#define GL_QUERY_BUFFER_BARRIER_BIT 0x00008000\n#define GL_QUERY_BUFFER_BINDING 0x9193\n#define GL_QUERY_BY_REGION_NO_WAIT 0x8E16\n#define GL_QUERY_BY_REGION_NO_WAIT_INVERTED 0x8E1A\n#define GL_QUERY_BY_REGION_WAIT 0x8E15\n#define GL_QUERY_BY_REGION_WAIT_INVERTED 0x8E19\n#define GL_QUERY_COUNTER_BITS 0x8864\n#define GL_QUERY_NO_WAIT 0x8E14\n#define GL_QUERY_NO_WAIT_INVERTED 0x8E18\n#define GL_QUERY_RESULT 0x8866\n#define GL_QUERY_RESULT_AVAILABLE 0x8867\n#define GL_QUERY_RESULT_NO_WAIT 0x9194\n#define GL_QUERY_TARGET 0x82EA\n#define GL_QUERY_WAIT 0x8E13\n#define GL_QUERY_WAIT_INVERTED 0x8E17\n#define GL_R 0x2002\n#define GL_R11F_G11F_B10F 0x8C3A\n#define GL_R16 0x822A\n#define GL_R16F 0x822D\n#define GL_R16I 0x8233\n#define GL_R16UI 0x8234\n#define GL_R16_SNORM 0x8F98\n#define GL_R32F 0x822E\n#define GL_R32I 0x8235\n#define GL_R32UI 0x8236\n#define GL_R3_G3_B2 0x2A10\n#define GL_R8 0x8229\n#define GL_R8I 0x8231\n#define GL_R8UI 0x8232\n#define GL_R8_SNORM 0x8F94\n#define GL_RASTERIZER_DISCARD 0x8C89\n#define GL_READ_BUFFER 0x0C02\n#define GL_READ_FRAMEBUFFER 0x8CA8\n#define GL_READ_FRAMEBUFFER_BINDING 0x8CAA\n#define GL_READ_ONLY 0x88B8\n#define GL_READ_PIXELS 0x828C\n#define GL_READ_PIXELS_FORMAT 0x828D\n#define GL_READ_PIXELS_TYPE 0x828E\n#define GL_READ_WRITE 0x88BA\n#define GL_RED 0x1903\n#define GL_RED_BIAS 0x0D15\n#define GL_RED_BITS 0x0D52\n#define GL_RED_INTEGER 0x8D94\n#define GL_RED_SCALE 0x0D14\n#define GL_REFERENCED_BY_COMPUTE_SHADER 0x930B\n#define GL_REFERENCED_BY_FRAGMENT_SHADER 0x930A\n#define GL_REFERENCED_BY_GEOMETRY_SHADER 0x9309\n#define GL_REFERENCED_BY_TESS_CONTROL_SHADER 0x9307\n#define GL_REFERENCED_BY_TESS_EVALUATION_SHADER 0x9308\n#define GL_REFERENCED_BY_VERTEX_SHADER 0x9306\n#define GL_REFLECTION_MAP 0x8512\n#define GL_RENDER 0x1C00\n#define GL_RENDERBUFFER 0x8D41\n#define GL_RENDERBUFFER_ALPHA_SIZE 0x8D53\n#define GL_RENDERBUFFER_BINDING 0x8CA7\n#define GL_RENDERBUFFER_BLUE_SIZE 0x8D52\n#define GL_RENDERBUFFER_DEPTH_SIZE 0x8D54\n#define GL_RENDERBUFFER_GREEN_SIZE 0x8D51\n#define GL_RENDERBUFFER_HEIGHT 0x8D43\n#define GL_RENDERBUFFER_INTERNAL_FORMAT 0x8D44\n#define GL_RENDERBUFFER_RED_SIZE 0x8D50\n#define GL_RENDERBUFFER_SAMPLES 0x8CAB\n#define GL_RENDERBUFFER_STENCIL_SIZE 0x8D55\n#define GL_RENDERBUFFER_WIDTH 0x8D42\n#define GL_RENDERER 0x1F01\n#define GL_RENDER_MODE 0x0C40\n#define GL_REPEAT 0x2901\n#define GL_REPLACE 0x1E01\n#define GL_RESCALE_NORMAL 0x803A\n#define GL_RESET_NOTIFICATION_STRATEGY 0x8256\n#define GL_RETURN 0x0102\n#define GL_RG 0x8227\n#define GL_RG16 0x822C\n#define GL_RG16F 0x822F\n#define GL_RG16I 0x8239\n#define GL_RG16UI 0x823A\n#define GL_RG16_SNORM 0x8F99\n#define GL_RG32F 0x8230\n#define GL_RG32I 0x823B\n#define GL_RG32UI 0x823C\n#define GL_RG8 0x822B\n#define GL_RG8I 0x8237\n#define GL_RG8UI 0x8238\n#define GL_RG8_SNORM 0x8F95\n#define GL_RGB 0x1907\n#define GL_RGB10 0x8052\n#define GL_RGB10_A2 0x8059\n#define GL_RGB10_A2UI 0x906F\n#define GL_RGB12 0x8053\n#define GL_RGB16 0x8054\n#define GL_RGB16F 0x881B\n#define GL_RGB16I 0x8D89\n#define GL_RGB16UI 0x8D77\n#define GL_RGB16_SNORM 0x8F9A\n#define GL_RGB32F 0x8815\n#define GL_RGB32I 0x8D83\n#define GL_RGB32UI 0x8D71\n#define GL_RGB4 0x804F\n#define GL_RGB5 0x8050\n#define GL_RGB565 0x8D62\n#define GL_RGB5_A1 0x8057\n#define GL_RGB8 0x8051\n#define GL_RGB8I 0x8D8F\n#define GL_RGB8UI 0x8D7D\n#define GL_RGB8_SNORM 0x8F96\n#define GL_RGB9_E5 0x8C3D\n#define GL_RGBA 0x1908\n#define GL_RGBA12 0x805A\n#define GL_RGBA16 0x805B\n#define GL_RGBA16F 0x881A\n#define GL_RGBA16I 0x8D88\n#define GL_RGBA16UI 0x8D76\n#define GL_RGBA16_SNORM 0x8F9B\n#define GL_RGBA2 0x8055\n#define GL_RGBA32F 0x8814\n#define GL_RGBA32I 0x8D82\n#define GL_RGBA32UI 0x8D70\n#define GL_RGBA4 0x8056\n#define GL_RGBA8 0x8058\n#define GL_RGBA8I 0x8D8E\n#define GL_RGBA8UI 0x8D7C\n#define GL_RGBA8_SNORM 0x8F97\n#define GL_RGBA_INTEGER 0x8D99\n#define GL_RGBA_MODE 0x0C31\n#define GL_RGB_INTEGER 0x8D98\n#define GL_RGB_SCALE 0x8573\n#define GL_RG_INTEGER 0x8228\n#define GL_RIGHT 0x0407\n#define GL_S 0x2000\n#define GL_SAMPLER 0x82E6\n#define GL_SAMPLER_1D 0x8B5D\n#define GL_SAMPLER_1D_ARRAY 0x8DC0\n#define GL_SAMPLER_1D_ARRAY_SHADOW 0x8DC3\n#define GL_SAMPLER_1D_SHADOW 0x8B61\n#define GL_SAMPLER_2D 0x8B5E\n#define GL_SAMPLER_2D_ARRAY 0x8DC1\n#define GL_SAMPLER_2D_ARRAY_SHADOW 0x8DC4\n#define GL_SAMPLER_2D_MULTISAMPLE 0x9108\n#define GL_SAMPLER_2D_MULTISAMPLE_ARRAY 0x910B\n#define GL_SAMPLER_2D_RECT 0x8B63\n#define GL_SAMPLER_2D_RECT_SHADOW 0x8B64\n#define GL_SAMPLER_2D_SHADOW 0x8B62\n#define GL_SAMPLER_3D 0x8B5F\n#define GL_SAMPLER_BINDING 0x8919\n#define GL_SAMPLER_BUFFER 0x8DC2\n#define GL_SAMPLER_CUBE 0x8B60\n#define GL_SAMPLER_CUBE_MAP_ARRAY 0x900C\n#define GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW 0x900D\n#define GL_SAMPLER_CUBE_SHADOW 0x8DC5\n#define GL_SAMPLES 0x80A9\n#define GL_SAMPLES_PASSED 0x8914\n#define GL_SAMPLE_ALPHA_TO_COVERAGE 0x809E\n#define GL_SAMPLE_ALPHA_TO_ONE 0x809F\n#define GL_SAMPLE_BUFFERS 0x80A8\n#define GL_SAMPLE_COVERAGE 0x80A0\n#define GL_SAMPLE_COVERAGE_INVERT 0x80AB\n#define GL_SAMPLE_COVERAGE_VALUE 0x80AA\n#define GL_SAMPLE_MASK 0x8E51\n#define GL_SAMPLE_MASK_VALUE 0x8E52\n#define GL_SAMPLE_POSITION 0x8E50\n#define GL_SAMPLE_SHADING 0x8C36\n#define GL_SCISSOR_BIT 0x00080000\n#define GL_SCISSOR_BOX 0x0C10\n#define GL_SCISSOR_TEST 0x0C11\n#define GL_SECONDARY_COLOR_ARRAY 0x845E\n#define GL_SECONDARY_COLOR_ARRAY_BUFFER_BINDING 0x889C\n#define GL_SECONDARY_COLOR_ARRAY_POINTER 0x845D\n#define GL_SECONDARY_COLOR_ARRAY_SIZE 0x845A\n#define GL_SECONDARY_COLOR_ARRAY_STRIDE 0x845C\n#define GL_SECONDARY_COLOR_ARRAY_TYPE 0x845B\n#define GL_SELECT 0x1C02\n#define GL_SELECTION_BUFFER_POINTER 0x0DF3\n#define GL_SELECTION_BUFFER_SIZE 0x0DF4\n#define GL_SEPARABLE_2D 0x8012\n#define GL_SEPARATE_ATTRIBS 0x8C8D\n#define GL_SEPARATE_SPECULAR_COLOR 0x81FA\n#define GL_SET 0x150F\n#define GL_SHADER 0x82E1\n#define GL_SHADER_BINARY_FORMATS 0x8DF8\n#define GL_SHADER_BINARY_FORMAT_SPIR_V 0x9551\n#define GL_SHADER_COMPILER 0x8DFA\n#define GL_SHADER_IMAGE_ACCESS_BARRIER_BIT 0x00000020\n#define GL_SHADER_IMAGE_ATOMIC 0x82A6\n#define GL_SHADER_IMAGE_LOAD 0x82A4\n#define GL_SHADER_IMAGE_STORE 0x82A5\n#define GL_SHADER_SOURCE_LENGTH 0x8B88\n#define GL_SHADER_STORAGE_BARRIER_BIT 0x00002000\n#define GL_SHADER_STORAGE_BLOCK 0x92E6\n#define GL_SHADER_STORAGE_BUFFER 0x90D2\n#define GL_SHADER_STORAGE_BUFFER_BINDING 0x90D3\n#define GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT 0x90DF\n#define GL_SHADER_STORAGE_BUFFER_SIZE 0x90D5\n#define GL_SHADER_STORAGE_BUFFER_START 0x90D4\n#define GL_SHADER_TYPE 0x8B4F\n#define GL_SHADE_MODEL 0x0B54\n#define GL_SHADING_LANGUAGE_VERSION 0x8B8C\n#define GL_SHININESS 0x1601\n#define GL_SHORT 0x1402\n#define GL_SIGNALED 0x9119\n#define GL_SIGNED_NORMALIZED 0x8F9C\n#define GL_SIMULTANEOUS_TEXTURE_AND_DEPTH_TEST 0x82AC\n#define GL_SIMULTANEOUS_TEXTURE_AND_DEPTH_WRITE 0x82AE\n#define GL_SIMULTANEOUS_TEXTURE_AND_STENCIL_TEST 0x82AD\n#define GL_SIMULTANEOUS_TEXTURE_AND_STENCIL_WRITE 0x82AF\n#define GL_SINGLE_COLOR 0x81F9\n#define GL_SLUMINANCE 0x8C46\n#define GL_SLUMINANCE8 0x8C47\n#define GL_SLUMINANCE8_ALPHA8 0x8C45\n#define GL_SLUMINANCE_ALPHA 0x8C44\n#define GL_SMOOTH 0x1D01\n#define GL_SMOOTH_LINE_WIDTH_GRANULARITY 0x0B23\n#define GL_SMOOTH_LINE_WIDTH_RANGE 0x0B22\n#define GL_SMOOTH_POINT_SIZE_GRANULARITY 0x0B13\n#define GL_SMOOTH_POINT_SIZE_RANGE 0x0B12\n#define GL_SOURCE0_ALPHA 0x8588\n#define GL_SOURCE0_RGB 0x8580\n#define GL_SOURCE1_ALPHA 0x8589\n#define GL_SOURCE1_RGB 0x8581\n#define GL_SOURCE2_ALPHA 0x858A\n#define GL_SOURCE2_RGB 0x8582\n#define GL_SPECULAR 0x1202\n#define GL_SPHERE_MAP 0x2402\n#define GL_SPIR_V_BINARY 0x9552\n#define GL_SPIR_V_EXTENSIONS 0x9553\n#define GL_SPOT_CUTOFF 0x1206\n#define GL_SPOT_DIRECTION 0x1204\n#define GL_SPOT_EXPONENT 0x1205\n#define GL_SRC0_ALPHA 0x8588\n#define GL_SRC0_RGB 0x8580\n#define GL_SRC1_ALPHA 0x8589\n#define GL_SRC1_COLOR 0x88F9\n#define GL_SRC1_RGB 0x8581\n#define GL_SRC2_ALPHA 0x858A\n#define GL_SRC2_RGB 0x8582\n#define GL_SRC_ALPHA 0x0302\n#define GL_SRC_ALPHA_SATURATE 0x0308\n#define GL_SRC_COLOR 0x0300\n#define GL_SRGB 0x8C40\n#define GL_SRGB8 0x8C41\n#define GL_SRGB8_ALPHA8 0x8C43\n#define GL_SRGB_ALPHA 0x8C42\n#define GL_SRGB_READ 0x8297\n#define GL_SRGB_WRITE 0x8298\n#define GL_STACK_OVERFLOW 0x0503\n#define GL_STACK_UNDERFLOW 0x0504\n#define GL_STATIC_COPY 0x88E6\n#define GL_STATIC_DRAW 0x88E4\n#define GL_STATIC_READ 0x88E5\n#define GL_STENCIL 0x1802\n#define GL_STENCIL_ATTACHMENT 0x8D20\n#define GL_STENCIL_BACK_FAIL 0x8801\n#define GL_STENCIL_BACK_FUNC 0x8800\n#define GL_STENCIL_BACK_PASS_DEPTH_FAIL 0x8802\n#define GL_STENCIL_BACK_PASS_DEPTH_PASS 0x8803\n#define GL_STENCIL_BACK_REF 0x8CA3\n#define GL_STENCIL_BACK_VALUE_MASK 0x8CA4\n#define GL_STENCIL_BACK_WRITEMASK 0x8CA5\n#define GL_STENCIL_BITS 0x0D57\n#define GL_STENCIL_BUFFER_BIT 0x00000400\n#define GL_STENCIL_CLEAR_VALUE 0x0B91\n#define GL_STENCIL_COMPONENTS 0x8285\n#define GL_STENCIL_FAIL 0x0B94\n#define GL_STENCIL_FUNC 0x0B92\n#define GL_STENCIL_INDEX 0x1901\n#define GL_STENCIL_INDEX1 0x8D46\n#define GL_STENCIL_INDEX16 0x8D49\n#define GL_STENCIL_INDEX4 0x8D47\n#define GL_STENCIL_INDEX8 0x8D48\n#define GL_STENCIL_PASS_DEPTH_FAIL 0x0B95\n#define GL_STENCIL_PASS_DEPTH_PASS 0x0B96\n#define GL_STENCIL_REF 0x0B97\n#define GL_STENCIL_RENDERABLE 0x8288\n#define GL_STENCIL_TEST 0x0B90\n#define GL_STENCIL_VALUE_MASK 0x0B93\n#define GL_STENCIL_WRITEMASK 0x0B98\n#define GL_STEREO 0x0C33\n#define GL_STREAM_COPY 0x88E2\n#define GL_STREAM_DRAW 0x88E0\n#define GL_STREAM_READ 0x88E1\n#define GL_SUBPIXEL_BITS 0x0D50\n#define GL_SUBTRACT 0x84E7\n#define GL_SYNC_CONDITION 0x9113\n#define GL_SYNC_FENCE 0x9116\n#define GL_SYNC_FLAGS 0x9115\n#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001\n#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117\n#define GL_SYNC_STATUS 0x9114\n#define GL_T 0x2001\n#define GL_T2F_C3F_V3F 0x2A2A\n#define GL_T2F_C4F_N3F_V3F 0x2A2C\n#define GL_T2F_C4UB_V3F 0x2A29\n#define GL_T2F_N3F_V3F 0x2A2B\n#define GL_T2F_V3F 0x2A27\n#define GL_T4F_C4F_N3F_V4F 0x2A2D\n#define GL_T4F_V4F 0x2A28\n#define GL_TESS_CONTROL_OUTPUT_VERTICES 0x8E75\n#define GL_TESS_CONTROL_SHADER 0x8E88\n#define GL_TESS_CONTROL_SHADER_BIT 0x00000008\n#define GL_TESS_CONTROL_SHADER_PATCHES 0x82F1\n#define GL_TESS_CONTROL_SUBROUTINE 0x92E9\n#define GL_TESS_CONTROL_SUBROUTINE_UNIFORM 0x92EF\n#define GL_TESS_CONTROL_TEXTURE 0x829C\n#define GL_TESS_EVALUATION_SHADER 0x8E87\n#define GL_TESS_EVALUATION_SHADER_BIT 0x00000010\n#define GL_TESS_EVALUATION_SHADER_INVOCATIONS 0x82F2\n#define GL_TESS_EVALUATION_SUBROUTINE 0x92EA\n#define GL_TESS_EVALUATION_SUBROUTINE_UNIFORM 0x92F0\n#define GL_TESS_EVALUATION_TEXTURE 0x829D\n#define GL_TESS_GEN_MODE 0x8E76\n#define GL_TESS_GEN_POINT_MODE 0x8E79\n#define GL_TESS_GEN_SPACING 0x8E77\n#define GL_TESS_GEN_VERTEX_ORDER 0x8E78\n#define GL_TEXTURE 0x1702\n#define GL_TEXTURE0 0x84C0\n#define GL_TEXTURE1 0x84C1\n#define GL_TEXTURE10 0x84CA\n#define GL_TEXTURE11 0x84CB\n#define GL_TEXTURE12 0x84CC\n#define GL_TEXTURE13 0x84CD\n#define GL_TEXTURE14 0x84CE\n#define GL_TEXTURE15 0x84CF\n#define GL_TEXTURE16 0x84D0\n#define GL_TEXTURE17 0x84D1\n#define GL_TEXTURE18 0x84D2\n#define GL_TEXTURE19 0x84D3\n#define GL_TEXTURE2 0x84C2\n#define GL_TEXTURE20 0x84D4\n#define GL_TEXTURE21 0x84D5\n#define GL_TEXTURE22 0x84D6\n#define GL_TEXTURE23 0x84D7\n#define GL_TEXTURE24 0x84D8\n#define GL_TEXTURE25 0x84D9\n#define GL_TEXTURE26 0x84DA\n#define GL_TEXTURE27 0x84DB\n#define GL_TEXTURE28 0x84DC\n#define GL_TEXTURE29 0x84DD\n#define GL_TEXTURE3 0x84C3\n#define GL_TEXTURE30 0x84DE\n#define GL_TEXTURE31 0x84DF\n#define GL_TEXTURE4 0x84C4\n#define GL_TEXTURE5 0x84C5\n#define GL_TEXTURE6 0x84C6\n#define GL_TEXTURE7 0x84C7\n#define GL_TEXTURE8 0x84C8\n#define GL_TEXTURE9 0x84C9\n#define GL_TEXTURE_1D 0x0DE0\n#define GL_TEXTURE_1D_ARRAY 0x8C18\n#define GL_TEXTURE_2D 0x0DE1\n#define GL_TEXTURE_2D_ARRAY 0x8C1A\n#define GL_TEXTURE_2D_MULTISAMPLE 0x9100\n#define GL_TEXTURE_2D_MULTISAMPLE_ARRAY 0x9102\n#define GL_TEXTURE_3D 0x806F\n#define GL_TEXTURE_ALPHA_SIZE 0x805F\n#define GL_TEXTURE_ALPHA_TYPE 0x8C13\n#define GL_TEXTURE_BASE_LEVEL 0x813C\n#define GL_TEXTURE_BINDING_1D 0x8068\n#define GL_TEXTURE_BINDING_1D_ARRAY 0x8C1C\n#define GL_TEXTURE_BINDING_2D 0x8069\n#define GL_TEXTURE_BINDING_2D_ARRAY 0x8C1D\n#define GL_TEXTURE_BINDING_2D_MULTISAMPLE 0x9104\n#define GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY 0x9105\n#define GL_TEXTURE_BINDING_3D 0x806A\n#define GL_TEXTURE_BINDING_BUFFER 0x8C2C\n#define GL_TEXTURE_BINDING_CUBE_MAP 0x8514\n#define GL_TEXTURE_BINDING_CUBE_MAP_ARRAY 0x900A\n#define GL_TEXTURE_BINDING_RECTANGLE 0x84F6\n#define GL_TEXTURE_BIT 0x00040000\n#define GL_TEXTURE_BLUE_SIZE 0x805E\n#define GL_TEXTURE_BLUE_TYPE 0x8C12\n#define GL_TEXTURE_BORDER 0x1005\n#define GL_TEXTURE_BORDER_COLOR 0x1004\n#define GL_TEXTURE_BUFFER 0x8C2A\n#define GL_TEXTURE_BUFFER_BINDING 0x8C2A\n#define GL_TEXTURE_BUFFER_DATA_STORE_BINDING 0x8C2D\n#define GL_TEXTURE_BUFFER_OFFSET 0x919D\n#define GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT 0x919F\n#define GL_TEXTURE_BUFFER_SIZE 0x919E\n#define GL_TEXTURE_COMPARE_FUNC 0x884D\n#define GL_TEXTURE_COMPARE_MODE 0x884C\n#define GL_TEXTURE_COMPONENTS 0x1003\n#define GL_TEXTURE_COMPRESSED 0x86A1\n#define GL_TEXTURE_COMPRESSED_BLOCK_HEIGHT 0x82B2\n#define GL_TEXTURE_COMPRESSED_BLOCK_SIZE 0x82B3\n#define GL_TEXTURE_COMPRESSED_BLOCK_WIDTH 0x82B1\n#define GL_TEXTURE_COMPRESSED_IMAGE_SIZE 0x86A0\n#define GL_TEXTURE_COMPRESSION_HINT 0x84EF\n#define GL_TEXTURE_COORD_ARRAY 0x8078\n#define GL_TEXTURE_COORD_ARRAY_BUFFER_BINDING 0x889A\n#define GL_TEXTURE_COORD_ARRAY_POINTER 0x8092\n#define GL_TEXTURE_COORD_ARRAY_SIZE 0x8088\n#define GL_TEXTURE_COORD_ARRAY_STRIDE 0x808A\n#define GL_TEXTURE_COORD_ARRAY_TYPE 0x8089\n#define GL_TEXTURE_CUBE_MAP 0x8513\n#define GL_TEXTURE_CUBE_MAP_ARRAY 0x9009\n#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x8516\n#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x8518\n#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x851A\n#define GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x8515\n#define GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x8517\n#define GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x8519\n#define GL_TEXTURE_CUBE_MAP_SEAMLESS 0x884F\n#define GL_TEXTURE_DEPTH 0x8071\n#define GL_TEXTURE_DEPTH_SIZE 0x884A\n#define GL_TEXTURE_DEPTH_TYPE 0x8C16\n#define GL_TEXTURE_ENV 0x2300\n#define GL_TEXTURE_ENV_COLOR 0x2201\n#define GL_TEXTURE_ENV_MODE 0x2200\n#define GL_TEXTURE_FETCH_BARRIER_BIT 0x00000008\n#define GL_TEXTURE_FILTER_CONTROL 0x8500\n#define GL_TEXTURE_FIXED_SAMPLE_LOCATIONS 0x9107\n#define GL_TEXTURE_GATHER 0x82A2\n#define GL_TEXTURE_GATHER_SHADOW 0x82A3\n#define GL_TEXTURE_GEN_MODE 0x2500\n#define GL_TEXTURE_GEN_Q 0x0C63\n#define GL_TEXTURE_GEN_R 0x0C62\n#define GL_TEXTURE_GEN_S 0x0C60\n#define GL_TEXTURE_GEN_T 0x0C61\n#define GL_TEXTURE_GREEN_SIZE 0x805D\n#define GL_TEXTURE_GREEN_TYPE 0x8C11\n#define GL_TEXTURE_HEIGHT 0x1001\n#define GL_TEXTURE_IMAGE_FORMAT 0x828F\n#define GL_TEXTURE_IMAGE_TYPE 0x8290\n#define GL_TEXTURE_IMMUTABLE_FORMAT 0x912F\n#define GL_TEXTURE_IMMUTABLE_LEVELS 0x82DF\n#define GL_TEXTURE_INTENSITY_SIZE 0x8061\n#define GL_TEXTURE_INTENSITY_TYPE 0x8C15\n#define GL_TEXTURE_INTERNAL_FORMAT 0x1003\n#define GL_TEXTURE_LOD_BIAS 0x8501\n#define GL_TEXTURE_LUMINANCE_SIZE 0x8060\n#define GL_TEXTURE_LUMINANCE_TYPE 0x8C14\n#define GL_TEXTURE_MAG_FILTER 0x2800\n#define GL_TEXTURE_MATRIX 0x0BA8\n#define GL_TEXTURE_MAX_ANISOTROPY 0x84FE\n#define GL_TEXTURE_MAX_LEVEL 0x813D\n#define GL_TEXTURE_MAX_LOD 0x813B\n#define GL_TEXTURE_MIN_FILTER 0x2801\n#define GL_TEXTURE_MIN_LOD 0x813A\n#define GL_TEXTURE_PRIORITY 0x8066\n#define GL_TEXTURE_RECTANGLE 0x84F5\n#define GL_TEXTURE_RED_SIZE 0x805C\n#define GL_TEXTURE_RED_TYPE 0x8C10\n#define GL_TEXTURE_RESIDENT 0x8067\n#define GL_TEXTURE_SAMPLES 0x9106\n#define GL_TEXTURE_SHADOW 0x82A1\n#define GL_TEXTURE_SHARED_SIZE 0x8C3F\n#define GL_TEXTURE_STACK_DEPTH 0x0BA5\n#define GL_TEXTURE_STENCIL_SIZE 0x88F1\n#define GL_TEXTURE_SWIZZLE_A 0x8E45\n#define GL_TEXTURE_SWIZZLE_B 0x8E44\n#define GL_TEXTURE_SWIZZLE_G 0x8E43\n#define GL_TEXTURE_SWIZZLE_R 0x8E42\n#define GL_TEXTURE_SWIZZLE_RGBA 0x8E46\n#define GL_TEXTURE_TARGET 0x1006\n#define GL_TEXTURE_UPDATE_BARRIER_BIT 0x00000100\n#define GL_TEXTURE_VIEW 0x82B5\n#define GL_TEXTURE_VIEW_MIN_LAYER 0x82DD\n#define GL_TEXTURE_VIEW_MIN_LEVEL 0x82DB\n#define GL_TEXTURE_VIEW_NUM_LAYERS 0x82DE\n#define GL_TEXTURE_VIEW_NUM_LEVELS 0x82DC\n#define GL_TEXTURE_WIDTH 0x1000\n#define GL_TEXTURE_WRAP_R 0x8072\n#define GL_TEXTURE_WRAP_S 0x2802\n#define GL_TEXTURE_WRAP_T 0x2803\n#define GL_TIMEOUT_EXPIRED 0x911B\n#define GL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFF\n#define GL_TIMESTAMP 0x8E28\n#define GL_TIME_ELAPSED 0x88BF\n#define GL_TOP_LEVEL_ARRAY_SIZE 0x930C\n#define GL_TOP_LEVEL_ARRAY_STRIDE 0x930D\n#define GL_TRANSFORM_BIT 0x00001000\n#define GL_TRANSFORM_FEEDBACK 0x8E22\n#define GL_TRANSFORM_FEEDBACK_ACTIVE 0x8E24\n#define GL_TRANSFORM_FEEDBACK_BARRIER_BIT 0x00000800\n#define GL_TRANSFORM_FEEDBACK_BINDING 0x8E25\n#define GL_TRANSFORM_FEEDBACK_BUFFER 0x8C8E\n#define GL_TRANSFORM_FEEDBACK_BUFFER_ACTIVE 0x8E24\n#define GL_TRANSFORM_FEEDBACK_BUFFER_BINDING 0x8C8F\n#define GL_TRANSFORM_FEEDBACK_BUFFER_INDEX 0x934B\n#define GL_TRANSFORM_FEEDBACK_BUFFER_MODE 0x8C7F\n#define GL_TRANSFORM_FEEDBACK_BUFFER_PAUSED 0x8E23\n#define GL_TRANSFORM_FEEDBACK_BUFFER_SIZE 0x8C85\n#define GL_TRANSFORM_FEEDBACK_BUFFER_START 0x8C84\n#define GL_TRANSFORM_FEEDBACK_BUFFER_STRIDE 0x934C\n#define GL_TRANSFORM_FEEDBACK_OVERFLOW 0x82EC\n#define GL_TRANSFORM_FEEDBACK_PAUSED 0x8E23\n#define GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN 0x8C88\n#define GL_TRANSFORM_FEEDBACK_STREAM_OVERFLOW 0x82ED\n#define GL_TRANSFORM_FEEDBACK_VARYING 0x92F4\n#define GL_TRANSFORM_FEEDBACK_VARYINGS 0x8C83\n#define GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH 0x8C76\n#define GL_TRANSPOSE_COLOR_MATRIX 0x84E6\n#define GL_TRANSPOSE_MODELVIEW_MATRIX 0x84E3\n#define GL_TRANSPOSE_PROJECTION_MATRIX 0x84E4\n#define GL_TRANSPOSE_TEXTURE_MATRIX 0x84E5\n#define GL_TRIANGLES 0x0004\n#define GL_TRIANGLES_ADJACENCY 0x000C\n#define GL_TRIANGLE_FAN 0x0006\n#define GL_TRIANGLE_STRIP 0x0005\n#define GL_TRIANGLE_STRIP_ADJACENCY 0x000D\n#define GL_TRUE 1\n#define GL_TYPE 0x92FA\n#define GL_UNDEFINED_VERTEX 0x8260\n#define GL_UNIFORM 0x92E1\n#define GL_UNIFORM_ARRAY_STRIDE 0x8A3C\n#define GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX 0x92DA\n#define GL_UNIFORM_BARRIER_BIT 0x00000004\n#define GL_UNIFORM_BLOCK 0x92E2\n#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS 0x8A42\n#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES 0x8A43\n#define GL_UNIFORM_BLOCK_BINDING 0x8A3F\n#define GL_UNIFORM_BLOCK_DATA_SIZE 0x8A40\n#define GL_UNIFORM_BLOCK_INDEX 0x8A3A\n#define GL_UNIFORM_BLOCK_NAME_LENGTH 0x8A41\n#define GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER 0x90EC\n#define GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER 0x8A46\n#define GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER 0x8A45\n#define GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER 0x84F0\n#define GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER 0x84F1\n#define GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER 0x8A44\n#define GL_UNIFORM_BUFFER 0x8A11\n#define GL_UNIFORM_BUFFER_BINDING 0x8A28\n#define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34\n#define GL_UNIFORM_BUFFER_SIZE 0x8A2A\n#define GL_UNIFORM_BUFFER_START 0x8A29\n#define GL_UNIFORM_IS_ROW_MAJOR 0x8A3E\n#define GL_UNIFORM_MATRIX_STRIDE 0x8A3D\n#define GL_UNIFORM_NAME_LENGTH 0x8A39\n#define GL_UNIFORM_OFFSET 0x8A3B\n#define GL_UNIFORM_SIZE 0x8A38\n#define GL_UNIFORM_TYPE 0x8A37\n#define GL_UNKNOWN_CONTEXT_RESET 0x8255\n#define GL_UNPACK_ALIGNMENT 0x0CF5\n#define GL_UNPACK_COMPRESSED_BLOCK_DEPTH 0x9129\n#define GL_UNPACK_COMPRESSED_BLOCK_HEIGHT 0x9128\n#define GL_UNPACK_COMPRESSED_BLOCK_SIZE 0x912A\n#define GL_UNPACK_COMPRESSED_BLOCK_WIDTH 0x9127\n#define GL_UNPACK_IMAGE_HEIGHT 0x806E\n#define GL_UNPACK_LSB_FIRST 0x0CF1\n#define GL_UNPACK_ROW_LENGTH 0x0CF2\n#define GL_UNPACK_SKIP_IMAGES 0x806D\n#define GL_UNPACK_SKIP_PIXELS 0x0CF4\n#define GL_UNPACK_SKIP_ROWS 0x0CF3\n#define GL_UNPACK_SWAP_BYTES 0x0CF0\n#define GL_UNSIGNALED 0x9118\n#define GL_UNSIGNED_BYTE 0x1401\n#define GL_UNSIGNED_BYTE_2_3_3_REV 0x8362\n#define GL_UNSIGNED_BYTE_3_3_2 0x8032\n#define GL_UNSIGNED_INT 0x1405\n#define GL_UNSIGNED_INT_10F_11F_11F_REV 0x8C3B\n#define GL_UNSIGNED_INT_10_10_10_2 0x8036\n#define GL_UNSIGNED_INT_24_8 0x84FA\n#define GL_UNSIGNED_INT_2_10_10_10_REV 0x8368\n#define GL_UNSIGNED_INT_5_9_9_9_REV 0x8C3E\n#define GL_UNSIGNED_INT_8_8_8_8 0x8035\n#define GL_UNSIGNED_INT_8_8_8_8_REV 0x8367\n#define GL_UNSIGNED_INT_ATOMIC_COUNTER 0x92DB\n#define GL_UNSIGNED_INT_IMAGE_1D 0x9062\n#define GL_UNSIGNED_INT_IMAGE_1D_ARRAY 0x9068\n#define GL_UNSIGNED_INT_IMAGE_2D 0x9063\n#define GL_UNSIGNED_INT_IMAGE_2D_ARRAY 0x9069\n#define GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE 0x906B\n#define GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x906C\n#define GL_UNSIGNED_INT_IMAGE_2D_RECT 0x9065\n#define GL_UNSIGNED_INT_IMAGE_3D 0x9064\n#define GL_UNSIGNED_INT_IMAGE_BUFFER 0x9067\n#define GL_UNSIGNED_INT_IMAGE_CUBE 0x9066\n#define GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY 0x906A\n#define GL_UNSIGNED_INT_SAMPLER_1D 0x8DD1\n#define GL_UNSIGNED_INT_SAMPLER_1D_ARRAY 0x8DD6\n#define GL_UNSIGNED_INT_SAMPLER_2D 0x8DD2\n#define GL_UNSIGNED_INT_SAMPLER_2D_ARRAY 0x8DD7\n#define GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE 0x910A\n#define GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY 0x910D\n#define GL_UNSIGNED_INT_SAMPLER_2D_RECT 0x8DD5\n#define GL_UNSIGNED_INT_SAMPLER_3D 0x8DD3\n#define GL_UNSIGNED_INT_SAMPLER_BUFFER 0x8DD8\n#define GL_UNSIGNED_INT_SAMPLER_CUBE 0x8DD4\n#define GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY 0x900F\n#define GL_UNSIGNED_INT_VEC2 0x8DC6\n#define GL_UNSIGNED_INT_VEC3 0x8DC7\n#define GL_UNSIGNED_INT_VEC4 0x8DC8\n#define GL_UNSIGNED_NORMALIZED 0x8C17\n#define GL_UNSIGNED_SHORT 0x1403\n#define GL_UNSIGNED_SHORT_1_5_5_5_REV 0x8366\n#define GL_UNSIGNED_SHORT_4_4_4_4 0x8033\n#define GL_UNSIGNED_SHORT_4_4_4_4_REV 0x8365\n#define GL_UNSIGNED_SHORT_5_5_5_1 0x8034\n#define GL_UNSIGNED_SHORT_5_6_5 0x8363\n#define GL_UNSIGNED_SHORT_5_6_5_REV 0x8364\n#define GL_UPPER_LEFT 0x8CA2\n#define GL_V2F 0x2A20\n#define GL_V3F 0x2A21\n#define GL_VALIDATE_STATUS 0x8B83\n#define GL_VENDOR 0x1F00\n#define GL_VERSION 0x1F02\n#define GL_VERTEX_ARRAY 0x8074\n#define GL_VERTEX_ARRAY_BINDING 0x85B5\n#define GL_VERTEX_ARRAY_BUFFER_BINDING 0x8896\n#define GL_VERTEX_ARRAY_POINTER 0x808E\n#define GL_VERTEX_ARRAY_SIZE 0x807A\n#define GL_VERTEX_ARRAY_STRIDE 0x807C\n#define GL_VERTEX_ARRAY_TYPE 0x807B\n#define GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT 0x00000001\n#define GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING 0x889F\n#define GL_VERTEX_ATTRIB_ARRAY_DIVISOR 0x88FE\n#define GL_VERTEX_ATTRIB_ARRAY_ENABLED 0x8622\n#define GL_VERTEX_ATTRIB_ARRAY_INTEGER 0x88FD\n#define GL_VERTEX_ATTRIB_ARRAY_LONG 0x874E\n#define GL_VERTEX_ATTRIB_ARRAY_NORMALIZED 0x886A\n#define GL_VERTEX_ATTRIB_ARRAY_POINTER 0x8645\n#define GL_VERTEX_ATTRIB_ARRAY_SIZE 0x8623\n#define GL_VERTEX_ATTRIB_ARRAY_STRIDE 0x8624\n#define GL_VERTEX_ATTRIB_ARRAY_TYPE 0x8625\n#define GL_VERTEX_ATTRIB_BINDING 0x82D4\n#define GL_VERTEX_ATTRIB_RELATIVE_OFFSET 0x82D5\n#define GL_VERTEX_BINDING_BUFFER 0x8F4F\n#define GL_VERTEX_BINDING_DIVISOR 0x82D6\n#define GL_VERTEX_BINDING_OFFSET 0x82D7\n#define GL_VERTEX_BINDING_STRIDE 0x82D8\n#define GL_VERTEX_PROGRAM_POINT_SIZE 0x8642\n#define GL_VERTEX_PROGRAM_TWO_SIDE 0x8643\n#define GL_VERTEX_SHADER 0x8B31\n#define GL_VERTEX_SHADER_BIT 0x00000001\n#define GL_VERTEX_SHADER_INVOCATIONS 0x82F0\n#define GL_VERTEX_SUBROUTINE 0x92E8\n#define GL_VERTEX_SUBROUTINE_UNIFORM 0x92EE\n#define GL_VERTEX_TEXTURE 0x829B\n#define GL_VERTICES_SUBMITTED 0x82EE\n#define GL_VIEWPORT 0x0BA2\n#define GL_VIEWPORT_BIT 0x00000800\n#define GL_VIEWPORT_BOUNDS_RANGE 0x825D\n#define GL_VIEWPORT_INDEX_PROVOKING_VERTEX 0x825F\n#define GL_VIEWPORT_SUBPIXEL_BITS 0x825C\n#define GL_VIEW_CLASS_128_BITS 0x82C4\n#define GL_VIEW_CLASS_16_BITS 0x82CA\n#define GL_VIEW_CLASS_24_BITS 0x82C9\n#define GL_VIEW_CLASS_32_BITS 0x82C8\n#define GL_VIEW_CLASS_48_BITS 0x82C7\n#define GL_VIEW_CLASS_64_BITS 0x82C6\n#define GL_VIEW_CLASS_8_BITS 0x82CB\n#define GL_VIEW_CLASS_96_BITS 0x82C5\n#define GL_VIEW_CLASS_BPTC_FLOAT 0x82D3\n#define GL_VIEW_CLASS_BPTC_UNORM 0x82D2\n#define GL_VIEW_CLASS_RGTC1_RED 0x82D0\n#define GL_VIEW_CLASS_RGTC2_RG 0x82D1\n#define GL_VIEW_CLASS_S3TC_DXT1_RGB 0x82CC\n#define GL_VIEW_CLASS_S3TC_DXT1_RGBA 0x82CD\n#define GL_VIEW_CLASS_S3TC_DXT3_RGBA 0x82CE\n#define GL_VIEW_CLASS_S3TC_DXT5_RGBA 0x82CF\n#define GL_VIEW_COMPATIBILITY_CLASS 0x82B6\n#define GL_WAIT_FAILED 0x911D\n#define GL_WEIGHT_ARRAY_BUFFER_BINDING 0x889E\n#define GL_WRITE_ONLY 0x88B9\n#define GL_XOR 0x1506\n#define GL_ZERO 0\n#define GL_ZERO_TO_ONE 0x935F\n#define GL_ZOOM_X 0x0D16\n#define GL_ZOOM_Y 0x0D17\n\n#include <KHR/khrplatform.h>\n\ntypedef unsigned int GLenum;\n\ntypedef unsigned char GLboolean;\n\ntypedef unsigned int GLbitfield;\n\ntypedef void GLvoid;\n\ntypedef khronos_int8_t GLbyte;\n\ntypedef khronos_uint8_t GLubyte;\n\ntypedef khronos_int16_t GLshort;\n\ntypedef khronos_uint16_t GLushort;\n\ntypedef int GLint;\n\ntypedef unsigned int GLuint;\n\ntypedef khronos_int32_t GLclampx;\n\ntypedef int GLsizei;\n\ntypedef khronos_float_t GLfloat;\n\ntypedef khronos_float_t GLclampf;\n\ntypedef double GLdouble;\n\ntypedef double GLclampd;\n\ntypedef void *GLeglClientBufferEXT;\n\ntypedef void *GLeglImageOES;\n\ntypedef char GLchar;\n\ntypedef char GLcharARB;\n\n#ifdef __APPLE__\ntypedef void *GLhandleARB;\n#else\ntypedef unsigned int GLhandleARB;\n#endif\n\ntypedef khronos_uint16_t GLhalf;\n\ntypedef khronos_uint16_t GLhalfARB;\n\ntypedef khronos_int32_t GLfixed;\n\n#if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060)\ntypedef khronos_intptr_t GLintptr;\n#else\ntypedef khronos_intptr_t GLintptr;\n#endif\n\n#if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060)\ntypedef khronos_intptr_t GLintptrARB;\n#else\ntypedef khronos_intptr_t GLintptrARB;\n#endif\n\n#if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060)\ntypedef khronos_ssize_t GLsizeiptr;\n#else\ntypedef khronos_ssize_t GLsizeiptr;\n#endif\n\n#if defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ > 1060)\ntypedef khronos_ssize_t GLsizeiptrARB;\n#else\ntypedef khronos_ssize_t GLsizeiptrARB;\n#endif\n\ntypedef khronos_int64_t GLint64;\n\ntypedef khronos_int64_t GLint64EXT;\n\ntypedef khronos_uint64_t GLuint64;\n\ntypedef khronos_uint64_t GLuint64EXT;\n\ntypedef struct __GLsync *GLsync;\n\nstruct _cl_context;\n\nstruct _cl_event;\n\ntypedef void(GLAD_API_PTR *GLDEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam);\n\ntypedef void(GLAD_API_PTR *GLDEBUGPROCARB)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam);\n\ntypedef void(GLAD_API_PTR *GLDEBUGPROCKHR)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam);\n\ntypedef void(GLAD_API_PTR *GLDEBUGPROCAMD)(GLuint id, GLenum category, GLenum severity, GLsizei length, const GLchar *message, void *userParam);\n\ntypedef unsigned short GLhalfNV;\n\ntypedef GLintptr GLvdpauSurfaceNV;\n\ntypedef void(GLAD_API_PTR *GLVULKANPROCNV)(void);\n\n#define GL_VERSION_1_0 1\n#define GL_VERSION_1_1 1\n#define GL_VERSION_1_2 1\n#define GL_VERSION_1_3 1\n#define GL_VERSION_1_4 1\n#define GL_VERSION_1_5 1\n#define GL_VERSION_2_0 1\n#define GL_VERSION_2_1 1\n#define GL_VERSION_3_0 1\n#define GL_VERSION_3_1 1\n#define GL_VERSION_3_2 1\n#define GL_VERSION_3_3 1\n#define GL_VERSION_4_0 1\n#define GL_VERSION_4_1 1\n#define GL_VERSION_4_2 1\n#define GL_VERSION_4_3 1\n#define GL_VERSION_4_4 1\n#define GL_VERSION_4_5 1\n#define GL_VERSION_4_6 1\n\ntypedef void(GLAD_API_PTR *PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)(GLenum target, GLeglImageOES image);\ntypedef void(GLAD_API_PTR *PFNGLACCUMPROC)(GLenum op, GLfloat value);\ntypedef void(GLAD_API_PTR *PFNGLACTIVESHADERPROGRAMPROC)(GLuint pipeline, GLuint program);\ntypedef void(GLAD_API_PTR *PFNGLACTIVETEXTUREPROC)(GLenum texture);\ntypedef void(GLAD_API_PTR *PFNGLALPHAFUNCPROC)(GLenum func, GLfloat ref);\ntypedef GLboolean(GLAD_API_PTR *PFNGLARETEXTURESRESIDENTPROC)(GLsizei n, const GLuint *textures, GLboolean *residences);\ntypedef void(GLAD_API_PTR *PFNGLARRAYELEMENTPROC)(GLint i);\ntypedef void(GLAD_API_PTR *PFNGLATTACHSHADERPROC)(GLuint program, GLuint shader);\ntypedef void(GLAD_API_PTR *PFNGLBEGINPROC)(GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLBEGINCONDITIONALRENDERPROC)(GLuint id, GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLBEGINQUERYPROC)(GLenum target, GLuint id);\ntypedef void(GLAD_API_PTR *PFNGLBEGINQUERYINDEXEDPROC)(GLenum target, GLuint index, GLuint id);\ntypedef void(GLAD_API_PTR *PFNGLBEGINTRANSFORMFEEDBACKPROC)(GLenum primitiveMode);\ntypedef void(GLAD_API_PTR *PFNGLBINDATTRIBLOCATIONPROC)(GLuint program, GLuint index, const GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLBINDBUFFERPROC)(GLenum target, GLuint buffer);\ntypedef void(GLAD_API_PTR *PFNGLBINDBUFFERBASEPROC)(GLenum target, GLuint index, GLuint buffer);\ntypedef void(GLAD_API_PTR *PFNGLBINDBUFFERRANGEPROC)(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);\ntypedef void(GLAD_API_PTR *PFNGLBINDBUFFERSBASEPROC)(GLenum target, GLuint first, GLsizei count, const GLuint *buffers);\ntypedef void(GLAD_API_PTR *PFNGLBINDBUFFERSRANGEPROC)(GLenum target, GLuint first, GLsizei count, const GLuint *buffers, const GLintptr *offsets, const GLsizeiptr *sizes);\ntypedef void(GLAD_API_PTR *PFNGLBINDFRAGDATALOCATIONPROC)(GLuint program, GLuint color, const GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLBINDFRAGDATALOCATIONINDEXEDPROC)(GLuint program, GLuint colorNumber, GLuint index, const GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLBINDFRAMEBUFFERPROC)(GLenum target, GLuint framebuffer);\ntypedef void(GLAD_API_PTR *PFNGLBINDIMAGETEXTUREPROC)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format);\ntypedef void(GLAD_API_PTR *PFNGLBINDIMAGETEXTURESPROC)(GLuint first, GLsizei count, const GLuint *textures);\ntypedef void(GLAD_API_PTR *PFNGLBINDPROGRAMPIPELINEPROC)(GLuint pipeline);\ntypedef void(GLAD_API_PTR *PFNGLBINDRENDERBUFFERPROC)(GLenum target, GLuint renderbuffer);\ntypedef void(GLAD_API_PTR *PFNGLBINDSAMPLERPROC)(GLuint unit, GLuint sampler);\ntypedef void(GLAD_API_PTR *PFNGLBINDSAMPLERSPROC)(GLuint first, GLsizei count, const GLuint *samplers);\ntypedef void(GLAD_API_PTR *PFNGLBINDTEXTUREPROC)(GLenum target, GLuint texture);\ntypedef void(GLAD_API_PTR *PFNGLBINDTEXTUREUNITPROC)(GLuint unit, GLuint texture);\ntypedef void(GLAD_API_PTR *PFNGLBINDTEXTURESPROC)(GLuint first, GLsizei count, const GLuint *textures);\ntypedef void(GLAD_API_PTR *PFNGLBINDTRANSFORMFEEDBACKPROC)(GLenum target, GLuint id);\ntypedef void(GLAD_API_PTR *PFNGLBINDVERTEXARRAYPROC)(GLuint array);\ntypedef void(GLAD_API_PTR *PFNGLBINDVERTEXBUFFERPROC)(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride);\ntypedef void(GLAD_API_PTR *PFNGLBINDVERTEXBUFFERSPROC)(GLuint first, GLsizei count, const GLuint *buffers, const GLintptr *offsets, const GLsizei *strides);\ntypedef void(GLAD_API_PTR *PFNGLBITMAPPROC)(GLsizei width, GLsizei height, GLfloat xorig, GLfloat yorig, GLfloat xmove, GLfloat ymove, const GLubyte *bitmap);\ntypedef void(GLAD_API_PTR *PFNGLBLENDCOLORPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);\ntypedef void(GLAD_API_PTR *PFNGLBLENDEQUATIONPROC)(GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLBLENDEQUATIONSEPARATEPROC)(GLenum modeRGB, GLenum modeAlpha);\ntypedef void(GLAD_API_PTR *PFNGLBLENDEQUATIONSEPARATEIPROC)(GLuint buf, GLenum modeRGB, GLenum modeAlpha);\ntypedef void(GLAD_API_PTR *PFNGLBLENDEQUATIONIPROC)(GLuint buf, GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLBLENDFUNCPROC)(GLenum sfactor, GLenum dfactor);\ntypedef void(GLAD_API_PTR *PFNGLBLENDFUNCSEPARATEPROC)(GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha);\ntypedef void(GLAD_API_PTR *PFNGLBLENDFUNCSEPARATEIPROC)(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha);\ntypedef void(GLAD_API_PTR *PFNGLBLENDFUNCIPROC)(GLuint buf, GLenum src, GLenum dst);\ntypedef void(GLAD_API_PTR *PFNGLBLITFRAMEBUFFERPROC)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);\ntypedef void(GLAD_API_PTR *PFNGLBLITNAMEDFRAMEBUFFERPROC)(GLuint readFramebuffer, GLuint drawFramebuffer, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);\ntypedef void(GLAD_API_PTR *PFNGLBUFFERDATAPROC)(GLenum target, GLsizeiptr size, const void *data, GLenum usage);\ntypedef void(GLAD_API_PTR *PFNGLBUFFERSTORAGEPROC)(GLenum target, GLsizeiptr size, const void *data, GLbitfield flags);\ntypedef void(GLAD_API_PTR *PFNGLBUFFERSUBDATAPROC)(GLenum target, GLintptr offset, GLsizeiptr size, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCALLLISTPROC)(GLuint list);\ntypedef void(GLAD_API_PTR *PFNGLCALLLISTSPROC)(GLsizei n, GLenum type, const void *lists);\ntypedef GLenum(GLAD_API_PTR *PFNGLCHECKFRAMEBUFFERSTATUSPROC)(GLenum target);\ntypedef GLenum(GLAD_API_PTR *PFNGLCHECKNAMEDFRAMEBUFFERSTATUSPROC)(GLuint framebuffer, GLenum target);\ntypedef void(GLAD_API_PTR *PFNGLCLAMPCOLORPROC)(GLenum target, GLenum clamp);\ntypedef void(GLAD_API_PTR *PFNGLCLEARPROC)(GLbitfield mask);\ntypedef void(GLAD_API_PTR *PFNGLCLEARACCUMPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);\ntypedef void(GLAD_API_PTR *PFNGLCLEARBUFFERDATAPROC)(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCLEARBUFFERSUBDATAPROC)(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCLEARBUFFERFIPROC)(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);\ntypedef void(GLAD_API_PTR *PFNGLCLEARBUFFERFVPROC)(GLenum buffer, GLint drawbuffer, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLCLEARBUFFERIVPROC)(GLenum buffer, GLint drawbuffer, const GLint *value);\ntypedef void(GLAD_API_PTR *PFNGLCLEARBUFFERUIVPROC)(GLenum buffer, GLint drawbuffer, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLCLEARCOLORPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);\ntypedef void(GLAD_API_PTR *PFNGLCLEARDEPTHPROC)(GLdouble depth);\ntypedef void(GLAD_API_PTR *PFNGLCLEARDEPTHFPROC)(GLfloat d);\ntypedef void(GLAD_API_PTR *PFNGLCLEARINDEXPROC)(GLfloat c);\ntypedef void(GLAD_API_PTR *PFNGLCLEARNAMEDBUFFERDATAPROC)(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCLEARNAMEDBUFFERSUBDATAPROC)(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCLEARNAMEDFRAMEBUFFERFIPROC)(GLuint framebuffer, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);\ntypedef void(GLAD_API_PTR *PFNGLCLEARNAMEDFRAMEBUFFERFVPROC)(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLCLEARNAMEDFRAMEBUFFERIVPROC)(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLint *value);\ntypedef void(GLAD_API_PTR *PFNGLCLEARNAMEDFRAMEBUFFERUIVPROC)(GLuint framebuffer, GLenum buffer, GLint drawbuffer, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLCLEARSTENCILPROC)(GLint s);\ntypedef void(GLAD_API_PTR *PFNGLCLEARTEXIMAGEPROC)(GLuint texture, GLint level, GLenum format, GLenum type, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCLEARTEXSUBIMAGEPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCLIENTACTIVETEXTUREPROC)(GLenum texture);\ntypedef GLenum(GLAD_API_PTR *PFNGLCLIENTWAITSYNCPROC)(GLsync sync, GLbitfield flags, GLuint64 timeout);\ntypedef void(GLAD_API_PTR *PFNGLCLIPCONTROLPROC)(GLenum origin, GLenum depth);\ntypedef void(GLAD_API_PTR *PFNGLCLIPPLANEPROC)(GLenum plane, const GLdouble *equation);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3BPROC)(GLbyte red, GLbyte green, GLbyte blue);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3BVPROC)(const GLbyte *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3DPROC)(GLdouble red, GLdouble green, GLdouble blue);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3FPROC)(GLfloat red, GLfloat green, GLfloat blue);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3IPROC)(GLint red, GLint green, GLint blue);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3SPROC)(GLshort red, GLshort green, GLshort blue);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3UBPROC)(GLubyte red, GLubyte green, GLubyte blue);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3UBVPROC)(const GLubyte *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3UIPROC)(GLuint red, GLuint green, GLuint blue);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3UIVPROC)(const GLuint *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3USPROC)(GLushort red, GLushort green, GLushort blue);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR3USVPROC)(const GLushort *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4BPROC)(GLbyte red, GLbyte green, GLbyte blue, GLbyte alpha);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4BVPROC)(const GLbyte *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4DPROC)(GLdouble red, GLdouble green, GLdouble blue, GLdouble alpha);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4FPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4IPROC)(GLint red, GLint green, GLint blue, GLint alpha);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4SPROC)(GLshort red, GLshort green, GLshort blue, GLshort alpha);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4UBPROC)(GLubyte red, GLubyte green, GLubyte blue, GLubyte alpha);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4UBVPROC)(const GLubyte *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4UIPROC)(GLuint red, GLuint green, GLuint blue, GLuint alpha);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4UIVPROC)(const GLuint *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4USPROC)(GLushort red, GLushort green, GLushort blue, GLushort alpha);\ntypedef void(GLAD_API_PTR *PFNGLCOLOR4USVPROC)(const GLushort *v);\ntypedef void(GLAD_API_PTR *PFNGLCOLORMASKPROC)(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);\ntypedef void(GLAD_API_PTR *PFNGLCOLORMASKIPROC)(GLuint index, GLboolean r, GLboolean g, GLboolean b, GLboolean a);\ntypedef void(GLAD_API_PTR *PFNGLCOLORMATERIALPROC)(GLenum face, GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLCOLORP3UIPROC)(GLenum type, GLuint color);\ntypedef void(GLAD_API_PTR *PFNGLCOLORP3UIVPROC)(GLenum type, const GLuint *color);\ntypedef void(GLAD_API_PTR *PFNGLCOLORP4UIPROC)(GLenum type, GLuint color);\ntypedef void(GLAD_API_PTR *PFNGLCOLORP4UIVPROC)(GLenum type, const GLuint *color);\ntypedef void(GLAD_API_PTR *PFNGLCOLORPOINTERPROC)(GLint size, GLenum type, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLCOMPILESHADERPROC)(GLuint shader);\ntypedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXIMAGE1DPROC)(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXIMAGE2DPROC)(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXIMAGE3DPROC)(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE1DPROC)(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXTURESUBIMAGE1DPROC)(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXTURESUBIMAGE2DPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCOMPRESSEDTEXTURESUBIMAGE3DPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLCOPYBUFFERSUBDATAPROC)(GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);\ntypedef void(GLAD_API_PTR *PFNGLCOPYIMAGESUBDATAPROC)(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);\ntypedef void(GLAD_API_PTR *PFNGLCOPYNAMEDBUFFERSUBDATAPROC)(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);\ntypedef void(GLAD_API_PTR *PFNGLCOPYPIXELSPROC)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum type);\ntypedef void(GLAD_API_PTR *PFNGLCOPYTEXIMAGE1DPROC)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border);\ntypedef void(GLAD_API_PTR *PFNGLCOPYTEXIMAGE2DPROC)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);\ntypedef void(GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE1DPROC)(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);\ntypedef void(GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE3DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLCOPYTEXTURESUBIMAGE1DPROC)(GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);\ntypedef void(GLAD_API_PTR *PFNGLCOPYTEXTURESUBIMAGE2DPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLCOPYTEXTURESUBIMAGE3DPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLCREATEBUFFERSPROC)(GLsizei n, GLuint *buffers);\ntypedef void(GLAD_API_PTR *PFNGLCREATEFRAMEBUFFERSPROC)(GLsizei n, GLuint *framebuffers);\ntypedef GLuint(GLAD_API_PTR *PFNGLCREATEPROGRAMPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLCREATEPROGRAMPIPELINESPROC)(GLsizei n, GLuint *pipelines);\ntypedef void(GLAD_API_PTR *PFNGLCREATEQUERIESPROC)(GLenum target, GLsizei n, GLuint *ids);\ntypedef void(GLAD_API_PTR *PFNGLCREATERENDERBUFFERSPROC)(GLsizei n, GLuint *renderbuffers);\ntypedef void(GLAD_API_PTR *PFNGLCREATESAMPLERSPROC)(GLsizei n, GLuint *samplers);\ntypedef GLuint(GLAD_API_PTR *PFNGLCREATESHADERPROC)(GLenum type);\ntypedef GLuint(GLAD_API_PTR *PFNGLCREATESHADERPROGRAMVPROC)(GLenum type, GLsizei count, const GLchar *const *strings);\ntypedef void(GLAD_API_PTR *PFNGLCREATETEXTURESPROC)(GLenum target, GLsizei n, GLuint *textures);\ntypedef void(GLAD_API_PTR *PFNGLCREATETRANSFORMFEEDBACKSPROC)(GLsizei n, GLuint *ids);\ntypedef void(GLAD_API_PTR *PFNGLCREATEVERTEXARRAYSPROC)(GLsizei n, GLuint *arrays);\ntypedef void(GLAD_API_PTR *PFNGLCULLFACEPROC)(GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLDEBUGMESSAGECALLBACKPROC)(GLDEBUGPROC callback, const void *userParam);\ntypedef void(GLAD_API_PTR *PFNGLDEBUGMESSAGECONTROLPROC)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint *ids, GLboolean enabled);\ntypedef void(GLAD_API_PTR *PFNGLDEBUGMESSAGEINSERTPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *buf);\ntypedef void(GLAD_API_PTR *PFNGLDELETEBUFFERSPROC)(GLsizei n, const GLuint *buffers);\ntypedef void(GLAD_API_PTR *PFNGLDELETEFRAMEBUFFERSPROC)(GLsizei n, const GLuint *framebuffers);\ntypedef void(GLAD_API_PTR *PFNGLDELETELISTSPROC)(GLuint list, GLsizei range);\ntypedef void(GLAD_API_PTR *PFNGLDELETEPROGRAMPROC)(GLuint program);\ntypedef void(GLAD_API_PTR *PFNGLDELETEPROGRAMPIPELINESPROC)(GLsizei n, const GLuint *pipelines);\ntypedef void(GLAD_API_PTR *PFNGLDELETEQUERIESPROC)(GLsizei n, const GLuint *ids);\ntypedef void(GLAD_API_PTR *PFNGLDELETERENDERBUFFERSPROC)(GLsizei n, const GLuint *renderbuffers);\ntypedef void(GLAD_API_PTR *PFNGLDELETESAMPLERSPROC)(GLsizei count, const GLuint *samplers);\ntypedef void(GLAD_API_PTR *PFNGLDELETESHADERPROC)(GLuint shader);\ntypedef void(GLAD_API_PTR *PFNGLDELETESYNCPROC)(GLsync sync);\ntypedef void(GLAD_API_PTR *PFNGLDELETETEXTURESPROC)(GLsizei n, const GLuint *textures);\ntypedef void(GLAD_API_PTR *PFNGLDELETETRANSFORMFEEDBACKSPROC)(GLsizei n, const GLuint *ids);\ntypedef void(GLAD_API_PTR *PFNGLDELETEVERTEXARRAYSPROC)(GLsizei n, const GLuint *arrays);\ntypedef void(GLAD_API_PTR *PFNGLDEPTHFUNCPROC)(GLenum func);\ntypedef void(GLAD_API_PTR *PFNGLDEPTHMASKPROC)(GLboolean flag);\ntypedef void(GLAD_API_PTR *PFNGLDEPTHRANGEPROC)(GLdouble n, GLdouble f);\ntypedef void(GLAD_API_PTR *PFNGLDEPTHRANGEARRAYVPROC)(GLuint first, GLsizei count, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLDEPTHRANGEINDEXEDPROC)(GLuint index, GLdouble n, GLdouble f);\ntypedef void(GLAD_API_PTR *PFNGLDEPTHRANGEFPROC)(GLfloat n, GLfloat f);\ntypedef void(GLAD_API_PTR *PFNGLDETACHSHADERPROC)(GLuint program, GLuint shader);\ntypedef void(GLAD_API_PTR *PFNGLDISABLEPROC)(GLenum cap);\ntypedef void(GLAD_API_PTR *PFNGLDISABLECLIENTSTATEPROC)(GLenum array);\ntypedef void(GLAD_API_PTR *PFNGLDISABLEVERTEXARRAYATTRIBPROC)(GLuint vaobj, GLuint index);\ntypedef void(GLAD_API_PTR *PFNGLDISABLEVERTEXATTRIBARRAYPROC)(GLuint index);\ntypedef void(GLAD_API_PTR *PFNGLDISABLEIPROC)(GLenum target, GLuint index);\ntypedef void(GLAD_API_PTR *PFNGLDISPATCHCOMPUTEPROC)(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z);\ntypedef void(GLAD_API_PTR *PFNGLDISPATCHCOMPUTEINDIRECTPROC)(GLintptr indirect);\ntypedef void(GLAD_API_PTR *PFNGLDRAWARRAYSPROC)(GLenum mode, GLint first, GLsizei count);\ntypedef void(GLAD_API_PTR *PFNGLDRAWARRAYSINDIRECTPROC)(GLenum mode, const void *indirect);\ntypedef void(GLAD_API_PTR *PFNGLDRAWARRAYSINSTANCEDPROC)(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);\ntypedef void(GLAD_API_PTR *PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC)(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance);\ntypedef void(GLAD_API_PTR *PFNGLDRAWBUFFERPROC)(GLenum buf);\ntypedef void(GLAD_API_PTR *PFNGLDRAWBUFFERSPROC)(GLsizei n, const GLenum *bufs);\ntypedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices);\ntypedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSBASEVERTEXPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices, GLint basevertex);\ntypedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSINDIRECTPROC)(GLenum mode, GLenum type, const void *indirect);\ntypedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount);\ntypedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount, GLuint baseinstance);\ntypedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount, GLint basevertex);\ntypedef void(GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC)(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance);\ntypedef void(GLAD_API_PTR *PFNGLDRAWPIXELSPROC)(GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLDRAWRANGEELEMENTSPROC)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void *indices);\ntypedef void(GLAD_API_PTR *PFNGLDRAWRANGEELEMENTSBASEVERTEXPROC)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void *indices, GLint basevertex);\ntypedef void(GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKPROC)(GLenum mode, GLuint id);\ntypedef void(GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC)(GLenum mode, GLuint id, GLsizei instancecount);\ntypedef void(GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC)(GLenum mode, GLuint id, GLuint stream);\ntypedef void(GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC)(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount);\ntypedef void(GLAD_API_PTR *PFNGLEDGEFLAGPROC)(GLboolean flag);\ntypedef void(GLAD_API_PTR *PFNGLEDGEFLAGPOINTERPROC)(GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLEDGEFLAGVPROC)(const GLboolean *flag);\ntypedef void(GLAD_API_PTR *PFNGLENABLEPROC)(GLenum cap);\ntypedef void(GLAD_API_PTR *PFNGLENABLECLIENTSTATEPROC)(GLenum array);\ntypedef void(GLAD_API_PTR *PFNGLENABLEVERTEXARRAYATTRIBPROC)(GLuint vaobj, GLuint index);\ntypedef void(GLAD_API_PTR *PFNGLENABLEVERTEXATTRIBARRAYPROC)(GLuint index);\ntypedef void(GLAD_API_PTR *PFNGLENABLEIPROC)(GLenum target, GLuint index);\ntypedef void(GLAD_API_PTR *PFNGLENDPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLENDCONDITIONALRENDERPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLENDLISTPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLENDQUERYPROC)(GLenum target);\ntypedef void(GLAD_API_PTR *PFNGLENDQUERYINDEXEDPROC)(GLenum target, GLuint index);\ntypedef void(GLAD_API_PTR *PFNGLENDTRANSFORMFEEDBACKPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLEVALCOORD1DPROC)(GLdouble u);\ntypedef void(GLAD_API_PTR *PFNGLEVALCOORD1DVPROC)(const GLdouble *u);\ntypedef void(GLAD_API_PTR *PFNGLEVALCOORD1FPROC)(GLfloat u);\ntypedef void(GLAD_API_PTR *PFNGLEVALCOORD1FVPROC)(const GLfloat *u);\ntypedef void(GLAD_API_PTR *PFNGLEVALCOORD2DPROC)(GLdouble u, GLdouble v);\ntypedef void(GLAD_API_PTR *PFNGLEVALCOORD2DVPROC)(const GLdouble *u);\ntypedef void(GLAD_API_PTR *PFNGLEVALCOORD2FPROC)(GLfloat u, GLfloat v);\ntypedef void(GLAD_API_PTR *PFNGLEVALCOORD2FVPROC)(const GLfloat *u);\ntypedef void(GLAD_API_PTR *PFNGLEVALMESH1PROC)(GLenum mode, GLint i1, GLint i2);\ntypedef void(GLAD_API_PTR *PFNGLEVALMESH2PROC)(GLenum mode, GLint i1, GLint i2, GLint j1, GLint j2);\ntypedef void(GLAD_API_PTR *PFNGLEVALPOINT1PROC)(GLint i);\ntypedef void(GLAD_API_PTR *PFNGLEVALPOINT2PROC)(GLint i, GLint j);\ntypedef void(GLAD_API_PTR *PFNGLFEEDBACKBUFFERPROC)(GLsizei size, GLenum type, GLfloat *buffer);\ntypedef GLsync(GLAD_API_PTR *PFNGLFENCESYNCPROC)(GLenum condition, GLbitfield flags);\ntypedef void(GLAD_API_PTR *PFNGLFINISHPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLFLUSHPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLFLUSHMAPPEDBUFFERRANGEPROC)(GLenum target, GLintptr offset, GLsizeiptr length);\ntypedef void(GLAD_API_PTR *PFNGLFLUSHMAPPEDNAMEDBUFFERRANGEPROC)(GLuint buffer, GLintptr offset, GLsizeiptr length);\ntypedef void(GLAD_API_PTR *PFNGLFOGCOORDPOINTERPROC)(GLenum type, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLFOGCOORDDPROC)(GLdouble coord);\ntypedef void(GLAD_API_PTR *PFNGLFOGCOORDDVPROC)(const GLdouble *coord);\ntypedef void(GLAD_API_PTR *PFNGLFOGCOORDFPROC)(GLfloat coord);\ntypedef void(GLAD_API_PTR *PFNGLFOGCOORDFVPROC)(const GLfloat *coord);\ntypedef void(GLAD_API_PTR *PFNGLFOGFPROC)(GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLFOGFVPROC)(GLenum pname, const GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLFOGIPROC)(GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLFOGIVPROC)(GLenum pname, const GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERPARAMETERIPROC)(GLenum target, GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERRENDERBUFFERPROC)(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);\ntypedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTUREPROC)(GLenum target, GLenum attachment, GLuint texture, GLint level);\ntypedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURE1DPROC)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);\ntypedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURE2DPROC)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);\ntypedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURE3DPROC)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level, GLint zoffset);\ntypedef void(GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURELAYERPROC)(GLenum target, GLenum attachment, GLuint texture, GLint level, GLint layer);\ntypedef void(GLAD_API_PTR *PFNGLFRONTFACEPROC)(GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLFRUSTUMPROC)(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);\ntypedef void(GLAD_API_PTR *PFNGLGENBUFFERSPROC)(GLsizei n, GLuint *buffers);\ntypedef void(GLAD_API_PTR *PFNGLGENFRAMEBUFFERSPROC)(GLsizei n, GLuint *framebuffers);\ntypedef GLuint(GLAD_API_PTR *PFNGLGENLISTSPROC)(GLsizei range);\ntypedef void(GLAD_API_PTR *PFNGLGENPROGRAMPIPELINESPROC)(GLsizei n, GLuint *pipelines);\ntypedef void(GLAD_API_PTR *PFNGLGENQUERIESPROC)(GLsizei n, GLuint *ids);\ntypedef void(GLAD_API_PTR *PFNGLGENRENDERBUFFERSPROC)(GLsizei n, GLuint *renderbuffers);\ntypedef void(GLAD_API_PTR *PFNGLGENSAMPLERSPROC)(GLsizei count, GLuint *samplers);\ntypedef void(GLAD_API_PTR *PFNGLGENTEXTURESPROC)(GLsizei n, GLuint *textures);\ntypedef void(GLAD_API_PTR *PFNGLGENTRANSFORMFEEDBACKSPROC)(GLsizei n, GLuint *ids);\ntypedef void(GLAD_API_PTR *PFNGLGENVERTEXARRAYSPROC)(GLsizei n, GLuint *arrays);\ntypedef void(GLAD_API_PTR *PFNGLGENERATEMIPMAPPROC)(GLenum target);\ntypedef void(GLAD_API_PTR *PFNGLGENERATETEXTUREMIPMAPPROC)(GLuint texture);\ntypedef void(GLAD_API_PTR *PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC)(GLuint program, GLuint bufferIndex, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETACTIVEATTRIBPROC)(GLuint program, GLuint index, GLsizei bufSize, GLsizei *length, GLint *size, GLenum *type, GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETACTIVESUBROUTINENAMEPROC)(GLuint program, GLenum shadertype, GLuint index, GLsizei bufSize, GLsizei *length, GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC)(GLuint program, GLenum shadertype, GLuint index, GLsizei bufSize, GLsizei *length, GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC)(GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint *values);\ntypedef void(GLAD_API_PTR *PFNGLGETACTIVEUNIFORMPROC)(GLuint program, GLuint index, GLsizei bufSize, GLsizei *length, GLint *size, GLenum *type, GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC)(GLuint program, GLuint uniformBlockIndex, GLsizei bufSize, GLsizei *length, GLchar *uniformBlockName);\ntypedef void(GLAD_API_PTR *PFNGLGETACTIVEUNIFORMBLOCKIVPROC)(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETACTIVEUNIFORMNAMEPROC)(GLuint program, GLuint uniformIndex, GLsizei bufSize, GLsizei *length, GLchar *uniformName);\ntypedef void(GLAD_API_PTR *PFNGLGETACTIVEUNIFORMSIVPROC)(GLuint program, GLsizei uniformCount, const GLuint *uniformIndices, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETATTACHEDSHADERSPROC)(GLuint program, GLsizei maxCount, GLsizei *count, GLuint *shaders);\ntypedef GLint(GLAD_API_PTR *PFNGLGETATTRIBLOCATIONPROC)(GLuint program, const GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETBOOLEANI_VPROC)(GLenum target, GLuint index, GLboolean *data);\ntypedef void(GLAD_API_PTR *PFNGLGETBOOLEANVPROC)(GLenum pname, GLboolean *data);\ntypedef void(GLAD_API_PTR *PFNGLGETBUFFERPARAMETERI64VPROC)(GLenum target, GLenum pname, GLint64 *params);\ntypedef void(GLAD_API_PTR *PFNGLGETBUFFERPARAMETERIVPROC)(GLenum target, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETBUFFERPOINTERVPROC)(GLenum target, GLenum pname, void **params);\ntypedef void(GLAD_API_PTR *PFNGLGETBUFFERSUBDATAPROC)(GLenum target, GLintptr offset, GLsizeiptr size, void *data);\ntypedef void(GLAD_API_PTR *PFNGLGETCLIPPLANEPROC)(GLenum plane, GLdouble *equation);\ntypedef void(GLAD_API_PTR *PFNGLGETCOMPRESSEDTEXIMAGEPROC)(GLenum target, GLint level, void *img);\ntypedef void(GLAD_API_PTR *PFNGLGETCOMPRESSEDTEXTUREIMAGEPROC)(GLuint texture, GLint level, GLsizei bufSize, void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLGETCOMPRESSEDTEXTURESUBIMAGEPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLsizei bufSize, void *pixels);\ntypedef GLuint(GLAD_API_PTR *PFNGLGETDEBUGMESSAGELOGPROC)(GLuint count, GLsizei bufSize, GLenum *sources, GLenum *types, GLuint *ids, GLenum *severities, GLsizei *lengths, GLchar *messageLog);\ntypedef void(GLAD_API_PTR *PFNGLGETDOUBLEI_VPROC)(GLenum target, GLuint index, GLdouble *data);\ntypedef void(GLAD_API_PTR *PFNGLGETDOUBLEVPROC)(GLenum pname, GLdouble *data);\ntypedef GLenum(GLAD_API_PTR *PFNGLGETERRORPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLGETFLOATI_VPROC)(GLenum target, GLuint index, GLfloat *data);\ntypedef void(GLAD_API_PTR *PFNGLGETFLOATVPROC)(GLenum pname, GLfloat *data);\ntypedef GLint(GLAD_API_PTR *PFNGLGETFRAGDATAINDEXPROC)(GLuint program, const GLchar *name);\ntypedef GLint(GLAD_API_PTR *PFNGLGETFRAGDATALOCATIONPROC)(GLuint program, const GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC)(GLenum target, GLenum attachment, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETFRAMEBUFFERPARAMETERIVPROC)(GLenum target, GLenum pname, GLint *params);\ntypedef GLenum(GLAD_API_PTR *PFNGLGETGRAPHICSRESETSTATUSPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLGETINTEGER64I_VPROC)(GLenum target, GLuint index, GLint64 *data);\ntypedef void(GLAD_API_PTR *PFNGLGETINTEGER64VPROC)(GLenum pname, GLint64 *data);\ntypedef void(GLAD_API_PTR *PFNGLGETINTEGERI_VPROC)(GLenum target, GLuint index, GLint *data);\ntypedef void(GLAD_API_PTR *PFNGLGETINTEGERVPROC)(GLenum pname, GLint *data);\ntypedef void(GLAD_API_PTR *PFNGLGETINTERNALFORMATI64VPROC)(GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64 *params);\ntypedef void(GLAD_API_PTR *PFNGLGETINTERNALFORMATIVPROC)(GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETLIGHTFVPROC)(GLenum light, GLenum pname, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETLIGHTIVPROC)(GLenum light, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETMAPDVPROC)(GLenum target, GLenum query, GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLGETMAPFVPROC)(GLenum target, GLenum query, GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLGETMAPIVPROC)(GLenum target, GLenum query, GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLGETMATERIALFVPROC)(GLenum face, GLenum pname, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETMATERIALIVPROC)(GLenum face, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETMULTISAMPLEFVPROC)(GLenum pname, GLuint index, GLfloat *val);\ntypedef void(GLAD_API_PTR *PFNGLGETNAMEDBUFFERPARAMETERI64VPROC)(GLuint buffer, GLenum pname, GLint64 *params);\ntypedef void(GLAD_API_PTR *PFNGLGETNAMEDBUFFERPARAMETERIVPROC)(GLuint buffer, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETNAMEDBUFFERPOINTERVPROC)(GLuint buffer, GLenum pname, void **params);\ntypedef void(GLAD_API_PTR *PFNGLGETNAMEDBUFFERSUBDATAPROC)(GLuint buffer, GLintptr offset, GLsizeiptr size, void *data);\ntypedef void(GLAD_API_PTR *PFNGLGETNAMEDFRAMEBUFFERATTACHMENTPARAMETERIVPROC)(GLuint framebuffer, GLenum attachment, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETNAMEDFRAMEBUFFERPARAMETERIVPROC)(GLuint framebuffer, GLenum pname, GLint *param);\ntypedef void(GLAD_API_PTR *PFNGLGETNAMEDRENDERBUFFERPARAMETERIVPROC)(GLuint renderbuffer, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETOBJECTLABELPROC)(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei *length, GLchar *label);\ntypedef void(GLAD_API_PTR *PFNGLGETOBJECTPTRLABELPROC)(const void *ptr, GLsizei bufSize, GLsizei *length, GLchar *label);\ntypedef void(GLAD_API_PTR *PFNGLGETPIXELMAPFVPROC)(GLenum map, GLfloat *values);\ntypedef void(GLAD_API_PTR *PFNGLGETPIXELMAPUIVPROC)(GLenum map, GLuint *values);\ntypedef void(GLAD_API_PTR *PFNGLGETPIXELMAPUSVPROC)(GLenum map, GLushort *values);\ntypedef void(GLAD_API_PTR *PFNGLGETPOINTERVPROC)(GLenum pname, void **params);\ntypedef void(GLAD_API_PTR *PFNGLGETPOLYGONSTIPPLEPROC)(GLubyte *mask);\ntypedef void(GLAD_API_PTR *PFNGLGETPROGRAMBINARYPROC)(GLuint program, GLsizei bufSize, GLsizei *length, GLenum *binaryFormat, void *binary);\ntypedef void(GLAD_API_PTR *PFNGLGETPROGRAMINFOLOGPROC)(GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog);\ntypedef void(GLAD_API_PTR *PFNGLGETPROGRAMINTERFACEIVPROC)(GLuint program, GLenum programInterface, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETPROGRAMPIPELINEINFOLOGPROC)(GLuint pipeline, GLsizei bufSize, GLsizei *length, GLchar *infoLog);\ntypedef void(GLAD_API_PTR *PFNGLGETPROGRAMPIPELINEIVPROC)(GLuint pipeline, GLenum pname, GLint *params);\ntypedef GLuint(GLAD_API_PTR *PFNGLGETPROGRAMRESOURCEINDEXPROC)(GLuint program, GLenum programInterface, const GLchar *name);\ntypedef GLint(GLAD_API_PTR *PFNGLGETPROGRAMRESOURCELOCATIONPROC)(GLuint program, GLenum programInterface, const GLchar *name);\ntypedef GLint(GLAD_API_PTR *PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC)(GLuint program, GLenum programInterface, const GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETPROGRAMRESOURCENAMEPROC)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei *length, GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETPROGRAMRESOURCEIVPROC)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount, const GLenum *props, GLsizei count, GLsizei *length, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETPROGRAMSTAGEIVPROC)(GLuint program, GLenum shadertype, GLenum pname, GLint *values);\ntypedef void(GLAD_API_PTR *PFNGLGETPROGRAMIVPROC)(GLuint program, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETQUERYBUFFEROBJECTI64VPROC)(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);\ntypedef void(GLAD_API_PTR *PFNGLGETQUERYBUFFEROBJECTIVPROC)(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);\ntypedef void(GLAD_API_PTR *PFNGLGETQUERYBUFFEROBJECTUI64VPROC)(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);\ntypedef void(GLAD_API_PTR *PFNGLGETQUERYBUFFEROBJECTUIVPROC)(GLuint id, GLuint buffer, GLenum pname, GLintptr offset);\ntypedef void(GLAD_API_PTR *PFNGLGETQUERYINDEXEDIVPROC)(GLenum target, GLuint index, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETQUERYOBJECTI64VPROC)(GLuint id, GLenum pname, GLint64 *params);\ntypedef void(GLAD_API_PTR *PFNGLGETQUERYOBJECTIVPROC)(GLuint id, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETQUERYOBJECTUI64VPROC)(GLuint id, GLenum pname, GLuint64 *params);\ntypedef void(GLAD_API_PTR *PFNGLGETQUERYOBJECTUIVPROC)(GLuint id, GLenum pname, GLuint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETQUERYIVPROC)(GLenum target, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETRENDERBUFFERPARAMETERIVPROC)(GLenum target, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERIIVPROC)(GLuint sampler, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERIUIVPROC)(GLuint sampler, GLenum pname, GLuint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERFVPROC)(GLuint sampler, GLenum pname, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERIVPROC)(GLuint sampler, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETSHADERINFOLOGPROC)(GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog);\ntypedef void(GLAD_API_PTR *PFNGLGETSHADERPRECISIONFORMATPROC)(GLenum shadertype, GLenum precisiontype, GLint *range, GLint *precision);\ntypedef void(GLAD_API_PTR *PFNGLGETSHADERSOURCEPROC)(GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *source);\ntypedef void(GLAD_API_PTR *PFNGLGETSHADERIVPROC)(GLuint shader, GLenum pname, GLint *params);\ntypedef const GLubyte *(GLAD_API_PTR *PFNGLGETSTRINGPROC)(GLenum name);\ntypedef const GLubyte *(GLAD_API_PTR *PFNGLGETSTRINGIPROC)(GLenum name, GLuint index);\ntypedef GLuint(GLAD_API_PTR *PFNGLGETSUBROUTINEINDEXPROC)(GLuint program, GLenum shadertype, const GLchar *name);\ntypedef GLint(GLAD_API_PTR *PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC)(GLuint program, GLenum shadertype, const GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETSYNCIVPROC)(GLsync sync, GLenum pname, GLsizei count, GLsizei *length, GLint *values);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXENVFVPROC)(GLenum target, GLenum pname, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXENVIVPROC)(GLenum target, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXGENDVPROC)(GLenum coord, GLenum pname, GLdouble *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXGENFVPROC)(GLenum coord, GLenum pname, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXGENIVPROC)(GLenum coord, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXIMAGEPROC)(GLenum target, GLint level, GLenum format, GLenum type, void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXLEVELPARAMETERFVPROC)(GLenum target, GLint level, GLenum pname, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXLEVELPARAMETERIVPROC)(GLenum target, GLint level, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXPARAMETERIIVPROC)(GLenum target, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXPARAMETERIUIVPROC)(GLenum target, GLenum pname, GLuint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXPARAMETERFVPROC)(GLenum target, GLenum pname, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXPARAMETERIVPROC)(GLenum target, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXTUREIMAGEPROC)(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXTURELEVELPARAMETERFVPROC)(GLuint texture, GLint level, GLenum pname, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXTURELEVELPARAMETERIVPROC)(GLuint texture, GLint level, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXTUREPARAMETERIIVPROC)(GLuint texture, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXTUREPARAMETERIUIVPROC)(GLuint texture, GLenum pname, GLuint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXTUREPARAMETERFVPROC)(GLuint texture, GLenum pname, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXTUREPARAMETERIVPROC)(GLuint texture, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETTEXTURESUBIMAGEPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLGETTRANSFORMFEEDBACKVARYINGPROC)(GLuint program, GLuint index, GLsizei bufSize, GLsizei *length, GLsizei *size, GLenum *type, GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETTRANSFORMFEEDBACKI64_VPROC)(GLuint xfb, GLenum pname, GLuint index, GLint64 *param);\ntypedef void(GLAD_API_PTR *PFNGLGETTRANSFORMFEEDBACKI_VPROC)(GLuint xfb, GLenum pname, GLuint index, GLint *param);\ntypedef void(GLAD_API_PTR *PFNGLGETTRANSFORMFEEDBACKIVPROC)(GLuint xfb, GLenum pname, GLint *param);\ntypedef GLuint(GLAD_API_PTR *PFNGLGETUNIFORMBLOCKINDEXPROC)(GLuint program, const GLchar *uniformBlockName);\ntypedef void(GLAD_API_PTR *PFNGLGETUNIFORMINDICESPROC)(GLuint program, GLsizei uniformCount, const GLchar *const *uniformNames, GLuint *uniformIndices);\ntypedef GLint(GLAD_API_PTR *PFNGLGETUNIFORMLOCATIONPROC)(GLuint program, const GLchar *name);\ntypedef void(GLAD_API_PTR *PFNGLGETUNIFORMSUBROUTINEUIVPROC)(GLenum shadertype, GLint location, GLuint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETUNIFORMDVPROC)(GLuint program, GLint location, GLdouble *params);\ntypedef void(GLAD_API_PTR *PFNGLGETUNIFORMFVPROC)(GLuint program, GLint location, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETUNIFORMIVPROC)(GLuint program, GLint location, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETUNIFORMUIVPROC)(GLuint program, GLint location, GLuint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETVERTEXARRAYINDEXED64IVPROC)(GLuint vaobj, GLuint index, GLenum pname, GLint64 *param);\ntypedef void(GLAD_API_PTR *PFNGLGETVERTEXARRAYINDEXEDIVPROC)(GLuint vaobj, GLuint index, GLenum pname, GLint *param);\ntypedef void(GLAD_API_PTR *PFNGLGETVERTEXARRAYIVPROC)(GLuint vaobj, GLenum pname, GLint *param);\ntypedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBIIVPROC)(GLuint index, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBIUIVPROC)(GLuint index, GLenum pname, GLuint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBLDVPROC)(GLuint index, GLenum pname, GLdouble *params);\ntypedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBPOINTERVPROC)(GLuint index, GLenum pname, void **pointer);\ntypedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBDVPROC)(GLuint index, GLenum pname, GLdouble *params);\ntypedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBFVPROC)(GLuint index, GLenum pname, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETVERTEXATTRIBIVPROC)(GLuint index, GLenum pname, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETNCOLORTABLEPROC)(GLenum target, GLenum format, GLenum type, GLsizei bufSize, void *table);\ntypedef void(GLAD_API_PTR *PFNGLGETNCOMPRESSEDTEXIMAGEPROC)(GLenum target, GLint lod, GLsizei bufSize, void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLGETNCONVOLUTIONFILTERPROC)(GLenum target, GLenum format, GLenum type, GLsizei bufSize, void *image);\ntypedef void(GLAD_API_PTR *PFNGLGETNHISTOGRAMPROC)(GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void *values);\ntypedef void(GLAD_API_PTR *PFNGLGETNMAPDVPROC)(GLenum target, GLenum query, GLsizei bufSize, GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLGETNMAPFVPROC)(GLenum target, GLenum query, GLsizei bufSize, GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLGETNMAPIVPROC)(GLenum target, GLenum query, GLsizei bufSize, GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLGETNMINMAXPROC)(GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void *values);\ntypedef void(GLAD_API_PTR *PFNGLGETNPIXELMAPFVPROC)(GLenum map, GLsizei bufSize, GLfloat *values);\ntypedef void(GLAD_API_PTR *PFNGLGETNPIXELMAPUIVPROC)(GLenum map, GLsizei bufSize, GLuint *values);\ntypedef void(GLAD_API_PTR *PFNGLGETNPIXELMAPUSVPROC)(GLenum map, GLsizei bufSize, GLushort *values);\ntypedef void(GLAD_API_PTR *PFNGLGETNPOLYGONSTIPPLEPROC)(GLsizei bufSize, GLubyte *pattern);\ntypedef void(GLAD_API_PTR *PFNGLGETNSEPARABLEFILTERPROC)(GLenum target, GLenum format, GLenum type, GLsizei rowBufSize, void *row, GLsizei columnBufSize, void *column, void *span);\ntypedef void(GLAD_API_PTR *PFNGLGETNTEXIMAGEPROC)(GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLGETNUNIFORMDVPROC)(GLuint program, GLint location, GLsizei bufSize, GLdouble *params);\ntypedef void(GLAD_API_PTR *PFNGLGETNUNIFORMFVPROC)(GLuint program, GLint location, GLsizei bufSize, GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLGETNUNIFORMIVPROC)(GLuint program, GLint location, GLsizei bufSize, GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLGETNUNIFORMUIVPROC)(GLuint program, GLint location, GLsizei bufSize, GLuint *params);\ntypedef void(GLAD_API_PTR *PFNGLHINTPROC)(GLenum target, GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLINDEXMASKPROC)(GLuint mask);\ntypedef void(GLAD_API_PTR *PFNGLINDEXPOINTERPROC)(GLenum type, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLINDEXDPROC)(GLdouble c);\ntypedef void(GLAD_API_PTR *PFNGLINDEXDVPROC)(const GLdouble *c);\ntypedef void(GLAD_API_PTR *PFNGLINDEXFPROC)(GLfloat c);\ntypedef void(GLAD_API_PTR *PFNGLINDEXFVPROC)(const GLfloat *c);\ntypedef void(GLAD_API_PTR *PFNGLINDEXIPROC)(GLint c);\ntypedef void(GLAD_API_PTR *PFNGLINDEXIVPROC)(const GLint *c);\ntypedef void(GLAD_API_PTR *PFNGLINDEXSPROC)(GLshort c);\ntypedef void(GLAD_API_PTR *PFNGLINDEXSVPROC)(const GLshort *c);\ntypedef void(GLAD_API_PTR *PFNGLINDEXUBPROC)(GLubyte c);\ntypedef void(GLAD_API_PTR *PFNGLINDEXUBVPROC)(const GLubyte *c);\ntypedef void(GLAD_API_PTR *PFNGLINITNAMESPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLINTERLEAVEDARRAYSPROC)(GLenum format, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLINVALIDATEBUFFERDATAPROC)(GLuint buffer);\ntypedef void(GLAD_API_PTR *PFNGLINVALIDATEBUFFERSUBDATAPROC)(GLuint buffer, GLintptr offset, GLsizeiptr length);\ntypedef void(GLAD_API_PTR *PFNGLINVALIDATEFRAMEBUFFERPROC)(GLenum target, GLsizei numAttachments, const GLenum *attachments);\ntypedef void(GLAD_API_PTR *PFNGLINVALIDATENAMEDFRAMEBUFFERDATAPROC)(GLuint framebuffer, GLsizei numAttachments, const GLenum *attachments);\ntypedef void(GLAD_API_PTR *PFNGLINVALIDATENAMEDFRAMEBUFFERSUBDATAPROC)(GLuint framebuffer, GLsizei numAttachments, const GLenum *attachments, GLint x, GLint y, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLINVALIDATESUBFRAMEBUFFERPROC)(GLenum target, GLsizei numAttachments, const GLenum *attachments, GLint x, GLint y, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLINVALIDATETEXIMAGEPROC)(GLuint texture, GLint level);\ntypedef void(GLAD_API_PTR *PFNGLINVALIDATETEXSUBIMAGEPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISBUFFERPROC)(GLuint buffer);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISENABLEDPROC)(GLenum cap);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISENABLEDIPROC)(GLenum target, GLuint index);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISFRAMEBUFFERPROC)(GLuint framebuffer);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISLISTPROC)(GLuint list);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISPROGRAMPROC)(GLuint program);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISPROGRAMPIPELINEPROC)(GLuint pipeline);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISQUERYPROC)(GLuint id);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISRENDERBUFFERPROC)(GLuint renderbuffer);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISSAMPLERPROC)(GLuint sampler);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISSHADERPROC)(GLuint shader);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISSYNCPROC)(GLsync sync);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISTEXTUREPROC)(GLuint texture);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISTRANSFORMFEEDBACKPROC)(GLuint id);\ntypedef GLboolean(GLAD_API_PTR *PFNGLISVERTEXARRAYPROC)(GLuint array);\ntypedef void(GLAD_API_PTR *PFNGLLIGHTMODELFPROC)(GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLLIGHTMODELFVPROC)(GLenum pname, const GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLLIGHTMODELIPROC)(GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLLIGHTMODELIVPROC)(GLenum pname, const GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLLIGHTFPROC)(GLenum light, GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLLIGHTFVPROC)(GLenum light, GLenum pname, const GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLLIGHTIPROC)(GLenum light, GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLLIGHTIVPROC)(GLenum light, GLenum pname, const GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLLINESTIPPLEPROC)(GLint factor, GLushort pattern);\ntypedef void(GLAD_API_PTR *PFNGLLINEWIDTHPROC)(GLfloat width);\ntypedef void(GLAD_API_PTR *PFNGLLINKPROGRAMPROC)(GLuint program);\ntypedef void(GLAD_API_PTR *PFNGLLISTBASEPROC)(GLuint base);\ntypedef void(GLAD_API_PTR *PFNGLLOADIDENTITYPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLLOADMATRIXDPROC)(const GLdouble *m);\ntypedef void(GLAD_API_PTR *PFNGLLOADMATRIXFPROC)(const GLfloat *m);\ntypedef void(GLAD_API_PTR *PFNGLLOADNAMEPROC)(GLuint name);\ntypedef void(GLAD_API_PTR *PFNGLLOADTRANSPOSEMATRIXDPROC)(const GLdouble *m);\ntypedef void(GLAD_API_PTR *PFNGLLOADTRANSPOSEMATRIXFPROC)(const GLfloat *m);\ntypedef void(GLAD_API_PTR *PFNGLLOGICOPPROC)(GLenum opcode);\ntypedef void(GLAD_API_PTR *PFNGLMAP1DPROC)(GLenum target, GLdouble u1, GLdouble u2, GLint stride, GLint order, const GLdouble *points);\ntypedef void(GLAD_API_PTR *PFNGLMAP1FPROC)(GLenum target, GLfloat u1, GLfloat u2, GLint stride, GLint order, const GLfloat *points);\ntypedef void(GLAD_API_PTR *PFNGLMAP2DPROC)(GLenum target, GLdouble u1, GLdouble u2, GLint ustride, GLint uorder, GLdouble v1, GLdouble v2, GLint vstride, GLint vorder, const GLdouble *points);\ntypedef void(GLAD_API_PTR *PFNGLMAP2FPROC)(GLenum target, GLfloat u1, GLfloat u2, GLint ustride, GLint uorder, GLfloat v1, GLfloat v2, GLint vstride, GLint vorder, const GLfloat *points);\ntypedef void *(GLAD_API_PTR *PFNGLMAPBUFFERPROC)(GLenum target, GLenum access);\ntypedef void *(GLAD_API_PTR *PFNGLMAPBUFFERRANGEPROC)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);\ntypedef void(GLAD_API_PTR *PFNGLMAPGRID1DPROC)(GLint un, GLdouble u1, GLdouble u2);\ntypedef void(GLAD_API_PTR *PFNGLMAPGRID1FPROC)(GLint un, GLfloat u1, GLfloat u2);\ntypedef void(GLAD_API_PTR *PFNGLMAPGRID2DPROC)(GLint un, GLdouble u1, GLdouble u2, GLint vn, GLdouble v1, GLdouble v2);\ntypedef void(GLAD_API_PTR *PFNGLMAPGRID2FPROC)(GLint un, GLfloat u1, GLfloat u2, GLint vn, GLfloat v1, GLfloat v2);\ntypedef void *(GLAD_API_PTR *PFNGLMAPNAMEDBUFFERPROC)(GLuint buffer, GLenum access);\ntypedef void *(GLAD_API_PTR *PFNGLMAPNAMEDBUFFERRANGEPROC)(GLuint buffer, GLintptr offset, GLsizeiptr length, GLbitfield access);\ntypedef void(GLAD_API_PTR *PFNGLMATERIALFPROC)(GLenum face, GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLMATERIALFVPROC)(GLenum face, GLenum pname, const GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLMATERIALIPROC)(GLenum face, GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLMATERIALIVPROC)(GLenum face, GLenum pname, const GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLMATRIXMODEPROC)(GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLMEMORYBARRIERPROC)(GLbitfield barriers);\ntypedef void(GLAD_API_PTR *PFNGLMEMORYBARRIERBYREGIONPROC)(GLbitfield barriers);\ntypedef void(GLAD_API_PTR *PFNGLMINSAMPLESHADINGPROC)(GLfloat value);\ntypedef void(GLAD_API_PTR *PFNGLMULTMATRIXDPROC)(const GLdouble *m);\ntypedef void(GLAD_API_PTR *PFNGLMULTMATRIXFPROC)(const GLfloat *m);\ntypedef void(GLAD_API_PTR *PFNGLMULTTRANSPOSEMATRIXDPROC)(const GLdouble *m);\ntypedef void(GLAD_API_PTR *PFNGLMULTTRANSPOSEMATRIXFPROC)(const GLfloat *m);\ntypedef void(GLAD_API_PTR *PFNGLMULTIDRAWARRAYSPROC)(GLenum mode, const GLint *first, const GLsizei *count, GLsizei drawcount);\ntypedef void(GLAD_API_PTR *PFNGLMULTIDRAWARRAYSINDIRECTPROC)(GLenum mode, const void *indirect, GLsizei drawcount, GLsizei stride);\ntypedef void(GLAD_API_PTR *PFNGLMULTIDRAWARRAYSINDIRECTCOUNTPROC)(GLenum mode, const void *indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride);\ntypedef void(GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSPROC)(GLenum mode, const GLsizei *count, GLenum type, const void *const *indices, GLsizei drawcount);\ntypedef void(GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSBASEVERTEXPROC)(GLenum mode, const GLsizei *count, GLenum type, const void *const *indices, GLsizei drawcount, const GLint *basevertex);\ntypedef void(GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSINDIRECTPROC)(GLenum mode, GLenum type, const void *indirect, GLsizei drawcount, GLsizei stride);\ntypedef void(GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSINDIRECTCOUNTPROC)(GLenum mode, GLenum type, const void *indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1DPROC)(GLenum target, GLdouble s);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1DVPROC)(GLenum target, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1FPROC)(GLenum target, GLfloat s);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1FVPROC)(GLenum target, const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1IPROC)(GLenum target, GLint s);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1IVPROC)(GLenum target, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1SPROC)(GLenum target, GLshort s);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD1SVPROC)(GLenum target, const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2DPROC)(GLenum target, GLdouble s, GLdouble t);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2DVPROC)(GLenum target, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2FPROC)(GLenum target, GLfloat s, GLfloat t);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2FVPROC)(GLenum target, const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2IPROC)(GLenum target, GLint s, GLint t);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2IVPROC)(GLenum target, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2SPROC)(GLenum target, GLshort s, GLshort t);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD2SVPROC)(GLenum target, const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3DPROC)(GLenum target, GLdouble s, GLdouble t, GLdouble r);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3DVPROC)(GLenum target, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3FPROC)(GLenum target, GLfloat s, GLfloat t, GLfloat r);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3FVPROC)(GLenum target, const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3IPROC)(GLenum target, GLint s, GLint t, GLint r);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3IVPROC)(GLenum target, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3SPROC)(GLenum target, GLshort s, GLshort t, GLshort r);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD3SVPROC)(GLenum target, const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4DPROC)(GLenum target, GLdouble s, GLdouble t, GLdouble r, GLdouble q);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4DVPROC)(GLenum target, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4FPROC)(GLenum target, GLfloat s, GLfloat t, GLfloat r, GLfloat q);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4FVPROC)(GLenum target, const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4IPROC)(GLenum target, GLint s, GLint t, GLint r, GLint q);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4IVPROC)(GLenum target, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4SPROC)(GLenum target, GLshort s, GLshort t, GLshort r, GLshort q);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORD4SVPROC)(GLenum target, const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP1UIPROC)(GLenum texture, GLenum type, GLuint coords);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP1UIVPROC)(GLenum texture, GLenum type, const GLuint *coords);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP2UIPROC)(GLenum texture, GLenum type, GLuint coords);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP2UIVPROC)(GLenum texture, GLenum type, const GLuint *coords);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP3UIPROC)(GLenum texture, GLenum type, GLuint coords);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP3UIVPROC)(GLenum texture, GLenum type, const GLuint *coords);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP4UIPROC)(GLenum texture, GLenum type, GLuint coords);\ntypedef void(GLAD_API_PTR *PFNGLMULTITEXCOORDP4UIVPROC)(GLenum texture, GLenum type, const GLuint *coords);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDBUFFERDATAPROC)(GLuint buffer, GLsizeiptr size, const void *data, GLenum usage);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDBUFFERSTORAGEPROC)(GLuint buffer, GLsizeiptr size, const void *data, GLbitfield flags);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDBUFFERSUBDATAPROC)(GLuint buffer, GLintptr offset, GLsizeiptr size, const void *data);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERDRAWBUFFERPROC)(GLuint framebuffer, GLenum buf);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERDRAWBUFFERSPROC)(GLuint framebuffer, GLsizei n, const GLenum *bufs);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERPARAMETERIPROC)(GLuint framebuffer, GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERREADBUFFERPROC)(GLuint framebuffer, GLenum src);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERRENDERBUFFERPROC)(GLuint framebuffer, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERTEXTUREPROC)(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDFRAMEBUFFERTEXTURELAYERPROC)(GLuint framebuffer, GLenum attachment, GLuint texture, GLint level, GLint layer);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDRENDERBUFFERSTORAGEPROC)(GLuint renderbuffer, GLenum internalformat, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLNAMEDRENDERBUFFERSTORAGEMULTISAMPLEPROC)(GLuint renderbuffer, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLNEWLISTPROC)(GLuint list, GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLNORMAL3BPROC)(GLbyte nx, GLbyte ny, GLbyte nz);\ntypedef void(GLAD_API_PTR *PFNGLNORMAL3BVPROC)(const GLbyte *v);\ntypedef void(GLAD_API_PTR *PFNGLNORMAL3DPROC)(GLdouble nx, GLdouble ny, GLdouble nz);\ntypedef void(GLAD_API_PTR *PFNGLNORMAL3DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLNORMAL3FPROC)(GLfloat nx, GLfloat ny, GLfloat nz);\ntypedef void(GLAD_API_PTR *PFNGLNORMAL3FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLNORMAL3IPROC)(GLint nx, GLint ny, GLint nz);\ntypedef void(GLAD_API_PTR *PFNGLNORMAL3IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLNORMAL3SPROC)(GLshort nx, GLshort ny, GLshort nz);\ntypedef void(GLAD_API_PTR *PFNGLNORMAL3SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLNORMALP3UIPROC)(GLenum type, GLuint coords);\ntypedef void(GLAD_API_PTR *PFNGLNORMALP3UIVPROC)(GLenum type, const GLuint *coords);\ntypedef void(GLAD_API_PTR *PFNGLNORMALPOINTERPROC)(GLenum type, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLOBJECTLABELPROC)(GLenum identifier, GLuint name, GLsizei length, const GLchar *label);\ntypedef void(GLAD_API_PTR *PFNGLOBJECTPTRLABELPROC)(const void *ptr, GLsizei length, const GLchar *label);\ntypedef void(GLAD_API_PTR *PFNGLORTHOPROC)(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);\ntypedef void(GLAD_API_PTR *PFNGLPASSTHROUGHPROC)(GLfloat token);\ntypedef void(GLAD_API_PTR *PFNGLPATCHPARAMETERFVPROC)(GLenum pname, const GLfloat *values);\ntypedef void(GLAD_API_PTR *PFNGLPATCHPARAMETERIPROC)(GLenum pname, GLint value);\ntypedef void(GLAD_API_PTR *PFNGLPAUSETRANSFORMFEEDBACKPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLPIXELMAPFVPROC)(GLenum map, GLsizei mapsize, const GLfloat *values);\ntypedef void(GLAD_API_PTR *PFNGLPIXELMAPUIVPROC)(GLenum map, GLsizei mapsize, const GLuint *values);\ntypedef void(GLAD_API_PTR *PFNGLPIXELMAPUSVPROC)(GLenum map, GLsizei mapsize, const GLushort *values);\ntypedef void(GLAD_API_PTR *PFNGLPIXELSTOREFPROC)(GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLPIXELSTOREIPROC)(GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLPIXELTRANSFERFPROC)(GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLPIXELTRANSFERIPROC)(GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLPIXELZOOMPROC)(GLfloat xfactor, GLfloat yfactor);\ntypedef void(GLAD_API_PTR *PFNGLPOINTPARAMETERFPROC)(GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLPOINTPARAMETERFVPROC)(GLenum pname, const GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLPOINTPARAMETERIPROC)(GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLPOINTPARAMETERIVPROC)(GLenum pname, const GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLPOINTSIZEPROC)(GLfloat size);\ntypedef void(GLAD_API_PTR *PFNGLPOLYGONMODEPROC)(GLenum face, GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLPOLYGONOFFSETPROC)(GLfloat factor, GLfloat units);\ntypedef void(GLAD_API_PTR *PFNGLPOLYGONOFFSETCLAMPPROC)(GLfloat factor, GLfloat units, GLfloat clamp);\ntypedef void(GLAD_API_PTR *PFNGLPOLYGONSTIPPLEPROC)(const GLubyte *mask);\ntypedef void(GLAD_API_PTR *PFNGLPOPATTRIBPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLPOPCLIENTATTRIBPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLPOPDEBUGGROUPPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLPOPMATRIXPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLPOPNAMEPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLPRIMITIVERESTARTINDEXPROC)(GLuint index);\ntypedef void(GLAD_API_PTR *PFNGLPRIORITIZETEXTURESPROC)(GLsizei n, const GLuint *textures, const GLfloat *priorities);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMBINARYPROC)(GLuint program, GLenum binaryFormat, const void *binary, GLsizei length);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMPARAMETERIPROC)(GLuint program, GLenum pname, GLint value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1DPROC)(GLuint program, GLint location, GLdouble v0);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1FPROC)(GLuint program, GLint location, GLfloat v0);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1IPROC)(GLuint program, GLint location, GLint v0);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1IVPROC)(GLuint program, GLint location, GLsizei count, const GLint *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1UIPROC)(GLuint program, GLint location, GLuint v0);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM1UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2IPROC)(GLuint program, GLint location, GLint v0, GLint v1);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2IVPROC)(GLuint program, GLint location, GLsizei count, const GLint *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM2UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3IPROC)(GLuint program, GLint location, GLint v0, GLint v1, GLint v2);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3IVPROC)(GLuint program, GLint location, GLsizei count, const GLint *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM3UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4IPROC)(GLuint program, GLint location, GLint v0, GLint v1, GLint v2, GLint v3);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4IVPROC)(GLuint program, GLint location, GLsizei count, const GLint *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORM4UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLPROVOKINGVERTEXPROC)(GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLPUSHATTRIBPROC)(GLbitfield mask);\ntypedef void(GLAD_API_PTR *PFNGLPUSHCLIENTATTRIBPROC)(GLbitfield mask);\ntypedef void(GLAD_API_PTR *PFNGLPUSHDEBUGGROUPPROC)(GLenum source, GLuint id, GLsizei length, const GLchar *message);\ntypedef void(GLAD_API_PTR *PFNGLPUSHMATRIXPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLPUSHNAMEPROC)(GLuint name);\ntypedef void(GLAD_API_PTR *PFNGLQUERYCOUNTERPROC)(GLuint id, GLenum target);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS2DPROC)(GLdouble x, GLdouble y);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS2DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS2FPROC)(GLfloat x, GLfloat y);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS2FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS2IPROC)(GLint x, GLint y);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS2IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS2SPROC)(GLshort x, GLshort y);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS2SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS3DPROC)(GLdouble x, GLdouble y, GLdouble z);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS3DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS3FPROC)(GLfloat x, GLfloat y, GLfloat z);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS3FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS3IPROC)(GLint x, GLint y, GLint z);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS3IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS3SPROC)(GLshort x, GLshort y, GLshort z);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS3SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS4DPROC)(GLdouble x, GLdouble y, GLdouble z, GLdouble w);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS4DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS4FPROC)(GLfloat x, GLfloat y, GLfloat z, GLfloat w);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS4FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS4IPROC)(GLint x, GLint y, GLint z, GLint w);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS4IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS4SPROC)(GLshort x, GLshort y, GLshort z, GLshort w);\ntypedef void(GLAD_API_PTR *PFNGLRASTERPOS4SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLREADBUFFERPROC)(GLenum src);\ntypedef void(GLAD_API_PTR *PFNGLREADPIXELSPROC)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLREADNPIXELSPROC)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void *data);\ntypedef void(GLAD_API_PTR *PFNGLRECTDPROC)(GLdouble x1, GLdouble y1, GLdouble x2, GLdouble y2);\ntypedef void(GLAD_API_PTR *PFNGLRECTDVPROC)(const GLdouble *v1, const GLdouble *v2);\ntypedef void(GLAD_API_PTR *PFNGLRECTFPROC)(GLfloat x1, GLfloat y1, GLfloat x2, GLfloat y2);\ntypedef void(GLAD_API_PTR *PFNGLRECTFVPROC)(const GLfloat *v1, const GLfloat *v2);\ntypedef void(GLAD_API_PTR *PFNGLRECTIPROC)(GLint x1, GLint y1, GLint x2, GLint y2);\ntypedef void(GLAD_API_PTR *PFNGLRECTIVPROC)(const GLint *v1, const GLint *v2);\ntypedef void(GLAD_API_PTR *PFNGLRECTSPROC)(GLshort x1, GLshort y1, GLshort x2, GLshort y2);\ntypedef void(GLAD_API_PTR *PFNGLRECTSVPROC)(const GLshort *v1, const GLshort *v2);\ntypedef void(GLAD_API_PTR *PFNGLRELEASESHADERCOMPILERPROC)(void);\ntypedef GLint(GLAD_API_PTR *PFNGLRENDERMODEPROC)(GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEPROC)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLRESUMETRANSFORMFEEDBACKPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLROTATEDPROC)(GLdouble angle, GLdouble x, GLdouble y, GLdouble z);\ntypedef void(GLAD_API_PTR *PFNGLROTATEFPROC)(GLfloat angle, GLfloat x, GLfloat y, GLfloat z);\ntypedef void(GLAD_API_PTR *PFNGLSAMPLECOVERAGEPROC)(GLfloat value, GLboolean invert);\ntypedef void(GLAD_API_PTR *PFNGLSAMPLEMASKIPROC)(GLuint maskNumber, GLbitfield mask);\ntypedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERIIVPROC)(GLuint sampler, GLenum pname, const GLint *param);\ntypedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERIUIVPROC)(GLuint sampler, GLenum pname, const GLuint *param);\ntypedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERFPROC)(GLuint sampler, GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERFVPROC)(GLuint sampler, GLenum pname, const GLfloat *param);\ntypedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERIPROC)(GLuint sampler, GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLSAMPLERPARAMETERIVPROC)(GLuint sampler, GLenum pname, const GLint *param);\ntypedef void(GLAD_API_PTR *PFNGLSCALEDPROC)(GLdouble x, GLdouble y, GLdouble z);\ntypedef void(GLAD_API_PTR *PFNGLSCALEFPROC)(GLfloat x, GLfloat y, GLfloat z);\ntypedef void(GLAD_API_PTR *PFNGLSCISSORPROC)(GLint x, GLint y, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLSCISSORARRAYVPROC)(GLuint first, GLsizei count, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLSCISSORINDEXEDPROC)(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLSCISSORINDEXEDVPROC)(GLuint index, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3BPROC)(GLbyte red, GLbyte green, GLbyte blue);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3BVPROC)(const GLbyte *v);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3DPROC)(GLdouble red, GLdouble green, GLdouble blue);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3FPROC)(GLfloat red, GLfloat green, GLfloat blue);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3IPROC)(GLint red, GLint green, GLint blue);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3SPROC)(GLshort red, GLshort green, GLshort blue);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3UBPROC)(GLubyte red, GLubyte green, GLubyte blue);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3UBVPROC)(const GLubyte *v);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3UIPROC)(GLuint red, GLuint green, GLuint blue);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3UIVPROC)(const GLuint *v);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3USPROC)(GLushort red, GLushort green, GLushort blue);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLOR3USVPROC)(const GLushort *v);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLORP3UIPROC)(GLenum type, GLuint color);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLORP3UIVPROC)(GLenum type, const GLuint *color);\ntypedef void(GLAD_API_PTR *PFNGLSECONDARYCOLORPOINTERPROC)(GLint size, GLenum type, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLSELECTBUFFERPROC)(GLsizei size, GLuint *buffer);\ntypedef void(GLAD_API_PTR *PFNGLSHADEMODELPROC)(GLenum mode);\ntypedef void(GLAD_API_PTR *PFNGLSHADERBINARYPROC)(GLsizei count, const GLuint *shaders, GLenum binaryFormat, const void *binary, GLsizei length);\ntypedef void(GLAD_API_PTR *PFNGLSHADERSOURCEPROC)(GLuint shader, GLsizei count, const GLchar *const *string, const GLint *length);\ntypedef void(GLAD_API_PTR *PFNGLSHADERSTORAGEBLOCKBINDINGPROC)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);\ntypedef void(GLAD_API_PTR *PFNGLSPECIALIZESHADERPROC)(GLuint shader, const GLchar *pEntryPoint, GLuint numSpecializationConstants, const GLuint *pConstantIndex, const GLuint *pConstantValue);\ntypedef void(GLAD_API_PTR *PFNGLSTENCILFUNCPROC)(GLenum func, GLint ref, GLuint mask);\ntypedef void(GLAD_API_PTR *PFNGLSTENCILFUNCSEPARATEPROC)(GLenum face, GLenum func, GLint ref, GLuint mask);\ntypedef void(GLAD_API_PTR *PFNGLSTENCILMASKPROC)(GLuint mask);\ntypedef void(GLAD_API_PTR *PFNGLSTENCILMASKSEPARATEPROC)(GLenum face, GLuint mask);\ntypedef void(GLAD_API_PTR *PFNGLSTENCILOPPROC)(GLenum fail, GLenum zfail, GLenum zpass);\ntypedef void(GLAD_API_PTR *PFNGLSTENCILOPSEPARATEPROC)(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);\ntypedef void(GLAD_API_PTR *PFNGLTEXBUFFERPROC)(GLenum target, GLenum internalformat, GLuint buffer);\ntypedef void(GLAD_API_PTR *PFNGLTEXBUFFERRANGEPROC)(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD1DPROC)(GLdouble s);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD1DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD1FPROC)(GLfloat s);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD1FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD1IPROC)(GLint s);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD1IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD1SPROC)(GLshort s);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD1SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD2DPROC)(GLdouble s, GLdouble t);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD2DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD2FPROC)(GLfloat s, GLfloat t);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD2FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD2IPROC)(GLint s, GLint t);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD2IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD2SPROC)(GLshort s, GLshort t);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD2SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD3DPROC)(GLdouble s, GLdouble t, GLdouble r);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD3DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD3FPROC)(GLfloat s, GLfloat t, GLfloat r);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD3FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD3IPROC)(GLint s, GLint t, GLint r);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD3IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD3SPROC)(GLshort s, GLshort t, GLshort r);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD3SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD4DPROC)(GLdouble s, GLdouble t, GLdouble r, GLdouble q);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD4DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD4FPROC)(GLfloat s, GLfloat t, GLfloat r, GLfloat q);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD4FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD4IPROC)(GLint s, GLint t, GLint r, GLint q);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD4IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD4SPROC)(GLshort s, GLshort t, GLshort r, GLshort q);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORD4SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORDP1UIPROC)(GLenum type, GLuint coords);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORDP1UIVPROC)(GLenum type, const GLuint *coords);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORDP2UIPROC)(GLenum type, GLuint coords);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORDP2UIVPROC)(GLenum type, const GLuint *coords);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORDP3UIPROC)(GLenum type, GLuint coords);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORDP3UIVPROC)(GLenum type, const GLuint *coords);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORDP4UIPROC)(GLenum type, GLuint coords);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORDP4UIVPROC)(GLenum type, const GLuint *coords);\ntypedef void(GLAD_API_PTR *PFNGLTEXCOORDPOINTERPROC)(GLint size, GLenum type, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLTEXENVFPROC)(GLenum target, GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLTEXENVFVPROC)(GLenum target, GLenum pname, const GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXENVIPROC)(GLenum target, GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLTEXENVIVPROC)(GLenum target, GLenum pname, const GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXGENDPROC)(GLenum coord, GLenum pname, GLdouble param);\ntypedef void(GLAD_API_PTR *PFNGLTEXGENDVPROC)(GLenum coord, GLenum pname, const GLdouble *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXGENFPROC)(GLenum coord, GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLTEXGENFVPROC)(GLenum coord, GLenum pname, const GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXGENIPROC)(GLenum coord, GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLTEXGENIVPROC)(GLenum coord, GLenum pname, const GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXIMAGE1DPROC)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, const void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLTEXIMAGE2DPROC)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLTEXIMAGE2DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations);\ntypedef void(GLAD_API_PTR *PFNGLTEXIMAGE3DPROC)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, const void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLTEXIMAGE3DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);\ntypedef void(GLAD_API_PTR *PFNGLTEXPARAMETERIIVPROC)(GLenum target, GLenum pname, const GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXPARAMETERIUIVPROC)(GLenum target, GLenum pname, const GLuint *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXPARAMETERFPROC)(GLenum target, GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLTEXPARAMETERFVPROC)(GLenum target, GLenum pname, const GLfloat *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXPARAMETERIPROC)(GLenum target, GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLTEXPARAMETERIVPROC)(GLenum target, GLenum pname, const GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXSTORAGE1DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);\ntypedef void(GLAD_API_PTR *PFNGLTEXSTORAGE2DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLTEXSTORAGE2DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations);\ntypedef void(GLAD_API_PTR *PFNGLTEXSTORAGE3DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth);\ntypedef void(GLAD_API_PTR *PFNGLTEXSTORAGE3DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);\ntypedef void(GLAD_API_PTR *PFNGLTEXSUBIMAGE1DPROC)(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLTEXSUBIMAGE3DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLTEXTUREBARRIERPROC)(void);\ntypedef void(GLAD_API_PTR *PFNGLTEXTUREBUFFERPROC)(GLuint texture, GLenum internalformat, GLuint buffer);\ntypedef void(GLAD_API_PTR *PFNGLTEXTUREBUFFERRANGEPROC)(GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);\ntypedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERIIVPROC)(GLuint texture, GLenum pname, const GLint *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERIUIVPROC)(GLuint texture, GLenum pname, const GLuint *params);\ntypedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERFPROC)(GLuint texture, GLenum pname, GLfloat param);\ntypedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERFVPROC)(GLuint texture, GLenum pname, const GLfloat *param);\ntypedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERIPROC)(GLuint texture, GLenum pname, GLint param);\ntypedef void(GLAD_API_PTR *PFNGLTEXTUREPARAMETERIVPROC)(GLuint texture, GLenum pname, const GLint *param);\ntypedef void(GLAD_API_PTR *PFNGLTEXTURESTORAGE1DPROC)(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width);\ntypedef void(GLAD_API_PTR *PFNGLTEXTURESTORAGE2DPROC)(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLTEXTURESTORAGE2DMULTISAMPLEPROC)(GLuint texture, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations);\ntypedef void(GLAD_API_PTR *PFNGLTEXTURESTORAGE3DPROC)(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth);\ntypedef void(GLAD_API_PTR *PFNGLTEXTURESTORAGE3DMULTISAMPLEPROC)(GLuint texture, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);\ntypedef void(GLAD_API_PTR *PFNGLTEXTURESUBIMAGE1DPROC)(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLTEXTURESUBIMAGE2DPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLTEXTURESUBIMAGE3DPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void *pixels);\ntypedef void(GLAD_API_PTR *PFNGLTEXTUREVIEWPROC)(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers);\ntypedef void(GLAD_API_PTR *PFNGLTRANSFORMFEEDBACKBUFFERBASEPROC)(GLuint xfb, GLuint index, GLuint buffer);\ntypedef void(GLAD_API_PTR *PFNGLTRANSFORMFEEDBACKBUFFERRANGEPROC)(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);\ntypedef void(GLAD_API_PTR *PFNGLTRANSFORMFEEDBACKVARYINGSPROC)(GLuint program, GLsizei count, const GLchar *const *varyings, GLenum bufferMode);\ntypedef void(GLAD_API_PTR *PFNGLTRANSLATEDPROC)(GLdouble x, GLdouble y, GLdouble z);\ntypedef void(GLAD_API_PTR *PFNGLTRANSLATEFPROC)(GLfloat x, GLfloat y, GLfloat z);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM1DPROC)(GLint location, GLdouble x);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM1DVPROC)(GLint location, GLsizei count, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM1FPROC)(GLint location, GLfloat v0);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM1FVPROC)(GLint location, GLsizei count, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM1IPROC)(GLint location, GLint v0);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM1IVPROC)(GLint location, GLsizei count, const GLint *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM1UIPROC)(GLint location, GLuint v0);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM1UIVPROC)(GLint location, GLsizei count, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM2DPROC)(GLint location, GLdouble x, GLdouble y);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM2DVPROC)(GLint location, GLsizei count, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM2FPROC)(GLint location, GLfloat v0, GLfloat v1);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM2FVPROC)(GLint location, GLsizei count, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM2IPROC)(GLint location, GLint v0, GLint v1);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM2IVPROC)(GLint location, GLsizei count, const GLint *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM2UIPROC)(GLint location, GLuint v0, GLuint v1);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM2UIVPROC)(GLint location, GLsizei count, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM3DPROC)(GLint location, GLdouble x, GLdouble y, GLdouble z);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM3DVPROC)(GLint location, GLsizei count, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM3FPROC)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM3FVPROC)(GLint location, GLsizei count, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM3IPROC)(GLint location, GLint v0, GLint v1, GLint v2);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM3IVPROC)(GLint location, GLsizei count, const GLint *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM3UIPROC)(GLint location, GLuint v0, GLuint v1, GLuint v2);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM3UIVPROC)(GLint location, GLsizei count, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM4DPROC)(GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM4DVPROC)(GLint location, GLsizei count, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM4FPROC)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM4FVPROC)(GLint location, GLsizei count, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM4IPROC)(GLint location, GLint v0, GLint v1, GLint v2, GLint v3);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM4IVPROC)(GLint location, GLsizei count, const GLint *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM4UIPROC)(GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORM4UIVPROC)(GLint location, GLsizei count, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMBLOCKBINDINGPROC)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2X3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2X3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2X4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX2X4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3X2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3X2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3X4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX3X4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4X2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4X2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4X3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMMATRIX4X3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);\ntypedef void(GLAD_API_PTR *PFNGLUNIFORMSUBROUTINESUIVPROC)(GLenum shadertype, GLsizei count, const GLuint *indices);\ntypedef GLboolean(GLAD_API_PTR *PFNGLUNMAPBUFFERPROC)(GLenum target);\ntypedef GLboolean(GLAD_API_PTR *PFNGLUNMAPNAMEDBUFFERPROC)(GLuint buffer);\ntypedef void(GLAD_API_PTR *PFNGLUSEPROGRAMPROC)(GLuint program);\ntypedef void(GLAD_API_PTR *PFNGLUSEPROGRAMSTAGESPROC)(GLuint pipeline, GLbitfield stages, GLuint program);\ntypedef void(GLAD_API_PTR *PFNGLVALIDATEPROGRAMPROC)(GLuint program);\ntypedef void(GLAD_API_PTR *PFNGLVALIDATEPROGRAMPIPELINEPROC)(GLuint pipeline);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX2DPROC)(GLdouble x, GLdouble y);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX2DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX2FPROC)(GLfloat x, GLfloat y);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX2FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX2IPROC)(GLint x, GLint y);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX2IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX2SPROC)(GLshort x, GLshort y);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX2SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX3DPROC)(GLdouble x, GLdouble y, GLdouble z);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX3DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX3FPROC)(GLfloat x, GLfloat y, GLfloat z);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX3FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX3IPROC)(GLint x, GLint y, GLint z);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX3IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX3SPROC)(GLshort x, GLshort y, GLshort z);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX3SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX4DPROC)(GLdouble x, GLdouble y, GLdouble z, GLdouble w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX4DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX4FPROC)(GLfloat x, GLfloat y, GLfloat z, GLfloat w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX4FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX4IPROC)(GLint x, GLint y, GLint z, GLint w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX4IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX4SPROC)(GLshort x, GLshort y, GLshort z, GLshort w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEX4SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXARRAYATTRIBBINDINGPROC)(GLuint vaobj, GLuint attribindex, GLuint bindingindex);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXARRAYATTRIBFORMATPROC)(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXARRAYATTRIBIFORMATPROC)(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXARRAYATTRIBLFORMATPROC)(GLuint vaobj, GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXARRAYBINDINGDIVISORPROC)(GLuint vaobj, GLuint bindingindex, GLuint divisor);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXARRAYELEMENTBUFFERPROC)(GLuint vaobj, GLuint buffer);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXARRAYVERTEXBUFFERPROC)(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXARRAYVERTEXBUFFERSPROC)(GLuint vaobj, GLuint first, GLsizei count, const GLuint *buffers, const GLintptr *offsets, const GLsizei *strides);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1DPROC)(GLuint index, GLdouble x);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1DVPROC)(GLuint index, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1FPROC)(GLuint index, GLfloat x);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1FVPROC)(GLuint index, const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1SPROC)(GLuint index, GLshort x);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB1SVPROC)(GLuint index, const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2DPROC)(GLuint index, GLdouble x, GLdouble y);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2DVPROC)(GLuint index, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2FPROC)(GLuint index, GLfloat x, GLfloat y);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2FVPROC)(GLuint index, const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2SPROC)(GLuint index, GLshort x, GLshort y);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB2SVPROC)(GLuint index, const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3DVPROC)(GLuint index, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3FPROC)(GLuint index, GLfloat x, GLfloat y, GLfloat z);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3FVPROC)(GLuint index, const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3SPROC)(GLuint index, GLshort x, GLshort y, GLshort z);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB3SVPROC)(GLuint index, const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NBVPROC)(GLuint index, const GLbyte *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NIVPROC)(GLuint index, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NSVPROC)(GLuint index, const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NUBPROC)(GLuint index, GLubyte x, GLubyte y, GLubyte z, GLubyte w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NUBVPROC)(GLuint index, const GLubyte *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NUIVPROC)(GLuint index, const GLuint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4NUSVPROC)(GLuint index, const GLushort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4BVPROC)(GLuint index, const GLbyte *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4DVPROC)(GLuint index, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4FPROC)(GLuint index, GLfloat x, GLfloat y, GLfloat z, GLfloat w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4FVPROC)(GLuint index, const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4IVPROC)(GLuint index, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4SPROC)(GLuint index, GLshort x, GLshort y, GLshort z, GLshort w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4SVPROC)(GLuint index, const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4UBVPROC)(GLuint index, const GLubyte *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4UIVPROC)(GLuint index, const GLuint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIB4USVPROC)(GLuint index, const GLushort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBBINDINGPROC)(GLuint attribindex, GLuint bindingindex);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBDIVISORPROC)(GLuint index, GLuint divisor);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI1IPROC)(GLuint index, GLint x);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI1IVPROC)(GLuint index, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI1UIPROC)(GLuint index, GLuint x);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI1UIVPROC)(GLuint index, const GLuint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI2IPROC)(GLuint index, GLint x, GLint y);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI2IVPROC)(GLuint index, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI2UIPROC)(GLuint index, GLuint x, GLuint y);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI2UIVPROC)(GLuint index, const GLuint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI3IPROC)(GLuint index, GLint x, GLint y, GLint z);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI3IVPROC)(GLuint index, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI3UIPROC)(GLuint index, GLuint x, GLuint y, GLuint z);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI3UIVPROC)(GLuint index, const GLuint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4BVPROC)(GLuint index, const GLbyte *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4IPROC)(GLuint index, GLint x, GLint y, GLint z, GLint w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4IVPROC)(GLuint index, const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4SVPROC)(GLuint index, const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4UBVPROC)(GLuint index, const GLubyte *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4UIPROC)(GLuint index, GLuint x, GLuint y, GLuint z, GLuint w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4UIVPROC)(GLuint index, const GLuint *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBI4USVPROC)(GLuint index, const GLushort *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBIFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBIPOINTERPROC)(GLuint index, GLint size, GLenum type, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL1DPROC)(GLuint index, GLdouble x);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL1DVPROC)(GLuint index, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL2DPROC)(GLuint index, GLdouble x, GLdouble y);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL2DVPROC)(GLuint index, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL3DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL3DVPROC)(GLuint index, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL4DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBL4DVPROC)(GLuint index, const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBLFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBLPOINTERPROC)(GLuint index, GLint size, GLenum type, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP1UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP1UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP2UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP2UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP3UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP3UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP4UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBP4UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXATTRIBPOINTERPROC)(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXBINDINGDIVISORPROC)(GLuint bindingindex, GLuint divisor);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXP2UIPROC)(GLenum type, GLuint value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXP2UIVPROC)(GLenum type, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXP3UIPROC)(GLenum type, GLuint value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXP3UIVPROC)(GLenum type, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXP4UIPROC)(GLenum type, GLuint value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXP4UIVPROC)(GLenum type, const GLuint *value);\ntypedef void(GLAD_API_PTR *PFNGLVERTEXPOINTERPROC)(GLint size, GLenum type, GLsizei stride, const void *pointer);\ntypedef void(GLAD_API_PTR *PFNGLVIEWPORTPROC)(GLint x, GLint y, GLsizei width, GLsizei height);\ntypedef void(GLAD_API_PTR *PFNGLVIEWPORTARRAYVPROC)(GLuint first, GLsizei count, const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLVIEWPORTINDEXEDFPROC)(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);\ntypedef void(GLAD_API_PTR *PFNGLVIEWPORTINDEXEDFVPROC)(GLuint index, const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLWAITSYNCPROC)(GLsync sync, GLbitfield flags, GLuint64 timeout);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS2DPROC)(GLdouble x, GLdouble y);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS2DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS2FPROC)(GLfloat x, GLfloat y);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS2FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS2IPROC)(GLint x, GLint y);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS2IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS2SPROC)(GLshort x, GLshort y);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS2SVPROC)(const GLshort *v);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS3DPROC)(GLdouble x, GLdouble y, GLdouble z);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS3DVPROC)(const GLdouble *v);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS3FPROC)(GLfloat x, GLfloat y, GLfloat z);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS3FVPROC)(const GLfloat *v);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS3IPROC)(GLint x, GLint y, GLint z);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS3IVPROC)(const GLint *v);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS3SPROC)(GLshort x, GLshort y, GLshort z);\ntypedef void(GLAD_API_PTR *PFNGLWINDOWPOS3SVPROC)(const GLshort *v);\n\ntypedef struct GladGLContext {\n  void *userptr;\n\n  int VERSION_1_0;\n  int VERSION_1_1;\n  int VERSION_1_2;\n  int VERSION_1_3;\n  int VERSION_1_4;\n  int VERSION_1_5;\n  int VERSION_2_0;\n  int VERSION_2_1;\n  int VERSION_3_0;\n  int VERSION_3_1;\n  int VERSION_3_2;\n  int VERSION_3_3;\n  int VERSION_4_0;\n  int VERSION_4_1;\n  int VERSION_4_2;\n  int VERSION_4_3;\n  int VERSION_4_4;\n  int VERSION_4_5;\n  int VERSION_4_6;\n\n  PFNGLEGLIMAGETARGETTEXTURE2DOESPROC EGLImageTargetTexture2DOES;\n  PFNGLACCUMPROC Accum;\n  PFNGLACTIVESHADERPROGRAMPROC ActiveShaderProgram;\n  PFNGLACTIVETEXTUREPROC ActiveTexture;\n  PFNGLALPHAFUNCPROC AlphaFunc;\n  PFNGLARETEXTURESRESIDENTPROC AreTexturesResident;\n  PFNGLARRAYELEMENTPROC ArrayElement;\n  PFNGLATTACHSHADERPROC AttachShader;\n  PFNGLBEGINPROC Begin;\n  PFNGLBEGINCONDITIONALRENDERPROC BeginConditionalRender;\n  PFNGLBEGINQUERYPROC BeginQuery;\n  PFNGLBEGINQUERYINDEXEDPROC BeginQueryIndexed;\n  PFNGLBEGINTRANSFORMFEEDBACKPROC BeginTransformFeedback;\n  PFNGLBINDATTRIBLOCATIONPROC BindAttribLocation;\n  PFNGLBINDBUFFERPROC BindBuffer;\n  PFNGLBINDBUFFERBASEPROC BindBufferBase;\n  PFNGLBINDBUFFERRANGEPROC BindBufferRange;\n  PFNGLBINDBUFFERSBASEPROC BindBuffersBase;\n  PFNGLBINDBUFFERSRANGEPROC BindBuffersRange;\n  PFNGLBINDFRAGDATALOCATIONPROC BindFragDataLocation;\n  PFNGLBINDFRAGDATALOCATIONINDEXEDPROC BindFragDataLocationIndexed;\n  PFNGLBINDFRAMEBUFFERPROC BindFramebuffer;\n  PFNGLBINDIMAGETEXTUREPROC BindImageTexture;\n  PFNGLBINDIMAGETEXTURESPROC BindImageTextures;\n  PFNGLBINDPROGRAMPIPELINEPROC BindProgramPipeline;\n  PFNGLBINDRENDERBUFFERPROC BindRenderbuffer;\n  PFNGLBINDSAMPLERPROC BindSampler;\n  PFNGLBINDSAMPLERSPROC BindSamplers;\n  PFNGLBINDTEXTUREPROC BindTexture;\n  PFNGLBINDTEXTUREUNITPROC BindTextureUnit;\n  PFNGLBINDTEXTURESPROC BindTextures;\n  PFNGLBINDTRANSFORMFEEDBACKPROC BindTransformFeedback;\n  PFNGLBINDVERTEXARRAYPROC BindVertexArray;\n  PFNGLBINDVERTEXBUFFERPROC BindVertexBuffer;\n  PFNGLBINDVERTEXBUFFERSPROC BindVertexBuffers;\n  PFNGLBITMAPPROC Bitmap;\n  PFNGLBLENDCOLORPROC BlendColor;\n  PFNGLBLENDEQUATIONPROC BlendEquation;\n  PFNGLBLENDEQUATIONSEPARATEPROC BlendEquationSeparate;\n  PFNGLBLENDEQUATIONSEPARATEIPROC BlendEquationSeparatei;\n  PFNGLBLENDEQUATIONIPROC BlendEquationi;\n  PFNGLBLENDFUNCPROC BlendFunc;\n  PFNGLBLENDFUNCSEPARATEPROC BlendFuncSeparate;\n  PFNGLBLENDFUNCSEPARATEIPROC BlendFuncSeparatei;\n  PFNGLBLENDFUNCIPROC BlendFunci;\n  PFNGLBLITFRAMEBUFFERPROC BlitFramebuffer;\n  PFNGLBLITNAMEDFRAMEBUFFERPROC BlitNamedFramebuffer;\n  PFNGLBUFFERDATAPROC BufferData;\n  PFNGLBUFFERSTORAGEPROC BufferStorage;\n  PFNGLBUFFERSUBDATAPROC BufferSubData;\n  PFNGLCALLLISTPROC CallList;\n  PFNGLCALLLISTSPROC CallLists;\n  PFNGLCHECKFRAMEBUFFERSTATUSPROC CheckFramebufferStatus;\n  PFNGLCHECKNAMEDFRAMEBUFFERSTATUSPROC CheckNamedFramebufferStatus;\n  PFNGLCLAMPCOLORPROC ClampColor;\n  PFNGLCLEARPROC Clear;\n  PFNGLCLEARACCUMPROC ClearAccum;\n  PFNGLCLEARBUFFERDATAPROC ClearBufferData;\n  PFNGLCLEARBUFFERSUBDATAPROC ClearBufferSubData;\n  PFNGLCLEARBUFFERFIPROC ClearBufferfi;\n  PFNGLCLEARBUFFERFVPROC ClearBufferfv;\n  PFNGLCLEARBUFFERIVPROC ClearBufferiv;\n  PFNGLCLEARBUFFERUIVPROC ClearBufferuiv;\n  PFNGLCLEARCOLORPROC ClearColor;\n  PFNGLCLEARDEPTHPROC ClearDepth;\n  PFNGLCLEARDEPTHFPROC ClearDepthf;\n  PFNGLCLEARINDEXPROC ClearIndex;\n  PFNGLCLEARNAMEDBUFFERDATAPROC ClearNamedBufferData;\n  PFNGLCLEARNAMEDBUFFERSUBDATAPROC ClearNamedBufferSubData;\n  PFNGLCLEARNAMEDFRAMEBUFFERFIPROC ClearNamedFramebufferfi;\n  PFNGLCLEARNAMEDFRAMEBUFFERFVPROC ClearNamedFramebufferfv;\n  PFNGLCLEARNAMEDFRAMEBUFFERIVPROC ClearNamedFramebufferiv;\n  PFNGLCLEARNAMEDFRAMEBUFFERUIVPROC ClearNamedFramebufferuiv;\n  PFNGLCLEARSTENCILPROC ClearStencil;\n  PFNGLCLEARTEXIMAGEPROC ClearTexImage;\n  PFNGLCLEARTEXSUBIMAGEPROC ClearTexSubImage;\n  PFNGLCLIENTACTIVETEXTUREPROC ClientActiveTexture;\n  PFNGLCLIENTWAITSYNCPROC ClientWaitSync;\n  PFNGLCLIPCONTROLPROC ClipControl;\n  PFNGLCLIPPLANEPROC ClipPlane;\n  PFNGLCOLOR3BPROC Color3b;\n  PFNGLCOLOR3BVPROC Color3bv;\n  PFNGLCOLOR3DPROC Color3d;\n  PFNGLCOLOR3DVPROC Color3dv;\n  PFNGLCOLOR3FPROC Color3f;\n  PFNGLCOLOR3FVPROC Color3fv;\n  PFNGLCOLOR3IPROC Color3i;\n  PFNGLCOLOR3IVPROC Color3iv;\n  PFNGLCOLOR3SPROC Color3s;\n  PFNGLCOLOR3SVPROC Color3sv;\n  PFNGLCOLOR3UBPROC Color3ub;\n  PFNGLCOLOR3UBVPROC Color3ubv;\n  PFNGLCOLOR3UIPROC Color3ui;\n  PFNGLCOLOR3UIVPROC Color3uiv;\n  PFNGLCOLOR3USPROC Color3us;\n  PFNGLCOLOR3USVPROC Color3usv;\n  PFNGLCOLOR4BPROC Color4b;\n  PFNGLCOLOR4BVPROC Color4bv;\n  PFNGLCOLOR4DPROC Color4d;\n  PFNGLCOLOR4DVPROC Color4dv;\n  PFNGLCOLOR4FPROC Color4f;\n  PFNGLCOLOR4FVPROC Color4fv;\n  PFNGLCOLOR4IPROC Color4i;\n  PFNGLCOLOR4IVPROC Color4iv;\n  PFNGLCOLOR4SPROC Color4s;\n  PFNGLCOLOR4SVPROC Color4sv;\n  PFNGLCOLOR4UBPROC Color4ub;\n  PFNGLCOLOR4UBVPROC Color4ubv;\n  PFNGLCOLOR4UIPROC Color4ui;\n  PFNGLCOLOR4UIVPROC Color4uiv;\n  PFNGLCOLOR4USPROC Color4us;\n  PFNGLCOLOR4USVPROC Color4usv;\n  PFNGLCOLORMASKPROC ColorMask;\n  PFNGLCOLORMASKIPROC ColorMaski;\n  PFNGLCOLORMATERIALPROC ColorMaterial;\n  PFNGLCOLORP3UIPROC ColorP3ui;\n  PFNGLCOLORP3UIVPROC ColorP3uiv;\n  PFNGLCOLORP4UIPROC ColorP4ui;\n  PFNGLCOLORP4UIVPROC ColorP4uiv;\n  PFNGLCOLORPOINTERPROC ColorPointer;\n  PFNGLCOMPILESHADERPROC CompileShader;\n  PFNGLCOMPRESSEDTEXIMAGE1DPROC CompressedTexImage1D;\n  PFNGLCOMPRESSEDTEXIMAGE2DPROC CompressedTexImage2D;\n  PFNGLCOMPRESSEDTEXIMAGE3DPROC CompressedTexImage3D;\n  PFNGLCOMPRESSEDTEXSUBIMAGE1DPROC CompressedTexSubImage1D;\n  PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC CompressedTexSubImage2D;\n  PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC CompressedTexSubImage3D;\n  PFNGLCOMPRESSEDTEXTURESUBIMAGE1DPROC CompressedTextureSubImage1D;\n  PFNGLCOMPRESSEDTEXTURESUBIMAGE2DPROC CompressedTextureSubImage2D;\n  PFNGLCOMPRESSEDTEXTURESUBIMAGE3DPROC CompressedTextureSubImage3D;\n  PFNGLCOPYBUFFERSUBDATAPROC CopyBufferSubData;\n  PFNGLCOPYIMAGESUBDATAPROC CopyImageSubData;\n  PFNGLCOPYNAMEDBUFFERSUBDATAPROC CopyNamedBufferSubData;\n  PFNGLCOPYPIXELSPROC CopyPixels;\n  PFNGLCOPYTEXIMAGE1DPROC CopyTexImage1D;\n  PFNGLCOPYTEXIMAGE2DPROC CopyTexImage2D;\n  PFNGLCOPYTEXSUBIMAGE1DPROC CopyTexSubImage1D;\n  PFNGLCOPYTEXSUBIMAGE2DPROC CopyTexSubImage2D;\n  PFNGLCOPYTEXSUBIMAGE3DPROC CopyTexSubImage3D;\n  PFNGLCOPYTEXTURESUBIMAGE1DPROC CopyTextureSubImage1D;\n  PFNGLCOPYTEXTURESUBIMAGE2DPROC CopyTextureSubImage2D;\n  PFNGLCOPYTEXTURESUBIMAGE3DPROC CopyTextureSubImage3D;\n  PFNGLCREATEBUFFERSPROC CreateBuffers;\n  PFNGLCREATEFRAMEBUFFERSPROC CreateFramebuffers;\n  PFNGLCREATEPROGRAMPROC CreateProgram;\n  PFNGLCREATEPROGRAMPIPELINESPROC CreateProgramPipelines;\n  PFNGLCREATEQUERIESPROC CreateQueries;\n  PFNGLCREATERENDERBUFFERSPROC CreateRenderbuffers;\n  PFNGLCREATESAMPLERSPROC CreateSamplers;\n  PFNGLCREATESHADERPROC CreateShader;\n  PFNGLCREATESHADERPROGRAMVPROC CreateShaderProgramv;\n  PFNGLCREATETEXTURESPROC CreateTextures;\n  PFNGLCREATETRANSFORMFEEDBACKSPROC CreateTransformFeedbacks;\n  PFNGLCREATEVERTEXARRAYSPROC CreateVertexArrays;\n  PFNGLCULLFACEPROC CullFace;\n  PFNGLDEBUGMESSAGECALLBACKPROC DebugMessageCallback;\n  PFNGLDEBUGMESSAGECONTROLPROC DebugMessageControl;\n  PFNGLDEBUGMESSAGEINSERTPROC DebugMessageInsert;\n  PFNGLDELETEBUFFERSPROC DeleteBuffers;\n  PFNGLDELETEFRAMEBUFFERSPROC DeleteFramebuffers;\n  PFNGLDELETELISTSPROC DeleteLists;\n  PFNGLDELETEPROGRAMPROC DeleteProgram;\n  PFNGLDELETEPROGRAMPIPELINESPROC DeleteProgramPipelines;\n  PFNGLDELETEQUERIESPROC DeleteQueries;\n  PFNGLDELETERENDERBUFFERSPROC DeleteRenderbuffers;\n  PFNGLDELETESAMPLERSPROC DeleteSamplers;\n  PFNGLDELETESHADERPROC DeleteShader;\n  PFNGLDELETESYNCPROC DeleteSync;\n  PFNGLDELETETEXTURESPROC DeleteTextures;\n  PFNGLDELETETRANSFORMFEEDBACKSPROC DeleteTransformFeedbacks;\n  PFNGLDELETEVERTEXARRAYSPROC DeleteVertexArrays;\n  PFNGLDEPTHFUNCPROC DepthFunc;\n  PFNGLDEPTHMASKPROC DepthMask;\n  PFNGLDEPTHRANGEPROC DepthRange;\n  PFNGLDEPTHRANGEARRAYVPROC DepthRangeArrayv;\n  PFNGLDEPTHRANGEINDEXEDPROC DepthRangeIndexed;\n  PFNGLDEPTHRANGEFPROC DepthRangef;\n  PFNGLDETACHSHADERPROC DetachShader;\n  PFNGLDISABLEPROC Disable;\n  PFNGLDISABLECLIENTSTATEPROC DisableClientState;\n  PFNGLDISABLEVERTEXARRAYATTRIBPROC DisableVertexArrayAttrib;\n  PFNGLDISABLEVERTEXATTRIBARRAYPROC DisableVertexAttribArray;\n  PFNGLDISABLEIPROC Disablei;\n  PFNGLDISPATCHCOMPUTEPROC DispatchCompute;\n  PFNGLDISPATCHCOMPUTEINDIRECTPROC DispatchComputeIndirect;\n  PFNGLDRAWARRAYSPROC DrawArrays;\n  PFNGLDRAWARRAYSINDIRECTPROC DrawArraysIndirect;\n  PFNGLDRAWARRAYSINSTANCEDPROC DrawArraysInstanced;\n  PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC DrawArraysInstancedBaseInstance;\n  PFNGLDRAWBUFFERPROC DrawBuffer;\n  PFNGLDRAWBUFFERSPROC DrawBuffers;\n  PFNGLDRAWELEMENTSPROC DrawElements;\n  PFNGLDRAWELEMENTSBASEVERTEXPROC DrawElementsBaseVertex;\n  PFNGLDRAWELEMENTSINDIRECTPROC DrawElementsIndirect;\n  PFNGLDRAWELEMENTSINSTANCEDPROC DrawElementsInstanced;\n  PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC DrawElementsInstancedBaseInstance;\n  PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXPROC DrawElementsInstancedBaseVertex;\n  PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC DrawElementsInstancedBaseVertexBaseInstance;\n  PFNGLDRAWPIXELSPROC DrawPixels;\n  PFNGLDRAWRANGEELEMENTSPROC DrawRangeElements;\n  PFNGLDRAWRANGEELEMENTSBASEVERTEXPROC DrawRangeElementsBaseVertex;\n  PFNGLDRAWTRANSFORMFEEDBACKPROC DrawTransformFeedback;\n  PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC DrawTransformFeedbackInstanced;\n  PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC DrawTransformFeedbackStream;\n  PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC DrawTransformFeedbackStreamInstanced;\n  PFNGLEDGEFLAGPROC EdgeFlag;\n  PFNGLEDGEFLAGPOINTERPROC EdgeFlagPointer;\n  PFNGLEDGEFLAGVPROC EdgeFlagv;\n  PFNGLENABLEPROC Enable;\n  PFNGLENABLECLIENTSTATEPROC EnableClientState;\n  PFNGLENABLEVERTEXARRAYATTRIBPROC EnableVertexArrayAttrib;\n  PFNGLENABLEVERTEXATTRIBARRAYPROC EnableVertexAttribArray;\n  PFNGLENABLEIPROC Enablei;\n  PFNGLENDPROC End;\n  PFNGLENDCONDITIONALRENDERPROC EndConditionalRender;\n  PFNGLENDLISTPROC EndList;\n  PFNGLENDQUERYPROC EndQuery;\n  PFNGLENDQUERYINDEXEDPROC EndQueryIndexed;\n  PFNGLENDTRANSFORMFEEDBACKPROC EndTransformFeedback;\n  PFNGLEVALCOORD1DPROC EvalCoord1d;\n  PFNGLEVALCOORD1DVPROC EvalCoord1dv;\n  PFNGLEVALCOORD1FPROC EvalCoord1f;\n  PFNGLEVALCOORD1FVPROC EvalCoord1fv;\n  PFNGLEVALCOORD2DPROC EvalCoord2d;\n  PFNGLEVALCOORD2DVPROC EvalCoord2dv;\n  PFNGLEVALCOORD2FPROC EvalCoord2f;\n  PFNGLEVALCOORD2FVPROC EvalCoord2fv;\n  PFNGLEVALMESH1PROC EvalMesh1;\n  PFNGLEVALMESH2PROC EvalMesh2;\n  PFNGLEVALPOINT1PROC EvalPoint1;\n  PFNGLEVALPOINT2PROC EvalPoint2;\n  PFNGLFEEDBACKBUFFERPROC FeedbackBuffer;\n  PFNGLFENCESYNCPROC FenceSync;\n  PFNGLFINISHPROC Finish;\n  PFNGLFLUSHPROC Flush;\n  PFNGLFLUSHMAPPEDBUFFERRANGEPROC FlushMappedBufferRange;\n  PFNGLFLUSHMAPPEDNAMEDBUFFERRANGEPROC FlushMappedNamedBufferRange;\n  PFNGLFOGCOORDPOINTERPROC FogCoordPointer;\n  PFNGLFOGCOORDDPROC FogCoordd;\n  PFNGLFOGCOORDDVPROC FogCoorddv;\n  PFNGLFOGCOORDFPROC FogCoordf;\n  PFNGLFOGCOORDFVPROC FogCoordfv;\n  PFNGLFOGFPROC Fogf;\n  PFNGLFOGFVPROC Fogfv;\n  PFNGLFOGIPROC Fogi;\n  PFNGLFOGIVPROC Fogiv;\n  PFNGLFRAMEBUFFERPARAMETERIPROC FramebufferParameteri;\n  PFNGLFRAMEBUFFERRENDERBUFFERPROC FramebufferRenderbuffer;\n  PFNGLFRAMEBUFFERTEXTUREPROC FramebufferTexture;\n  PFNGLFRAMEBUFFERTEXTURE1DPROC FramebufferTexture1D;\n  PFNGLFRAMEBUFFERTEXTURE2DPROC FramebufferTexture2D;\n  PFNGLFRAMEBUFFERTEXTURE3DPROC FramebufferTexture3D;\n  PFNGLFRAMEBUFFERTEXTURELAYERPROC FramebufferTextureLayer;\n  PFNGLFRONTFACEPROC FrontFace;\n  PFNGLFRUSTUMPROC Frustum;\n  PFNGLGENBUFFERSPROC GenBuffers;\n  PFNGLGENFRAMEBUFFERSPROC GenFramebuffers;\n  PFNGLGENLISTSPROC GenLists;\n  PFNGLGENPROGRAMPIPELINESPROC GenProgramPipelines;\n  PFNGLGENQUERIESPROC GenQueries;\n  PFNGLGENRENDERBUFFERSPROC GenRenderbuffers;\n  PFNGLGENSAMPLERSPROC GenSamplers;\n  PFNGLGENTEXTURESPROC GenTextures;\n  PFNGLGENTRANSFORMFEEDBACKSPROC GenTransformFeedbacks;\n  PFNGLGENVERTEXARRAYSPROC GenVertexArrays;\n  PFNGLGENERATEMIPMAPPROC GenerateMipmap;\n  PFNGLGENERATETEXTUREMIPMAPPROC GenerateTextureMipmap;\n  PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC GetActiveAtomicCounterBufferiv;\n  PFNGLGETACTIVEATTRIBPROC GetActiveAttrib;\n  PFNGLGETACTIVESUBROUTINENAMEPROC GetActiveSubroutineName;\n  PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC GetActiveSubroutineUniformName;\n  PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC GetActiveSubroutineUniformiv;\n  PFNGLGETACTIVEUNIFORMPROC GetActiveUniform;\n  PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC GetActiveUniformBlockName;\n  PFNGLGETACTIVEUNIFORMBLOCKIVPROC GetActiveUniformBlockiv;\n  PFNGLGETACTIVEUNIFORMNAMEPROC GetActiveUniformName;\n  PFNGLGETACTIVEUNIFORMSIVPROC GetActiveUniformsiv;\n  PFNGLGETATTACHEDSHADERSPROC GetAttachedShaders;\n  PFNGLGETATTRIBLOCATIONPROC GetAttribLocation;\n  PFNGLGETBOOLEANI_VPROC GetBooleani_v;\n  PFNGLGETBOOLEANVPROC GetBooleanv;\n  PFNGLGETBUFFERPARAMETERI64VPROC GetBufferParameteri64v;\n  PFNGLGETBUFFERPARAMETERIVPROC GetBufferParameteriv;\n  PFNGLGETBUFFERPOINTERVPROC GetBufferPointerv;\n  PFNGLGETBUFFERSUBDATAPROC GetBufferSubData;\n  PFNGLGETCLIPPLANEPROC GetClipPlane;\n  PFNGLGETCOMPRESSEDTEXIMAGEPROC GetCompressedTexImage;\n  PFNGLGETCOMPRESSEDTEXTUREIMAGEPROC GetCompressedTextureImage;\n  PFNGLGETCOMPRESSEDTEXTURESUBIMAGEPROC GetCompressedTextureSubImage;\n  PFNGLGETDEBUGMESSAGELOGPROC GetDebugMessageLog;\n  PFNGLGETDOUBLEI_VPROC GetDoublei_v;\n  PFNGLGETDOUBLEVPROC GetDoublev;\n  PFNGLGETERRORPROC GetError;\n  PFNGLGETFLOATI_VPROC GetFloati_v;\n  PFNGLGETFLOATVPROC GetFloatv;\n  PFNGLGETFRAGDATAINDEXPROC GetFragDataIndex;\n  PFNGLGETFRAGDATALOCATIONPROC GetFragDataLocation;\n  PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC GetFramebufferAttachmentParameteriv;\n  PFNGLGETFRAMEBUFFERPARAMETERIVPROC GetFramebufferParameteriv;\n  PFNGLGETGRAPHICSRESETSTATUSPROC GetGraphicsResetStatus;\n  PFNGLGETINTEGER64I_VPROC GetInteger64i_v;\n  PFNGLGETINTEGER64VPROC GetInteger64v;\n  PFNGLGETINTEGERI_VPROC GetIntegeri_v;\n  PFNGLGETINTEGERVPROC GetIntegerv;\n  PFNGLGETINTERNALFORMATI64VPROC GetInternalformati64v;\n  PFNGLGETINTERNALFORMATIVPROC GetInternalformativ;\n  PFNGLGETLIGHTFVPROC GetLightfv;\n  PFNGLGETLIGHTIVPROC GetLightiv;\n  PFNGLGETMAPDVPROC GetMapdv;\n  PFNGLGETMAPFVPROC GetMapfv;\n  PFNGLGETMAPIVPROC GetMapiv;\n  PFNGLGETMATERIALFVPROC GetMaterialfv;\n  PFNGLGETMATERIALIVPROC GetMaterialiv;\n  PFNGLGETMULTISAMPLEFVPROC GetMultisamplefv;\n  PFNGLGETNAMEDBUFFERPARAMETERI64VPROC GetNamedBufferParameteri64v;\n  PFNGLGETNAMEDBUFFERPARAMETERIVPROC GetNamedBufferParameteriv;\n  PFNGLGETNAMEDBUFFERPOINTERVPROC GetNamedBufferPointerv;\n  PFNGLGETNAMEDBUFFERSUBDATAPROC GetNamedBufferSubData;\n  PFNGLGETNAMEDFRAMEBUFFERATTACHMENTPARAMETERIVPROC GetNamedFramebufferAttachmentParameteriv;\n  PFNGLGETNAMEDFRAMEBUFFERPARAMETERIVPROC GetNamedFramebufferParameteriv;\n  PFNGLGETNAMEDRENDERBUFFERPARAMETERIVPROC GetNamedRenderbufferParameteriv;\n  PFNGLGETOBJECTLABELPROC GetObjectLabel;\n  PFNGLGETOBJECTPTRLABELPROC GetObjectPtrLabel;\n  PFNGLGETPIXELMAPFVPROC GetPixelMapfv;\n  PFNGLGETPIXELMAPUIVPROC GetPixelMapuiv;\n  PFNGLGETPIXELMAPUSVPROC GetPixelMapusv;\n  PFNGLGETPOINTERVPROC GetPointerv;\n  PFNGLGETPOLYGONSTIPPLEPROC GetPolygonStipple;\n  PFNGLGETPROGRAMBINARYPROC GetProgramBinary;\n  PFNGLGETPROGRAMINFOLOGPROC GetProgramInfoLog;\n  PFNGLGETPROGRAMINTERFACEIVPROC GetProgramInterfaceiv;\n  PFNGLGETPROGRAMPIPELINEINFOLOGPROC GetProgramPipelineInfoLog;\n  PFNGLGETPROGRAMPIPELINEIVPROC GetProgramPipelineiv;\n  PFNGLGETPROGRAMRESOURCEINDEXPROC GetProgramResourceIndex;\n  PFNGLGETPROGRAMRESOURCELOCATIONPROC GetProgramResourceLocation;\n  PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC GetProgramResourceLocationIndex;\n  PFNGLGETPROGRAMRESOURCENAMEPROC GetProgramResourceName;\n  PFNGLGETPROGRAMRESOURCEIVPROC GetProgramResourceiv;\n  PFNGLGETPROGRAMSTAGEIVPROC GetProgramStageiv;\n  PFNGLGETPROGRAMIVPROC GetProgramiv;\n  PFNGLGETQUERYBUFFEROBJECTI64VPROC GetQueryBufferObjecti64v;\n  PFNGLGETQUERYBUFFEROBJECTIVPROC GetQueryBufferObjectiv;\n  PFNGLGETQUERYBUFFEROBJECTUI64VPROC GetQueryBufferObjectui64v;\n  PFNGLGETQUERYBUFFEROBJECTUIVPROC GetQueryBufferObjectuiv;\n  PFNGLGETQUERYINDEXEDIVPROC GetQueryIndexediv;\n  PFNGLGETQUERYOBJECTI64VPROC GetQueryObjecti64v;\n  PFNGLGETQUERYOBJECTIVPROC GetQueryObjectiv;\n  PFNGLGETQUERYOBJECTUI64VPROC GetQueryObjectui64v;\n  PFNGLGETQUERYOBJECTUIVPROC GetQueryObjectuiv;\n  PFNGLGETQUERYIVPROC GetQueryiv;\n  PFNGLGETRENDERBUFFERPARAMETERIVPROC GetRenderbufferParameteriv;\n  PFNGLGETSAMPLERPARAMETERIIVPROC GetSamplerParameterIiv;\n  PFNGLGETSAMPLERPARAMETERIUIVPROC GetSamplerParameterIuiv;\n  PFNGLGETSAMPLERPARAMETERFVPROC GetSamplerParameterfv;\n  PFNGLGETSAMPLERPARAMETERIVPROC GetSamplerParameteriv;\n  PFNGLGETSHADERINFOLOGPROC GetShaderInfoLog;\n  PFNGLGETSHADERPRECISIONFORMATPROC GetShaderPrecisionFormat;\n  PFNGLGETSHADERSOURCEPROC GetShaderSource;\n  PFNGLGETSHADERIVPROC GetShaderiv;\n  PFNGLGETSTRINGPROC GetString;\n  PFNGLGETSTRINGIPROC GetStringi;\n  PFNGLGETSUBROUTINEINDEXPROC GetSubroutineIndex;\n  PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC GetSubroutineUniformLocation;\n  PFNGLGETSYNCIVPROC GetSynciv;\n  PFNGLGETTEXENVFVPROC GetTexEnvfv;\n  PFNGLGETTEXENVIVPROC GetTexEnviv;\n  PFNGLGETTEXGENDVPROC GetTexGendv;\n  PFNGLGETTEXGENFVPROC GetTexGenfv;\n  PFNGLGETTEXGENIVPROC GetTexGeniv;\n  PFNGLGETTEXIMAGEPROC GetTexImage;\n  PFNGLGETTEXLEVELPARAMETERFVPROC GetTexLevelParameterfv;\n  PFNGLGETTEXLEVELPARAMETERIVPROC GetTexLevelParameteriv;\n  PFNGLGETTEXPARAMETERIIVPROC GetTexParameterIiv;\n  PFNGLGETTEXPARAMETERIUIVPROC GetTexParameterIuiv;\n  PFNGLGETTEXPARAMETERFVPROC GetTexParameterfv;\n  PFNGLGETTEXPARAMETERIVPROC GetTexParameteriv;\n  PFNGLGETTEXTUREIMAGEPROC GetTextureImage;\n  PFNGLGETTEXTURELEVELPARAMETERFVPROC GetTextureLevelParameterfv;\n  PFNGLGETTEXTURELEVELPARAMETERIVPROC GetTextureLevelParameteriv;\n  PFNGLGETTEXTUREPARAMETERIIVPROC GetTextureParameterIiv;\n  PFNGLGETTEXTUREPARAMETERIUIVPROC GetTextureParameterIuiv;\n  PFNGLGETTEXTUREPARAMETERFVPROC GetTextureParameterfv;\n  PFNGLGETTEXTUREPARAMETERIVPROC GetTextureParameteriv;\n  PFNGLGETTEXTURESUBIMAGEPROC GetTextureSubImage;\n  PFNGLGETTRANSFORMFEEDBACKVARYINGPROC GetTransformFeedbackVarying;\n  PFNGLGETTRANSFORMFEEDBACKI64_VPROC GetTransformFeedbacki64_v;\n  PFNGLGETTRANSFORMFEEDBACKI_VPROC GetTransformFeedbacki_v;\n  PFNGLGETTRANSFORMFEEDBACKIVPROC GetTransformFeedbackiv;\n  PFNGLGETUNIFORMBLOCKINDEXPROC GetUniformBlockIndex;\n  PFNGLGETUNIFORMINDICESPROC GetUniformIndices;\n  PFNGLGETUNIFORMLOCATIONPROC GetUniformLocation;\n  PFNGLGETUNIFORMSUBROUTINEUIVPROC GetUniformSubroutineuiv;\n  PFNGLGETUNIFORMDVPROC GetUniformdv;\n  PFNGLGETUNIFORMFVPROC GetUniformfv;\n  PFNGLGETUNIFORMIVPROC GetUniformiv;\n  PFNGLGETUNIFORMUIVPROC GetUniformuiv;\n  PFNGLGETVERTEXARRAYINDEXED64IVPROC GetVertexArrayIndexed64iv;\n  PFNGLGETVERTEXARRAYINDEXEDIVPROC GetVertexArrayIndexediv;\n  PFNGLGETVERTEXARRAYIVPROC GetVertexArrayiv;\n  PFNGLGETVERTEXATTRIBIIVPROC GetVertexAttribIiv;\n  PFNGLGETVERTEXATTRIBIUIVPROC GetVertexAttribIuiv;\n  PFNGLGETVERTEXATTRIBLDVPROC GetVertexAttribLdv;\n  PFNGLGETVERTEXATTRIBPOINTERVPROC GetVertexAttribPointerv;\n  PFNGLGETVERTEXATTRIBDVPROC GetVertexAttribdv;\n  PFNGLGETVERTEXATTRIBFVPROC GetVertexAttribfv;\n  PFNGLGETVERTEXATTRIBIVPROC GetVertexAttribiv;\n  PFNGLGETNCOLORTABLEPROC GetnColorTable;\n  PFNGLGETNCOMPRESSEDTEXIMAGEPROC GetnCompressedTexImage;\n  PFNGLGETNCONVOLUTIONFILTERPROC GetnConvolutionFilter;\n  PFNGLGETNHISTOGRAMPROC GetnHistogram;\n  PFNGLGETNMAPDVPROC GetnMapdv;\n  PFNGLGETNMAPFVPROC GetnMapfv;\n  PFNGLGETNMAPIVPROC GetnMapiv;\n  PFNGLGETNMINMAXPROC GetnMinmax;\n  PFNGLGETNPIXELMAPFVPROC GetnPixelMapfv;\n  PFNGLGETNPIXELMAPUIVPROC GetnPixelMapuiv;\n  PFNGLGETNPIXELMAPUSVPROC GetnPixelMapusv;\n  PFNGLGETNPOLYGONSTIPPLEPROC GetnPolygonStipple;\n  PFNGLGETNSEPARABLEFILTERPROC GetnSeparableFilter;\n  PFNGLGETNTEXIMAGEPROC GetnTexImage;\n  PFNGLGETNUNIFORMDVPROC GetnUniformdv;\n  PFNGLGETNUNIFORMFVPROC GetnUniformfv;\n  PFNGLGETNUNIFORMIVPROC GetnUniformiv;\n  PFNGLGETNUNIFORMUIVPROC GetnUniformuiv;\n  PFNGLHINTPROC Hint;\n  PFNGLINDEXMASKPROC IndexMask;\n  PFNGLINDEXPOINTERPROC IndexPointer;\n  PFNGLINDEXDPROC Indexd;\n  PFNGLINDEXDVPROC Indexdv;\n  PFNGLINDEXFPROC Indexf;\n  PFNGLINDEXFVPROC Indexfv;\n  PFNGLINDEXIPROC Indexi;\n  PFNGLINDEXIVPROC Indexiv;\n  PFNGLINDEXSPROC Indexs;\n  PFNGLINDEXSVPROC Indexsv;\n  PFNGLINDEXUBPROC Indexub;\n  PFNGLINDEXUBVPROC Indexubv;\n  PFNGLINITNAMESPROC InitNames;\n  PFNGLINTERLEAVEDARRAYSPROC InterleavedArrays;\n  PFNGLINVALIDATEBUFFERDATAPROC InvalidateBufferData;\n  PFNGLINVALIDATEBUFFERSUBDATAPROC InvalidateBufferSubData;\n  PFNGLINVALIDATEFRAMEBUFFERPROC InvalidateFramebuffer;\n  PFNGLINVALIDATENAMEDFRAMEBUFFERDATAPROC InvalidateNamedFramebufferData;\n  PFNGLINVALIDATENAMEDFRAMEBUFFERSUBDATAPROC InvalidateNamedFramebufferSubData;\n  PFNGLINVALIDATESUBFRAMEBUFFERPROC InvalidateSubFramebuffer;\n  PFNGLINVALIDATETEXIMAGEPROC InvalidateTexImage;\n  PFNGLINVALIDATETEXSUBIMAGEPROC InvalidateTexSubImage;\n  PFNGLISBUFFERPROC IsBuffer;\n  PFNGLISENABLEDPROC IsEnabled;\n  PFNGLISENABLEDIPROC IsEnabledi;\n  PFNGLISFRAMEBUFFERPROC IsFramebuffer;\n  PFNGLISLISTPROC IsList;\n  PFNGLISPROGRAMPROC IsProgram;\n  PFNGLISPROGRAMPIPELINEPROC IsProgramPipeline;\n  PFNGLISQUERYPROC IsQuery;\n  PFNGLISRENDERBUFFERPROC IsRenderbuffer;\n  PFNGLISSAMPLERPROC IsSampler;\n  PFNGLISSHADERPROC IsShader;\n  PFNGLISSYNCPROC IsSync;\n  PFNGLISTEXTUREPROC IsTexture;\n  PFNGLISTRANSFORMFEEDBACKPROC IsTransformFeedback;\n  PFNGLISVERTEXARRAYPROC IsVertexArray;\n  PFNGLLIGHTMODELFPROC LightModelf;\n  PFNGLLIGHTMODELFVPROC LightModelfv;\n  PFNGLLIGHTMODELIPROC LightModeli;\n  PFNGLLIGHTMODELIVPROC LightModeliv;\n  PFNGLLIGHTFPROC Lightf;\n  PFNGLLIGHTFVPROC Lightfv;\n  PFNGLLIGHTIPROC Lighti;\n  PFNGLLIGHTIVPROC Lightiv;\n  PFNGLLINESTIPPLEPROC LineStipple;\n  PFNGLLINEWIDTHPROC LineWidth;\n  PFNGLLINKPROGRAMPROC LinkProgram;\n  PFNGLLISTBASEPROC ListBase;\n  PFNGLLOADIDENTITYPROC LoadIdentity;\n  PFNGLLOADMATRIXDPROC LoadMatrixd;\n  PFNGLLOADMATRIXFPROC LoadMatrixf;\n  PFNGLLOADNAMEPROC LoadName;\n  PFNGLLOADTRANSPOSEMATRIXDPROC LoadTransposeMatrixd;\n  PFNGLLOADTRANSPOSEMATRIXFPROC LoadTransposeMatrixf;\n  PFNGLLOGICOPPROC LogicOp;\n  PFNGLMAP1DPROC Map1d;\n  PFNGLMAP1FPROC Map1f;\n  PFNGLMAP2DPROC Map2d;\n  PFNGLMAP2FPROC Map2f;\n  PFNGLMAPBUFFERPROC MapBuffer;\n  PFNGLMAPBUFFERRANGEPROC MapBufferRange;\n  PFNGLMAPGRID1DPROC MapGrid1d;\n  PFNGLMAPGRID1FPROC MapGrid1f;\n  PFNGLMAPGRID2DPROC MapGrid2d;\n  PFNGLMAPGRID2FPROC MapGrid2f;\n  PFNGLMAPNAMEDBUFFERPROC MapNamedBuffer;\n  PFNGLMAPNAMEDBUFFERRANGEPROC MapNamedBufferRange;\n  PFNGLMATERIALFPROC Materialf;\n  PFNGLMATERIALFVPROC Materialfv;\n  PFNGLMATERIALIPROC Materiali;\n  PFNGLMATERIALIVPROC Materialiv;\n  PFNGLMATRIXMODEPROC MatrixMode;\n  PFNGLMEMORYBARRIERPROC MemoryBarrier;\n  PFNGLMEMORYBARRIERBYREGIONPROC MemoryBarrierByRegion;\n  PFNGLMINSAMPLESHADINGPROC MinSampleShading;\n  PFNGLMULTMATRIXDPROC MultMatrixd;\n  PFNGLMULTMATRIXFPROC MultMatrixf;\n  PFNGLMULTTRANSPOSEMATRIXDPROC MultTransposeMatrixd;\n  PFNGLMULTTRANSPOSEMATRIXFPROC MultTransposeMatrixf;\n  PFNGLMULTIDRAWARRAYSPROC MultiDrawArrays;\n  PFNGLMULTIDRAWARRAYSINDIRECTPROC MultiDrawArraysIndirect;\n  PFNGLMULTIDRAWARRAYSINDIRECTCOUNTPROC MultiDrawArraysIndirectCount;\n  PFNGLMULTIDRAWELEMENTSPROC MultiDrawElements;\n  PFNGLMULTIDRAWELEMENTSBASEVERTEXPROC MultiDrawElementsBaseVertex;\n  PFNGLMULTIDRAWELEMENTSINDIRECTPROC MultiDrawElementsIndirect;\n  PFNGLMULTIDRAWELEMENTSINDIRECTCOUNTPROC MultiDrawElementsIndirectCount;\n  PFNGLMULTITEXCOORD1DPROC MultiTexCoord1d;\n  PFNGLMULTITEXCOORD1DVPROC MultiTexCoord1dv;\n  PFNGLMULTITEXCOORD1FPROC MultiTexCoord1f;\n  PFNGLMULTITEXCOORD1FVPROC MultiTexCoord1fv;\n  PFNGLMULTITEXCOORD1IPROC MultiTexCoord1i;\n  PFNGLMULTITEXCOORD1IVPROC MultiTexCoord1iv;\n  PFNGLMULTITEXCOORD1SPROC MultiTexCoord1s;\n  PFNGLMULTITEXCOORD1SVPROC MultiTexCoord1sv;\n  PFNGLMULTITEXCOORD2DPROC MultiTexCoord2d;\n  PFNGLMULTITEXCOORD2DVPROC MultiTexCoord2dv;\n  PFNGLMULTITEXCOORD2FPROC MultiTexCoord2f;\n  PFNGLMULTITEXCOORD2FVPROC MultiTexCoord2fv;\n  PFNGLMULTITEXCOORD2IPROC MultiTexCoord2i;\n  PFNGLMULTITEXCOORD2IVPROC MultiTexCoord2iv;\n  PFNGLMULTITEXCOORD2SPROC MultiTexCoord2s;\n  PFNGLMULTITEXCOORD2SVPROC MultiTexCoord2sv;\n  PFNGLMULTITEXCOORD3DPROC MultiTexCoord3d;\n  PFNGLMULTITEXCOORD3DVPROC MultiTexCoord3dv;\n  PFNGLMULTITEXCOORD3FPROC MultiTexCoord3f;\n  PFNGLMULTITEXCOORD3FVPROC MultiTexCoord3fv;\n  PFNGLMULTITEXCOORD3IPROC MultiTexCoord3i;\n  PFNGLMULTITEXCOORD3IVPROC MultiTexCoord3iv;\n  PFNGLMULTITEXCOORD3SPROC MultiTexCoord3s;\n  PFNGLMULTITEXCOORD3SVPROC MultiTexCoord3sv;\n  PFNGLMULTITEXCOORD4DPROC MultiTexCoord4d;\n  PFNGLMULTITEXCOORD4DVPROC MultiTexCoord4dv;\n  PFNGLMULTITEXCOORD4FPROC MultiTexCoord4f;\n  PFNGLMULTITEXCOORD4FVPROC MultiTexCoord4fv;\n  PFNGLMULTITEXCOORD4IPROC MultiTexCoord4i;\n  PFNGLMULTITEXCOORD4IVPROC MultiTexCoord4iv;\n  PFNGLMULTITEXCOORD4SPROC MultiTexCoord4s;\n  PFNGLMULTITEXCOORD4SVPROC MultiTexCoord4sv;\n  PFNGLMULTITEXCOORDP1UIPROC MultiTexCoordP1ui;\n  PFNGLMULTITEXCOORDP1UIVPROC MultiTexCoordP1uiv;\n  PFNGLMULTITEXCOORDP2UIPROC MultiTexCoordP2ui;\n  PFNGLMULTITEXCOORDP2UIVPROC MultiTexCoordP2uiv;\n  PFNGLMULTITEXCOORDP3UIPROC MultiTexCoordP3ui;\n  PFNGLMULTITEXCOORDP3UIVPROC MultiTexCoordP3uiv;\n  PFNGLMULTITEXCOORDP4UIPROC MultiTexCoordP4ui;\n  PFNGLMULTITEXCOORDP4UIVPROC MultiTexCoordP4uiv;\n  PFNGLNAMEDBUFFERDATAPROC NamedBufferData;\n  PFNGLNAMEDBUFFERSTORAGEPROC NamedBufferStorage;\n  PFNGLNAMEDBUFFERSUBDATAPROC NamedBufferSubData;\n  PFNGLNAMEDFRAMEBUFFERDRAWBUFFERPROC NamedFramebufferDrawBuffer;\n  PFNGLNAMEDFRAMEBUFFERDRAWBUFFERSPROC NamedFramebufferDrawBuffers;\n  PFNGLNAMEDFRAMEBUFFERPARAMETERIPROC NamedFramebufferParameteri;\n  PFNGLNAMEDFRAMEBUFFERREADBUFFERPROC NamedFramebufferReadBuffer;\n  PFNGLNAMEDFRAMEBUFFERRENDERBUFFERPROC NamedFramebufferRenderbuffer;\n  PFNGLNAMEDFRAMEBUFFERTEXTUREPROC NamedFramebufferTexture;\n  PFNGLNAMEDFRAMEBUFFERTEXTURELAYERPROC NamedFramebufferTextureLayer;\n  PFNGLNAMEDRENDERBUFFERSTORAGEPROC NamedRenderbufferStorage;\n  PFNGLNAMEDRENDERBUFFERSTORAGEMULTISAMPLEPROC NamedRenderbufferStorageMultisample;\n  PFNGLNEWLISTPROC NewList;\n  PFNGLNORMAL3BPROC Normal3b;\n  PFNGLNORMAL3BVPROC Normal3bv;\n  PFNGLNORMAL3DPROC Normal3d;\n  PFNGLNORMAL3DVPROC Normal3dv;\n  PFNGLNORMAL3FPROC Normal3f;\n  PFNGLNORMAL3FVPROC Normal3fv;\n  PFNGLNORMAL3IPROC Normal3i;\n  PFNGLNORMAL3IVPROC Normal3iv;\n  PFNGLNORMAL3SPROC Normal3s;\n  PFNGLNORMAL3SVPROC Normal3sv;\n  PFNGLNORMALP3UIPROC NormalP3ui;\n  PFNGLNORMALP3UIVPROC NormalP3uiv;\n  PFNGLNORMALPOINTERPROC NormalPointer;\n  PFNGLOBJECTLABELPROC ObjectLabel;\n  PFNGLOBJECTPTRLABELPROC ObjectPtrLabel;\n  PFNGLORTHOPROC Ortho;\n  PFNGLPASSTHROUGHPROC PassThrough;\n  PFNGLPATCHPARAMETERFVPROC PatchParameterfv;\n  PFNGLPATCHPARAMETERIPROC PatchParameteri;\n  PFNGLPAUSETRANSFORMFEEDBACKPROC PauseTransformFeedback;\n  PFNGLPIXELMAPFVPROC PixelMapfv;\n  PFNGLPIXELMAPUIVPROC PixelMapuiv;\n  PFNGLPIXELMAPUSVPROC PixelMapusv;\n  PFNGLPIXELSTOREFPROC PixelStoref;\n  PFNGLPIXELSTOREIPROC PixelStorei;\n  PFNGLPIXELTRANSFERFPROC PixelTransferf;\n  PFNGLPIXELTRANSFERIPROC PixelTransferi;\n  PFNGLPIXELZOOMPROC PixelZoom;\n  PFNGLPOINTPARAMETERFPROC PointParameterf;\n  PFNGLPOINTPARAMETERFVPROC PointParameterfv;\n  PFNGLPOINTPARAMETERIPROC PointParameteri;\n  PFNGLPOINTPARAMETERIVPROC PointParameteriv;\n  PFNGLPOINTSIZEPROC PointSize;\n  PFNGLPOLYGONMODEPROC PolygonMode;\n  PFNGLPOLYGONOFFSETPROC PolygonOffset;\n  PFNGLPOLYGONOFFSETCLAMPPROC PolygonOffsetClamp;\n  PFNGLPOLYGONSTIPPLEPROC PolygonStipple;\n  PFNGLPOPATTRIBPROC PopAttrib;\n  PFNGLPOPCLIENTATTRIBPROC PopClientAttrib;\n  PFNGLPOPDEBUGGROUPPROC PopDebugGroup;\n  PFNGLPOPMATRIXPROC PopMatrix;\n  PFNGLPOPNAMEPROC PopName;\n  PFNGLPRIMITIVERESTARTINDEXPROC PrimitiveRestartIndex;\n  PFNGLPRIORITIZETEXTURESPROC PrioritizeTextures;\n  PFNGLPROGRAMBINARYPROC ProgramBinary;\n  PFNGLPROGRAMPARAMETERIPROC ProgramParameteri;\n  PFNGLPROGRAMUNIFORM1DPROC ProgramUniform1d;\n  PFNGLPROGRAMUNIFORM1DVPROC ProgramUniform1dv;\n  PFNGLPROGRAMUNIFORM1FPROC ProgramUniform1f;\n  PFNGLPROGRAMUNIFORM1FVPROC ProgramUniform1fv;\n  PFNGLPROGRAMUNIFORM1IPROC ProgramUniform1i;\n  PFNGLPROGRAMUNIFORM1IVPROC ProgramUniform1iv;\n  PFNGLPROGRAMUNIFORM1UIPROC ProgramUniform1ui;\n  PFNGLPROGRAMUNIFORM1UIVPROC ProgramUniform1uiv;\n  PFNGLPROGRAMUNIFORM2DPROC ProgramUniform2d;\n  PFNGLPROGRAMUNIFORM2DVPROC ProgramUniform2dv;\n  PFNGLPROGRAMUNIFORM2FPROC ProgramUniform2f;\n  PFNGLPROGRAMUNIFORM2FVPROC ProgramUniform2fv;\n  PFNGLPROGRAMUNIFORM2IPROC ProgramUniform2i;\n  PFNGLPROGRAMUNIFORM2IVPROC ProgramUniform2iv;\n  PFNGLPROGRAMUNIFORM2UIPROC ProgramUniform2ui;\n  PFNGLPROGRAMUNIFORM2UIVPROC ProgramUniform2uiv;\n  PFNGLPROGRAMUNIFORM3DPROC ProgramUniform3d;\n  PFNGLPROGRAMUNIFORM3DVPROC ProgramUniform3dv;\n  PFNGLPROGRAMUNIFORM3FPROC ProgramUniform3f;\n  PFNGLPROGRAMUNIFORM3FVPROC ProgramUniform3fv;\n  PFNGLPROGRAMUNIFORM3IPROC ProgramUniform3i;\n  PFNGLPROGRAMUNIFORM3IVPROC ProgramUniform3iv;\n  PFNGLPROGRAMUNIFORM3UIPROC ProgramUniform3ui;\n  PFNGLPROGRAMUNIFORM3UIVPROC ProgramUniform3uiv;\n  PFNGLPROGRAMUNIFORM4DPROC ProgramUniform4d;\n  PFNGLPROGRAMUNIFORM4DVPROC ProgramUniform4dv;\n  PFNGLPROGRAMUNIFORM4FPROC ProgramUniform4f;\n  PFNGLPROGRAMUNIFORM4FVPROC ProgramUniform4fv;\n  PFNGLPROGRAMUNIFORM4IPROC ProgramUniform4i;\n  PFNGLPROGRAMUNIFORM4IVPROC ProgramUniform4iv;\n  PFNGLPROGRAMUNIFORM4UIPROC ProgramUniform4ui;\n  PFNGLPROGRAMUNIFORM4UIVPROC ProgramUniform4uiv;\n  PFNGLPROGRAMUNIFORMMATRIX2DVPROC ProgramUniformMatrix2dv;\n  PFNGLPROGRAMUNIFORMMATRIX2FVPROC ProgramUniformMatrix2fv;\n  PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC ProgramUniformMatrix2x3dv;\n  PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC ProgramUniformMatrix2x3fv;\n  PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC ProgramUniformMatrix2x4dv;\n  PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC ProgramUniformMatrix2x4fv;\n  PFNGLPROGRAMUNIFORMMATRIX3DVPROC ProgramUniformMatrix3dv;\n  PFNGLPROGRAMUNIFORMMATRIX3FVPROC ProgramUniformMatrix3fv;\n  PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC ProgramUniformMatrix3x2dv;\n  PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC ProgramUniformMatrix3x2fv;\n  PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC ProgramUniformMatrix3x4dv;\n  PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC ProgramUniformMatrix3x4fv;\n  PFNGLPROGRAMUNIFORMMATRIX4DVPROC ProgramUniformMatrix4dv;\n  PFNGLPROGRAMUNIFORMMATRIX4FVPROC ProgramUniformMatrix4fv;\n  PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC ProgramUniformMatrix4x2dv;\n  PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC ProgramUniformMatrix4x2fv;\n  PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC ProgramUniformMatrix4x3dv;\n  PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC ProgramUniformMatrix4x3fv;\n  PFNGLPROVOKINGVERTEXPROC ProvokingVertex;\n  PFNGLPUSHATTRIBPROC PushAttrib;\n  PFNGLPUSHCLIENTATTRIBPROC PushClientAttrib;\n  PFNGLPUSHDEBUGGROUPPROC PushDebugGroup;\n  PFNGLPUSHMATRIXPROC PushMatrix;\n  PFNGLPUSHNAMEPROC PushName;\n  PFNGLQUERYCOUNTERPROC QueryCounter;\n  PFNGLRASTERPOS2DPROC RasterPos2d;\n  PFNGLRASTERPOS2DVPROC RasterPos2dv;\n  PFNGLRASTERPOS2FPROC RasterPos2f;\n  PFNGLRASTERPOS2FVPROC RasterPos2fv;\n  PFNGLRASTERPOS2IPROC RasterPos2i;\n  PFNGLRASTERPOS2IVPROC RasterPos2iv;\n  PFNGLRASTERPOS2SPROC RasterPos2s;\n  PFNGLRASTERPOS2SVPROC RasterPos2sv;\n  PFNGLRASTERPOS3DPROC RasterPos3d;\n  PFNGLRASTERPOS3DVPROC RasterPos3dv;\n  PFNGLRASTERPOS3FPROC RasterPos3f;\n  PFNGLRASTERPOS3FVPROC RasterPos3fv;\n  PFNGLRASTERPOS3IPROC RasterPos3i;\n  PFNGLRASTERPOS3IVPROC RasterPos3iv;\n  PFNGLRASTERPOS3SPROC RasterPos3s;\n  PFNGLRASTERPOS3SVPROC RasterPos3sv;\n  PFNGLRASTERPOS4DPROC RasterPos4d;\n  PFNGLRASTERPOS4DVPROC RasterPos4dv;\n  PFNGLRASTERPOS4FPROC RasterPos4f;\n  PFNGLRASTERPOS4FVPROC RasterPos4fv;\n  PFNGLRASTERPOS4IPROC RasterPos4i;\n  PFNGLRASTERPOS4IVPROC RasterPos4iv;\n  PFNGLRASTERPOS4SPROC RasterPos4s;\n  PFNGLRASTERPOS4SVPROC RasterPos4sv;\n  PFNGLREADBUFFERPROC ReadBuffer;\n  PFNGLREADPIXELSPROC ReadPixels;\n  PFNGLREADNPIXELSPROC ReadnPixels;\n  PFNGLRECTDPROC Rectd;\n  PFNGLRECTDVPROC Rectdv;\n  PFNGLRECTFPROC Rectf;\n  PFNGLRECTFVPROC Rectfv;\n  PFNGLRECTIPROC Recti;\n  PFNGLRECTIVPROC Rectiv;\n  PFNGLRECTSPROC Rects;\n  PFNGLRECTSVPROC Rectsv;\n  PFNGLRELEASESHADERCOMPILERPROC ReleaseShaderCompiler;\n  PFNGLRENDERMODEPROC RenderMode;\n  PFNGLRENDERBUFFERSTORAGEPROC RenderbufferStorage;\n  PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC RenderbufferStorageMultisample;\n  PFNGLRESUMETRANSFORMFEEDBACKPROC ResumeTransformFeedback;\n  PFNGLROTATEDPROC Rotated;\n  PFNGLROTATEFPROC Rotatef;\n  PFNGLSAMPLECOVERAGEPROC SampleCoverage;\n  PFNGLSAMPLEMASKIPROC SampleMaski;\n  PFNGLSAMPLERPARAMETERIIVPROC SamplerParameterIiv;\n  PFNGLSAMPLERPARAMETERIUIVPROC SamplerParameterIuiv;\n  PFNGLSAMPLERPARAMETERFPROC SamplerParameterf;\n  PFNGLSAMPLERPARAMETERFVPROC SamplerParameterfv;\n  PFNGLSAMPLERPARAMETERIPROC SamplerParameteri;\n  PFNGLSAMPLERPARAMETERIVPROC SamplerParameteriv;\n  PFNGLSCALEDPROC Scaled;\n  PFNGLSCALEFPROC Scalef;\n  PFNGLSCISSORPROC Scissor;\n  PFNGLSCISSORARRAYVPROC ScissorArrayv;\n  PFNGLSCISSORINDEXEDPROC ScissorIndexed;\n  PFNGLSCISSORINDEXEDVPROC ScissorIndexedv;\n  PFNGLSECONDARYCOLOR3BPROC SecondaryColor3b;\n  PFNGLSECONDARYCOLOR3BVPROC SecondaryColor3bv;\n  PFNGLSECONDARYCOLOR3DPROC SecondaryColor3d;\n  PFNGLSECONDARYCOLOR3DVPROC SecondaryColor3dv;\n  PFNGLSECONDARYCOLOR3FPROC SecondaryColor3f;\n  PFNGLSECONDARYCOLOR3FVPROC SecondaryColor3fv;\n  PFNGLSECONDARYCOLOR3IPROC SecondaryColor3i;\n  PFNGLSECONDARYCOLOR3IVPROC SecondaryColor3iv;\n  PFNGLSECONDARYCOLOR3SPROC SecondaryColor3s;\n  PFNGLSECONDARYCOLOR3SVPROC SecondaryColor3sv;\n  PFNGLSECONDARYCOLOR3UBPROC SecondaryColor3ub;\n  PFNGLSECONDARYCOLOR3UBVPROC SecondaryColor3ubv;\n  PFNGLSECONDARYCOLOR3UIPROC SecondaryColor3ui;\n  PFNGLSECONDARYCOLOR3UIVPROC SecondaryColor3uiv;\n  PFNGLSECONDARYCOLOR3USPROC SecondaryColor3us;\n  PFNGLSECONDARYCOLOR3USVPROC SecondaryColor3usv;\n  PFNGLSECONDARYCOLORP3UIPROC SecondaryColorP3ui;\n  PFNGLSECONDARYCOLORP3UIVPROC SecondaryColorP3uiv;\n  PFNGLSECONDARYCOLORPOINTERPROC SecondaryColorPointer;\n  PFNGLSELECTBUFFERPROC SelectBuffer;\n  PFNGLSHADEMODELPROC ShadeModel;\n  PFNGLSHADERBINARYPROC ShaderBinary;\n  PFNGLSHADERSOURCEPROC ShaderSource;\n  PFNGLSHADERSTORAGEBLOCKBINDINGPROC ShaderStorageBlockBinding;\n  PFNGLSPECIALIZESHADERPROC SpecializeShader;\n  PFNGLSTENCILFUNCPROC StencilFunc;\n  PFNGLSTENCILFUNCSEPARATEPROC StencilFuncSeparate;\n  PFNGLSTENCILMASKPROC StencilMask;\n  PFNGLSTENCILMASKSEPARATEPROC StencilMaskSeparate;\n  PFNGLSTENCILOPPROC StencilOp;\n  PFNGLSTENCILOPSEPARATEPROC StencilOpSeparate;\n  PFNGLTEXBUFFERPROC TexBuffer;\n  PFNGLTEXBUFFERRANGEPROC TexBufferRange;\n  PFNGLTEXCOORD1DPROC TexCoord1d;\n  PFNGLTEXCOORD1DVPROC TexCoord1dv;\n  PFNGLTEXCOORD1FPROC TexCoord1f;\n  PFNGLTEXCOORD1FVPROC TexCoord1fv;\n  PFNGLTEXCOORD1IPROC TexCoord1i;\n  PFNGLTEXCOORD1IVPROC TexCoord1iv;\n  PFNGLTEXCOORD1SPROC TexCoord1s;\n  PFNGLTEXCOORD1SVPROC TexCoord1sv;\n  PFNGLTEXCOORD2DPROC TexCoord2d;\n  PFNGLTEXCOORD2DVPROC TexCoord2dv;\n  PFNGLTEXCOORD2FPROC TexCoord2f;\n  PFNGLTEXCOORD2FVPROC TexCoord2fv;\n  PFNGLTEXCOORD2IPROC TexCoord2i;\n  PFNGLTEXCOORD2IVPROC TexCoord2iv;\n  PFNGLTEXCOORD2SPROC TexCoord2s;\n  PFNGLTEXCOORD2SVPROC TexCoord2sv;\n  PFNGLTEXCOORD3DPROC TexCoord3d;\n  PFNGLTEXCOORD3DVPROC TexCoord3dv;\n  PFNGLTEXCOORD3FPROC TexCoord3f;\n  PFNGLTEXCOORD3FVPROC TexCoord3fv;\n  PFNGLTEXCOORD3IPROC TexCoord3i;\n  PFNGLTEXCOORD3IVPROC TexCoord3iv;\n  PFNGLTEXCOORD3SPROC TexCoord3s;\n  PFNGLTEXCOORD3SVPROC TexCoord3sv;\n  PFNGLTEXCOORD4DPROC TexCoord4d;\n  PFNGLTEXCOORD4DVPROC TexCoord4dv;\n  PFNGLTEXCOORD4FPROC TexCoord4f;\n  PFNGLTEXCOORD4FVPROC TexCoord4fv;\n  PFNGLTEXCOORD4IPROC TexCoord4i;\n  PFNGLTEXCOORD4IVPROC TexCoord4iv;\n  PFNGLTEXCOORD4SPROC TexCoord4s;\n  PFNGLTEXCOORD4SVPROC TexCoord4sv;\n  PFNGLTEXCOORDP1UIPROC TexCoordP1ui;\n  PFNGLTEXCOORDP1UIVPROC TexCoordP1uiv;\n  PFNGLTEXCOORDP2UIPROC TexCoordP2ui;\n  PFNGLTEXCOORDP2UIVPROC TexCoordP2uiv;\n  PFNGLTEXCOORDP3UIPROC TexCoordP3ui;\n  PFNGLTEXCOORDP3UIVPROC TexCoordP3uiv;\n  PFNGLTEXCOORDP4UIPROC TexCoordP4ui;\n  PFNGLTEXCOORDP4UIVPROC TexCoordP4uiv;\n  PFNGLTEXCOORDPOINTERPROC TexCoordPointer;\n  PFNGLTEXENVFPROC TexEnvf;\n  PFNGLTEXENVFVPROC TexEnvfv;\n  PFNGLTEXENVIPROC TexEnvi;\n  PFNGLTEXENVIVPROC TexEnviv;\n  PFNGLTEXGENDPROC TexGend;\n  PFNGLTEXGENDVPROC TexGendv;\n  PFNGLTEXGENFPROC TexGenf;\n  PFNGLTEXGENFVPROC TexGenfv;\n  PFNGLTEXGENIPROC TexGeni;\n  PFNGLTEXGENIVPROC TexGeniv;\n  PFNGLTEXIMAGE1DPROC TexImage1D;\n  PFNGLTEXIMAGE2DPROC TexImage2D;\n  PFNGLTEXIMAGE2DMULTISAMPLEPROC TexImage2DMultisample;\n  PFNGLTEXIMAGE3DPROC TexImage3D;\n  PFNGLTEXIMAGE3DMULTISAMPLEPROC TexImage3DMultisample;\n  PFNGLTEXPARAMETERIIVPROC TexParameterIiv;\n  PFNGLTEXPARAMETERIUIVPROC TexParameterIuiv;\n  PFNGLTEXPARAMETERFPROC TexParameterf;\n  PFNGLTEXPARAMETERFVPROC TexParameterfv;\n  PFNGLTEXPARAMETERIPROC TexParameteri;\n  PFNGLTEXPARAMETERIVPROC TexParameteriv;\n  PFNGLTEXSTORAGE1DPROC TexStorage1D;\n  PFNGLTEXSTORAGE2DPROC TexStorage2D;\n  PFNGLTEXSTORAGE2DMULTISAMPLEPROC TexStorage2DMultisample;\n  PFNGLTEXSTORAGE3DPROC TexStorage3D;\n  PFNGLTEXSTORAGE3DMULTISAMPLEPROC TexStorage3DMultisample;\n  PFNGLTEXSUBIMAGE1DPROC TexSubImage1D;\n  PFNGLTEXSUBIMAGE2DPROC TexSubImage2D;\n  PFNGLTEXSUBIMAGE3DPROC TexSubImage3D;\n  PFNGLTEXTUREBARRIERPROC TextureBarrier;\n  PFNGLTEXTUREBUFFERPROC TextureBuffer;\n  PFNGLTEXTUREBUFFERRANGEPROC TextureBufferRange;\n  PFNGLTEXTUREPARAMETERIIVPROC TextureParameterIiv;\n  PFNGLTEXTUREPARAMETERIUIVPROC TextureParameterIuiv;\n  PFNGLTEXTUREPARAMETERFPROC TextureParameterf;\n  PFNGLTEXTUREPARAMETERFVPROC TextureParameterfv;\n  PFNGLTEXTUREPARAMETERIPROC TextureParameteri;\n  PFNGLTEXTUREPARAMETERIVPROC TextureParameteriv;\n  PFNGLTEXTURESTORAGE1DPROC TextureStorage1D;\n  PFNGLTEXTURESTORAGE2DPROC TextureStorage2D;\n  PFNGLTEXTURESTORAGE2DMULTISAMPLEPROC TextureStorage2DMultisample;\n  PFNGLTEXTURESTORAGE3DPROC TextureStorage3D;\n  PFNGLTEXTURESTORAGE3DMULTISAMPLEPROC TextureStorage3DMultisample;\n  PFNGLTEXTURESUBIMAGE1DPROC TextureSubImage1D;\n  PFNGLTEXTURESUBIMAGE2DPROC TextureSubImage2D;\n  PFNGLTEXTURESUBIMAGE3DPROC TextureSubImage3D;\n  PFNGLTEXTUREVIEWPROC TextureView;\n  PFNGLTRANSFORMFEEDBACKBUFFERBASEPROC TransformFeedbackBufferBase;\n  PFNGLTRANSFORMFEEDBACKBUFFERRANGEPROC TransformFeedbackBufferRange;\n  PFNGLTRANSFORMFEEDBACKVARYINGSPROC TransformFeedbackVaryings;\n  PFNGLTRANSLATEDPROC Translated;\n  PFNGLTRANSLATEFPROC Translatef;\n  PFNGLUNIFORM1DPROC Uniform1d;\n  PFNGLUNIFORM1DVPROC Uniform1dv;\n  PFNGLUNIFORM1FPROC Uniform1f;\n  PFNGLUNIFORM1FVPROC Uniform1fv;\n  PFNGLUNIFORM1IPROC Uniform1i;\n  PFNGLUNIFORM1IVPROC Uniform1iv;\n  PFNGLUNIFORM1UIPROC Uniform1ui;\n  PFNGLUNIFORM1UIVPROC Uniform1uiv;\n  PFNGLUNIFORM2DPROC Uniform2d;\n  PFNGLUNIFORM2DVPROC Uniform2dv;\n  PFNGLUNIFORM2FPROC Uniform2f;\n  PFNGLUNIFORM2FVPROC Uniform2fv;\n  PFNGLUNIFORM2IPROC Uniform2i;\n  PFNGLUNIFORM2IVPROC Uniform2iv;\n  PFNGLUNIFORM2UIPROC Uniform2ui;\n  PFNGLUNIFORM2UIVPROC Uniform2uiv;\n  PFNGLUNIFORM3DPROC Uniform3d;\n  PFNGLUNIFORM3DVPROC Uniform3dv;\n  PFNGLUNIFORM3FPROC Uniform3f;\n  PFNGLUNIFORM3FVPROC Uniform3fv;\n  PFNGLUNIFORM3IPROC Uniform3i;\n  PFNGLUNIFORM3IVPROC Uniform3iv;\n  PFNGLUNIFORM3UIPROC Uniform3ui;\n  PFNGLUNIFORM3UIVPROC Uniform3uiv;\n  PFNGLUNIFORM4DPROC Uniform4d;\n  PFNGLUNIFORM4DVPROC Uniform4dv;\n  PFNGLUNIFORM4FPROC Uniform4f;\n  PFNGLUNIFORM4FVPROC Uniform4fv;\n  PFNGLUNIFORM4IPROC Uniform4i;\n  PFNGLUNIFORM4IVPROC Uniform4iv;\n  PFNGLUNIFORM4UIPROC Uniform4ui;\n  PFNGLUNIFORM4UIVPROC Uniform4uiv;\n  PFNGLUNIFORMBLOCKBINDINGPROC UniformBlockBinding;\n  PFNGLUNIFORMMATRIX2DVPROC UniformMatrix2dv;\n  PFNGLUNIFORMMATRIX2FVPROC UniformMatrix2fv;\n  PFNGLUNIFORMMATRIX2X3DVPROC UniformMatrix2x3dv;\n  PFNGLUNIFORMMATRIX2X3FVPROC UniformMatrix2x3fv;\n  PFNGLUNIFORMMATRIX2X4DVPROC UniformMatrix2x4dv;\n  PFNGLUNIFORMMATRIX2X4FVPROC UniformMatrix2x4fv;\n  PFNGLUNIFORMMATRIX3DVPROC UniformMatrix3dv;\n  PFNGLUNIFORMMATRIX3FVPROC UniformMatrix3fv;\n  PFNGLUNIFORMMATRIX3X2DVPROC UniformMatrix3x2dv;\n  PFNGLUNIFORMMATRIX3X2FVPROC UniformMatrix3x2fv;\n  PFNGLUNIFORMMATRIX3X4DVPROC UniformMatrix3x4dv;\n  PFNGLUNIFORMMATRIX3X4FVPROC UniformMatrix3x4fv;\n  PFNGLUNIFORMMATRIX4DVPROC UniformMatrix4dv;\n  PFNGLUNIFORMMATRIX4FVPROC UniformMatrix4fv;\n  PFNGLUNIFORMMATRIX4X2DVPROC UniformMatrix4x2dv;\n  PFNGLUNIFORMMATRIX4X2FVPROC UniformMatrix4x2fv;\n  PFNGLUNIFORMMATRIX4X3DVPROC UniformMatrix4x3dv;\n  PFNGLUNIFORMMATRIX4X3FVPROC UniformMatrix4x3fv;\n  PFNGLUNIFORMSUBROUTINESUIVPROC UniformSubroutinesuiv;\n  PFNGLUNMAPBUFFERPROC UnmapBuffer;\n  PFNGLUNMAPNAMEDBUFFERPROC UnmapNamedBuffer;\n  PFNGLUSEPROGRAMPROC UseProgram;\n  PFNGLUSEPROGRAMSTAGESPROC UseProgramStages;\n  PFNGLVALIDATEPROGRAMPROC ValidateProgram;\n  PFNGLVALIDATEPROGRAMPIPELINEPROC ValidateProgramPipeline;\n  PFNGLVERTEX2DPROC Vertex2d;\n  PFNGLVERTEX2DVPROC Vertex2dv;\n  PFNGLVERTEX2FPROC Vertex2f;\n  PFNGLVERTEX2FVPROC Vertex2fv;\n  PFNGLVERTEX2IPROC Vertex2i;\n  PFNGLVERTEX2IVPROC Vertex2iv;\n  PFNGLVERTEX2SPROC Vertex2s;\n  PFNGLVERTEX2SVPROC Vertex2sv;\n  PFNGLVERTEX3DPROC Vertex3d;\n  PFNGLVERTEX3DVPROC Vertex3dv;\n  PFNGLVERTEX3FPROC Vertex3f;\n  PFNGLVERTEX3FVPROC Vertex3fv;\n  PFNGLVERTEX3IPROC Vertex3i;\n  PFNGLVERTEX3IVPROC Vertex3iv;\n  PFNGLVERTEX3SPROC Vertex3s;\n  PFNGLVERTEX3SVPROC Vertex3sv;\n  PFNGLVERTEX4DPROC Vertex4d;\n  PFNGLVERTEX4DVPROC Vertex4dv;\n  PFNGLVERTEX4FPROC Vertex4f;\n  PFNGLVERTEX4FVPROC Vertex4fv;\n  PFNGLVERTEX4IPROC Vertex4i;\n  PFNGLVERTEX4IVPROC Vertex4iv;\n  PFNGLVERTEX4SPROC Vertex4s;\n  PFNGLVERTEX4SVPROC Vertex4sv;\n  PFNGLVERTEXARRAYATTRIBBINDINGPROC VertexArrayAttribBinding;\n  PFNGLVERTEXARRAYATTRIBFORMATPROC VertexArrayAttribFormat;\n  PFNGLVERTEXARRAYATTRIBIFORMATPROC VertexArrayAttribIFormat;\n  PFNGLVERTEXARRAYATTRIBLFORMATPROC VertexArrayAttribLFormat;\n  PFNGLVERTEXARRAYBINDINGDIVISORPROC VertexArrayBindingDivisor;\n  PFNGLVERTEXARRAYELEMENTBUFFERPROC VertexArrayElementBuffer;\n  PFNGLVERTEXARRAYVERTEXBUFFERPROC VertexArrayVertexBuffer;\n  PFNGLVERTEXARRAYVERTEXBUFFERSPROC VertexArrayVertexBuffers;\n  PFNGLVERTEXATTRIB1DPROC VertexAttrib1d;\n  PFNGLVERTEXATTRIB1DVPROC VertexAttrib1dv;\n  PFNGLVERTEXATTRIB1FPROC VertexAttrib1f;\n  PFNGLVERTEXATTRIB1FVPROC VertexAttrib1fv;\n  PFNGLVERTEXATTRIB1SPROC VertexAttrib1s;\n  PFNGLVERTEXATTRIB1SVPROC VertexAttrib1sv;\n  PFNGLVERTEXATTRIB2DPROC VertexAttrib2d;\n  PFNGLVERTEXATTRIB2DVPROC VertexAttrib2dv;\n  PFNGLVERTEXATTRIB2FPROC VertexAttrib2f;\n  PFNGLVERTEXATTRIB2FVPROC VertexAttrib2fv;\n  PFNGLVERTEXATTRIB2SPROC VertexAttrib2s;\n  PFNGLVERTEXATTRIB2SVPROC VertexAttrib2sv;\n  PFNGLVERTEXATTRIB3DPROC VertexAttrib3d;\n  PFNGLVERTEXATTRIB3DVPROC VertexAttrib3dv;\n  PFNGLVERTEXATTRIB3FPROC VertexAttrib3f;\n  PFNGLVERTEXATTRIB3FVPROC VertexAttrib3fv;\n  PFNGLVERTEXATTRIB3SPROC VertexAttrib3s;\n  PFNGLVERTEXATTRIB3SVPROC VertexAttrib3sv;\n  PFNGLVERTEXATTRIB4NBVPROC VertexAttrib4Nbv;\n  PFNGLVERTEXATTRIB4NIVPROC VertexAttrib4Niv;\n  PFNGLVERTEXATTRIB4NSVPROC VertexAttrib4Nsv;\n  PFNGLVERTEXATTRIB4NUBPROC VertexAttrib4Nub;\n  PFNGLVERTEXATTRIB4NUBVPROC VertexAttrib4Nubv;\n  PFNGLVERTEXATTRIB4NUIVPROC VertexAttrib4Nuiv;\n  PFNGLVERTEXATTRIB4NUSVPROC VertexAttrib4Nusv;\n  PFNGLVERTEXATTRIB4BVPROC VertexAttrib4bv;\n  PFNGLVERTEXATTRIB4DPROC VertexAttrib4d;\n  PFNGLVERTEXATTRIB4DVPROC VertexAttrib4dv;\n  PFNGLVERTEXATTRIB4FPROC VertexAttrib4f;\n  PFNGLVERTEXATTRIB4FVPROC VertexAttrib4fv;\n  PFNGLVERTEXATTRIB4IVPROC VertexAttrib4iv;\n  PFNGLVERTEXATTRIB4SPROC VertexAttrib4s;\n  PFNGLVERTEXATTRIB4SVPROC VertexAttrib4sv;\n  PFNGLVERTEXATTRIB4UBVPROC VertexAttrib4ubv;\n  PFNGLVERTEXATTRIB4UIVPROC VertexAttrib4uiv;\n  PFNGLVERTEXATTRIB4USVPROC VertexAttrib4usv;\n  PFNGLVERTEXATTRIBBINDINGPROC VertexAttribBinding;\n  PFNGLVERTEXATTRIBDIVISORPROC VertexAttribDivisor;\n  PFNGLVERTEXATTRIBFORMATPROC VertexAttribFormat;\n  PFNGLVERTEXATTRIBI1IPROC VertexAttribI1i;\n  PFNGLVERTEXATTRIBI1IVPROC VertexAttribI1iv;\n  PFNGLVERTEXATTRIBI1UIPROC VertexAttribI1ui;\n  PFNGLVERTEXATTRIBI1UIVPROC VertexAttribI1uiv;\n  PFNGLVERTEXATTRIBI2IPROC VertexAttribI2i;\n  PFNGLVERTEXATTRIBI2IVPROC VertexAttribI2iv;\n  PFNGLVERTEXATTRIBI2UIPROC VertexAttribI2ui;\n  PFNGLVERTEXATTRIBI2UIVPROC VertexAttribI2uiv;\n  PFNGLVERTEXATTRIBI3IPROC VertexAttribI3i;\n  PFNGLVERTEXATTRIBI3IVPROC VertexAttribI3iv;\n  PFNGLVERTEXATTRIBI3UIPROC VertexAttribI3ui;\n  PFNGLVERTEXATTRIBI3UIVPROC VertexAttribI3uiv;\n  PFNGLVERTEXATTRIBI4BVPROC VertexAttribI4bv;\n  PFNGLVERTEXATTRIBI4IPROC VertexAttribI4i;\n  PFNGLVERTEXATTRIBI4IVPROC VertexAttribI4iv;\n  PFNGLVERTEXATTRIBI4SVPROC VertexAttribI4sv;\n  PFNGLVERTEXATTRIBI4UBVPROC VertexAttribI4ubv;\n  PFNGLVERTEXATTRIBI4UIPROC VertexAttribI4ui;\n  PFNGLVERTEXATTRIBI4UIVPROC VertexAttribI4uiv;\n  PFNGLVERTEXATTRIBI4USVPROC VertexAttribI4usv;\n  PFNGLVERTEXATTRIBIFORMATPROC VertexAttribIFormat;\n  PFNGLVERTEXATTRIBIPOINTERPROC VertexAttribIPointer;\n  PFNGLVERTEXATTRIBL1DPROC VertexAttribL1d;\n  PFNGLVERTEXATTRIBL1DVPROC VertexAttribL1dv;\n  PFNGLVERTEXATTRIBL2DPROC VertexAttribL2d;\n  PFNGLVERTEXATTRIBL2DVPROC VertexAttribL2dv;\n  PFNGLVERTEXATTRIBL3DPROC VertexAttribL3d;\n  PFNGLVERTEXATTRIBL3DVPROC VertexAttribL3dv;\n  PFNGLVERTEXATTRIBL4DPROC VertexAttribL4d;\n  PFNGLVERTEXATTRIBL4DVPROC VertexAttribL4dv;\n  PFNGLVERTEXATTRIBLFORMATPROC VertexAttribLFormat;\n  PFNGLVERTEXATTRIBLPOINTERPROC VertexAttribLPointer;\n  PFNGLVERTEXATTRIBP1UIPROC VertexAttribP1ui;\n  PFNGLVERTEXATTRIBP1UIVPROC VertexAttribP1uiv;\n  PFNGLVERTEXATTRIBP2UIPROC VertexAttribP2ui;\n  PFNGLVERTEXATTRIBP2UIVPROC VertexAttribP2uiv;\n  PFNGLVERTEXATTRIBP3UIPROC VertexAttribP3ui;\n  PFNGLVERTEXATTRIBP3UIVPROC VertexAttribP3uiv;\n  PFNGLVERTEXATTRIBP4UIPROC VertexAttribP4ui;\n  PFNGLVERTEXATTRIBP4UIVPROC VertexAttribP4uiv;\n  PFNGLVERTEXATTRIBPOINTERPROC VertexAttribPointer;\n  PFNGLVERTEXBINDINGDIVISORPROC VertexBindingDivisor;\n  PFNGLVERTEXP2UIPROC VertexP2ui;\n  PFNGLVERTEXP2UIVPROC VertexP2uiv;\n  PFNGLVERTEXP3UIPROC VertexP3ui;\n  PFNGLVERTEXP3UIVPROC VertexP3uiv;\n  PFNGLVERTEXP4UIPROC VertexP4ui;\n  PFNGLVERTEXP4UIVPROC VertexP4uiv;\n  PFNGLVERTEXPOINTERPROC VertexPointer;\n  PFNGLVIEWPORTPROC Viewport;\n  PFNGLVIEWPORTARRAYVPROC ViewportArrayv;\n  PFNGLVIEWPORTINDEXEDFPROC ViewportIndexedf;\n  PFNGLVIEWPORTINDEXEDFVPROC ViewportIndexedfv;\n  PFNGLWAITSYNCPROC WaitSync;\n  PFNGLWINDOWPOS2DPROC WindowPos2d;\n  PFNGLWINDOWPOS2DVPROC WindowPos2dv;\n  PFNGLWINDOWPOS2FPROC WindowPos2f;\n  PFNGLWINDOWPOS2FVPROC WindowPos2fv;\n  PFNGLWINDOWPOS2IPROC WindowPos2i;\n  PFNGLWINDOWPOS2IVPROC WindowPos2iv;\n  PFNGLWINDOWPOS2SPROC WindowPos2s;\n  PFNGLWINDOWPOS2SVPROC WindowPos2sv;\n  PFNGLWINDOWPOS3DPROC WindowPos3d;\n  PFNGLWINDOWPOS3DVPROC WindowPos3dv;\n  PFNGLWINDOWPOS3FPROC WindowPos3f;\n  PFNGLWINDOWPOS3FVPROC WindowPos3fv;\n  PFNGLWINDOWPOS3IPROC WindowPos3i;\n  PFNGLWINDOWPOS3IVPROC WindowPos3iv;\n  PFNGLWINDOWPOS3SPROC WindowPos3s;\n  PFNGLWINDOWPOS3SVPROC WindowPos3sv;\n} GladGLContext;\n\nGLAD_API_CALL int\ngladLoadGLContextUserPtr(GladGLContext *context, GLADuserptrloadfunc load, void *userptr);\nGLAD_API_CALL int\ngladLoadGLContext(GladGLContext *context, GLADloadfunc load);\n\n#ifdef GLAD_GL\n\nGLAD_API_CALL int\ngladLoaderLoadGLContext(GladGLContext *context);\nGLAD_API_CALL void\ngladLoaderUnloadGL(void);\n\n#endif\n\n#ifdef __cplusplus\n}\n#endif\n#endif"
  },
  {
    "path": "third-party/glad/src/egl.c",
    "content": "#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <glad/egl.h>\n\n#ifndef GLAD_IMPL_UTIL_C_\n#define GLAD_IMPL_UTIL_C_\n\n#ifdef _MSC_VER\n#define GLAD_IMPL_UTIL_SSCANF sscanf_s\n#else\n#define GLAD_IMPL_UTIL_SSCANF sscanf\n#endif\n\n#endif /* GLAD_IMPL_UTIL_C_ */\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n\n\nint GLAD_EGL_VERSION_1_0 = 0;\nint GLAD_EGL_VERSION_1_1 = 0;\nint GLAD_EGL_VERSION_1_2 = 0;\nint GLAD_EGL_VERSION_1_3 = 0;\nint GLAD_EGL_VERSION_1_4 = 0;\nint GLAD_EGL_VERSION_1_5 = 0;\n\n\n\nPFNEGLBINDAPIPROC glad_eglBindAPI = NULL;\nPFNEGLBINDTEXIMAGEPROC glad_eglBindTexImage = NULL;\nPFNEGLCHOOSECONFIGPROC glad_eglChooseConfig = NULL;\nPFNEGLCLIENTWAITSYNCPROC glad_eglClientWaitSync = NULL;\nPFNEGLCOPYBUFFERSPROC glad_eglCopyBuffers = NULL;\nPFNEGLCREATECONTEXTPROC glad_eglCreateContext = NULL;\nPFNEGLCREATEIMAGEPROC glad_eglCreateImage = NULL;\nPFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC glad_eglCreatePbufferFromClientBuffer = NULL;\nPFNEGLCREATEPBUFFERSURFACEPROC glad_eglCreatePbufferSurface = NULL;\nPFNEGLCREATEPIXMAPSURFACEPROC glad_eglCreatePixmapSurface = NULL;\nPFNEGLCREATEPLATFORMPIXMAPSURFACEPROC glad_eglCreatePlatformPixmapSurface = NULL;\nPFNEGLCREATEPLATFORMWINDOWSURFACEPROC glad_eglCreatePlatformWindowSurface = NULL;\nPFNEGLCREATESYNCPROC glad_eglCreateSync = NULL;\nPFNEGLCREATEWINDOWSURFACEPROC glad_eglCreateWindowSurface = NULL;\nPFNEGLDESTROYCONTEXTPROC glad_eglDestroyContext = NULL;\nPFNEGLDESTROYIMAGEPROC glad_eglDestroyImage = NULL;\nPFNEGLDESTROYSURFACEPROC glad_eglDestroySurface = NULL;\nPFNEGLDESTROYSYNCPROC glad_eglDestroySync = NULL;\nPFNEGLGETCONFIGATTRIBPROC glad_eglGetConfigAttrib = NULL;\nPFNEGLGETCONFIGSPROC glad_eglGetConfigs = NULL;\nPFNEGLGETCURRENTCONTEXTPROC glad_eglGetCurrentContext = NULL;\nPFNEGLGETCURRENTDISPLAYPROC glad_eglGetCurrentDisplay = NULL;\nPFNEGLGETCURRENTSURFACEPROC glad_eglGetCurrentSurface = NULL;\nPFNEGLGETDISPLAYPROC glad_eglGetDisplay = NULL;\nPFNEGLGETERRORPROC glad_eglGetError = NULL;\nPFNEGLGETPLATFORMDISPLAYPROC glad_eglGetPlatformDisplay = NULL;\nPFNEGLGETPROCADDRESSPROC glad_eglGetProcAddress = NULL;\nPFNEGLGETSYNCATTRIBPROC glad_eglGetSyncAttrib = NULL;\nPFNEGLINITIALIZEPROC glad_eglInitialize = NULL;\nPFNEGLMAKECURRENTPROC glad_eglMakeCurrent = NULL;\nPFNEGLQUERYAPIPROC glad_eglQueryAPI = NULL;\nPFNEGLQUERYCONTEXTPROC glad_eglQueryContext = NULL;\nPFNEGLQUERYSTRINGPROC glad_eglQueryString = NULL;\nPFNEGLQUERYSURFACEPROC glad_eglQuerySurface = NULL;\nPFNEGLRELEASETEXIMAGEPROC glad_eglReleaseTexImage = NULL;\nPFNEGLRELEASETHREADPROC glad_eglReleaseThread = NULL;\nPFNEGLSURFACEATTRIBPROC glad_eglSurfaceAttrib = NULL;\nPFNEGLSWAPBUFFERSPROC glad_eglSwapBuffers = NULL;\nPFNEGLSWAPINTERVALPROC glad_eglSwapInterval = NULL;\nPFNEGLTERMINATEPROC glad_eglTerminate = NULL;\nPFNEGLWAITCLIENTPROC glad_eglWaitClient = NULL;\nPFNEGLWAITGLPROC glad_eglWaitGL = NULL;\nPFNEGLWAITNATIVEPROC glad_eglWaitNative = NULL;\nPFNEGLWAITSYNCPROC glad_eglWaitSync = NULL;\nPFNEGLCREATEIMAGEKHRPROC glad_eglCreateImageKHR = NULL;\nPFNEGLDESTROYIMAGEKHRPROC glad_eglDestroyImageKHR = NULL;\n\n\nstatic void glad_egl_load_EGL_VERSION_1_0( GLADuserptrloadfunc load, void* userptr) {\n    if(!GLAD_EGL_VERSION_1_0) return;\n    glad_eglChooseConfig = (PFNEGLCHOOSECONFIGPROC) load(userptr, \"eglChooseConfig\");\n    glad_eglCopyBuffers = (PFNEGLCOPYBUFFERSPROC) load(userptr, \"eglCopyBuffers\");\n    glad_eglCreateContext = (PFNEGLCREATECONTEXTPROC) load(userptr, \"eglCreateContext\");\n    glad_eglCreatePbufferSurface = (PFNEGLCREATEPBUFFERSURFACEPROC) load(userptr, \"eglCreatePbufferSurface\");\n    glad_eglCreatePixmapSurface = (PFNEGLCREATEPIXMAPSURFACEPROC) load(userptr, \"eglCreatePixmapSurface\");\n    glad_eglCreateWindowSurface = (PFNEGLCREATEWINDOWSURFACEPROC) load(userptr, \"eglCreateWindowSurface\");\n    glad_eglDestroyContext = (PFNEGLDESTROYCONTEXTPROC) load(userptr, \"eglDestroyContext\");\n    glad_eglDestroySurface = (PFNEGLDESTROYSURFACEPROC) load(userptr, \"eglDestroySurface\");\n    glad_eglGetConfigAttrib = (PFNEGLGETCONFIGATTRIBPROC) load(userptr, \"eglGetConfigAttrib\");\n    glad_eglGetConfigs = (PFNEGLGETCONFIGSPROC) load(userptr, \"eglGetConfigs\");\n    glad_eglGetCurrentDisplay = (PFNEGLGETCURRENTDISPLAYPROC) load(userptr, \"eglGetCurrentDisplay\");\n    glad_eglGetCurrentSurface = (PFNEGLGETCURRENTSURFACEPROC) load(userptr, \"eglGetCurrentSurface\");\n    glad_eglGetDisplay = (PFNEGLGETDISPLAYPROC) load(userptr, \"eglGetDisplay\");\n    glad_eglGetError = (PFNEGLGETERRORPROC) load(userptr, \"eglGetError\");\n    glad_eglGetProcAddress = (PFNEGLGETPROCADDRESSPROC) load(userptr, \"eglGetProcAddress\");\n    glad_eglInitialize = (PFNEGLINITIALIZEPROC) load(userptr, \"eglInitialize\");\n    glad_eglMakeCurrent = (PFNEGLMAKECURRENTPROC) load(userptr, \"eglMakeCurrent\");\n    glad_eglQueryContext = (PFNEGLQUERYCONTEXTPROC) load(userptr, \"eglQueryContext\");\n    glad_eglQueryString = (PFNEGLQUERYSTRINGPROC) load(userptr, \"eglQueryString\");\n    glad_eglQuerySurface = (PFNEGLQUERYSURFACEPROC) load(userptr, \"eglQuerySurface\");\n    glad_eglSwapBuffers = (PFNEGLSWAPBUFFERSPROC) load(userptr, \"eglSwapBuffers\");\n    glad_eglTerminate = (PFNEGLTERMINATEPROC) load(userptr, \"eglTerminate\");\n    glad_eglWaitGL = (PFNEGLWAITGLPROC) load(userptr, \"eglWaitGL\");\n    glad_eglWaitNative = (PFNEGLWAITNATIVEPROC) load(userptr, \"eglWaitNative\");\n}\nstatic void glad_egl_load_EGL_VERSION_1_1( GLADuserptrloadfunc load, void* userptr) {\n    if(!GLAD_EGL_VERSION_1_1) return;\n    glad_eglBindTexImage = (PFNEGLBINDTEXIMAGEPROC) load(userptr, \"eglBindTexImage\");\n    glad_eglReleaseTexImage = (PFNEGLRELEASETEXIMAGEPROC) load(userptr, \"eglReleaseTexImage\");\n    glad_eglSurfaceAttrib = (PFNEGLSURFACEATTRIBPROC) load(userptr, \"eglSurfaceAttrib\");\n    glad_eglSwapInterval = (PFNEGLSWAPINTERVALPROC) load(userptr, \"eglSwapInterval\");\n}\nstatic void glad_egl_load_EGL_VERSION_1_2( GLADuserptrloadfunc load, void* userptr) {\n    if(!GLAD_EGL_VERSION_1_2) return;\n    glad_eglBindAPI = (PFNEGLBINDAPIPROC) load(userptr, \"eglBindAPI\");\n    glad_eglCreatePbufferFromClientBuffer = (PFNEGLCREATEPBUFFERFROMCLIENTBUFFERPROC) load(userptr, \"eglCreatePbufferFromClientBuffer\");\n    glad_eglQueryAPI = (PFNEGLQUERYAPIPROC) load(userptr, \"eglQueryAPI\");\n    glad_eglReleaseThread = (PFNEGLRELEASETHREADPROC) load(userptr, \"eglReleaseThread\");\n    glad_eglWaitClient = (PFNEGLWAITCLIENTPROC) load(userptr, \"eglWaitClient\");\n    glad_eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC) load(userptr, \"eglCreateImageKHR\");\n    glad_eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC) load(userptr, \"eglDestroyImageKHR\");\n}\nstatic void glad_egl_load_EGL_VERSION_1_4( GLADuserptrloadfunc load, void* userptr) {\n    if(!GLAD_EGL_VERSION_1_4) return;\n    glad_eglGetCurrentContext = (PFNEGLGETCURRENTCONTEXTPROC) load(userptr, \"eglGetCurrentContext\");\n}\nstatic void glad_egl_load_EGL_VERSION_1_5( GLADuserptrloadfunc load, void* userptr) {\n    if(!GLAD_EGL_VERSION_1_5) return;\n    glad_eglClientWaitSync = (PFNEGLCLIENTWAITSYNCPROC) load(userptr, \"eglClientWaitSync\");\n    glad_eglCreateImage = (PFNEGLCREATEIMAGEPROC) load(userptr, \"eglCreateImage\");\n    glad_eglCreatePlatformPixmapSurface = (PFNEGLCREATEPLATFORMPIXMAPSURFACEPROC) load(userptr, \"eglCreatePlatformPixmapSurface\");\n    glad_eglCreatePlatformWindowSurface = (PFNEGLCREATEPLATFORMWINDOWSURFACEPROC) load(userptr, \"eglCreatePlatformWindowSurface\");\n    glad_eglCreateSync = (PFNEGLCREATESYNCPROC) load(userptr, \"eglCreateSync\");\n    glad_eglDestroyImage = (PFNEGLDESTROYIMAGEPROC) load(userptr, \"eglDestroyImage\");\n    glad_eglDestroySync = (PFNEGLDESTROYSYNCPROC) load(userptr, \"eglDestroySync\");\n    glad_eglGetPlatformDisplay = (PFNEGLGETPLATFORMDISPLAYPROC) load(userptr, \"eglGetPlatformDisplay\");\n    glad_eglGetSyncAttrib = (PFNEGLGETSYNCATTRIBPROC) load(userptr, \"eglGetSyncAttrib\");\n    glad_eglWaitSync = (PFNEGLWAITSYNCPROC) load(userptr, \"eglWaitSync\");\n}\n\n\n\nstatic int glad_egl_get_extensions(EGLDisplay display, const char **extensions) {\n    *extensions = eglQueryString(display, EGL_EXTENSIONS);\n\n    return extensions != NULL;\n}\n\nstatic int glad_egl_has_extension(const char *extensions, const char *ext) {\n    const char *loc;\n    const char *terminator;\n    if(extensions == NULL) {\n        return 0;\n    }\n    while(1) {\n        loc = strstr(extensions, ext);\n        if(loc == NULL) {\n            return 0;\n        }\n        terminator = loc + strlen(ext);\n        if((loc == extensions || *(loc - 1) == ' ') &&\n            (*terminator == ' ' || *terminator == '\\0')) {\n            return 1;\n        }\n        extensions = terminator;\n    }\n}\n\nstatic GLADapiproc glad_egl_get_proc_from_userptr(void *userptr, const char *name) {\n    return (GLAD_GNUC_EXTENSION (GLADapiproc (*)(const char *name)) userptr)(name);\n}\n\nstatic int glad_egl_find_extensions_egl(EGLDisplay display) {\n    const char *extensions;\n    if (!glad_egl_get_extensions(display, &extensions)) return 0;\n\n    (void) glad_egl_has_extension;\n\n    return 1;\n}\n\nstatic int glad_egl_find_core_egl(EGLDisplay display) {\n    int major, minor;\n    const char *version;\n\n    if (display == NULL) {\n        display = EGL_NO_DISPLAY; /* this is usually NULL, better safe than sorry */\n    }\n    if (display == EGL_NO_DISPLAY) {\n        display = eglGetCurrentDisplay();\n    }\n#ifdef EGL_VERSION_1_4\n    if (display == EGL_NO_DISPLAY) {\n        display = eglGetDisplay(EGL_DEFAULT_DISPLAY);\n    }\n#endif\n#ifndef EGL_VERSION_1_5\n    if (display == EGL_NO_DISPLAY) {\n        return 0;\n    }\n#endif\n\n    version = eglQueryString(display, EGL_VERSION);\n    (void) eglGetError();\n\n    if (version == NULL) {\n        major = 1;\n        minor = 5; // We need version 1.5 anyway\n    } else {\n        GLAD_IMPL_UTIL_SSCANF(version, \"%d.%d\", &major, &minor);\n    }\n\n    GLAD_EGL_VERSION_1_0 = (major == 1 && minor >= 0) || major > 1;\n    GLAD_EGL_VERSION_1_1 = (major == 1 && minor >= 1) || major > 1;\n    GLAD_EGL_VERSION_1_2 = (major == 1 && minor >= 2) || major > 1;\n    GLAD_EGL_VERSION_1_3 = (major == 1 && minor >= 3) || major > 1;\n    GLAD_EGL_VERSION_1_4 = (major == 1 && minor >= 4) || major > 1;\n    GLAD_EGL_VERSION_1_5 = (major == 1 && minor >= 5) || major > 1;\n\n    return GLAD_MAKE_VERSION(major, minor);\n}\n\nint gladLoadEGLUserPtr(EGLDisplay display, GLADuserptrloadfunc load, void* userptr) {\n    int version;\n    eglGetDisplay = (PFNEGLGETDISPLAYPROC) load(userptr, \"eglGetDisplay\");\n    eglGetCurrentDisplay = (PFNEGLGETCURRENTDISPLAYPROC) load(userptr, \"eglGetCurrentDisplay\");\n    eglQueryString = (PFNEGLQUERYSTRINGPROC) load(userptr, \"eglQueryString\");\n    eglGetError = (PFNEGLGETERRORPROC) load(userptr, \"eglGetError\");\n    if (eglGetDisplay == NULL || eglGetCurrentDisplay == NULL || eglQueryString == NULL || eglGetError == NULL) return 0;\n\n    version = glad_egl_find_core_egl(display);\n    if (!version) return 0;\n    glad_egl_load_EGL_VERSION_1_0(load, userptr);\n    glad_egl_load_EGL_VERSION_1_1(load, userptr);\n    glad_egl_load_EGL_VERSION_1_2(load, userptr);\n    glad_egl_load_EGL_VERSION_1_4(load, userptr);\n    glad_egl_load_EGL_VERSION_1_5(load, userptr);\n\n    if (!glad_egl_find_extensions_egl(display)) return 0;\n\n    return version;\n}\n\nint gladLoadEGL(EGLDisplay display, GLADloadfunc load) {\n    return gladLoadEGLUserPtr(display, glad_egl_get_proc_from_userptr, GLAD_GNUC_EXTENSION (void*) load);\n}\n\n \n\n#ifdef GLAD_EGL\n\n#ifndef GLAD_LOADER_LIBRARY_C_\n#define GLAD_LOADER_LIBRARY_C_\n\n#include <stddef.h>\n#include <stdlib.h>\n\n#if GLAD_PLATFORM_WIN32\n#include <windows.h>\n#else\n#include <dlfcn.h>\n#endif\n\n\nstatic void* glad_get_dlopen_handle(const char *lib_names[], int length) {\n    void *handle = NULL;\n    int i;\n\n    for (i = 0; i < length; ++i) {\n#if GLAD_PLATFORM_WIN32\n  #if GLAD_PLATFORM_UWP\n        size_t buffer_size = (strlen(lib_names[i]) + 1) * sizeof(WCHAR);\n        LPWSTR buffer = (LPWSTR) malloc(buffer_size);\n        if (buffer != NULL) {\n            int ret = MultiByteToWideChar(CP_ACP, 0, lib_names[i], -1, buffer, buffer_size);\n            if (ret != 0) {\n                handle = (void*) LoadPackagedLibrary(buffer, 0);\n            }\n            free((void*) buffer);\n        }\n  #else\n        handle = (void*) LoadLibraryA(lib_names[i]);\n  #endif\n#else\n        handle = dlopen(lib_names[i], RTLD_LAZY | RTLD_LOCAL);\n#endif\n        if (handle != NULL) {\n            return handle;\n        }\n    }\n\n    return NULL;\n}\n\nstatic void glad_close_dlopen_handle(void* handle) {\n    if (handle != NULL) {\n#if GLAD_PLATFORM_WIN32\n        FreeLibrary((HMODULE) handle);\n#else\n        dlclose(handle);\n#endif\n    }\n}\n\nstatic GLADapiproc glad_dlsym_handle(void* handle, const char *name) {\n    if (handle == NULL) {\n        return NULL;\n    }\n\n#if GLAD_PLATFORM_WIN32\n    return (GLADapiproc) GetProcAddress((HMODULE) handle, name);\n#else\n    return GLAD_GNUC_EXTENSION (GLADapiproc) dlsym(handle, name);\n#endif\n}\n\n#endif /* GLAD_LOADER_LIBRARY_C_ */\n\nstruct _glad_egl_userptr {\n    void *handle;\n    PFNEGLGETPROCADDRESSPROC get_proc_address_ptr;\n};\n\nstatic GLADapiproc glad_egl_get_proc(void *vuserptr, const char* name) {\n    struct _glad_egl_userptr userptr = *(struct _glad_egl_userptr*) vuserptr;\n    GLADapiproc result = NULL;\n\n    result = glad_dlsym_handle(userptr.handle, name);\n    if (result == NULL) {\n        result = GLAD_GNUC_EXTENSION (GLADapiproc) userptr.get_proc_address_ptr(name);\n    }\n\n    return result;\n}\n\nstatic void* _egl_handle = NULL;\n\nstatic void* glad_egl_dlopen_handle(void) {\n#if GLAD_PLATFORM_APPLE\n    static const char *NAMES[] = {\"libEGL.dylib\"};\n#elif GLAD_PLATFORM_WIN32\n    static const char *NAMES[] = {\"libEGL.dll\", \"EGL.dll\"};\n#else\n    static const char *NAMES[] = {\"libEGL.so.1\", \"libEGL.so\"};\n#endif\n\n    if (_egl_handle == NULL) {\n        _egl_handle = glad_get_dlopen_handle(NAMES, sizeof(NAMES) / sizeof(NAMES[0]));\n    }\n\n    return _egl_handle;\n}\n\nstatic struct _glad_egl_userptr glad_egl_build_userptr(void *handle) {\n    struct _glad_egl_userptr userptr;\n    userptr.handle = handle;\n    userptr.get_proc_address_ptr = (PFNEGLGETPROCADDRESSPROC) glad_dlsym_handle(handle, \"eglGetProcAddress\");\n    return userptr;\n}\n\nint gladLoaderLoadEGL(EGLDisplay display) {\n    int version = 0;\n    void *handle = NULL;\n    int did_load = 0;\n    struct _glad_egl_userptr userptr;\n\n    did_load = _egl_handle == NULL;\n    handle = glad_egl_dlopen_handle();\n    if (handle != NULL) {\n        userptr = glad_egl_build_userptr(handle);\n\n        if (userptr.get_proc_address_ptr != NULL) {\n            version = gladLoadEGLUserPtr(display, glad_egl_get_proc, &userptr);\n        }\n\n        if (!version && did_load) {\n            gladLoaderUnloadEGL();\n        }\n    }\n\n    return version;\n}\n\n\nvoid gladLoaderUnloadEGL() {\n    if (_egl_handle != NULL) {\n        glad_close_dlopen_handle(_egl_handle);\n        _egl_handle = NULL;\n    }\n}\n\n#endif /* GLAD_EGL */\n\n#ifdef __cplusplus\n}\n#endif"
  },
  {
    "path": "third-party/glad/src/gl.c",
    "content": "#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <glad/gl.h>\n\n#ifndef GLAD_IMPL_UTIL_C_\n#define GLAD_IMPL_UTIL_C_\n\n#ifdef _MSC_VER\n#define GLAD_IMPL_UTIL_SSCANF sscanf_s\n#else\n#define GLAD_IMPL_UTIL_SSCANF sscanf\n#endif\n\n#endif /* GLAD_IMPL_UTIL_C_ */\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n\n\n\n\n\n\n\nstatic void glad_gl_load_GL_VERSION_1_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_1_0) return;\n    context->Accum = (PFNGLACCUMPROC) load(userptr, \"glAccum\");\n    context->AlphaFunc = (PFNGLALPHAFUNCPROC) load(userptr, \"glAlphaFunc\");\n    context->Begin = (PFNGLBEGINPROC) load(userptr, \"glBegin\");\n    context->Bitmap = (PFNGLBITMAPPROC) load(userptr, \"glBitmap\");\n    context->BlendFunc = (PFNGLBLENDFUNCPROC) load(userptr, \"glBlendFunc\");\n    context->CallList = (PFNGLCALLLISTPROC) load(userptr, \"glCallList\");\n    context->CallLists = (PFNGLCALLLISTSPROC) load(userptr, \"glCallLists\");\n    context->Clear = (PFNGLCLEARPROC) load(userptr, \"glClear\");\n    context->ClearAccum = (PFNGLCLEARACCUMPROC) load(userptr, \"glClearAccum\");\n    context->ClearColor = (PFNGLCLEARCOLORPROC) load(userptr, \"glClearColor\");\n    context->ClearDepth = (PFNGLCLEARDEPTHPROC) load(userptr, \"glClearDepth\");\n    context->ClearIndex = (PFNGLCLEARINDEXPROC) load(userptr, \"glClearIndex\");\n    context->ClearStencil = (PFNGLCLEARSTENCILPROC) load(userptr, \"glClearStencil\");\n    context->ClipPlane = (PFNGLCLIPPLANEPROC) load(userptr, \"glClipPlane\");\n    context->Color3b = (PFNGLCOLOR3BPROC) load(userptr, \"glColor3b\");\n    context->Color3bv = (PFNGLCOLOR3BVPROC) load(userptr, \"glColor3bv\");\n    context->Color3d = (PFNGLCOLOR3DPROC) load(userptr, \"glColor3d\");\n    context->Color3dv = (PFNGLCOLOR3DVPROC) load(userptr, \"glColor3dv\");\n    context->Color3f = (PFNGLCOLOR3FPROC) load(userptr, \"glColor3f\");\n    context->Color3fv = (PFNGLCOLOR3FVPROC) load(userptr, \"glColor3fv\");\n    context->Color3i = (PFNGLCOLOR3IPROC) load(userptr, \"glColor3i\");\n    context->Color3iv = (PFNGLCOLOR3IVPROC) load(userptr, \"glColor3iv\");\n    context->Color3s = (PFNGLCOLOR3SPROC) load(userptr, \"glColor3s\");\n    context->Color3sv = (PFNGLCOLOR3SVPROC) load(userptr, \"glColor3sv\");\n    context->Color3ub = (PFNGLCOLOR3UBPROC) load(userptr, \"glColor3ub\");\n    context->Color3ubv = (PFNGLCOLOR3UBVPROC) load(userptr, \"glColor3ubv\");\n    context->Color3ui = (PFNGLCOLOR3UIPROC) load(userptr, \"glColor3ui\");\n    context->Color3uiv = (PFNGLCOLOR3UIVPROC) load(userptr, \"glColor3uiv\");\n    context->Color3us = (PFNGLCOLOR3USPROC) load(userptr, \"glColor3us\");\n    context->Color3usv = (PFNGLCOLOR3USVPROC) load(userptr, \"glColor3usv\");\n    context->Color4b = (PFNGLCOLOR4BPROC) load(userptr, \"glColor4b\");\n    context->Color4bv = (PFNGLCOLOR4BVPROC) load(userptr, \"glColor4bv\");\n    context->Color4d = (PFNGLCOLOR4DPROC) load(userptr, \"glColor4d\");\n    context->Color4dv = (PFNGLCOLOR4DVPROC) load(userptr, \"glColor4dv\");\n    context->Color4f = (PFNGLCOLOR4FPROC) load(userptr, \"glColor4f\");\n    context->Color4fv = (PFNGLCOLOR4FVPROC) load(userptr, \"glColor4fv\");\n    context->Color4i = (PFNGLCOLOR4IPROC) load(userptr, \"glColor4i\");\n    context->Color4iv = (PFNGLCOLOR4IVPROC) load(userptr, \"glColor4iv\");\n    context->Color4s = (PFNGLCOLOR4SPROC) load(userptr, \"glColor4s\");\n    context->Color4sv = (PFNGLCOLOR4SVPROC) load(userptr, \"glColor4sv\");\n    context->Color4ub = (PFNGLCOLOR4UBPROC) load(userptr, \"glColor4ub\");\n    context->Color4ubv = (PFNGLCOLOR4UBVPROC) load(userptr, \"glColor4ubv\");\n    context->Color4ui = (PFNGLCOLOR4UIPROC) load(userptr, \"glColor4ui\");\n    context->Color4uiv = (PFNGLCOLOR4UIVPROC) load(userptr, \"glColor4uiv\");\n    context->Color4us = (PFNGLCOLOR4USPROC) load(userptr, \"glColor4us\");\n    context->Color4usv = (PFNGLCOLOR4USVPROC) load(userptr, \"glColor4usv\");\n    context->ColorMask = (PFNGLCOLORMASKPROC) load(userptr, \"glColorMask\");\n    context->ColorMaterial = (PFNGLCOLORMATERIALPROC) load(userptr, \"glColorMaterial\");\n    context->CopyPixels = (PFNGLCOPYPIXELSPROC) load(userptr, \"glCopyPixels\");\n    context->CullFace = (PFNGLCULLFACEPROC) load(userptr, \"glCullFace\");\n    context->DeleteLists = (PFNGLDELETELISTSPROC) load(userptr, \"glDeleteLists\");\n    context->DepthFunc = (PFNGLDEPTHFUNCPROC) load(userptr, \"glDepthFunc\");\n    context->DepthMask = (PFNGLDEPTHMASKPROC) load(userptr, \"glDepthMask\");\n    context->DepthRange = (PFNGLDEPTHRANGEPROC) load(userptr, \"glDepthRange\");\n    context->Disable = (PFNGLDISABLEPROC) load(userptr, \"glDisable\");\n    context->DrawBuffer = (PFNGLDRAWBUFFERPROC) load(userptr, \"glDrawBuffer\");\n    context->DrawPixels = (PFNGLDRAWPIXELSPROC) load(userptr, \"glDrawPixels\");\n    context->EdgeFlag = (PFNGLEDGEFLAGPROC) load(userptr, \"glEdgeFlag\");\n    context->EdgeFlagv = (PFNGLEDGEFLAGVPROC) load(userptr, \"glEdgeFlagv\");\n    context->Enable = (PFNGLENABLEPROC) load(userptr, \"glEnable\");\n    context->End = (PFNGLENDPROC) load(userptr, \"glEnd\");\n    context->EndList = (PFNGLENDLISTPROC) load(userptr, \"glEndList\");\n    context->EvalCoord1d = (PFNGLEVALCOORD1DPROC) load(userptr, \"glEvalCoord1d\");\n    context->EvalCoord1dv = (PFNGLEVALCOORD1DVPROC) load(userptr, \"glEvalCoord1dv\");\n    context->EvalCoord1f = (PFNGLEVALCOORD1FPROC) load(userptr, \"glEvalCoord1f\");\n    context->EvalCoord1fv = (PFNGLEVALCOORD1FVPROC) load(userptr, \"glEvalCoord1fv\");\n    context->EvalCoord2d = (PFNGLEVALCOORD2DPROC) load(userptr, \"glEvalCoord2d\");\n    context->EvalCoord2dv = (PFNGLEVALCOORD2DVPROC) load(userptr, \"glEvalCoord2dv\");\n    context->EvalCoord2f = (PFNGLEVALCOORD2FPROC) load(userptr, \"glEvalCoord2f\");\n    context->EvalCoord2fv = (PFNGLEVALCOORD2FVPROC) load(userptr, \"glEvalCoord2fv\");\n    context->EvalMesh1 = (PFNGLEVALMESH1PROC) load(userptr, \"glEvalMesh1\");\n    context->EvalMesh2 = (PFNGLEVALMESH2PROC) load(userptr, \"glEvalMesh2\");\n    context->EvalPoint1 = (PFNGLEVALPOINT1PROC) load(userptr, \"glEvalPoint1\");\n    context->EvalPoint2 = (PFNGLEVALPOINT2PROC) load(userptr, \"glEvalPoint2\");\n    context->FeedbackBuffer = (PFNGLFEEDBACKBUFFERPROC) load(userptr, \"glFeedbackBuffer\");\n    context->Finish = (PFNGLFINISHPROC) load(userptr, \"glFinish\");\n    context->Flush = (PFNGLFLUSHPROC) load(userptr, \"glFlush\");\n    context->Fogf = (PFNGLFOGFPROC) load(userptr, \"glFogf\");\n    context->Fogfv = (PFNGLFOGFVPROC) load(userptr, \"glFogfv\");\n    context->Fogi = (PFNGLFOGIPROC) load(userptr, \"glFogi\");\n    context->Fogiv = (PFNGLFOGIVPROC) load(userptr, \"glFogiv\");\n    context->FrontFace = (PFNGLFRONTFACEPROC) load(userptr, \"glFrontFace\");\n    context->Frustum = (PFNGLFRUSTUMPROC) load(userptr, \"glFrustum\");\n    context->GenLists = (PFNGLGENLISTSPROC) load(userptr, \"glGenLists\");\n    context->GetBooleanv = (PFNGLGETBOOLEANVPROC) load(userptr, \"glGetBooleanv\");\n    context->GetClipPlane = (PFNGLGETCLIPPLANEPROC) load(userptr, \"glGetClipPlane\");\n    context->GetDoublev = (PFNGLGETDOUBLEVPROC) load(userptr, \"glGetDoublev\");\n    context->GetError = (PFNGLGETERRORPROC) load(userptr, \"glGetError\");\n    context->GetFloatv = (PFNGLGETFLOATVPROC) load(userptr, \"glGetFloatv\");\n    context->GetIntegerv = (PFNGLGETINTEGERVPROC) load(userptr, \"glGetIntegerv\");\n    context->GetLightfv = (PFNGLGETLIGHTFVPROC) load(userptr, \"glGetLightfv\");\n    context->GetLightiv = (PFNGLGETLIGHTIVPROC) load(userptr, \"glGetLightiv\");\n    context->GetMapdv = (PFNGLGETMAPDVPROC) load(userptr, \"glGetMapdv\");\n    context->GetMapfv = (PFNGLGETMAPFVPROC) load(userptr, \"glGetMapfv\");\n    context->GetMapiv = (PFNGLGETMAPIVPROC) load(userptr, \"glGetMapiv\");\n    context->GetMaterialfv = (PFNGLGETMATERIALFVPROC) load(userptr, \"glGetMaterialfv\");\n    context->GetMaterialiv = (PFNGLGETMATERIALIVPROC) load(userptr, \"glGetMaterialiv\");\n    context->GetPixelMapfv = (PFNGLGETPIXELMAPFVPROC) load(userptr, \"glGetPixelMapfv\");\n    context->GetPixelMapuiv = (PFNGLGETPIXELMAPUIVPROC) load(userptr, \"glGetPixelMapuiv\");\n    context->GetPixelMapusv = (PFNGLGETPIXELMAPUSVPROC) load(userptr, \"glGetPixelMapusv\");\n    context->GetPolygonStipple = (PFNGLGETPOLYGONSTIPPLEPROC) load(userptr, \"glGetPolygonStipple\");\n    context->GetString = (PFNGLGETSTRINGPROC) load(userptr, \"glGetString\");\n    context->GetTexEnvfv = (PFNGLGETTEXENVFVPROC) load(userptr, \"glGetTexEnvfv\");\n    context->GetTexEnviv = (PFNGLGETTEXENVIVPROC) load(userptr, \"glGetTexEnviv\");\n    context->GetTexGendv = (PFNGLGETTEXGENDVPROC) load(userptr, \"glGetTexGendv\");\n    context->GetTexGenfv = (PFNGLGETTEXGENFVPROC) load(userptr, \"glGetTexGenfv\");\n    context->GetTexGeniv = (PFNGLGETTEXGENIVPROC) load(userptr, \"glGetTexGeniv\");\n    context->GetTexImage = (PFNGLGETTEXIMAGEPROC) load(userptr, \"glGetTexImage\");\n    context->GetTexLevelParameterfv = (PFNGLGETTEXLEVELPARAMETERFVPROC) load(userptr, \"glGetTexLevelParameterfv\");\n    context->GetTexLevelParameteriv = (PFNGLGETTEXLEVELPARAMETERIVPROC) load(userptr, \"glGetTexLevelParameteriv\");\n    context->GetTexParameterfv = (PFNGLGETTEXPARAMETERFVPROC) load(userptr, \"glGetTexParameterfv\");\n    context->GetTexParameteriv = (PFNGLGETTEXPARAMETERIVPROC) load(userptr, \"glGetTexParameteriv\");\n    context->Hint = (PFNGLHINTPROC) load(userptr, \"glHint\");\n    context->IndexMask = (PFNGLINDEXMASKPROC) load(userptr, \"glIndexMask\");\n    context->Indexd = (PFNGLINDEXDPROC) load(userptr, \"glIndexd\");\n    context->Indexdv = (PFNGLINDEXDVPROC) load(userptr, \"glIndexdv\");\n    context->Indexf = (PFNGLINDEXFPROC) load(userptr, \"glIndexf\");\n    context->Indexfv = (PFNGLINDEXFVPROC) load(userptr, \"glIndexfv\");\n    context->Indexi = (PFNGLINDEXIPROC) load(userptr, \"glIndexi\");\n    context->Indexiv = (PFNGLINDEXIVPROC) load(userptr, \"glIndexiv\");\n    context->Indexs = (PFNGLINDEXSPROC) load(userptr, \"glIndexs\");\n    context->Indexsv = (PFNGLINDEXSVPROC) load(userptr, \"glIndexsv\");\n    context->InitNames = (PFNGLINITNAMESPROC) load(userptr, \"glInitNames\");\n    context->IsEnabled = (PFNGLISENABLEDPROC) load(userptr, \"glIsEnabled\");\n    context->IsList = (PFNGLISLISTPROC) load(userptr, \"glIsList\");\n    context->LightModelf = (PFNGLLIGHTMODELFPROC) load(userptr, \"glLightModelf\");\n    context->LightModelfv = (PFNGLLIGHTMODELFVPROC) load(userptr, \"glLightModelfv\");\n    context->LightModeli = (PFNGLLIGHTMODELIPROC) load(userptr, \"glLightModeli\");\n    context->LightModeliv = (PFNGLLIGHTMODELIVPROC) load(userptr, \"glLightModeliv\");\n    context->Lightf = (PFNGLLIGHTFPROC) load(userptr, \"glLightf\");\n    context->Lightfv = (PFNGLLIGHTFVPROC) load(userptr, \"glLightfv\");\n    context->Lighti = (PFNGLLIGHTIPROC) load(userptr, \"glLighti\");\n    context->Lightiv = (PFNGLLIGHTIVPROC) load(userptr, \"glLightiv\");\n    context->LineStipple = (PFNGLLINESTIPPLEPROC) load(userptr, \"glLineStipple\");\n    context->LineWidth = (PFNGLLINEWIDTHPROC) load(userptr, \"glLineWidth\");\n    context->ListBase = (PFNGLLISTBASEPROC) load(userptr, \"glListBase\");\n    context->LoadIdentity = (PFNGLLOADIDENTITYPROC) load(userptr, \"glLoadIdentity\");\n    context->LoadMatrixd = (PFNGLLOADMATRIXDPROC) load(userptr, \"glLoadMatrixd\");\n    context->LoadMatrixf = (PFNGLLOADMATRIXFPROC) load(userptr, \"glLoadMatrixf\");\n    context->LoadName = (PFNGLLOADNAMEPROC) load(userptr, \"glLoadName\");\n    context->LogicOp = (PFNGLLOGICOPPROC) load(userptr, \"glLogicOp\");\n    context->Map1d = (PFNGLMAP1DPROC) load(userptr, \"glMap1d\");\n    context->Map1f = (PFNGLMAP1FPROC) load(userptr, \"glMap1f\");\n    context->Map2d = (PFNGLMAP2DPROC) load(userptr, \"glMap2d\");\n    context->Map2f = (PFNGLMAP2FPROC) load(userptr, \"glMap2f\");\n    context->MapGrid1d = (PFNGLMAPGRID1DPROC) load(userptr, \"glMapGrid1d\");\n    context->MapGrid1f = (PFNGLMAPGRID1FPROC) load(userptr, \"glMapGrid1f\");\n    context->MapGrid2d = (PFNGLMAPGRID2DPROC) load(userptr, \"glMapGrid2d\");\n    context->MapGrid2f = (PFNGLMAPGRID2FPROC) load(userptr, \"glMapGrid2f\");\n    context->Materialf = (PFNGLMATERIALFPROC) load(userptr, \"glMaterialf\");\n    context->Materialfv = (PFNGLMATERIALFVPROC) load(userptr, \"glMaterialfv\");\n    context->Materiali = (PFNGLMATERIALIPROC) load(userptr, \"glMateriali\");\n    context->Materialiv = (PFNGLMATERIALIVPROC) load(userptr, \"glMaterialiv\");\n    context->MatrixMode = (PFNGLMATRIXMODEPROC) load(userptr, \"glMatrixMode\");\n    context->MultMatrixd = (PFNGLMULTMATRIXDPROC) load(userptr, \"glMultMatrixd\");\n    context->MultMatrixf = (PFNGLMULTMATRIXFPROC) load(userptr, \"glMultMatrixf\");\n    context->NewList = (PFNGLNEWLISTPROC) load(userptr, \"glNewList\");\n    context->Normal3b = (PFNGLNORMAL3BPROC) load(userptr, \"glNormal3b\");\n    context->Normal3bv = (PFNGLNORMAL3BVPROC) load(userptr, \"glNormal3bv\");\n    context->Normal3d = (PFNGLNORMAL3DPROC) load(userptr, \"glNormal3d\");\n    context->Normal3dv = (PFNGLNORMAL3DVPROC) load(userptr, \"glNormal3dv\");\n    context->Normal3f = (PFNGLNORMAL3FPROC) load(userptr, \"glNormal3f\");\n    context->Normal3fv = (PFNGLNORMAL3FVPROC) load(userptr, \"glNormal3fv\");\n    context->Normal3i = (PFNGLNORMAL3IPROC) load(userptr, \"glNormal3i\");\n    context->Normal3iv = (PFNGLNORMAL3IVPROC) load(userptr, \"glNormal3iv\");\n    context->Normal3s = (PFNGLNORMAL3SPROC) load(userptr, \"glNormal3s\");\n    context->Normal3sv = (PFNGLNORMAL3SVPROC) load(userptr, \"glNormal3sv\");\n    context->Ortho = (PFNGLORTHOPROC) load(userptr, \"glOrtho\");\n    context->PassThrough = (PFNGLPASSTHROUGHPROC) load(userptr, \"glPassThrough\");\n    context->PixelMapfv = (PFNGLPIXELMAPFVPROC) load(userptr, \"glPixelMapfv\");\n    context->PixelMapuiv = (PFNGLPIXELMAPUIVPROC) load(userptr, \"glPixelMapuiv\");\n    context->PixelMapusv = (PFNGLPIXELMAPUSVPROC) load(userptr, \"glPixelMapusv\");\n    context->PixelStoref = (PFNGLPIXELSTOREFPROC) load(userptr, \"glPixelStoref\");\n    context->PixelStorei = (PFNGLPIXELSTOREIPROC) load(userptr, \"glPixelStorei\");\n    context->PixelTransferf = (PFNGLPIXELTRANSFERFPROC) load(userptr, \"glPixelTransferf\");\n    context->PixelTransferi = (PFNGLPIXELTRANSFERIPROC) load(userptr, \"glPixelTransferi\");\n    context->PixelZoom = (PFNGLPIXELZOOMPROC) load(userptr, \"glPixelZoom\");\n    context->PointSize = (PFNGLPOINTSIZEPROC) load(userptr, \"glPointSize\");\n    context->PolygonMode = (PFNGLPOLYGONMODEPROC) load(userptr, \"glPolygonMode\");\n    context->PolygonStipple = (PFNGLPOLYGONSTIPPLEPROC) load(userptr, \"glPolygonStipple\");\n    context->PopAttrib = (PFNGLPOPATTRIBPROC) load(userptr, \"glPopAttrib\");\n    context->PopMatrix = (PFNGLPOPMATRIXPROC) load(userptr, \"glPopMatrix\");\n    context->PopName = (PFNGLPOPNAMEPROC) load(userptr, \"glPopName\");\n    context->PushAttrib = (PFNGLPUSHATTRIBPROC) load(userptr, \"glPushAttrib\");\n    context->PushMatrix = (PFNGLPUSHMATRIXPROC) load(userptr, \"glPushMatrix\");\n    context->PushName = (PFNGLPUSHNAMEPROC) load(userptr, \"glPushName\");\n    context->RasterPos2d = (PFNGLRASTERPOS2DPROC) load(userptr, \"glRasterPos2d\");\n    context->RasterPos2dv = (PFNGLRASTERPOS2DVPROC) load(userptr, \"glRasterPos2dv\");\n    context->RasterPos2f = (PFNGLRASTERPOS2FPROC) load(userptr, \"glRasterPos2f\");\n    context->RasterPos2fv = (PFNGLRASTERPOS2FVPROC) load(userptr, \"glRasterPos2fv\");\n    context->RasterPos2i = (PFNGLRASTERPOS2IPROC) load(userptr, \"glRasterPos2i\");\n    context->RasterPos2iv = (PFNGLRASTERPOS2IVPROC) load(userptr, \"glRasterPos2iv\");\n    context->RasterPos2s = (PFNGLRASTERPOS2SPROC) load(userptr, \"glRasterPos2s\");\n    context->RasterPos2sv = (PFNGLRASTERPOS2SVPROC) load(userptr, \"glRasterPos2sv\");\n    context->RasterPos3d = (PFNGLRASTERPOS3DPROC) load(userptr, \"glRasterPos3d\");\n    context->RasterPos3dv = (PFNGLRASTERPOS3DVPROC) load(userptr, \"glRasterPos3dv\");\n    context->RasterPos3f = (PFNGLRASTERPOS3FPROC) load(userptr, \"glRasterPos3f\");\n    context->RasterPos3fv = (PFNGLRASTERPOS3FVPROC) load(userptr, \"glRasterPos3fv\");\n    context->RasterPos3i = (PFNGLRASTERPOS3IPROC) load(userptr, \"glRasterPos3i\");\n    context->RasterPos3iv = (PFNGLRASTERPOS3IVPROC) load(userptr, \"glRasterPos3iv\");\n    context->RasterPos3s = (PFNGLRASTERPOS3SPROC) load(userptr, \"glRasterPos3s\");\n    context->RasterPos3sv = (PFNGLRASTERPOS3SVPROC) load(userptr, \"glRasterPos3sv\");\n    context->RasterPos4d = (PFNGLRASTERPOS4DPROC) load(userptr, \"glRasterPos4d\");\n    context->RasterPos4dv = (PFNGLRASTERPOS4DVPROC) load(userptr, \"glRasterPos4dv\");\n    context->RasterPos4f = (PFNGLRASTERPOS4FPROC) load(userptr, \"glRasterPos4f\");\n    context->RasterPos4fv = (PFNGLRASTERPOS4FVPROC) load(userptr, \"glRasterPos4fv\");\n    context->RasterPos4i = (PFNGLRASTERPOS4IPROC) load(userptr, \"glRasterPos4i\");\n    context->RasterPos4iv = (PFNGLRASTERPOS4IVPROC) load(userptr, \"glRasterPos4iv\");\n    context->RasterPos4s = (PFNGLRASTERPOS4SPROC) load(userptr, \"glRasterPos4s\");\n    context->RasterPos4sv = (PFNGLRASTERPOS4SVPROC) load(userptr, \"glRasterPos4sv\");\n    context->ReadBuffer = (PFNGLREADBUFFERPROC) load(userptr, \"glReadBuffer\");\n    context->ReadPixels = (PFNGLREADPIXELSPROC) load(userptr, \"glReadPixels\");\n    context->Rectd = (PFNGLRECTDPROC) load(userptr, \"glRectd\");\n    context->Rectdv = (PFNGLRECTDVPROC) load(userptr, \"glRectdv\");\n    context->Rectf = (PFNGLRECTFPROC) load(userptr, \"glRectf\");\n    context->Rectfv = (PFNGLRECTFVPROC) load(userptr, \"glRectfv\");\n    context->Recti = (PFNGLRECTIPROC) load(userptr, \"glRecti\");\n    context->Rectiv = (PFNGLRECTIVPROC) load(userptr, \"glRectiv\");\n    context->Rects = (PFNGLRECTSPROC) load(userptr, \"glRects\");\n    context->Rectsv = (PFNGLRECTSVPROC) load(userptr, \"glRectsv\");\n    context->RenderMode = (PFNGLRENDERMODEPROC) load(userptr, \"glRenderMode\");\n    context->Rotated = (PFNGLROTATEDPROC) load(userptr, \"glRotated\");\n    context->Rotatef = (PFNGLROTATEFPROC) load(userptr, \"glRotatef\");\n    context->Scaled = (PFNGLSCALEDPROC) load(userptr, \"glScaled\");\n    context->Scalef = (PFNGLSCALEFPROC) load(userptr, \"glScalef\");\n    context->Scissor = (PFNGLSCISSORPROC) load(userptr, \"glScissor\");\n    context->SelectBuffer = (PFNGLSELECTBUFFERPROC) load(userptr, \"glSelectBuffer\");\n    context->ShadeModel = (PFNGLSHADEMODELPROC) load(userptr, \"glShadeModel\");\n    context->StencilFunc = (PFNGLSTENCILFUNCPROC) load(userptr, \"glStencilFunc\");\n    context->StencilMask = (PFNGLSTENCILMASKPROC) load(userptr, \"glStencilMask\");\n    context->StencilOp = (PFNGLSTENCILOPPROC) load(userptr, \"glStencilOp\");\n    context->TexCoord1d = (PFNGLTEXCOORD1DPROC) load(userptr, \"glTexCoord1d\");\n    context->TexCoord1dv = (PFNGLTEXCOORD1DVPROC) load(userptr, \"glTexCoord1dv\");\n    context->TexCoord1f = (PFNGLTEXCOORD1FPROC) load(userptr, \"glTexCoord1f\");\n    context->TexCoord1fv = (PFNGLTEXCOORD1FVPROC) load(userptr, \"glTexCoord1fv\");\n    context->TexCoord1i = (PFNGLTEXCOORD1IPROC) load(userptr, \"glTexCoord1i\");\n    context->TexCoord1iv = (PFNGLTEXCOORD1IVPROC) load(userptr, \"glTexCoord1iv\");\n    context->TexCoord1s = (PFNGLTEXCOORD1SPROC) load(userptr, \"glTexCoord1s\");\n    context->TexCoord1sv = (PFNGLTEXCOORD1SVPROC) load(userptr, \"glTexCoord1sv\");\n    context->TexCoord2d = (PFNGLTEXCOORD2DPROC) load(userptr, \"glTexCoord2d\");\n    context->TexCoord2dv = (PFNGLTEXCOORD2DVPROC) load(userptr, \"glTexCoord2dv\");\n    context->TexCoord2f = (PFNGLTEXCOORD2FPROC) load(userptr, \"glTexCoord2f\");\n    context->TexCoord2fv = (PFNGLTEXCOORD2FVPROC) load(userptr, \"glTexCoord2fv\");\n    context->TexCoord2i = (PFNGLTEXCOORD2IPROC) load(userptr, \"glTexCoord2i\");\n    context->TexCoord2iv = (PFNGLTEXCOORD2IVPROC) load(userptr, \"glTexCoord2iv\");\n    context->TexCoord2s = (PFNGLTEXCOORD2SPROC) load(userptr, \"glTexCoord2s\");\n    context->TexCoord2sv = (PFNGLTEXCOORD2SVPROC) load(userptr, \"glTexCoord2sv\");\n    context->TexCoord3d = (PFNGLTEXCOORD3DPROC) load(userptr, \"glTexCoord3d\");\n    context->TexCoord3dv = (PFNGLTEXCOORD3DVPROC) load(userptr, \"glTexCoord3dv\");\n    context->TexCoord3f = (PFNGLTEXCOORD3FPROC) load(userptr, \"glTexCoord3f\");\n    context->TexCoord3fv = (PFNGLTEXCOORD3FVPROC) load(userptr, \"glTexCoord3fv\");\n    context->TexCoord3i = (PFNGLTEXCOORD3IPROC) load(userptr, \"glTexCoord3i\");\n    context->TexCoord3iv = (PFNGLTEXCOORD3IVPROC) load(userptr, \"glTexCoord3iv\");\n    context->TexCoord3s = (PFNGLTEXCOORD3SPROC) load(userptr, \"glTexCoord3s\");\n    context->TexCoord3sv = (PFNGLTEXCOORD3SVPROC) load(userptr, \"glTexCoord3sv\");\n    context->TexCoord4d = (PFNGLTEXCOORD4DPROC) load(userptr, \"glTexCoord4d\");\n    context->TexCoord4dv = (PFNGLTEXCOORD4DVPROC) load(userptr, \"glTexCoord4dv\");\n    context->TexCoord4f = (PFNGLTEXCOORD4FPROC) load(userptr, \"glTexCoord4f\");\n    context->TexCoord4fv = (PFNGLTEXCOORD4FVPROC) load(userptr, \"glTexCoord4fv\");\n    context->TexCoord4i = (PFNGLTEXCOORD4IPROC) load(userptr, \"glTexCoord4i\");\n    context->TexCoord4iv = (PFNGLTEXCOORD4IVPROC) load(userptr, \"glTexCoord4iv\");\n    context->TexCoord4s = (PFNGLTEXCOORD4SPROC) load(userptr, \"glTexCoord4s\");\n    context->TexCoord4sv = (PFNGLTEXCOORD4SVPROC) load(userptr, \"glTexCoord4sv\");\n    context->TexEnvf = (PFNGLTEXENVFPROC) load(userptr, \"glTexEnvf\");\n    context->TexEnvfv = (PFNGLTEXENVFVPROC) load(userptr, \"glTexEnvfv\");\n    context->TexEnvi = (PFNGLTEXENVIPROC) load(userptr, \"glTexEnvi\");\n    context->TexEnviv = (PFNGLTEXENVIVPROC) load(userptr, \"glTexEnviv\");\n    context->TexGend = (PFNGLTEXGENDPROC) load(userptr, \"glTexGend\");\n    context->TexGendv = (PFNGLTEXGENDVPROC) load(userptr, \"glTexGendv\");\n    context->TexGenf = (PFNGLTEXGENFPROC) load(userptr, \"glTexGenf\");\n    context->TexGenfv = (PFNGLTEXGENFVPROC) load(userptr, \"glTexGenfv\");\n    context->TexGeni = (PFNGLTEXGENIPROC) load(userptr, \"glTexGeni\");\n    context->TexGeniv = (PFNGLTEXGENIVPROC) load(userptr, \"glTexGeniv\");\n    context->TexImage1D = (PFNGLTEXIMAGE1DPROC) load(userptr, \"glTexImage1D\");\n    context->TexImage2D = (PFNGLTEXIMAGE2DPROC) load(userptr, \"glTexImage2D\");\n    context->TexParameterf = (PFNGLTEXPARAMETERFPROC) load(userptr, \"glTexParameterf\");\n    context->TexParameterfv = (PFNGLTEXPARAMETERFVPROC) load(userptr, \"glTexParameterfv\");\n    context->TexParameteri = (PFNGLTEXPARAMETERIPROC) load(userptr, \"glTexParameteri\");\n    context->TexParameteriv = (PFNGLTEXPARAMETERIVPROC) load(userptr, \"glTexParameteriv\");\n    context->Translated = (PFNGLTRANSLATEDPROC) load(userptr, \"glTranslated\");\n    context->Translatef = (PFNGLTRANSLATEFPROC) load(userptr, \"glTranslatef\");\n    context->Vertex2d = (PFNGLVERTEX2DPROC) load(userptr, \"glVertex2d\");\n    context->Vertex2dv = (PFNGLVERTEX2DVPROC) load(userptr, \"glVertex2dv\");\n    context->Vertex2f = (PFNGLVERTEX2FPROC) load(userptr, \"glVertex2f\");\n    context->Vertex2fv = (PFNGLVERTEX2FVPROC) load(userptr, \"glVertex2fv\");\n    context->Vertex2i = (PFNGLVERTEX2IPROC) load(userptr, \"glVertex2i\");\n    context->Vertex2iv = (PFNGLVERTEX2IVPROC) load(userptr, \"glVertex2iv\");\n    context->Vertex2s = (PFNGLVERTEX2SPROC) load(userptr, \"glVertex2s\");\n    context->Vertex2sv = (PFNGLVERTEX2SVPROC) load(userptr, \"glVertex2sv\");\n    context->Vertex3d = (PFNGLVERTEX3DPROC) load(userptr, \"glVertex3d\");\n    context->Vertex3dv = (PFNGLVERTEX3DVPROC) load(userptr, \"glVertex3dv\");\n    context->Vertex3f = (PFNGLVERTEX3FPROC) load(userptr, \"glVertex3f\");\n    context->Vertex3fv = (PFNGLVERTEX3FVPROC) load(userptr, \"glVertex3fv\");\n    context->Vertex3i = (PFNGLVERTEX3IPROC) load(userptr, \"glVertex3i\");\n    context->Vertex3iv = (PFNGLVERTEX3IVPROC) load(userptr, \"glVertex3iv\");\n    context->Vertex3s = (PFNGLVERTEX3SPROC) load(userptr, \"glVertex3s\");\n    context->Vertex3sv = (PFNGLVERTEX3SVPROC) load(userptr, \"glVertex3sv\");\n    context->Vertex4d = (PFNGLVERTEX4DPROC) load(userptr, \"glVertex4d\");\n    context->Vertex4dv = (PFNGLVERTEX4DVPROC) load(userptr, \"glVertex4dv\");\n    context->Vertex4f = (PFNGLVERTEX4FPROC) load(userptr, \"glVertex4f\");\n    context->Vertex4fv = (PFNGLVERTEX4FVPROC) load(userptr, \"glVertex4fv\");\n    context->Vertex4i = (PFNGLVERTEX4IPROC) load(userptr, \"glVertex4i\");\n    context->Vertex4iv = (PFNGLVERTEX4IVPROC) load(userptr, \"glVertex4iv\");\n    context->Vertex4s = (PFNGLVERTEX4SPROC) load(userptr, \"glVertex4s\");\n    context->Vertex4sv = (PFNGLVERTEX4SVPROC) load(userptr, \"glVertex4sv\");\n    context->Viewport = (PFNGLVIEWPORTPROC) load(userptr, \"glViewport\");\n}\nstatic void glad_gl_load_GL_VERSION_1_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_1_1) return;\n    context->AreTexturesResident = (PFNGLARETEXTURESRESIDENTPROC) load(userptr, \"glAreTexturesResident\");\n    context->ArrayElement = (PFNGLARRAYELEMENTPROC) load(userptr, \"glArrayElement\");\n    context->BindTexture = (PFNGLBINDTEXTUREPROC) load(userptr, \"glBindTexture\");\n    context->ColorPointer = (PFNGLCOLORPOINTERPROC) load(userptr, \"glColorPointer\");\n    context->CopyTexImage1D = (PFNGLCOPYTEXIMAGE1DPROC) load(userptr, \"glCopyTexImage1D\");\n    context->CopyTexImage2D = (PFNGLCOPYTEXIMAGE2DPROC) load(userptr, \"glCopyTexImage2D\");\n    context->CopyTexSubImage1D = (PFNGLCOPYTEXSUBIMAGE1DPROC) load(userptr, \"glCopyTexSubImage1D\");\n    context->CopyTexSubImage2D = (PFNGLCOPYTEXSUBIMAGE2DPROC) load(userptr, \"glCopyTexSubImage2D\");\n    context->DeleteTextures = (PFNGLDELETETEXTURESPROC) load(userptr, \"glDeleteTextures\");\n    context->DisableClientState = (PFNGLDISABLECLIENTSTATEPROC) load(userptr, \"glDisableClientState\");\n    context->DrawArrays = (PFNGLDRAWARRAYSPROC) load(userptr, \"glDrawArrays\");\n    context->DrawElements = (PFNGLDRAWELEMENTSPROC) load(userptr, \"glDrawElements\");\n    context->EdgeFlagPointer = (PFNGLEDGEFLAGPOINTERPROC) load(userptr, \"glEdgeFlagPointer\");\n    context->EnableClientState = (PFNGLENABLECLIENTSTATEPROC) load(userptr, \"glEnableClientState\");\n    context->GenTextures = (PFNGLGENTEXTURESPROC) load(userptr, \"glGenTextures\");\n    context->GetPointerv = (PFNGLGETPOINTERVPROC) load(userptr, \"glGetPointerv\");\n    context->IndexPointer = (PFNGLINDEXPOINTERPROC) load(userptr, \"glIndexPointer\");\n    context->Indexub = (PFNGLINDEXUBPROC) load(userptr, \"glIndexub\");\n    context->Indexubv = (PFNGLINDEXUBVPROC) load(userptr, \"glIndexubv\");\n    context->InterleavedArrays = (PFNGLINTERLEAVEDARRAYSPROC) load(userptr, \"glInterleavedArrays\");\n    context->IsTexture = (PFNGLISTEXTUREPROC) load(userptr, \"glIsTexture\");\n    context->NormalPointer = (PFNGLNORMALPOINTERPROC) load(userptr, \"glNormalPointer\");\n    context->PolygonOffset = (PFNGLPOLYGONOFFSETPROC) load(userptr, \"glPolygonOffset\");\n    context->PopClientAttrib = (PFNGLPOPCLIENTATTRIBPROC) load(userptr, \"glPopClientAttrib\");\n    context->PrioritizeTextures = (PFNGLPRIORITIZETEXTURESPROC) load(userptr, \"glPrioritizeTextures\");\n    context->PushClientAttrib = (PFNGLPUSHCLIENTATTRIBPROC) load(userptr, \"glPushClientAttrib\");\n    context->TexCoordPointer = (PFNGLTEXCOORDPOINTERPROC) load(userptr, \"glTexCoordPointer\");\n    context->TexSubImage1D = (PFNGLTEXSUBIMAGE1DPROC) load(userptr, \"glTexSubImage1D\");\n    context->TexSubImage2D = (PFNGLTEXSUBIMAGE2DPROC) load(userptr, \"glTexSubImage2D\");\n    context->VertexPointer = (PFNGLVERTEXPOINTERPROC) load(userptr, \"glVertexPointer\");\n}\nstatic void glad_gl_load_GL_VERSION_1_2(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_1_2) return;\n    context->CopyTexSubImage3D = (PFNGLCOPYTEXSUBIMAGE3DPROC) load(userptr, \"glCopyTexSubImage3D\");\n    context->DrawRangeElements = (PFNGLDRAWRANGEELEMENTSPROC) load(userptr, \"glDrawRangeElements\");\n    context->TexImage3D = (PFNGLTEXIMAGE3DPROC) load(userptr, \"glTexImage3D\");\n    context->TexSubImage3D = (PFNGLTEXSUBIMAGE3DPROC) load(userptr, \"glTexSubImage3D\");\n}\nstatic void glad_gl_load_GL_VERSION_1_3(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_1_3) return;\n    context->ActiveTexture = (PFNGLACTIVETEXTUREPROC) load(userptr, \"glActiveTexture\");\n    context->ClientActiveTexture = (PFNGLCLIENTACTIVETEXTUREPROC) load(userptr, \"glClientActiveTexture\");\n    context->CompressedTexImage1D = (PFNGLCOMPRESSEDTEXIMAGE1DPROC) load(userptr, \"glCompressedTexImage1D\");\n    context->CompressedTexImage2D = (PFNGLCOMPRESSEDTEXIMAGE2DPROC) load(userptr, \"glCompressedTexImage2D\");\n    context->CompressedTexImage3D = (PFNGLCOMPRESSEDTEXIMAGE3DPROC) load(userptr, \"glCompressedTexImage3D\");\n    context->CompressedTexSubImage1D = (PFNGLCOMPRESSEDTEXSUBIMAGE1DPROC) load(userptr, \"glCompressedTexSubImage1D\");\n    context->CompressedTexSubImage2D = (PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC) load(userptr, \"glCompressedTexSubImage2D\");\n    context->CompressedTexSubImage3D = (PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC) load(userptr, \"glCompressedTexSubImage3D\");\n    context->GetCompressedTexImage = (PFNGLGETCOMPRESSEDTEXIMAGEPROC) load(userptr, \"glGetCompressedTexImage\");\n    context->LoadTransposeMatrixd = (PFNGLLOADTRANSPOSEMATRIXDPROC) load(userptr, \"glLoadTransposeMatrixd\");\n    context->LoadTransposeMatrixf = (PFNGLLOADTRANSPOSEMATRIXFPROC) load(userptr, \"glLoadTransposeMatrixf\");\n    context->MultTransposeMatrixd = (PFNGLMULTTRANSPOSEMATRIXDPROC) load(userptr, \"glMultTransposeMatrixd\");\n    context->MultTransposeMatrixf = (PFNGLMULTTRANSPOSEMATRIXFPROC) load(userptr, \"glMultTransposeMatrixf\");\n    context->MultiTexCoord1d = (PFNGLMULTITEXCOORD1DPROC) load(userptr, \"glMultiTexCoord1d\");\n    context->MultiTexCoord1dv = (PFNGLMULTITEXCOORD1DVPROC) load(userptr, \"glMultiTexCoord1dv\");\n    context->MultiTexCoord1f = (PFNGLMULTITEXCOORD1FPROC) load(userptr, \"glMultiTexCoord1f\");\n    context->MultiTexCoord1fv = (PFNGLMULTITEXCOORD1FVPROC) load(userptr, \"glMultiTexCoord1fv\");\n    context->MultiTexCoord1i = (PFNGLMULTITEXCOORD1IPROC) load(userptr, \"glMultiTexCoord1i\");\n    context->MultiTexCoord1iv = (PFNGLMULTITEXCOORD1IVPROC) load(userptr, \"glMultiTexCoord1iv\");\n    context->MultiTexCoord1s = (PFNGLMULTITEXCOORD1SPROC) load(userptr, \"glMultiTexCoord1s\");\n    context->MultiTexCoord1sv = (PFNGLMULTITEXCOORD1SVPROC) load(userptr, \"glMultiTexCoord1sv\");\n    context->MultiTexCoord2d = (PFNGLMULTITEXCOORD2DPROC) load(userptr, \"glMultiTexCoord2d\");\n    context->MultiTexCoord2dv = (PFNGLMULTITEXCOORD2DVPROC) load(userptr, \"glMultiTexCoord2dv\");\n    context->MultiTexCoord2f = (PFNGLMULTITEXCOORD2FPROC) load(userptr, \"glMultiTexCoord2f\");\n    context->MultiTexCoord2fv = (PFNGLMULTITEXCOORD2FVPROC) load(userptr, \"glMultiTexCoord2fv\");\n    context->MultiTexCoord2i = (PFNGLMULTITEXCOORD2IPROC) load(userptr, \"glMultiTexCoord2i\");\n    context->MultiTexCoord2iv = (PFNGLMULTITEXCOORD2IVPROC) load(userptr, \"glMultiTexCoord2iv\");\n    context->MultiTexCoord2s = (PFNGLMULTITEXCOORD2SPROC) load(userptr, \"glMultiTexCoord2s\");\n    context->MultiTexCoord2sv = (PFNGLMULTITEXCOORD2SVPROC) load(userptr, \"glMultiTexCoord2sv\");\n    context->MultiTexCoord3d = (PFNGLMULTITEXCOORD3DPROC) load(userptr, \"glMultiTexCoord3d\");\n    context->MultiTexCoord3dv = (PFNGLMULTITEXCOORD3DVPROC) load(userptr, \"glMultiTexCoord3dv\");\n    context->MultiTexCoord3f = (PFNGLMULTITEXCOORD3FPROC) load(userptr, \"glMultiTexCoord3f\");\n    context->MultiTexCoord3fv = (PFNGLMULTITEXCOORD3FVPROC) load(userptr, \"glMultiTexCoord3fv\");\n    context->MultiTexCoord3i = (PFNGLMULTITEXCOORD3IPROC) load(userptr, \"glMultiTexCoord3i\");\n    context->MultiTexCoord3iv = (PFNGLMULTITEXCOORD3IVPROC) load(userptr, \"glMultiTexCoord3iv\");\n    context->MultiTexCoord3s = (PFNGLMULTITEXCOORD3SPROC) load(userptr, \"glMultiTexCoord3s\");\n    context->MultiTexCoord3sv = (PFNGLMULTITEXCOORD3SVPROC) load(userptr, \"glMultiTexCoord3sv\");\n    context->MultiTexCoord4d = (PFNGLMULTITEXCOORD4DPROC) load(userptr, \"glMultiTexCoord4d\");\n    context->MultiTexCoord4dv = (PFNGLMULTITEXCOORD4DVPROC) load(userptr, \"glMultiTexCoord4dv\");\n    context->MultiTexCoord4f = (PFNGLMULTITEXCOORD4FPROC) load(userptr, \"glMultiTexCoord4f\");\n    context->MultiTexCoord4fv = (PFNGLMULTITEXCOORD4FVPROC) load(userptr, \"glMultiTexCoord4fv\");\n    context->MultiTexCoord4i = (PFNGLMULTITEXCOORD4IPROC) load(userptr, \"glMultiTexCoord4i\");\n    context->MultiTexCoord4iv = (PFNGLMULTITEXCOORD4IVPROC) load(userptr, \"glMultiTexCoord4iv\");\n    context->MultiTexCoord4s = (PFNGLMULTITEXCOORD4SPROC) load(userptr, \"glMultiTexCoord4s\");\n    context->MultiTexCoord4sv = (PFNGLMULTITEXCOORD4SVPROC) load(userptr, \"glMultiTexCoord4sv\");\n    context->SampleCoverage = (PFNGLSAMPLECOVERAGEPROC) load(userptr, \"glSampleCoverage\");\n}\nstatic void glad_gl_load_GL_VERSION_1_4(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_1_4) return;\n    context->BlendColor = (PFNGLBLENDCOLORPROC) load(userptr, \"glBlendColor\");\n    context->BlendEquation = (PFNGLBLENDEQUATIONPROC) load(userptr, \"glBlendEquation\");\n    context->BlendFuncSeparate = (PFNGLBLENDFUNCSEPARATEPROC) load(userptr, \"glBlendFuncSeparate\");\n    context->FogCoordPointer = (PFNGLFOGCOORDPOINTERPROC) load(userptr, \"glFogCoordPointer\");\n    context->FogCoordd = (PFNGLFOGCOORDDPROC) load(userptr, \"glFogCoordd\");\n    context->FogCoorddv = (PFNGLFOGCOORDDVPROC) load(userptr, \"glFogCoorddv\");\n    context->FogCoordf = (PFNGLFOGCOORDFPROC) load(userptr, \"glFogCoordf\");\n    context->FogCoordfv = (PFNGLFOGCOORDFVPROC) load(userptr, \"glFogCoordfv\");\n    context->MultiDrawArrays = (PFNGLMULTIDRAWARRAYSPROC) load(userptr, \"glMultiDrawArrays\");\n    context->MultiDrawElements = (PFNGLMULTIDRAWELEMENTSPROC) load(userptr, \"glMultiDrawElements\");\n    context->PointParameterf = (PFNGLPOINTPARAMETERFPROC) load(userptr, \"glPointParameterf\");\n    context->PointParameterfv = (PFNGLPOINTPARAMETERFVPROC) load(userptr, \"glPointParameterfv\");\n    context->PointParameteri = (PFNGLPOINTPARAMETERIPROC) load(userptr, \"glPointParameteri\");\n    context->PointParameteriv = (PFNGLPOINTPARAMETERIVPROC) load(userptr, \"glPointParameteriv\");\n    context->SecondaryColor3b = (PFNGLSECONDARYCOLOR3BPROC) load(userptr, \"glSecondaryColor3b\");\n    context->SecondaryColor3bv = (PFNGLSECONDARYCOLOR3BVPROC) load(userptr, \"glSecondaryColor3bv\");\n    context->SecondaryColor3d = (PFNGLSECONDARYCOLOR3DPROC) load(userptr, \"glSecondaryColor3d\");\n    context->SecondaryColor3dv = (PFNGLSECONDARYCOLOR3DVPROC) load(userptr, \"glSecondaryColor3dv\");\n    context->SecondaryColor3f = (PFNGLSECONDARYCOLOR3FPROC) load(userptr, \"glSecondaryColor3f\");\n    context->SecondaryColor3fv = (PFNGLSECONDARYCOLOR3FVPROC) load(userptr, \"glSecondaryColor3fv\");\n    context->SecondaryColor3i = (PFNGLSECONDARYCOLOR3IPROC) load(userptr, \"glSecondaryColor3i\");\n    context->SecondaryColor3iv = (PFNGLSECONDARYCOLOR3IVPROC) load(userptr, \"glSecondaryColor3iv\");\n    context->SecondaryColor3s = (PFNGLSECONDARYCOLOR3SPROC) load(userptr, \"glSecondaryColor3s\");\n    context->SecondaryColor3sv = (PFNGLSECONDARYCOLOR3SVPROC) load(userptr, \"glSecondaryColor3sv\");\n    context->SecondaryColor3ub = (PFNGLSECONDARYCOLOR3UBPROC) load(userptr, \"glSecondaryColor3ub\");\n    context->SecondaryColor3ubv = (PFNGLSECONDARYCOLOR3UBVPROC) load(userptr, \"glSecondaryColor3ubv\");\n    context->SecondaryColor3ui = (PFNGLSECONDARYCOLOR3UIPROC) load(userptr, \"glSecondaryColor3ui\");\n    context->SecondaryColor3uiv = (PFNGLSECONDARYCOLOR3UIVPROC) load(userptr, \"glSecondaryColor3uiv\");\n    context->SecondaryColor3us = (PFNGLSECONDARYCOLOR3USPROC) load(userptr, \"glSecondaryColor3us\");\n    context->SecondaryColor3usv = (PFNGLSECONDARYCOLOR3USVPROC) load(userptr, \"glSecondaryColor3usv\");\n    context->SecondaryColorPointer = (PFNGLSECONDARYCOLORPOINTERPROC) load(userptr, \"glSecondaryColorPointer\");\n    context->WindowPos2d = (PFNGLWINDOWPOS2DPROC) load(userptr, \"glWindowPos2d\");\n    context->WindowPos2dv = (PFNGLWINDOWPOS2DVPROC) load(userptr, \"glWindowPos2dv\");\n    context->WindowPos2f = (PFNGLWINDOWPOS2FPROC) load(userptr, \"glWindowPos2f\");\n    context->WindowPos2fv = (PFNGLWINDOWPOS2FVPROC) load(userptr, \"glWindowPos2fv\");\n    context->WindowPos2i = (PFNGLWINDOWPOS2IPROC) load(userptr, \"glWindowPos2i\");\n    context->WindowPos2iv = (PFNGLWINDOWPOS2IVPROC) load(userptr, \"glWindowPos2iv\");\n    context->WindowPos2s = (PFNGLWINDOWPOS2SPROC) load(userptr, \"glWindowPos2s\");\n    context->WindowPos2sv = (PFNGLWINDOWPOS2SVPROC) load(userptr, \"glWindowPos2sv\");\n    context->WindowPos3d = (PFNGLWINDOWPOS3DPROC) load(userptr, \"glWindowPos3d\");\n    context->WindowPos3dv = (PFNGLWINDOWPOS3DVPROC) load(userptr, \"glWindowPos3dv\");\n    context->WindowPos3f = (PFNGLWINDOWPOS3FPROC) load(userptr, \"glWindowPos3f\");\n    context->WindowPos3fv = (PFNGLWINDOWPOS3FVPROC) load(userptr, \"glWindowPos3fv\");\n    context->WindowPos3i = (PFNGLWINDOWPOS3IPROC) load(userptr, \"glWindowPos3i\");\n    context->WindowPos3iv = (PFNGLWINDOWPOS3IVPROC) load(userptr, \"glWindowPos3iv\");\n    context->WindowPos3s = (PFNGLWINDOWPOS3SPROC) load(userptr, \"glWindowPos3s\");\n    context->WindowPos3sv = (PFNGLWINDOWPOS3SVPROC) load(userptr, \"glWindowPos3sv\");\n}\nstatic void glad_gl_load_GL_VERSION_1_5(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_1_5) return;\n    context->BeginQuery = (PFNGLBEGINQUERYPROC) load(userptr, \"glBeginQuery\");\n    context->BindBuffer = (PFNGLBINDBUFFERPROC) load(userptr, \"glBindBuffer\");\n    context->BufferData = (PFNGLBUFFERDATAPROC) load(userptr, \"glBufferData\");\n    context->BufferSubData = (PFNGLBUFFERSUBDATAPROC) load(userptr, \"glBufferSubData\");\n    context->DeleteBuffers = (PFNGLDELETEBUFFERSPROC) load(userptr, \"glDeleteBuffers\");\n    context->DeleteQueries = (PFNGLDELETEQUERIESPROC) load(userptr, \"glDeleteQueries\");\n    context->EndQuery = (PFNGLENDQUERYPROC) load(userptr, \"glEndQuery\");\n    context->GenBuffers = (PFNGLGENBUFFERSPROC) load(userptr, \"glGenBuffers\");\n    context->GenQueries = (PFNGLGENQUERIESPROC) load(userptr, \"glGenQueries\");\n    context->GetBufferParameteriv = (PFNGLGETBUFFERPARAMETERIVPROC) load(userptr, \"glGetBufferParameteriv\");\n    context->GetBufferPointerv = (PFNGLGETBUFFERPOINTERVPROC) load(userptr, \"glGetBufferPointerv\");\n    context->GetBufferSubData = (PFNGLGETBUFFERSUBDATAPROC) load(userptr, \"glGetBufferSubData\");\n    context->GetQueryObjectiv = (PFNGLGETQUERYOBJECTIVPROC) load(userptr, \"glGetQueryObjectiv\");\n    context->GetQueryObjectuiv = (PFNGLGETQUERYOBJECTUIVPROC) load(userptr, \"glGetQueryObjectuiv\");\n    context->GetQueryiv = (PFNGLGETQUERYIVPROC) load(userptr, \"glGetQueryiv\");\n    context->IsBuffer = (PFNGLISBUFFERPROC) load(userptr, \"glIsBuffer\");\n    context->IsQuery = (PFNGLISQUERYPROC) load(userptr, \"glIsQuery\");\n    context->MapBuffer = (PFNGLMAPBUFFERPROC) load(userptr, \"glMapBuffer\");\n    context->UnmapBuffer = (PFNGLUNMAPBUFFERPROC) load(userptr, \"glUnmapBuffer\");\n}\nstatic void glad_gl_load_GL_VERSION_2_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_2_0) return;\n    context->EGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC) load(userptr, \"glEGLImageTargetTexture2DOES\");\n    context->AttachShader = (PFNGLATTACHSHADERPROC) load(userptr, \"glAttachShader\");\n    context->BindAttribLocation = (PFNGLBINDATTRIBLOCATIONPROC) load(userptr, \"glBindAttribLocation\");\n    context->BlendEquationSeparate = (PFNGLBLENDEQUATIONSEPARATEPROC) load(userptr, \"glBlendEquationSeparate\");\n    context->CompileShader = (PFNGLCOMPILESHADERPROC) load(userptr, \"glCompileShader\");\n    context->CreateProgram = (PFNGLCREATEPROGRAMPROC) load(userptr, \"glCreateProgram\");\n    context->CreateShader = (PFNGLCREATESHADERPROC) load(userptr, \"glCreateShader\");\n    context->DeleteProgram = (PFNGLDELETEPROGRAMPROC) load(userptr, \"glDeleteProgram\");\n    context->DeleteShader = (PFNGLDELETESHADERPROC) load(userptr, \"glDeleteShader\");\n    context->DetachShader = (PFNGLDETACHSHADERPROC) load(userptr, \"glDetachShader\");\n    context->DisableVertexAttribArray = (PFNGLDISABLEVERTEXATTRIBARRAYPROC) load(userptr, \"glDisableVertexAttribArray\");\n    context->DrawBuffers = (PFNGLDRAWBUFFERSPROC) load(userptr, \"glDrawBuffers\");\n    context->EnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC) load(userptr, \"glEnableVertexAttribArray\");\n    context->GetActiveAttrib = (PFNGLGETACTIVEATTRIBPROC) load(userptr, \"glGetActiveAttrib\");\n    context->GetActiveUniform = (PFNGLGETACTIVEUNIFORMPROC) load(userptr, \"glGetActiveUniform\");\n    context->GetAttachedShaders = (PFNGLGETATTACHEDSHADERSPROC) load(userptr, \"glGetAttachedShaders\");\n    context->GetAttribLocation = (PFNGLGETATTRIBLOCATIONPROC) load(userptr, \"glGetAttribLocation\");\n    context->GetProgramInfoLog = (PFNGLGETPROGRAMINFOLOGPROC) load(userptr, \"glGetProgramInfoLog\");\n    context->GetProgramiv = (PFNGLGETPROGRAMIVPROC) load(userptr, \"glGetProgramiv\");\n    context->GetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC) load(userptr, \"glGetShaderInfoLog\");\n    context->GetShaderSource = (PFNGLGETSHADERSOURCEPROC) load(userptr, \"glGetShaderSource\");\n    context->GetShaderiv = (PFNGLGETSHADERIVPROC) load(userptr, \"glGetShaderiv\");\n    context->GetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC) load(userptr, \"glGetUniformLocation\");\n    context->GetUniformfv = (PFNGLGETUNIFORMFVPROC) load(userptr, \"glGetUniformfv\");\n    context->GetUniformiv = (PFNGLGETUNIFORMIVPROC) load(userptr, \"glGetUniformiv\");\n    context->GetVertexAttribPointerv = (PFNGLGETVERTEXATTRIBPOINTERVPROC) load(userptr, \"glGetVertexAttribPointerv\");\n    context->GetVertexAttribdv = (PFNGLGETVERTEXATTRIBDVPROC) load(userptr, \"glGetVertexAttribdv\");\n    context->GetVertexAttribfv = (PFNGLGETVERTEXATTRIBFVPROC) load(userptr, \"glGetVertexAttribfv\");\n    context->GetVertexAttribiv = (PFNGLGETVERTEXATTRIBIVPROC) load(userptr, \"glGetVertexAttribiv\");\n    context->IsProgram = (PFNGLISPROGRAMPROC) load(userptr, \"glIsProgram\");\n    context->IsShader = (PFNGLISSHADERPROC) load(userptr, \"glIsShader\");\n    context->LinkProgram = (PFNGLLINKPROGRAMPROC) load(userptr, \"glLinkProgram\");\n    context->ShaderSource = (PFNGLSHADERSOURCEPROC) load(userptr, \"glShaderSource\");\n    context->StencilFuncSeparate = (PFNGLSTENCILFUNCSEPARATEPROC) load(userptr, \"glStencilFuncSeparate\");\n    context->StencilMaskSeparate = (PFNGLSTENCILMASKSEPARATEPROC) load(userptr, \"glStencilMaskSeparate\");\n    context->StencilOpSeparate = (PFNGLSTENCILOPSEPARATEPROC) load(userptr, \"glStencilOpSeparate\");\n    context->Uniform1f = (PFNGLUNIFORM1FPROC) load(userptr, \"glUniform1f\");\n    context->Uniform1fv = (PFNGLUNIFORM1FVPROC) load(userptr, \"glUniform1fv\");\n    context->Uniform1i = (PFNGLUNIFORM1IPROC) load(userptr, \"glUniform1i\");\n    context->Uniform1iv = (PFNGLUNIFORM1IVPROC) load(userptr, \"glUniform1iv\");\n    context->Uniform2f = (PFNGLUNIFORM2FPROC) load(userptr, \"glUniform2f\");\n    context->Uniform2fv = (PFNGLUNIFORM2FVPROC) load(userptr, \"glUniform2fv\");\n    context->Uniform2i = (PFNGLUNIFORM2IPROC) load(userptr, \"glUniform2i\");\n    context->Uniform2iv = (PFNGLUNIFORM2IVPROC) load(userptr, \"glUniform2iv\");\n    context->Uniform3f = (PFNGLUNIFORM3FPROC) load(userptr, \"glUniform3f\");\n    context->Uniform3fv = (PFNGLUNIFORM3FVPROC) load(userptr, \"glUniform3fv\");\n    context->Uniform3i = (PFNGLUNIFORM3IPROC) load(userptr, \"glUniform3i\");\n    context->Uniform3iv = (PFNGLUNIFORM3IVPROC) load(userptr, \"glUniform3iv\");\n    context->Uniform4f = (PFNGLUNIFORM4FPROC) load(userptr, \"glUniform4f\");\n    context->Uniform4fv = (PFNGLUNIFORM4FVPROC) load(userptr, \"glUniform4fv\");\n    context->Uniform4i = (PFNGLUNIFORM4IPROC) load(userptr, \"glUniform4i\");\n    context->Uniform4iv = (PFNGLUNIFORM4IVPROC) load(userptr, \"glUniform4iv\");\n    context->UniformMatrix2fv = (PFNGLUNIFORMMATRIX2FVPROC) load(userptr, \"glUniformMatrix2fv\");\n    context->UniformMatrix3fv = (PFNGLUNIFORMMATRIX3FVPROC) load(userptr, \"glUniformMatrix3fv\");\n    context->UniformMatrix4fv = (PFNGLUNIFORMMATRIX4FVPROC) load(userptr, \"glUniformMatrix4fv\");\n    context->UseProgram = (PFNGLUSEPROGRAMPROC) load(userptr, \"glUseProgram\");\n    context->ValidateProgram = (PFNGLVALIDATEPROGRAMPROC) load(userptr, \"glValidateProgram\");\n    context->VertexAttrib1d = (PFNGLVERTEXATTRIB1DPROC) load(userptr, \"glVertexAttrib1d\");\n    context->VertexAttrib1dv = (PFNGLVERTEXATTRIB1DVPROC) load(userptr, \"glVertexAttrib1dv\");\n    context->VertexAttrib1f = (PFNGLVERTEXATTRIB1FPROC) load(userptr, \"glVertexAttrib1f\");\n    context->VertexAttrib1fv = (PFNGLVERTEXATTRIB1FVPROC) load(userptr, \"glVertexAttrib1fv\");\n    context->VertexAttrib1s = (PFNGLVERTEXATTRIB1SPROC) load(userptr, \"glVertexAttrib1s\");\n    context->VertexAttrib1sv = (PFNGLVERTEXATTRIB1SVPROC) load(userptr, \"glVertexAttrib1sv\");\n    context->VertexAttrib2d = (PFNGLVERTEXATTRIB2DPROC) load(userptr, \"glVertexAttrib2d\");\n    context->VertexAttrib2dv = (PFNGLVERTEXATTRIB2DVPROC) load(userptr, \"glVertexAttrib2dv\");\n    context->VertexAttrib2f = (PFNGLVERTEXATTRIB2FPROC) load(userptr, \"glVertexAttrib2f\");\n    context->VertexAttrib2fv = (PFNGLVERTEXATTRIB2FVPROC) load(userptr, \"glVertexAttrib2fv\");\n    context->VertexAttrib2s = (PFNGLVERTEXATTRIB2SPROC) load(userptr, \"glVertexAttrib2s\");\n    context->VertexAttrib2sv = (PFNGLVERTEXATTRIB2SVPROC) load(userptr, \"glVertexAttrib2sv\");\n    context->VertexAttrib3d = (PFNGLVERTEXATTRIB3DPROC) load(userptr, \"glVertexAttrib3d\");\n    context->VertexAttrib3dv = (PFNGLVERTEXATTRIB3DVPROC) load(userptr, \"glVertexAttrib3dv\");\n    context->VertexAttrib3f = (PFNGLVERTEXATTRIB3FPROC) load(userptr, \"glVertexAttrib3f\");\n    context->VertexAttrib3fv = (PFNGLVERTEXATTRIB3FVPROC) load(userptr, \"glVertexAttrib3fv\");\n    context->VertexAttrib3s = (PFNGLVERTEXATTRIB3SPROC) load(userptr, \"glVertexAttrib3s\");\n    context->VertexAttrib3sv = (PFNGLVERTEXATTRIB3SVPROC) load(userptr, \"glVertexAttrib3sv\");\n    context->VertexAttrib4Nbv = (PFNGLVERTEXATTRIB4NBVPROC) load(userptr, \"glVertexAttrib4Nbv\");\n    context->VertexAttrib4Niv = (PFNGLVERTEXATTRIB4NIVPROC) load(userptr, \"glVertexAttrib4Niv\");\n    context->VertexAttrib4Nsv = (PFNGLVERTEXATTRIB4NSVPROC) load(userptr, \"glVertexAttrib4Nsv\");\n    context->VertexAttrib4Nub = (PFNGLVERTEXATTRIB4NUBPROC) load(userptr, \"glVertexAttrib4Nub\");\n    context->VertexAttrib4Nubv = (PFNGLVERTEXATTRIB4NUBVPROC) load(userptr, \"glVertexAttrib4Nubv\");\n    context->VertexAttrib4Nuiv = (PFNGLVERTEXATTRIB4NUIVPROC) load(userptr, \"glVertexAttrib4Nuiv\");\n    context->VertexAttrib4Nusv = (PFNGLVERTEXATTRIB4NUSVPROC) load(userptr, \"glVertexAttrib4Nusv\");\n    context->VertexAttrib4bv = (PFNGLVERTEXATTRIB4BVPROC) load(userptr, \"glVertexAttrib4bv\");\n    context->VertexAttrib4d = (PFNGLVERTEXATTRIB4DPROC) load(userptr, \"glVertexAttrib4d\");\n    context->VertexAttrib4dv = (PFNGLVERTEXATTRIB4DVPROC) load(userptr, \"glVertexAttrib4dv\");\n    context->VertexAttrib4f = (PFNGLVERTEXATTRIB4FPROC) load(userptr, \"glVertexAttrib4f\");\n    context->VertexAttrib4fv = (PFNGLVERTEXATTRIB4FVPROC) load(userptr, \"glVertexAttrib4fv\");\n    context->VertexAttrib4iv = (PFNGLVERTEXATTRIB4IVPROC) load(userptr, \"glVertexAttrib4iv\");\n    context->VertexAttrib4s = (PFNGLVERTEXATTRIB4SPROC) load(userptr, \"glVertexAttrib4s\");\n    context->VertexAttrib4sv = (PFNGLVERTEXATTRIB4SVPROC) load(userptr, \"glVertexAttrib4sv\");\n    context->VertexAttrib4ubv = (PFNGLVERTEXATTRIB4UBVPROC) load(userptr, \"glVertexAttrib4ubv\");\n    context->VertexAttrib4uiv = (PFNGLVERTEXATTRIB4UIVPROC) load(userptr, \"glVertexAttrib4uiv\");\n    context->VertexAttrib4usv = (PFNGLVERTEXATTRIB4USVPROC) load(userptr, \"glVertexAttrib4usv\");\n    context->VertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC) load(userptr, \"glVertexAttribPointer\");\n}\nstatic void glad_gl_load_GL_VERSION_2_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_2_1) return;\n    context->UniformMatrix2x3fv = (PFNGLUNIFORMMATRIX2X3FVPROC) load(userptr, \"glUniformMatrix2x3fv\");\n    context->UniformMatrix2x4fv = (PFNGLUNIFORMMATRIX2X4FVPROC) load(userptr, \"glUniformMatrix2x4fv\");\n    context->UniformMatrix3x2fv = (PFNGLUNIFORMMATRIX3X2FVPROC) load(userptr, \"glUniformMatrix3x2fv\");\n    context->UniformMatrix3x4fv = (PFNGLUNIFORMMATRIX3X4FVPROC) load(userptr, \"glUniformMatrix3x4fv\");\n    context->UniformMatrix4x2fv = (PFNGLUNIFORMMATRIX4X2FVPROC) load(userptr, \"glUniformMatrix4x2fv\");\n    context->UniformMatrix4x3fv = (PFNGLUNIFORMMATRIX4X3FVPROC) load(userptr, \"glUniformMatrix4x3fv\");\n}\nstatic void glad_gl_load_GL_VERSION_3_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_3_0) return;\n    context->BeginConditionalRender = (PFNGLBEGINCONDITIONALRENDERPROC) load(userptr, \"glBeginConditionalRender\");\n    context->BeginTransformFeedback = (PFNGLBEGINTRANSFORMFEEDBACKPROC) load(userptr, \"glBeginTransformFeedback\");\n    context->BindBufferBase = (PFNGLBINDBUFFERBASEPROC) load(userptr, \"glBindBufferBase\");\n    context->BindBufferRange = (PFNGLBINDBUFFERRANGEPROC) load(userptr, \"glBindBufferRange\");\n    context->BindFragDataLocation = (PFNGLBINDFRAGDATALOCATIONPROC) load(userptr, \"glBindFragDataLocation\");\n    context->BindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC) load(userptr, \"glBindFramebuffer\");\n    context->BindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC) load(userptr, \"glBindRenderbuffer\");\n    context->BindVertexArray = (PFNGLBINDVERTEXARRAYPROC) load(userptr, \"glBindVertexArray\");\n    context->BlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC) load(userptr, \"glBlitFramebuffer\");\n    context->CheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) load(userptr, \"glCheckFramebufferStatus\");\n    context->ClampColor = (PFNGLCLAMPCOLORPROC) load(userptr, \"glClampColor\");\n    context->ClearBufferfi = (PFNGLCLEARBUFFERFIPROC) load(userptr, \"glClearBufferfi\");\n    context->ClearBufferfv = (PFNGLCLEARBUFFERFVPROC) load(userptr, \"glClearBufferfv\");\n    context->ClearBufferiv = (PFNGLCLEARBUFFERIVPROC) load(userptr, \"glClearBufferiv\");\n    context->ClearBufferuiv = (PFNGLCLEARBUFFERUIVPROC) load(userptr, \"glClearBufferuiv\");\n    context->ColorMaski = (PFNGLCOLORMASKIPROC) load(userptr, \"glColorMaski\");\n    context->DeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC) load(userptr, \"glDeleteFramebuffers\");\n    context->DeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC) load(userptr, \"glDeleteRenderbuffers\");\n    context->DeleteVertexArrays = (PFNGLDELETEVERTEXARRAYSPROC) load(userptr, \"glDeleteVertexArrays\");\n    context->Disablei = (PFNGLDISABLEIPROC) load(userptr, \"glDisablei\");\n    context->Enablei = (PFNGLENABLEIPROC) load(userptr, \"glEnablei\");\n    context->EndConditionalRender = (PFNGLENDCONDITIONALRENDERPROC) load(userptr, \"glEndConditionalRender\");\n    context->EndTransformFeedback = (PFNGLENDTRANSFORMFEEDBACKPROC) load(userptr, \"glEndTransformFeedback\");\n    context->FlushMappedBufferRange = (PFNGLFLUSHMAPPEDBUFFERRANGEPROC) load(userptr, \"glFlushMappedBufferRange\");\n    context->FramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC) load(userptr, \"glFramebufferRenderbuffer\");\n    context->FramebufferTexture1D = (PFNGLFRAMEBUFFERTEXTURE1DPROC) load(userptr, \"glFramebufferTexture1D\");\n    context->FramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC) load(userptr, \"glFramebufferTexture2D\");\n    context->FramebufferTexture3D = (PFNGLFRAMEBUFFERTEXTURE3DPROC) load(userptr, \"glFramebufferTexture3D\");\n    context->FramebufferTextureLayer = (PFNGLFRAMEBUFFERTEXTURELAYERPROC) load(userptr, \"glFramebufferTextureLayer\");\n    context->GenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC) load(userptr, \"glGenFramebuffers\");\n    context->GenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC) load(userptr, \"glGenRenderbuffers\");\n    context->GenVertexArrays = (PFNGLGENVERTEXARRAYSPROC) load(userptr, \"glGenVertexArrays\");\n    context->GenerateMipmap = (PFNGLGENERATEMIPMAPPROC) load(userptr, \"glGenerateMipmap\");\n    context->GetBooleani_v = (PFNGLGETBOOLEANI_VPROC) load(userptr, \"glGetBooleani_v\");\n    context->GetFragDataLocation = (PFNGLGETFRAGDATALOCATIONPROC) load(userptr, \"glGetFragDataLocation\");\n    context->GetFramebufferAttachmentParameteriv = (PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC) load(userptr, \"glGetFramebufferAttachmentParameteriv\");\n    context->GetIntegeri_v = (PFNGLGETINTEGERI_VPROC) load(userptr, \"glGetIntegeri_v\");\n    context->GetRenderbufferParameteriv = (PFNGLGETRENDERBUFFERPARAMETERIVPROC) load(userptr, \"glGetRenderbufferParameteriv\");\n    context->GetStringi = (PFNGLGETSTRINGIPROC) load(userptr, \"glGetStringi\");\n    context->GetTexParameterIiv = (PFNGLGETTEXPARAMETERIIVPROC) load(userptr, \"glGetTexParameterIiv\");\n    context->GetTexParameterIuiv = (PFNGLGETTEXPARAMETERIUIVPROC) load(userptr, \"glGetTexParameterIuiv\");\n    context->GetTransformFeedbackVarying = (PFNGLGETTRANSFORMFEEDBACKVARYINGPROC) load(userptr, \"glGetTransformFeedbackVarying\");\n    context->GetUniformuiv = (PFNGLGETUNIFORMUIVPROC) load(userptr, \"glGetUniformuiv\");\n    context->GetVertexAttribIiv = (PFNGLGETVERTEXATTRIBIIVPROC) load(userptr, \"glGetVertexAttribIiv\");\n    context->GetVertexAttribIuiv = (PFNGLGETVERTEXATTRIBIUIVPROC) load(userptr, \"glGetVertexAttribIuiv\");\n    context->IsEnabledi = (PFNGLISENABLEDIPROC) load(userptr, \"glIsEnabledi\");\n    context->IsFramebuffer = (PFNGLISFRAMEBUFFERPROC) load(userptr, \"glIsFramebuffer\");\n    context->IsRenderbuffer = (PFNGLISRENDERBUFFERPROC) load(userptr, \"glIsRenderbuffer\");\n    context->IsVertexArray = (PFNGLISVERTEXARRAYPROC) load(userptr, \"glIsVertexArray\");\n    context->MapBufferRange = (PFNGLMAPBUFFERRANGEPROC) load(userptr, \"glMapBufferRange\");\n    context->RenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC) load(userptr, \"glRenderbufferStorage\");\n    context->RenderbufferStorageMultisample = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC) load(userptr, \"glRenderbufferStorageMultisample\");\n    context->TexParameterIiv = (PFNGLTEXPARAMETERIIVPROC) load(userptr, \"glTexParameterIiv\");\n    context->TexParameterIuiv = (PFNGLTEXPARAMETERIUIVPROC) load(userptr, \"glTexParameterIuiv\");\n    context->TransformFeedbackVaryings = (PFNGLTRANSFORMFEEDBACKVARYINGSPROC) load(userptr, \"glTransformFeedbackVaryings\");\n    context->Uniform1ui = (PFNGLUNIFORM1UIPROC) load(userptr, \"glUniform1ui\");\n    context->Uniform1uiv = (PFNGLUNIFORM1UIVPROC) load(userptr, \"glUniform1uiv\");\n    context->Uniform2ui = (PFNGLUNIFORM2UIPROC) load(userptr, \"glUniform2ui\");\n    context->Uniform2uiv = (PFNGLUNIFORM2UIVPROC) load(userptr, \"glUniform2uiv\");\n    context->Uniform3ui = (PFNGLUNIFORM3UIPROC) load(userptr, \"glUniform3ui\");\n    context->Uniform3uiv = (PFNGLUNIFORM3UIVPROC) load(userptr, \"glUniform3uiv\");\n    context->Uniform4ui = (PFNGLUNIFORM4UIPROC) load(userptr, \"glUniform4ui\");\n    context->Uniform4uiv = (PFNGLUNIFORM4UIVPROC) load(userptr, \"glUniform4uiv\");\n    context->VertexAttribI1i = (PFNGLVERTEXATTRIBI1IPROC) load(userptr, \"glVertexAttribI1i\");\n    context->VertexAttribI1iv = (PFNGLVERTEXATTRIBI1IVPROC) load(userptr, \"glVertexAttribI1iv\");\n    context->VertexAttribI1ui = (PFNGLVERTEXATTRIBI1UIPROC) load(userptr, \"glVertexAttribI1ui\");\n    context->VertexAttribI1uiv = (PFNGLVERTEXATTRIBI1UIVPROC) load(userptr, \"glVertexAttribI1uiv\");\n    context->VertexAttribI2i = (PFNGLVERTEXATTRIBI2IPROC) load(userptr, \"glVertexAttribI2i\");\n    context->VertexAttribI2iv = (PFNGLVERTEXATTRIBI2IVPROC) load(userptr, \"glVertexAttribI2iv\");\n    context->VertexAttribI2ui = (PFNGLVERTEXATTRIBI2UIPROC) load(userptr, \"glVertexAttribI2ui\");\n    context->VertexAttribI2uiv = (PFNGLVERTEXATTRIBI2UIVPROC) load(userptr, \"glVertexAttribI2uiv\");\n    context->VertexAttribI3i = (PFNGLVERTEXATTRIBI3IPROC) load(userptr, \"glVertexAttribI3i\");\n    context->VertexAttribI3iv = (PFNGLVERTEXATTRIBI3IVPROC) load(userptr, \"glVertexAttribI3iv\");\n    context->VertexAttribI3ui = (PFNGLVERTEXATTRIBI3UIPROC) load(userptr, \"glVertexAttribI3ui\");\n    context->VertexAttribI3uiv = (PFNGLVERTEXATTRIBI3UIVPROC) load(userptr, \"glVertexAttribI3uiv\");\n    context->VertexAttribI4bv = (PFNGLVERTEXATTRIBI4BVPROC) load(userptr, \"glVertexAttribI4bv\");\n    context->VertexAttribI4i = (PFNGLVERTEXATTRIBI4IPROC) load(userptr, \"glVertexAttribI4i\");\n    context->VertexAttribI4iv = (PFNGLVERTEXATTRIBI4IVPROC) load(userptr, \"glVertexAttribI4iv\");\n    context->VertexAttribI4sv = (PFNGLVERTEXATTRIBI4SVPROC) load(userptr, \"glVertexAttribI4sv\");\n    context->VertexAttribI4ubv = (PFNGLVERTEXATTRIBI4UBVPROC) load(userptr, \"glVertexAttribI4ubv\");\n    context->VertexAttribI4ui = (PFNGLVERTEXATTRIBI4UIPROC) load(userptr, \"glVertexAttribI4ui\");\n    context->VertexAttribI4uiv = (PFNGLVERTEXATTRIBI4UIVPROC) load(userptr, \"glVertexAttribI4uiv\");\n    context->VertexAttribI4usv = (PFNGLVERTEXATTRIBI4USVPROC) load(userptr, \"glVertexAttribI4usv\");\n    context->VertexAttribIPointer = (PFNGLVERTEXATTRIBIPOINTERPROC) load(userptr, \"glVertexAttribIPointer\");\n}\nstatic void glad_gl_load_GL_VERSION_3_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_3_1) return;\n    context->BindBufferBase = (PFNGLBINDBUFFERBASEPROC) load(userptr, \"glBindBufferBase\");\n    context->BindBufferRange = (PFNGLBINDBUFFERRANGEPROC) load(userptr, \"glBindBufferRange\");\n    context->CopyBufferSubData = (PFNGLCOPYBUFFERSUBDATAPROC) load(userptr, \"glCopyBufferSubData\");\n    context->DrawArraysInstanced = (PFNGLDRAWARRAYSINSTANCEDPROC) load(userptr, \"glDrawArraysInstanced\");\n    context->DrawElementsInstanced = (PFNGLDRAWELEMENTSINSTANCEDPROC) load(userptr, \"glDrawElementsInstanced\");\n    context->GetActiveUniformBlockName = (PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC) load(userptr, \"glGetActiveUniformBlockName\");\n    context->GetActiveUniformBlockiv = (PFNGLGETACTIVEUNIFORMBLOCKIVPROC) load(userptr, \"glGetActiveUniformBlockiv\");\n    context->GetActiveUniformName = (PFNGLGETACTIVEUNIFORMNAMEPROC) load(userptr, \"glGetActiveUniformName\");\n    context->GetActiveUniformsiv = (PFNGLGETACTIVEUNIFORMSIVPROC) load(userptr, \"glGetActiveUniformsiv\");\n    context->GetIntegeri_v = (PFNGLGETINTEGERI_VPROC) load(userptr, \"glGetIntegeri_v\");\n    context->GetUniformBlockIndex = (PFNGLGETUNIFORMBLOCKINDEXPROC) load(userptr, \"glGetUniformBlockIndex\");\n    context->GetUniformIndices = (PFNGLGETUNIFORMINDICESPROC) load(userptr, \"glGetUniformIndices\");\n    context->PrimitiveRestartIndex = (PFNGLPRIMITIVERESTARTINDEXPROC) load(userptr, \"glPrimitiveRestartIndex\");\n    context->TexBuffer = (PFNGLTEXBUFFERPROC) load(userptr, \"glTexBuffer\");\n    context->UniformBlockBinding = (PFNGLUNIFORMBLOCKBINDINGPROC) load(userptr, \"glUniformBlockBinding\");\n}\nstatic void glad_gl_load_GL_VERSION_3_2(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_3_2) return;\n    context->ClientWaitSync = (PFNGLCLIENTWAITSYNCPROC) load(userptr, \"glClientWaitSync\");\n    context->DeleteSync = (PFNGLDELETESYNCPROC) load(userptr, \"glDeleteSync\");\n    context->DrawElementsBaseVertex = (PFNGLDRAWELEMENTSBASEVERTEXPROC) load(userptr, \"glDrawElementsBaseVertex\");\n    context->DrawElementsInstancedBaseVertex = (PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXPROC) load(userptr, \"glDrawElementsInstancedBaseVertex\");\n    context->DrawRangeElementsBaseVertex = (PFNGLDRAWRANGEELEMENTSBASEVERTEXPROC) load(userptr, \"glDrawRangeElementsBaseVertex\");\n    context->FenceSync = (PFNGLFENCESYNCPROC) load(userptr, \"glFenceSync\");\n    context->FramebufferTexture = (PFNGLFRAMEBUFFERTEXTUREPROC) load(userptr, \"glFramebufferTexture\");\n    context->GetBufferParameteri64v = (PFNGLGETBUFFERPARAMETERI64VPROC) load(userptr, \"glGetBufferParameteri64v\");\n    context->GetInteger64i_v = (PFNGLGETINTEGER64I_VPROC) load(userptr, \"glGetInteger64i_v\");\n    context->GetInteger64v = (PFNGLGETINTEGER64VPROC) load(userptr, \"glGetInteger64v\");\n    context->GetMultisamplefv = (PFNGLGETMULTISAMPLEFVPROC) load(userptr, \"glGetMultisamplefv\");\n    context->GetSynciv = (PFNGLGETSYNCIVPROC) load(userptr, \"glGetSynciv\");\n    context->IsSync = (PFNGLISSYNCPROC) load(userptr, \"glIsSync\");\n    context->MultiDrawElementsBaseVertex = (PFNGLMULTIDRAWELEMENTSBASEVERTEXPROC) load(userptr, \"glMultiDrawElementsBaseVertex\");\n    context->ProvokingVertex = (PFNGLPROVOKINGVERTEXPROC) load(userptr, \"glProvokingVertex\");\n    context->SampleMaski = (PFNGLSAMPLEMASKIPROC) load(userptr, \"glSampleMaski\");\n    context->TexImage2DMultisample = (PFNGLTEXIMAGE2DMULTISAMPLEPROC) load(userptr, \"glTexImage2DMultisample\");\n    context->TexImage3DMultisample = (PFNGLTEXIMAGE3DMULTISAMPLEPROC) load(userptr, \"glTexImage3DMultisample\");\n    context->WaitSync = (PFNGLWAITSYNCPROC) load(userptr, \"glWaitSync\");\n}\nstatic void glad_gl_load_GL_VERSION_3_3(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_3_3) return;\n    context->BindFragDataLocationIndexed = (PFNGLBINDFRAGDATALOCATIONINDEXEDPROC) load(userptr, \"glBindFragDataLocationIndexed\");\n    context->BindSampler = (PFNGLBINDSAMPLERPROC) load(userptr, \"glBindSampler\");\n    context->ColorP3ui = (PFNGLCOLORP3UIPROC) load(userptr, \"glColorP3ui\");\n    context->ColorP3uiv = (PFNGLCOLORP3UIVPROC) load(userptr, \"glColorP3uiv\");\n    context->ColorP4ui = (PFNGLCOLORP4UIPROC) load(userptr, \"glColorP4ui\");\n    context->ColorP4uiv = (PFNGLCOLORP4UIVPROC) load(userptr, \"glColorP4uiv\");\n    context->DeleteSamplers = (PFNGLDELETESAMPLERSPROC) load(userptr, \"glDeleteSamplers\");\n    context->GenSamplers = (PFNGLGENSAMPLERSPROC) load(userptr, \"glGenSamplers\");\n    context->GetFragDataIndex = (PFNGLGETFRAGDATAINDEXPROC) load(userptr, \"glGetFragDataIndex\");\n    context->GetQueryObjecti64v = (PFNGLGETQUERYOBJECTI64VPROC) load(userptr, \"glGetQueryObjecti64v\");\n    context->GetQueryObjectui64v = (PFNGLGETQUERYOBJECTUI64VPROC) load(userptr, \"glGetQueryObjectui64v\");\n    context->GetSamplerParameterIiv = (PFNGLGETSAMPLERPARAMETERIIVPROC) load(userptr, \"glGetSamplerParameterIiv\");\n    context->GetSamplerParameterIuiv = (PFNGLGETSAMPLERPARAMETERIUIVPROC) load(userptr, \"glGetSamplerParameterIuiv\");\n    context->GetSamplerParameterfv = (PFNGLGETSAMPLERPARAMETERFVPROC) load(userptr, \"glGetSamplerParameterfv\");\n    context->GetSamplerParameteriv = (PFNGLGETSAMPLERPARAMETERIVPROC) load(userptr, \"glGetSamplerParameteriv\");\n    context->IsSampler = (PFNGLISSAMPLERPROC) load(userptr, \"glIsSampler\");\n    context->MultiTexCoordP1ui = (PFNGLMULTITEXCOORDP1UIPROC) load(userptr, \"glMultiTexCoordP1ui\");\n    context->MultiTexCoordP1uiv = (PFNGLMULTITEXCOORDP1UIVPROC) load(userptr, \"glMultiTexCoordP1uiv\");\n    context->MultiTexCoordP2ui = (PFNGLMULTITEXCOORDP2UIPROC) load(userptr, \"glMultiTexCoordP2ui\");\n    context->MultiTexCoordP2uiv = (PFNGLMULTITEXCOORDP2UIVPROC) load(userptr, \"glMultiTexCoordP2uiv\");\n    context->MultiTexCoordP3ui = (PFNGLMULTITEXCOORDP3UIPROC) load(userptr, \"glMultiTexCoordP3ui\");\n    context->MultiTexCoordP3uiv = (PFNGLMULTITEXCOORDP3UIVPROC) load(userptr, \"glMultiTexCoordP3uiv\");\n    context->MultiTexCoordP4ui = (PFNGLMULTITEXCOORDP4UIPROC) load(userptr, \"glMultiTexCoordP4ui\");\n    context->MultiTexCoordP4uiv = (PFNGLMULTITEXCOORDP4UIVPROC) load(userptr, \"glMultiTexCoordP4uiv\");\n    context->NormalP3ui = (PFNGLNORMALP3UIPROC) load(userptr, \"glNormalP3ui\");\n    context->NormalP3uiv = (PFNGLNORMALP3UIVPROC) load(userptr, \"glNormalP3uiv\");\n    context->QueryCounter = (PFNGLQUERYCOUNTERPROC) load(userptr, \"glQueryCounter\");\n    context->SamplerParameterIiv = (PFNGLSAMPLERPARAMETERIIVPROC) load(userptr, \"glSamplerParameterIiv\");\n    context->SamplerParameterIuiv = (PFNGLSAMPLERPARAMETERIUIVPROC) load(userptr, \"glSamplerParameterIuiv\");\n    context->SamplerParameterf = (PFNGLSAMPLERPARAMETERFPROC) load(userptr, \"glSamplerParameterf\");\n    context->SamplerParameterfv = (PFNGLSAMPLERPARAMETERFVPROC) load(userptr, \"glSamplerParameterfv\");\n    context->SamplerParameteri = (PFNGLSAMPLERPARAMETERIPROC) load(userptr, \"glSamplerParameteri\");\n    context->SamplerParameteriv = (PFNGLSAMPLERPARAMETERIVPROC) load(userptr, \"glSamplerParameteriv\");\n    context->SecondaryColorP3ui = (PFNGLSECONDARYCOLORP3UIPROC) load(userptr, \"glSecondaryColorP3ui\");\n    context->SecondaryColorP3uiv = (PFNGLSECONDARYCOLORP3UIVPROC) load(userptr, \"glSecondaryColorP3uiv\");\n    context->TexCoordP1ui = (PFNGLTEXCOORDP1UIPROC) load(userptr, \"glTexCoordP1ui\");\n    context->TexCoordP1uiv = (PFNGLTEXCOORDP1UIVPROC) load(userptr, \"glTexCoordP1uiv\");\n    context->TexCoordP2ui = (PFNGLTEXCOORDP2UIPROC) load(userptr, \"glTexCoordP2ui\");\n    context->TexCoordP2uiv = (PFNGLTEXCOORDP2UIVPROC) load(userptr, \"glTexCoordP2uiv\");\n    context->TexCoordP3ui = (PFNGLTEXCOORDP3UIPROC) load(userptr, \"glTexCoordP3ui\");\n    context->TexCoordP3uiv = (PFNGLTEXCOORDP3UIVPROC) load(userptr, \"glTexCoordP3uiv\");\n    context->TexCoordP4ui = (PFNGLTEXCOORDP4UIPROC) load(userptr, \"glTexCoordP4ui\");\n    context->TexCoordP4uiv = (PFNGLTEXCOORDP4UIVPROC) load(userptr, \"glTexCoordP4uiv\");\n    context->VertexAttribDivisor = (PFNGLVERTEXATTRIBDIVISORPROC) load(userptr, \"glVertexAttribDivisor\");\n    context->VertexAttribP1ui = (PFNGLVERTEXATTRIBP1UIPROC) load(userptr, \"glVertexAttribP1ui\");\n    context->VertexAttribP1uiv = (PFNGLVERTEXATTRIBP1UIVPROC) load(userptr, \"glVertexAttribP1uiv\");\n    context->VertexAttribP2ui = (PFNGLVERTEXATTRIBP2UIPROC) load(userptr, \"glVertexAttribP2ui\");\n    context->VertexAttribP2uiv = (PFNGLVERTEXATTRIBP2UIVPROC) load(userptr, \"glVertexAttribP2uiv\");\n    context->VertexAttribP3ui = (PFNGLVERTEXATTRIBP3UIPROC) load(userptr, \"glVertexAttribP3ui\");\n    context->VertexAttribP3uiv = (PFNGLVERTEXATTRIBP3UIVPROC) load(userptr, \"glVertexAttribP3uiv\");\n    context->VertexAttribP4ui = (PFNGLVERTEXATTRIBP4UIPROC) load(userptr, \"glVertexAttribP4ui\");\n    context->VertexAttribP4uiv = (PFNGLVERTEXATTRIBP4UIVPROC) load(userptr, \"glVertexAttribP4uiv\");\n    context->VertexP2ui = (PFNGLVERTEXP2UIPROC) load(userptr, \"glVertexP2ui\");\n    context->VertexP2uiv = (PFNGLVERTEXP2UIVPROC) load(userptr, \"glVertexP2uiv\");\n    context->VertexP3ui = (PFNGLVERTEXP3UIPROC) load(userptr, \"glVertexP3ui\");\n    context->VertexP3uiv = (PFNGLVERTEXP3UIVPROC) load(userptr, \"glVertexP3uiv\");\n    context->VertexP4ui = (PFNGLVERTEXP4UIPROC) load(userptr, \"glVertexP4ui\");\n    context->VertexP4uiv = (PFNGLVERTEXP4UIVPROC) load(userptr, \"glVertexP4uiv\");\n}\nstatic void glad_gl_load_GL_VERSION_4_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_4_0) return;\n    context->BeginQueryIndexed = (PFNGLBEGINQUERYINDEXEDPROC) load(userptr, \"glBeginQueryIndexed\");\n    context->BindTransformFeedback = (PFNGLBINDTRANSFORMFEEDBACKPROC) load(userptr, \"glBindTransformFeedback\");\n    context->BlendEquationSeparatei = (PFNGLBLENDEQUATIONSEPARATEIPROC) load(userptr, \"glBlendEquationSeparatei\");\n    context->BlendEquationi = (PFNGLBLENDEQUATIONIPROC) load(userptr, \"glBlendEquationi\");\n    context->BlendFuncSeparatei = (PFNGLBLENDFUNCSEPARATEIPROC) load(userptr, \"glBlendFuncSeparatei\");\n    context->BlendFunci = (PFNGLBLENDFUNCIPROC) load(userptr, \"glBlendFunci\");\n    context->DeleteTransformFeedbacks = (PFNGLDELETETRANSFORMFEEDBACKSPROC) load(userptr, \"glDeleteTransformFeedbacks\");\n    context->DrawArraysIndirect = (PFNGLDRAWARRAYSINDIRECTPROC) load(userptr, \"glDrawArraysIndirect\");\n    context->DrawElementsIndirect = (PFNGLDRAWELEMENTSINDIRECTPROC) load(userptr, \"glDrawElementsIndirect\");\n    context->DrawTransformFeedback = (PFNGLDRAWTRANSFORMFEEDBACKPROC) load(userptr, \"glDrawTransformFeedback\");\n    context->DrawTransformFeedbackStream = (PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC) load(userptr, \"glDrawTransformFeedbackStream\");\n    context->EndQueryIndexed = (PFNGLENDQUERYINDEXEDPROC) load(userptr, \"glEndQueryIndexed\");\n    context->GenTransformFeedbacks = (PFNGLGENTRANSFORMFEEDBACKSPROC) load(userptr, \"glGenTransformFeedbacks\");\n    context->GetActiveSubroutineName = (PFNGLGETACTIVESUBROUTINENAMEPROC) load(userptr, \"glGetActiveSubroutineName\");\n    context->GetActiveSubroutineUniformName = (PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC) load(userptr, \"glGetActiveSubroutineUniformName\");\n    context->GetActiveSubroutineUniformiv = (PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC) load(userptr, \"glGetActiveSubroutineUniformiv\");\n    context->GetProgramStageiv = (PFNGLGETPROGRAMSTAGEIVPROC) load(userptr, \"glGetProgramStageiv\");\n    context->GetQueryIndexediv = (PFNGLGETQUERYINDEXEDIVPROC) load(userptr, \"glGetQueryIndexediv\");\n    context->GetSubroutineIndex = (PFNGLGETSUBROUTINEINDEXPROC) load(userptr, \"glGetSubroutineIndex\");\n    context->GetSubroutineUniformLocation = (PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC) load(userptr, \"glGetSubroutineUniformLocation\");\n    context->GetUniformSubroutineuiv = (PFNGLGETUNIFORMSUBROUTINEUIVPROC) load(userptr, \"glGetUniformSubroutineuiv\");\n    context->GetUniformdv = (PFNGLGETUNIFORMDVPROC) load(userptr, \"glGetUniformdv\");\n    context->IsTransformFeedback = (PFNGLISTRANSFORMFEEDBACKPROC) load(userptr, \"glIsTransformFeedback\");\n    context->MinSampleShading = (PFNGLMINSAMPLESHADINGPROC) load(userptr, \"glMinSampleShading\");\n    context->PatchParameterfv = (PFNGLPATCHPARAMETERFVPROC) load(userptr, \"glPatchParameterfv\");\n    context->PatchParameteri = (PFNGLPATCHPARAMETERIPROC) load(userptr, \"glPatchParameteri\");\n    context->PauseTransformFeedback = (PFNGLPAUSETRANSFORMFEEDBACKPROC) load(userptr, \"glPauseTransformFeedback\");\n    context->ResumeTransformFeedback = (PFNGLRESUMETRANSFORMFEEDBACKPROC) load(userptr, \"glResumeTransformFeedback\");\n    context->Uniform1d = (PFNGLUNIFORM1DPROC) load(userptr, \"glUniform1d\");\n    context->Uniform1dv = (PFNGLUNIFORM1DVPROC) load(userptr, \"glUniform1dv\");\n    context->Uniform2d = (PFNGLUNIFORM2DPROC) load(userptr, \"glUniform2d\");\n    context->Uniform2dv = (PFNGLUNIFORM2DVPROC) load(userptr, \"glUniform2dv\");\n    context->Uniform3d = (PFNGLUNIFORM3DPROC) load(userptr, \"glUniform3d\");\n    context->Uniform3dv = (PFNGLUNIFORM3DVPROC) load(userptr, \"glUniform3dv\");\n    context->Uniform4d = (PFNGLUNIFORM4DPROC) load(userptr, \"glUniform4d\");\n    context->Uniform4dv = (PFNGLUNIFORM4DVPROC) load(userptr, \"glUniform4dv\");\n    context->UniformMatrix2dv = (PFNGLUNIFORMMATRIX2DVPROC) load(userptr, \"glUniformMatrix2dv\");\n    context->UniformMatrix2x3dv = (PFNGLUNIFORMMATRIX2X3DVPROC) load(userptr, \"glUniformMatrix2x3dv\");\n    context->UniformMatrix2x4dv = (PFNGLUNIFORMMATRIX2X4DVPROC) load(userptr, \"glUniformMatrix2x4dv\");\n    context->UniformMatrix3dv = (PFNGLUNIFORMMATRIX3DVPROC) load(userptr, \"glUniformMatrix3dv\");\n    context->UniformMatrix3x2dv = (PFNGLUNIFORMMATRIX3X2DVPROC) load(userptr, \"glUniformMatrix3x2dv\");\n    context->UniformMatrix3x4dv = (PFNGLUNIFORMMATRIX3X4DVPROC) load(userptr, \"glUniformMatrix3x4dv\");\n    context->UniformMatrix4dv = (PFNGLUNIFORMMATRIX4DVPROC) load(userptr, \"glUniformMatrix4dv\");\n    context->UniformMatrix4x2dv = (PFNGLUNIFORMMATRIX4X2DVPROC) load(userptr, \"glUniformMatrix4x2dv\");\n    context->UniformMatrix4x3dv = (PFNGLUNIFORMMATRIX4X3DVPROC) load(userptr, \"glUniformMatrix4x3dv\");\n    context->UniformSubroutinesuiv = (PFNGLUNIFORMSUBROUTINESUIVPROC) load(userptr, \"glUniformSubroutinesuiv\");\n}\nstatic void glad_gl_load_GL_VERSION_4_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_4_1) return;\n    context->ActiveShaderProgram = (PFNGLACTIVESHADERPROGRAMPROC) load(userptr, \"glActiveShaderProgram\");\n    context->BindProgramPipeline = (PFNGLBINDPROGRAMPIPELINEPROC) load(userptr, \"glBindProgramPipeline\");\n    context->ClearDepthf = (PFNGLCLEARDEPTHFPROC) load(userptr, \"glClearDepthf\");\n    context->CreateShaderProgramv = (PFNGLCREATESHADERPROGRAMVPROC) load(userptr, \"glCreateShaderProgramv\");\n    context->DeleteProgramPipelines = (PFNGLDELETEPROGRAMPIPELINESPROC) load(userptr, \"glDeleteProgramPipelines\");\n    context->DepthRangeArrayv = (PFNGLDEPTHRANGEARRAYVPROC) load(userptr, \"glDepthRangeArrayv\");\n    context->DepthRangeIndexed = (PFNGLDEPTHRANGEINDEXEDPROC) load(userptr, \"glDepthRangeIndexed\");\n    context->DepthRangef = (PFNGLDEPTHRANGEFPROC) load(userptr, \"glDepthRangef\");\n    context->GenProgramPipelines = (PFNGLGENPROGRAMPIPELINESPROC) load(userptr, \"glGenProgramPipelines\");\n    context->GetDoublei_v = (PFNGLGETDOUBLEI_VPROC) load(userptr, \"glGetDoublei_v\");\n    context->GetFloati_v = (PFNGLGETFLOATI_VPROC) load(userptr, \"glGetFloati_v\");\n    context->GetProgramBinary = (PFNGLGETPROGRAMBINARYPROC) load(userptr, \"glGetProgramBinary\");\n    context->GetProgramPipelineInfoLog = (PFNGLGETPROGRAMPIPELINEINFOLOGPROC) load(userptr, \"glGetProgramPipelineInfoLog\");\n    context->GetProgramPipelineiv = (PFNGLGETPROGRAMPIPELINEIVPROC) load(userptr, \"glGetProgramPipelineiv\");\n    context->GetShaderPrecisionFormat = (PFNGLGETSHADERPRECISIONFORMATPROC) load(userptr, \"glGetShaderPrecisionFormat\");\n    context->GetVertexAttribLdv = (PFNGLGETVERTEXATTRIBLDVPROC) load(userptr, \"glGetVertexAttribLdv\");\n    context->IsProgramPipeline = (PFNGLISPROGRAMPIPELINEPROC) load(userptr, \"glIsProgramPipeline\");\n    context->ProgramBinary = (PFNGLPROGRAMBINARYPROC) load(userptr, \"glProgramBinary\");\n    context->ProgramParameteri = (PFNGLPROGRAMPARAMETERIPROC) load(userptr, \"glProgramParameteri\");\n    context->ProgramUniform1d = (PFNGLPROGRAMUNIFORM1DPROC) load(userptr, \"glProgramUniform1d\");\n    context->ProgramUniform1dv = (PFNGLPROGRAMUNIFORM1DVPROC) load(userptr, \"glProgramUniform1dv\");\n    context->ProgramUniform1f = (PFNGLPROGRAMUNIFORM1FPROC) load(userptr, \"glProgramUniform1f\");\n    context->ProgramUniform1fv = (PFNGLPROGRAMUNIFORM1FVPROC) load(userptr, \"glProgramUniform1fv\");\n    context->ProgramUniform1i = (PFNGLPROGRAMUNIFORM1IPROC) load(userptr, \"glProgramUniform1i\");\n    context->ProgramUniform1iv = (PFNGLPROGRAMUNIFORM1IVPROC) load(userptr, \"glProgramUniform1iv\");\n    context->ProgramUniform1ui = (PFNGLPROGRAMUNIFORM1UIPROC) load(userptr, \"glProgramUniform1ui\");\n    context->ProgramUniform1uiv = (PFNGLPROGRAMUNIFORM1UIVPROC) load(userptr, \"glProgramUniform1uiv\");\n    context->ProgramUniform2d = (PFNGLPROGRAMUNIFORM2DPROC) load(userptr, \"glProgramUniform2d\");\n    context->ProgramUniform2dv = (PFNGLPROGRAMUNIFORM2DVPROC) load(userptr, \"glProgramUniform2dv\");\n    context->ProgramUniform2f = (PFNGLPROGRAMUNIFORM2FPROC) load(userptr, \"glProgramUniform2f\");\n    context->ProgramUniform2fv = (PFNGLPROGRAMUNIFORM2FVPROC) load(userptr, \"glProgramUniform2fv\");\n    context->ProgramUniform2i = (PFNGLPROGRAMUNIFORM2IPROC) load(userptr, \"glProgramUniform2i\");\n    context->ProgramUniform2iv = (PFNGLPROGRAMUNIFORM2IVPROC) load(userptr, \"glProgramUniform2iv\");\n    context->ProgramUniform2ui = (PFNGLPROGRAMUNIFORM2UIPROC) load(userptr, \"glProgramUniform2ui\");\n    context->ProgramUniform2uiv = (PFNGLPROGRAMUNIFORM2UIVPROC) load(userptr, \"glProgramUniform2uiv\");\n    context->ProgramUniform3d = (PFNGLPROGRAMUNIFORM3DPROC) load(userptr, \"glProgramUniform3d\");\n    context->ProgramUniform3dv = (PFNGLPROGRAMUNIFORM3DVPROC) load(userptr, \"glProgramUniform3dv\");\n    context->ProgramUniform3f = (PFNGLPROGRAMUNIFORM3FPROC) load(userptr, \"glProgramUniform3f\");\n    context->ProgramUniform3fv = (PFNGLPROGRAMUNIFORM3FVPROC) load(userptr, \"glProgramUniform3fv\");\n    context->ProgramUniform3i = (PFNGLPROGRAMUNIFORM3IPROC) load(userptr, \"glProgramUniform3i\");\n    context->ProgramUniform3iv = (PFNGLPROGRAMUNIFORM3IVPROC) load(userptr, \"glProgramUniform3iv\");\n    context->ProgramUniform3ui = (PFNGLPROGRAMUNIFORM3UIPROC) load(userptr, \"glProgramUniform3ui\");\n    context->ProgramUniform3uiv = (PFNGLPROGRAMUNIFORM3UIVPROC) load(userptr, \"glProgramUniform3uiv\");\n    context->ProgramUniform4d = (PFNGLPROGRAMUNIFORM4DPROC) load(userptr, \"glProgramUniform4d\");\n    context->ProgramUniform4dv = (PFNGLPROGRAMUNIFORM4DVPROC) load(userptr, \"glProgramUniform4dv\");\n    context->ProgramUniform4f = (PFNGLPROGRAMUNIFORM4FPROC) load(userptr, \"glProgramUniform4f\");\n    context->ProgramUniform4fv = (PFNGLPROGRAMUNIFORM4FVPROC) load(userptr, \"glProgramUniform4fv\");\n    context->ProgramUniform4i = (PFNGLPROGRAMUNIFORM4IPROC) load(userptr, \"glProgramUniform4i\");\n    context->ProgramUniform4iv = (PFNGLPROGRAMUNIFORM4IVPROC) load(userptr, \"glProgramUniform4iv\");\n    context->ProgramUniform4ui = (PFNGLPROGRAMUNIFORM4UIPROC) load(userptr, \"glProgramUniform4ui\");\n    context->ProgramUniform4uiv = (PFNGLPROGRAMUNIFORM4UIVPROC) load(userptr, \"glProgramUniform4uiv\");\n    context->ProgramUniformMatrix2dv = (PFNGLPROGRAMUNIFORMMATRIX2DVPROC) load(userptr, \"glProgramUniformMatrix2dv\");\n    context->ProgramUniformMatrix2fv = (PFNGLPROGRAMUNIFORMMATRIX2FVPROC) load(userptr, \"glProgramUniformMatrix2fv\");\n    context->ProgramUniformMatrix2x3dv = (PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC) load(userptr, \"glProgramUniformMatrix2x3dv\");\n    context->ProgramUniformMatrix2x3fv = (PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC) load(userptr, \"glProgramUniformMatrix2x3fv\");\n    context->ProgramUniformMatrix2x4dv = (PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC) load(userptr, \"glProgramUniformMatrix2x4dv\");\n    context->ProgramUniformMatrix2x4fv = (PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC) load(userptr, \"glProgramUniformMatrix2x4fv\");\n    context->ProgramUniformMatrix3dv = (PFNGLPROGRAMUNIFORMMATRIX3DVPROC) load(userptr, \"glProgramUniformMatrix3dv\");\n    context->ProgramUniformMatrix3fv = (PFNGLPROGRAMUNIFORMMATRIX3FVPROC) load(userptr, \"glProgramUniformMatrix3fv\");\n    context->ProgramUniformMatrix3x2dv = (PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC) load(userptr, \"glProgramUniformMatrix3x2dv\");\n    context->ProgramUniformMatrix3x2fv = (PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC) load(userptr, \"glProgramUniformMatrix3x2fv\");\n    context->ProgramUniformMatrix3x4dv = (PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC) load(userptr, \"glProgramUniformMatrix3x4dv\");\n    context->ProgramUniformMatrix3x4fv = (PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC) load(userptr, \"glProgramUniformMatrix3x4fv\");\n    context->ProgramUniformMatrix4dv = (PFNGLPROGRAMUNIFORMMATRIX4DVPROC) load(userptr, \"glProgramUniformMatrix4dv\");\n    context->ProgramUniformMatrix4fv = (PFNGLPROGRAMUNIFORMMATRIX4FVPROC) load(userptr, \"glProgramUniformMatrix4fv\");\n    context->ProgramUniformMatrix4x2dv = (PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC) load(userptr, \"glProgramUniformMatrix4x2dv\");\n    context->ProgramUniformMatrix4x2fv = (PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC) load(userptr, \"glProgramUniformMatrix4x2fv\");\n    context->ProgramUniformMatrix4x3dv = (PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC) load(userptr, \"glProgramUniformMatrix4x3dv\");\n    context->ProgramUniformMatrix4x3fv = (PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC) load(userptr, \"glProgramUniformMatrix4x3fv\");\n    context->ReleaseShaderCompiler = (PFNGLRELEASESHADERCOMPILERPROC) load(userptr, \"glReleaseShaderCompiler\");\n    context->ScissorArrayv = (PFNGLSCISSORARRAYVPROC) load(userptr, \"glScissorArrayv\");\n    context->ScissorIndexed = (PFNGLSCISSORINDEXEDPROC) load(userptr, \"glScissorIndexed\");\n    context->ScissorIndexedv = (PFNGLSCISSORINDEXEDVPROC) load(userptr, \"glScissorIndexedv\");\n    context->ShaderBinary = (PFNGLSHADERBINARYPROC) load(userptr, \"glShaderBinary\");\n    context->UseProgramStages = (PFNGLUSEPROGRAMSTAGESPROC) load(userptr, \"glUseProgramStages\");\n    context->ValidateProgramPipeline = (PFNGLVALIDATEPROGRAMPIPELINEPROC) load(userptr, \"glValidateProgramPipeline\");\n    context->VertexAttribL1d = (PFNGLVERTEXATTRIBL1DPROC) load(userptr, \"glVertexAttribL1d\");\n    context->VertexAttribL1dv = (PFNGLVERTEXATTRIBL1DVPROC) load(userptr, \"glVertexAttribL1dv\");\n    context->VertexAttribL2d = (PFNGLVERTEXATTRIBL2DPROC) load(userptr, \"glVertexAttribL2d\");\n    context->VertexAttribL2dv = (PFNGLVERTEXATTRIBL2DVPROC) load(userptr, \"glVertexAttribL2dv\");\n    context->VertexAttribL3d = (PFNGLVERTEXATTRIBL3DPROC) load(userptr, \"glVertexAttribL3d\");\n    context->VertexAttribL3dv = (PFNGLVERTEXATTRIBL3DVPROC) load(userptr, \"glVertexAttribL3dv\");\n    context->VertexAttribL4d = (PFNGLVERTEXATTRIBL4DPROC) load(userptr, \"glVertexAttribL4d\");\n    context->VertexAttribL4dv = (PFNGLVERTEXATTRIBL4DVPROC) load(userptr, \"glVertexAttribL4dv\");\n    context->VertexAttribLPointer = (PFNGLVERTEXATTRIBLPOINTERPROC) load(userptr, \"glVertexAttribLPointer\");\n    context->ViewportArrayv = (PFNGLVIEWPORTARRAYVPROC) load(userptr, \"glViewportArrayv\");\n    context->ViewportIndexedf = (PFNGLVIEWPORTINDEXEDFPROC) load(userptr, \"glViewportIndexedf\");\n    context->ViewportIndexedfv = (PFNGLVIEWPORTINDEXEDFVPROC) load(userptr, \"glViewportIndexedfv\");\n}\nstatic void glad_gl_load_GL_VERSION_4_2(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_4_2) return;\n    context->BindImageTexture = (PFNGLBINDIMAGETEXTUREPROC) load(userptr, \"glBindImageTexture\");\n    context->DrawArraysInstancedBaseInstance = (PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC) load(userptr, \"glDrawArraysInstancedBaseInstance\");\n    context->DrawElementsInstancedBaseInstance = (PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC) load(userptr, \"glDrawElementsInstancedBaseInstance\");\n    context->DrawElementsInstancedBaseVertexBaseInstance = (PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC) load(userptr, \"glDrawElementsInstancedBaseVertexBaseInstance\");\n    context->DrawTransformFeedbackInstanced = (PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC) load(userptr, \"glDrawTransformFeedbackInstanced\");\n    context->DrawTransformFeedbackStreamInstanced = (PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC) load(userptr, \"glDrawTransformFeedbackStreamInstanced\");\n    context->GetActiveAtomicCounterBufferiv = (PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC) load(userptr, \"glGetActiveAtomicCounterBufferiv\");\n    context->GetInternalformativ = (PFNGLGETINTERNALFORMATIVPROC) load(userptr, \"glGetInternalformativ\");\n    context->MemoryBarrier = (PFNGLMEMORYBARRIERPROC) load(userptr, \"glMemoryBarrier\");\n    context->TexStorage1D = (PFNGLTEXSTORAGE1DPROC) load(userptr, \"glTexStorage1D\");\n    context->TexStorage2D = (PFNGLTEXSTORAGE2DPROC) load(userptr, \"glTexStorage2D\");\n    context->TexStorage3D = (PFNGLTEXSTORAGE3DPROC) load(userptr, \"glTexStorage3D\");\n}\nstatic void glad_gl_load_GL_VERSION_4_3(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_4_3) return;\n    context->BindVertexBuffer = (PFNGLBINDVERTEXBUFFERPROC) load(userptr, \"glBindVertexBuffer\");\n    context->ClearBufferData = (PFNGLCLEARBUFFERDATAPROC) load(userptr, \"glClearBufferData\");\n    context->ClearBufferSubData = (PFNGLCLEARBUFFERSUBDATAPROC) load(userptr, \"glClearBufferSubData\");\n    context->CopyImageSubData = (PFNGLCOPYIMAGESUBDATAPROC) load(userptr, \"glCopyImageSubData\");\n    context->DebugMessageCallback = (PFNGLDEBUGMESSAGECALLBACKPROC) load(userptr, \"glDebugMessageCallback\");\n    context->DebugMessageControl = (PFNGLDEBUGMESSAGECONTROLPROC) load(userptr, \"glDebugMessageControl\");\n    context->DebugMessageInsert = (PFNGLDEBUGMESSAGEINSERTPROC) load(userptr, \"glDebugMessageInsert\");\n    context->DispatchCompute = (PFNGLDISPATCHCOMPUTEPROC) load(userptr, \"glDispatchCompute\");\n    context->DispatchComputeIndirect = (PFNGLDISPATCHCOMPUTEINDIRECTPROC) load(userptr, \"glDispatchComputeIndirect\");\n    context->FramebufferParameteri = (PFNGLFRAMEBUFFERPARAMETERIPROC) load(userptr, \"glFramebufferParameteri\");\n    context->GetDebugMessageLog = (PFNGLGETDEBUGMESSAGELOGPROC) load(userptr, \"glGetDebugMessageLog\");\n    context->GetFramebufferParameteriv = (PFNGLGETFRAMEBUFFERPARAMETERIVPROC) load(userptr, \"glGetFramebufferParameteriv\");\n    context->GetInternalformati64v = (PFNGLGETINTERNALFORMATI64VPROC) load(userptr, \"glGetInternalformati64v\");\n    context->GetObjectLabel = (PFNGLGETOBJECTLABELPROC) load(userptr, \"glGetObjectLabel\");\n    context->GetObjectPtrLabel = (PFNGLGETOBJECTPTRLABELPROC) load(userptr, \"glGetObjectPtrLabel\");\n    context->GetPointerv = (PFNGLGETPOINTERVPROC) load(userptr, \"glGetPointerv\");\n    context->GetProgramInterfaceiv = (PFNGLGETPROGRAMINTERFACEIVPROC) load(userptr, \"glGetProgramInterfaceiv\");\n    context->GetProgramResourceIndex = (PFNGLGETPROGRAMRESOURCEINDEXPROC) load(userptr, \"glGetProgramResourceIndex\");\n    context->GetProgramResourceLocation = (PFNGLGETPROGRAMRESOURCELOCATIONPROC) load(userptr, \"glGetProgramResourceLocation\");\n    context->GetProgramResourceLocationIndex = (PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC) load(userptr, \"glGetProgramResourceLocationIndex\");\n    context->GetProgramResourceName = (PFNGLGETPROGRAMRESOURCENAMEPROC) load(userptr, \"glGetProgramResourceName\");\n    context->GetProgramResourceiv = (PFNGLGETPROGRAMRESOURCEIVPROC) load(userptr, \"glGetProgramResourceiv\");\n    context->InvalidateBufferData = (PFNGLINVALIDATEBUFFERDATAPROC) load(userptr, \"glInvalidateBufferData\");\n    context->InvalidateBufferSubData = (PFNGLINVALIDATEBUFFERSUBDATAPROC) load(userptr, \"glInvalidateBufferSubData\");\n    context->InvalidateFramebuffer = (PFNGLINVALIDATEFRAMEBUFFERPROC) load(userptr, \"glInvalidateFramebuffer\");\n    context->InvalidateSubFramebuffer = (PFNGLINVALIDATESUBFRAMEBUFFERPROC) load(userptr, \"glInvalidateSubFramebuffer\");\n    context->InvalidateTexImage = (PFNGLINVALIDATETEXIMAGEPROC) load(userptr, \"glInvalidateTexImage\");\n    context->InvalidateTexSubImage = (PFNGLINVALIDATETEXSUBIMAGEPROC) load(userptr, \"glInvalidateTexSubImage\");\n    context->MultiDrawArraysIndirect = (PFNGLMULTIDRAWARRAYSINDIRECTPROC) load(userptr, \"glMultiDrawArraysIndirect\");\n    context->MultiDrawElementsIndirect = (PFNGLMULTIDRAWELEMENTSINDIRECTPROC) load(userptr, \"glMultiDrawElementsIndirect\");\n    context->ObjectLabel = (PFNGLOBJECTLABELPROC) load(userptr, \"glObjectLabel\");\n    context->ObjectPtrLabel = (PFNGLOBJECTPTRLABELPROC) load(userptr, \"glObjectPtrLabel\");\n    context->PopDebugGroup = (PFNGLPOPDEBUGGROUPPROC) load(userptr, \"glPopDebugGroup\");\n    context->PushDebugGroup = (PFNGLPUSHDEBUGGROUPPROC) load(userptr, \"glPushDebugGroup\");\n    context->ShaderStorageBlockBinding = (PFNGLSHADERSTORAGEBLOCKBINDINGPROC) load(userptr, \"glShaderStorageBlockBinding\");\n    context->TexBufferRange = (PFNGLTEXBUFFERRANGEPROC) load(userptr, \"glTexBufferRange\");\n    context->TexStorage2DMultisample = (PFNGLTEXSTORAGE2DMULTISAMPLEPROC) load(userptr, \"glTexStorage2DMultisample\");\n    context->TexStorage3DMultisample = (PFNGLTEXSTORAGE3DMULTISAMPLEPROC) load(userptr, \"glTexStorage3DMultisample\");\n    context->TextureView = (PFNGLTEXTUREVIEWPROC) load(userptr, \"glTextureView\");\n    context->VertexAttribBinding = (PFNGLVERTEXATTRIBBINDINGPROC) load(userptr, \"glVertexAttribBinding\");\n    context->VertexAttribFormat = (PFNGLVERTEXATTRIBFORMATPROC) load(userptr, \"glVertexAttribFormat\");\n    context->VertexAttribIFormat = (PFNGLVERTEXATTRIBIFORMATPROC) load(userptr, \"glVertexAttribIFormat\");\n    context->VertexAttribLFormat = (PFNGLVERTEXATTRIBLFORMATPROC) load(userptr, \"glVertexAttribLFormat\");\n    context->VertexBindingDivisor = (PFNGLVERTEXBINDINGDIVISORPROC) load(userptr, \"glVertexBindingDivisor\");\n}\nstatic void glad_gl_load_GL_VERSION_4_4(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_4_4) return;\n    context->BindBuffersBase = (PFNGLBINDBUFFERSBASEPROC) load(userptr, \"glBindBuffersBase\");\n    context->BindBuffersRange = (PFNGLBINDBUFFERSRANGEPROC) load(userptr, \"glBindBuffersRange\");\n    context->BindImageTextures = (PFNGLBINDIMAGETEXTURESPROC) load(userptr, \"glBindImageTextures\");\n    context->BindSamplers = (PFNGLBINDSAMPLERSPROC) load(userptr, \"glBindSamplers\");\n    context->BindTextures = (PFNGLBINDTEXTURESPROC) load(userptr, \"glBindTextures\");\n    context->BindVertexBuffers = (PFNGLBINDVERTEXBUFFERSPROC) load(userptr, \"glBindVertexBuffers\");\n    context->BufferStorage = (PFNGLBUFFERSTORAGEPROC) load(userptr, \"glBufferStorage\");\n    context->ClearTexImage = (PFNGLCLEARTEXIMAGEPROC) load(userptr, \"glClearTexImage\");\n    context->ClearTexSubImage = (PFNGLCLEARTEXSUBIMAGEPROC) load(userptr, \"glClearTexSubImage\");\n}\nstatic void glad_gl_load_GL_VERSION_4_5(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_4_5) return;\n    context->BindTextureUnit = (PFNGLBINDTEXTUREUNITPROC) load(userptr, \"glBindTextureUnit\");\n    context->BlitNamedFramebuffer = (PFNGLBLITNAMEDFRAMEBUFFERPROC) load(userptr, \"glBlitNamedFramebuffer\");\n    context->CheckNamedFramebufferStatus = (PFNGLCHECKNAMEDFRAMEBUFFERSTATUSPROC) load(userptr, \"glCheckNamedFramebufferStatus\");\n    context->ClearNamedBufferData = (PFNGLCLEARNAMEDBUFFERDATAPROC) load(userptr, \"glClearNamedBufferData\");\n    context->ClearNamedBufferSubData = (PFNGLCLEARNAMEDBUFFERSUBDATAPROC) load(userptr, \"glClearNamedBufferSubData\");\n    context->ClearNamedFramebufferfi = (PFNGLCLEARNAMEDFRAMEBUFFERFIPROC) load(userptr, \"glClearNamedFramebufferfi\");\n    context->ClearNamedFramebufferfv = (PFNGLCLEARNAMEDFRAMEBUFFERFVPROC) load(userptr, \"glClearNamedFramebufferfv\");\n    context->ClearNamedFramebufferiv = (PFNGLCLEARNAMEDFRAMEBUFFERIVPROC) load(userptr, \"glClearNamedFramebufferiv\");\n    context->ClearNamedFramebufferuiv = (PFNGLCLEARNAMEDFRAMEBUFFERUIVPROC) load(userptr, \"glClearNamedFramebufferuiv\");\n    context->ClipControl = (PFNGLCLIPCONTROLPROC) load(userptr, \"glClipControl\");\n    context->CompressedTextureSubImage1D = (PFNGLCOMPRESSEDTEXTURESUBIMAGE1DPROC) load(userptr, \"glCompressedTextureSubImage1D\");\n    context->CompressedTextureSubImage2D = (PFNGLCOMPRESSEDTEXTURESUBIMAGE2DPROC) load(userptr, \"glCompressedTextureSubImage2D\");\n    context->CompressedTextureSubImage3D = (PFNGLCOMPRESSEDTEXTURESUBIMAGE3DPROC) load(userptr, \"glCompressedTextureSubImage3D\");\n    context->CopyNamedBufferSubData = (PFNGLCOPYNAMEDBUFFERSUBDATAPROC) load(userptr, \"glCopyNamedBufferSubData\");\n    context->CopyTextureSubImage1D = (PFNGLCOPYTEXTURESUBIMAGE1DPROC) load(userptr, \"glCopyTextureSubImage1D\");\n    context->CopyTextureSubImage2D = (PFNGLCOPYTEXTURESUBIMAGE2DPROC) load(userptr, \"glCopyTextureSubImage2D\");\n    context->CopyTextureSubImage3D = (PFNGLCOPYTEXTURESUBIMAGE3DPROC) load(userptr, \"glCopyTextureSubImage3D\");\n    context->CreateBuffers = (PFNGLCREATEBUFFERSPROC) load(userptr, \"glCreateBuffers\");\n    context->CreateFramebuffers = (PFNGLCREATEFRAMEBUFFERSPROC) load(userptr, \"glCreateFramebuffers\");\n    context->CreateProgramPipelines = (PFNGLCREATEPROGRAMPIPELINESPROC) load(userptr, \"glCreateProgramPipelines\");\n    context->CreateQueries = (PFNGLCREATEQUERIESPROC) load(userptr, \"glCreateQueries\");\n    context->CreateRenderbuffers = (PFNGLCREATERENDERBUFFERSPROC) load(userptr, \"glCreateRenderbuffers\");\n    context->CreateSamplers = (PFNGLCREATESAMPLERSPROC) load(userptr, \"glCreateSamplers\");\n    context->CreateTextures = (PFNGLCREATETEXTURESPROC) load(userptr, \"glCreateTextures\");\n    context->CreateTransformFeedbacks = (PFNGLCREATETRANSFORMFEEDBACKSPROC) load(userptr, \"glCreateTransformFeedbacks\");\n    context->CreateVertexArrays = (PFNGLCREATEVERTEXARRAYSPROC) load(userptr, \"glCreateVertexArrays\");\n    context->DisableVertexArrayAttrib = (PFNGLDISABLEVERTEXARRAYATTRIBPROC) load(userptr, \"glDisableVertexArrayAttrib\");\n    context->EnableVertexArrayAttrib = (PFNGLENABLEVERTEXARRAYATTRIBPROC) load(userptr, \"glEnableVertexArrayAttrib\");\n    context->FlushMappedNamedBufferRange = (PFNGLFLUSHMAPPEDNAMEDBUFFERRANGEPROC) load(userptr, \"glFlushMappedNamedBufferRange\");\n    context->GenerateTextureMipmap = (PFNGLGENERATETEXTUREMIPMAPPROC) load(userptr, \"glGenerateTextureMipmap\");\n    context->GetCompressedTextureImage = (PFNGLGETCOMPRESSEDTEXTUREIMAGEPROC) load(userptr, \"glGetCompressedTextureImage\");\n    context->GetCompressedTextureSubImage = (PFNGLGETCOMPRESSEDTEXTURESUBIMAGEPROC) load(userptr, \"glGetCompressedTextureSubImage\");\n    context->GetGraphicsResetStatus = (PFNGLGETGRAPHICSRESETSTATUSPROC) load(userptr, \"glGetGraphicsResetStatus\");\n    context->GetNamedBufferParameteri64v = (PFNGLGETNAMEDBUFFERPARAMETERI64VPROC) load(userptr, \"glGetNamedBufferParameteri64v\");\n    context->GetNamedBufferParameteriv = (PFNGLGETNAMEDBUFFERPARAMETERIVPROC) load(userptr, \"glGetNamedBufferParameteriv\");\n    context->GetNamedBufferPointerv = (PFNGLGETNAMEDBUFFERPOINTERVPROC) load(userptr, \"glGetNamedBufferPointerv\");\n    context->GetNamedBufferSubData = (PFNGLGETNAMEDBUFFERSUBDATAPROC) load(userptr, \"glGetNamedBufferSubData\");\n    context->GetNamedFramebufferAttachmentParameteriv = (PFNGLGETNAMEDFRAMEBUFFERATTACHMENTPARAMETERIVPROC) load(userptr, \"glGetNamedFramebufferAttachmentParameteriv\");\n    context->GetNamedFramebufferParameteriv = (PFNGLGETNAMEDFRAMEBUFFERPARAMETERIVPROC) load(userptr, \"glGetNamedFramebufferParameteriv\");\n    context->GetNamedRenderbufferParameteriv = (PFNGLGETNAMEDRENDERBUFFERPARAMETERIVPROC) load(userptr, \"glGetNamedRenderbufferParameteriv\");\n    context->GetQueryBufferObjecti64v = (PFNGLGETQUERYBUFFEROBJECTI64VPROC) load(userptr, \"glGetQueryBufferObjecti64v\");\n    context->GetQueryBufferObjectiv = (PFNGLGETQUERYBUFFEROBJECTIVPROC) load(userptr, \"glGetQueryBufferObjectiv\");\n    context->GetQueryBufferObjectui64v = (PFNGLGETQUERYBUFFEROBJECTUI64VPROC) load(userptr, \"glGetQueryBufferObjectui64v\");\n    context->GetQueryBufferObjectuiv = (PFNGLGETQUERYBUFFEROBJECTUIVPROC) load(userptr, \"glGetQueryBufferObjectuiv\");\n    context->GetTextureImage = (PFNGLGETTEXTUREIMAGEPROC) load(userptr, \"glGetTextureImage\");\n    context->GetTextureLevelParameterfv = (PFNGLGETTEXTURELEVELPARAMETERFVPROC) load(userptr, \"glGetTextureLevelParameterfv\");\n    context->GetTextureLevelParameteriv = (PFNGLGETTEXTURELEVELPARAMETERIVPROC) load(userptr, \"glGetTextureLevelParameteriv\");\n    context->GetTextureParameterIiv = (PFNGLGETTEXTUREPARAMETERIIVPROC) load(userptr, \"glGetTextureParameterIiv\");\n    context->GetTextureParameterIuiv = (PFNGLGETTEXTUREPARAMETERIUIVPROC) load(userptr, \"glGetTextureParameterIuiv\");\n    context->GetTextureParameterfv = (PFNGLGETTEXTUREPARAMETERFVPROC) load(userptr, \"glGetTextureParameterfv\");\n    context->GetTextureParameteriv = (PFNGLGETTEXTUREPARAMETERIVPROC) load(userptr, \"glGetTextureParameteriv\");\n    context->GetTextureSubImage = (PFNGLGETTEXTURESUBIMAGEPROC) load(userptr, \"glGetTextureSubImage\");\n    context->GetTransformFeedbacki64_v = (PFNGLGETTRANSFORMFEEDBACKI64_VPROC) load(userptr, \"glGetTransformFeedbacki64_v\");\n    context->GetTransformFeedbacki_v = (PFNGLGETTRANSFORMFEEDBACKI_VPROC) load(userptr, \"glGetTransformFeedbacki_v\");\n    context->GetTransformFeedbackiv = (PFNGLGETTRANSFORMFEEDBACKIVPROC) load(userptr, \"glGetTransformFeedbackiv\");\n    context->GetVertexArrayIndexed64iv = (PFNGLGETVERTEXARRAYINDEXED64IVPROC) load(userptr, \"glGetVertexArrayIndexed64iv\");\n    context->GetVertexArrayIndexediv = (PFNGLGETVERTEXARRAYINDEXEDIVPROC) load(userptr, \"glGetVertexArrayIndexediv\");\n    context->GetVertexArrayiv = (PFNGLGETVERTEXARRAYIVPROC) load(userptr, \"glGetVertexArrayiv\");\n    context->GetnColorTable = (PFNGLGETNCOLORTABLEPROC) load(userptr, \"glGetnColorTable\");\n    context->GetnCompressedTexImage = (PFNGLGETNCOMPRESSEDTEXIMAGEPROC) load(userptr, \"glGetnCompressedTexImage\");\n    context->GetnConvolutionFilter = (PFNGLGETNCONVOLUTIONFILTERPROC) load(userptr, \"glGetnConvolutionFilter\");\n    context->GetnHistogram = (PFNGLGETNHISTOGRAMPROC) load(userptr, \"glGetnHistogram\");\n    context->GetnMapdv = (PFNGLGETNMAPDVPROC) load(userptr, \"glGetnMapdv\");\n    context->GetnMapfv = (PFNGLGETNMAPFVPROC) load(userptr, \"glGetnMapfv\");\n    context->GetnMapiv = (PFNGLGETNMAPIVPROC) load(userptr, \"glGetnMapiv\");\n    context->GetnMinmax = (PFNGLGETNMINMAXPROC) load(userptr, \"glGetnMinmax\");\n    context->GetnPixelMapfv = (PFNGLGETNPIXELMAPFVPROC) load(userptr, \"glGetnPixelMapfv\");\n    context->GetnPixelMapuiv = (PFNGLGETNPIXELMAPUIVPROC) load(userptr, \"glGetnPixelMapuiv\");\n    context->GetnPixelMapusv = (PFNGLGETNPIXELMAPUSVPROC) load(userptr, \"glGetnPixelMapusv\");\n    context->GetnPolygonStipple = (PFNGLGETNPOLYGONSTIPPLEPROC) load(userptr, \"glGetnPolygonStipple\");\n    context->GetnSeparableFilter = (PFNGLGETNSEPARABLEFILTERPROC) load(userptr, \"glGetnSeparableFilter\");\n    context->GetnTexImage = (PFNGLGETNTEXIMAGEPROC) load(userptr, \"glGetnTexImage\");\n    context->GetnUniformdv = (PFNGLGETNUNIFORMDVPROC) load(userptr, \"glGetnUniformdv\");\n    context->GetnUniformfv = (PFNGLGETNUNIFORMFVPROC) load(userptr, \"glGetnUniformfv\");\n    context->GetnUniformiv = (PFNGLGETNUNIFORMIVPROC) load(userptr, \"glGetnUniformiv\");\n    context->GetnUniformuiv = (PFNGLGETNUNIFORMUIVPROC) load(userptr, \"glGetnUniformuiv\");\n    context->InvalidateNamedFramebufferData = (PFNGLINVALIDATENAMEDFRAMEBUFFERDATAPROC) load(userptr, \"glInvalidateNamedFramebufferData\");\n    context->InvalidateNamedFramebufferSubData = (PFNGLINVALIDATENAMEDFRAMEBUFFERSUBDATAPROC) load(userptr, \"glInvalidateNamedFramebufferSubData\");\n    context->MapNamedBuffer = (PFNGLMAPNAMEDBUFFERPROC) load(userptr, \"glMapNamedBuffer\");\n    context->MapNamedBufferRange = (PFNGLMAPNAMEDBUFFERRANGEPROC) load(userptr, \"glMapNamedBufferRange\");\n    context->MemoryBarrierByRegion = (PFNGLMEMORYBARRIERBYREGIONPROC) load(userptr, \"glMemoryBarrierByRegion\");\n    context->NamedBufferData = (PFNGLNAMEDBUFFERDATAPROC) load(userptr, \"glNamedBufferData\");\n    context->NamedBufferStorage = (PFNGLNAMEDBUFFERSTORAGEPROC) load(userptr, \"glNamedBufferStorage\");\n    context->NamedBufferSubData = (PFNGLNAMEDBUFFERSUBDATAPROC) load(userptr, \"glNamedBufferSubData\");\n    context->NamedFramebufferDrawBuffer = (PFNGLNAMEDFRAMEBUFFERDRAWBUFFERPROC) load(userptr, \"glNamedFramebufferDrawBuffer\");\n    context->NamedFramebufferDrawBuffers = (PFNGLNAMEDFRAMEBUFFERDRAWBUFFERSPROC) load(userptr, \"glNamedFramebufferDrawBuffers\");\n    context->NamedFramebufferParameteri = (PFNGLNAMEDFRAMEBUFFERPARAMETERIPROC) load(userptr, \"glNamedFramebufferParameteri\");\n    context->NamedFramebufferReadBuffer = (PFNGLNAMEDFRAMEBUFFERREADBUFFERPROC) load(userptr, \"glNamedFramebufferReadBuffer\");\n    context->NamedFramebufferRenderbuffer = (PFNGLNAMEDFRAMEBUFFERRENDERBUFFERPROC) load(userptr, \"glNamedFramebufferRenderbuffer\");\n    context->NamedFramebufferTexture = (PFNGLNAMEDFRAMEBUFFERTEXTUREPROC) load(userptr, \"glNamedFramebufferTexture\");\n    context->NamedFramebufferTextureLayer = (PFNGLNAMEDFRAMEBUFFERTEXTURELAYERPROC) load(userptr, \"glNamedFramebufferTextureLayer\");\n    context->NamedRenderbufferStorage = (PFNGLNAMEDRENDERBUFFERSTORAGEPROC) load(userptr, \"glNamedRenderbufferStorage\");\n    context->NamedRenderbufferStorageMultisample = (PFNGLNAMEDRENDERBUFFERSTORAGEMULTISAMPLEPROC) load(userptr, \"glNamedRenderbufferStorageMultisample\");\n    context->ReadnPixels = (PFNGLREADNPIXELSPROC) load(userptr, \"glReadnPixels\");\n    context->TextureBarrier = (PFNGLTEXTUREBARRIERPROC) load(userptr, \"glTextureBarrier\");\n    context->TextureBuffer = (PFNGLTEXTUREBUFFERPROC) load(userptr, \"glTextureBuffer\");\n    context->TextureBufferRange = (PFNGLTEXTUREBUFFERRANGEPROC) load(userptr, \"glTextureBufferRange\");\n    context->TextureParameterIiv = (PFNGLTEXTUREPARAMETERIIVPROC) load(userptr, \"glTextureParameterIiv\");\n    context->TextureParameterIuiv = (PFNGLTEXTUREPARAMETERIUIVPROC) load(userptr, \"glTextureParameterIuiv\");\n    context->TextureParameterf = (PFNGLTEXTUREPARAMETERFPROC) load(userptr, \"glTextureParameterf\");\n    context->TextureParameterfv = (PFNGLTEXTUREPARAMETERFVPROC) load(userptr, \"glTextureParameterfv\");\n    context->TextureParameteri = (PFNGLTEXTUREPARAMETERIPROC) load(userptr, \"glTextureParameteri\");\n    context->TextureParameteriv = (PFNGLTEXTUREPARAMETERIVPROC) load(userptr, \"glTextureParameteriv\");\n    context->TextureStorage1D = (PFNGLTEXTURESTORAGE1DPROC) load(userptr, \"glTextureStorage1D\");\n    context->TextureStorage2D = (PFNGLTEXTURESTORAGE2DPROC) load(userptr, \"glTextureStorage2D\");\n    context->TextureStorage2DMultisample = (PFNGLTEXTURESTORAGE2DMULTISAMPLEPROC) load(userptr, \"glTextureStorage2DMultisample\");\n    context->TextureStorage3D = (PFNGLTEXTURESTORAGE3DPROC) load(userptr, \"glTextureStorage3D\");\n    context->TextureStorage3DMultisample = (PFNGLTEXTURESTORAGE3DMULTISAMPLEPROC) load(userptr, \"glTextureStorage3DMultisample\");\n    context->TextureSubImage1D = (PFNGLTEXTURESUBIMAGE1DPROC) load(userptr, \"glTextureSubImage1D\");\n    context->TextureSubImage2D = (PFNGLTEXTURESUBIMAGE2DPROC) load(userptr, \"glTextureSubImage2D\");\n    context->TextureSubImage3D = (PFNGLTEXTURESUBIMAGE3DPROC) load(userptr, \"glTextureSubImage3D\");\n    context->TransformFeedbackBufferBase = (PFNGLTRANSFORMFEEDBACKBUFFERBASEPROC) load(userptr, \"glTransformFeedbackBufferBase\");\n    context->TransformFeedbackBufferRange = (PFNGLTRANSFORMFEEDBACKBUFFERRANGEPROC) load(userptr, \"glTransformFeedbackBufferRange\");\n    context->UnmapNamedBuffer = (PFNGLUNMAPNAMEDBUFFERPROC) load(userptr, \"glUnmapNamedBuffer\");\n    context->VertexArrayAttribBinding = (PFNGLVERTEXARRAYATTRIBBINDINGPROC) load(userptr, \"glVertexArrayAttribBinding\");\n    context->VertexArrayAttribFormat = (PFNGLVERTEXARRAYATTRIBFORMATPROC) load(userptr, \"glVertexArrayAttribFormat\");\n    context->VertexArrayAttribIFormat = (PFNGLVERTEXARRAYATTRIBIFORMATPROC) load(userptr, \"glVertexArrayAttribIFormat\");\n    context->VertexArrayAttribLFormat = (PFNGLVERTEXARRAYATTRIBLFORMATPROC) load(userptr, \"glVertexArrayAttribLFormat\");\n    context->VertexArrayBindingDivisor = (PFNGLVERTEXARRAYBINDINGDIVISORPROC) load(userptr, \"glVertexArrayBindingDivisor\");\n    context->VertexArrayElementBuffer = (PFNGLVERTEXARRAYELEMENTBUFFERPROC) load(userptr, \"glVertexArrayElementBuffer\");\n    context->VertexArrayVertexBuffer = (PFNGLVERTEXARRAYVERTEXBUFFERPROC) load(userptr, \"glVertexArrayVertexBuffer\");\n    context->VertexArrayVertexBuffers = (PFNGLVERTEXARRAYVERTEXBUFFERSPROC) load(userptr, \"glVertexArrayVertexBuffers\");\n}\nstatic void glad_gl_load_GL_VERSION_4_6(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {\n    if(!context->VERSION_4_6) return;\n    context->MultiDrawArraysIndirectCount = (PFNGLMULTIDRAWARRAYSINDIRECTCOUNTPROC) load(userptr, \"glMultiDrawArraysIndirectCount\");\n    context->MultiDrawElementsIndirectCount = (PFNGLMULTIDRAWELEMENTSINDIRECTCOUNTPROC) load(userptr, \"glMultiDrawElementsIndirectCount\");\n    context->PolygonOffsetClamp = (PFNGLPOLYGONOFFSETCLAMPPROC) load(userptr, \"glPolygonOffsetClamp\");\n    context->SpecializeShader = (PFNGLSPECIALIZESHADERPROC) load(userptr, \"glSpecializeShader\");\n}\n\n\n\n#if defined(GL_ES_VERSION_3_0) || defined(GL_VERSION_3_0)\n#define GLAD_GL_IS_SOME_NEW_VERSION 1\n#else\n#define GLAD_GL_IS_SOME_NEW_VERSION 0\n#endif\n\nstatic int glad_gl_get_extensions(GladGLContext *context, int version, const char **out_exts, unsigned int *out_num_exts_i, char ***out_exts_i) {\n#if GLAD_GL_IS_SOME_NEW_VERSION\n    if(GLAD_VERSION_MAJOR(version) < 3) {\n#else\n    (void) version;\n    (void) out_num_exts_i;\n    (void) out_exts_i;\n#endif\n        if (context->GetString == NULL) {\n            return 0;\n        }\n        *out_exts = (const char *)context->GetString(GL_EXTENSIONS);\n#if GLAD_GL_IS_SOME_NEW_VERSION\n    } else {\n        unsigned int index = 0;\n        unsigned int num_exts_i = 0;\n        char **exts_i = NULL;\n        if (context->GetStringi == NULL || context->GetIntegerv == NULL) {\n            return 0;\n        }\n        context->GetIntegerv(GL_NUM_EXTENSIONS, (int*) &num_exts_i);\n        if (num_exts_i > 0) {\n            exts_i = (char **) malloc(num_exts_i * (sizeof *exts_i));\n        }\n        if (exts_i == NULL) {\n            return 0;\n        }\n        for(index = 0; index < num_exts_i; index++) {\n            const char *gl_str_tmp = (const char*) context->GetStringi(GL_EXTENSIONS, index);\n            size_t len = strlen(gl_str_tmp) + 1;\n\n            char *local_str = (char*) malloc(len * sizeof(char));\n            if(local_str != NULL) {\n                memcpy(local_str, gl_str_tmp, len * sizeof(char));\n            }\n\n            exts_i[index] = local_str;\n        }\n\n        *out_num_exts_i = num_exts_i;\n        *out_exts_i = exts_i;\n    }\n#endif\n    return 1;\n}\nstatic void glad_gl_free_extensions(char **exts_i, unsigned int num_exts_i) {\n    if (exts_i != NULL) {\n        unsigned int index;\n        for(index = 0; index < num_exts_i; index++) {\n            free((void *) (exts_i[index]));\n        }\n        free((void *)exts_i);\n        exts_i = NULL;\n    }\n}\nstatic int glad_gl_has_extension(int version, const char *exts, unsigned int num_exts_i, char **exts_i, const char *ext) {\n    if(GLAD_VERSION_MAJOR(version) < 3 || !GLAD_GL_IS_SOME_NEW_VERSION) {\n        const char *extensions;\n        const char *loc;\n        const char *terminator;\n        extensions = exts;\n        if(extensions == NULL || ext == NULL) {\n            return 0;\n        }\n        while(1) {\n            loc = strstr(extensions, ext);\n            if(loc == NULL) {\n                return 0;\n            }\n            terminator = loc + strlen(ext);\n            if((loc == extensions || *(loc - 1) == ' ') &&\n                (*terminator == ' ' || *terminator == '\\0')) {\n                return 1;\n            }\n            extensions = terminator;\n        }\n    } else {\n        unsigned int index;\n        for(index = 0; index < num_exts_i; index++) {\n            const char *e = exts_i[index];\n            if(strcmp(e, ext) == 0) {\n                return 1;\n            }\n        }\n    }\n    return 0;\n}\n\nstatic GLADapiproc glad_gl_get_proc_from_userptr(void *userptr, const char* name) {\n    return (GLAD_GNUC_EXTENSION (GLADapiproc (*)(const char *name)) userptr)(name);\n}\n\nstatic int glad_gl_find_extensions_gl(GladGLContext *context, int version) {\n    const char *exts = NULL;\n    unsigned int num_exts_i = 0;\n    char **exts_i = NULL;\n    if (!glad_gl_get_extensions(context, version, &exts, &num_exts_i, &exts_i)) return 0;\n\n    (void) glad_gl_has_extension;\n\n    glad_gl_free_extensions(exts_i, num_exts_i);\n\n    return 1;\n}\n\nstatic int glad_gl_find_core_gl(GladGLContext *context) {\n    int i, major, minor;\n    const char* version;\n    const char* prefixes[] = {\n        \"OpenGL ES-CM \",\n        \"OpenGL ES-CL \",\n        \"OpenGL ES \",\n        NULL\n    };\n    version = (const char*) context->GetString(GL_VERSION);\n    if (!version) return 0;\n    for (i = 0;  prefixes[i];  i++) {\n        const size_t length = strlen(prefixes[i]);\n        if (strncmp(version, prefixes[i], length) == 0) {\n            version += length;\n            break;\n        }\n    }\n\n    GLAD_IMPL_UTIL_SSCANF(version, \"%d.%d\", &major, &minor);\n\n    // attempt to grab whatever we can\n    int temp = major;\n    major = 5;\n\n    context->VERSION_1_0 = (major == 1 && minor >= 0) || major > 1;\n    context->VERSION_1_1 = (major == 1 && minor >= 1) || major > 1;\n    context->VERSION_1_2 = (major == 1 && minor >= 2) || major > 1;\n    context->VERSION_1_3 = (major == 1 && minor >= 3) || major > 1;\n    context->VERSION_1_4 = (major == 1 && minor >= 4) || major > 1;\n    context->VERSION_1_5 = (major == 1 && minor >= 5) || major > 1;\n    context->VERSION_2_0 = (major == 2 && minor >= 0) || major > 2;\n    context->VERSION_2_1 = (major == 2 && minor >= 1) || major > 2;\n    context->VERSION_3_0 = (major == 3 && minor >= 0) || major > 3;\n    context->VERSION_3_1 = (major == 3 && minor >= 1) || major > 3;\n    context->VERSION_3_2 = (major == 3 && minor >= 2) || major > 3;\n    context->VERSION_3_3 = (major == 3 && minor >= 3) || major > 3;\n    context->VERSION_4_0 = (major == 4 && minor >= 0) || major > 4;\n    context->VERSION_4_1 = (major == 4 && minor >= 1) || major > 4;\n    context->VERSION_4_2 = (major == 4 && minor >= 2) || major > 4;\n    context->VERSION_4_3 = (major == 4 && minor >= 3) || major > 4;\n    context->VERSION_4_4 = (major == 4 && minor >= 4) || major > 4;\n    context->VERSION_4_5 = (major == 4 && minor >= 5) || major > 4;\n    context->VERSION_4_6 = (major == 4 && minor >= 6) || major > 4;\n\n    major = temp;\n    return GLAD_MAKE_VERSION(major, minor);\n}\n\nint gladLoadGLContextUserPtr(GladGLContext *context, GLADuserptrloadfunc load, void *userptr) {\n    int version;\n\n    context->GetString = (PFNGLGETSTRINGPROC) load(userptr, \"glGetString\");\n    if(context->GetString == NULL) return 0;\n    if(context->GetString(GL_VERSION) == NULL) return 0;\n    version = glad_gl_find_core_gl(context);\n\n    glad_gl_load_GL_VERSION_1_0(context, load, userptr);\n    glad_gl_load_GL_VERSION_1_1(context, load, userptr);\n    glad_gl_load_GL_VERSION_1_2(context, load, userptr);\n    glad_gl_load_GL_VERSION_1_3(context, load, userptr);\n    glad_gl_load_GL_VERSION_1_4(context, load, userptr);\n    glad_gl_load_GL_VERSION_1_5(context, load, userptr);\n    glad_gl_load_GL_VERSION_2_0(context, load, userptr);\n    glad_gl_load_GL_VERSION_2_1(context, load, userptr);\n    glad_gl_load_GL_VERSION_3_0(context, load, userptr);\n    glad_gl_load_GL_VERSION_3_1(context, load, userptr);\n    glad_gl_load_GL_VERSION_3_2(context, load, userptr);\n    glad_gl_load_GL_VERSION_3_3(context, load, userptr);\n    glad_gl_load_GL_VERSION_4_0(context, load, userptr);\n    glad_gl_load_GL_VERSION_4_1(context, load, userptr);\n    glad_gl_load_GL_VERSION_4_2(context, load, userptr);\n    glad_gl_load_GL_VERSION_4_3(context, load, userptr);\n    glad_gl_load_GL_VERSION_4_4(context, load, userptr);\n    glad_gl_load_GL_VERSION_4_5(context, load, userptr);\n    glad_gl_load_GL_VERSION_4_6(context, load, userptr);\n\n    if (!glad_gl_find_extensions_gl(context, version)) return 0;\n\n\n\n    return version;\n}\n\n\nint gladLoadGLContext(GladGLContext *context, GLADloadfunc load) {\n    return gladLoadGLContextUserPtr(context, glad_gl_get_proc_from_userptr, GLAD_GNUC_EXTENSION (void*) load);\n}\n\n\n\n \n\n#ifdef GLAD_GL\n\n#ifndef GLAD_LOADER_LIBRARY_C_\n#define GLAD_LOADER_LIBRARY_C_\n\n#include <stddef.h>\n#include <stdlib.h>\n\n#if GLAD_PLATFORM_WIN32\n#include <windows.h>\n#else\n#include <dlfcn.h>\n#endif\n\n\nstatic void* glad_get_dlopen_handle(const char *lib_names[], int length) {\n    void *handle = NULL;\n    int i;\n\n    for (i = 0; i < length; ++i) {\n#if GLAD_PLATFORM_WIN32\n  #if GLAD_PLATFORM_UWP\n        size_t buffer_size = (strlen(lib_names[i]) + 1) * sizeof(WCHAR);\n        LPWSTR buffer = (LPWSTR) malloc(buffer_size);\n        if (buffer != NULL) {\n            int ret = MultiByteToWideChar(CP_ACP, 0, lib_names[i], -1, buffer, buffer_size);\n            if (ret != 0) {\n                handle = (void*) LoadPackagedLibrary(buffer, 0);\n            }\n            free((void*) buffer);\n        }\n  #else\n        handle = (void*) LoadLibraryA(lib_names[i]);\n  #endif\n#else\n        handle = dlopen(lib_names[i], RTLD_LAZY | RTLD_LOCAL);\n#endif\n        if (handle != NULL) {\n            return handle;\n        }\n    }\n\n    return NULL;\n}\n\nstatic void glad_close_dlopen_handle(void* handle) {\n    if (handle != NULL) {\n#if GLAD_PLATFORM_WIN32\n        FreeLibrary((HMODULE) handle);\n#else\n        dlclose(handle);\n#endif\n    }\n}\n\nstatic GLADapiproc glad_dlsym_handle(void* handle, const char *name) {\n    if (handle == NULL) {\n        return NULL;\n    }\n\n#if GLAD_PLATFORM_WIN32\n    return (GLADapiproc) GetProcAddress((HMODULE) handle, name);\n#else\n    return GLAD_GNUC_EXTENSION (GLADapiproc) dlsym(handle, name);\n#endif\n}\n\n#endif /* GLAD_LOADER_LIBRARY_C_ */\n\ntypedef void* (GLAD_API_PTR *GLADglprocaddrfunc)(const char*);\nstruct _glad_gl_userptr {\n    void *handle;\n    GLADglprocaddrfunc gl_get_proc_address_ptr;\n};\n\nstatic GLADapiproc glad_gl_get_proc(void *vuserptr, const char *name) {\n    struct _glad_gl_userptr userptr = *(struct _glad_gl_userptr*) vuserptr;\n    GLADapiproc result = NULL;\n\n    if(userptr.gl_get_proc_address_ptr != NULL) {\n        result = GLAD_GNUC_EXTENSION (GLADapiproc) userptr.gl_get_proc_address_ptr(name);\n    }\n    if(result == NULL) {\n        result = glad_dlsym_handle(userptr.handle, name);\n    }\n\n    return result;\n}\n\nstatic void* _gl_handle = NULL;\n\nstatic void* glad_gl_dlopen_handle(void) {\n#if GLAD_PLATFORM_APPLE\n    static const char *NAMES[] = {\n        \"../Frameworks/OpenGL.framework/OpenGL\",\n        \"/Library/Frameworks/OpenGL.framework/OpenGL\",\n        \"/System/Library/Frameworks/OpenGL.framework/OpenGL\",\n        \"/System/Library/Frameworks/OpenGL.framework/Versions/Current/OpenGL\"\n    };\n#elif GLAD_PLATFORM_WIN32\n    static const char *NAMES[] = {\"opengl32.dll\"};\n#else\n    static const char *NAMES[] = {\n  #if defined(__CYGWIN__)\n        \"libGL-1.so\",\n  #endif\n        \"libGL.so.1\",\n        \"libGL.so\"\n    };\n#endif\n\n    if (_gl_handle == NULL) {\n        _gl_handle = glad_get_dlopen_handle(NAMES, sizeof(NAMES) / sizeof(NAMES[0]));\n    }\n\n    return _gl_handle;\n}\n\nstatic struct _glad_gl_userptr glad_gl_build_userptr(void *handle) {\n    struct _glad_gl_userptr userptr;\n\n    userptr.handle = handle;\n#if GLAD_PLATFORM_APPLE || defined(__HAIKU__)\n    userptr.gl_get_proc_address_ptr = NULL;\n#elif GLAD_PLATFORM_WIN32\n    userptr.gl_get_proc_address_ptr =\n        (GLADglprocaddrfunc) glad_dlsym_handle(handle, \"wglGetProcAddress\");\n#else\n    userptr.gl_get_proc_address_ptr =\n        (GLADglprocaddrfunc) glad_dlsym_handle(handle, \"glXGetProcAddressARB\");\n#endif\n\n    return userptr;\n}\n\nint gladLoaderLoadGLContext(GladGLContext *context) {\n    int version = 0;\n    void *handle;\n    int did_load = 0;\n    struct _glad_gl_userptr userptr;\n\n    did_load = _gl_handle == NULL;\n    handle = glad_gl_dlopen_handle();\n    if (handle) {\n        userptr = glad_gl_build_userptr(handle);\n\n        version = gladLoadGLContextUserPtr(context,glad_gl_get_proc, &userptr);\n\n        if (did_load) {\n            gladLoaderUnloadGL();\n        }\n    }\n\n    return version;\n}\n\n\n\nvoid gladLoaderUnloadGL(void) {\n    if (_gl_handle != NULL) {\n        glad_close_dlopen_handle(_gl_handle);\n        _gl_handle = NULL;\n    }\n}\n\n#endif /* GLAD_GL */\n\n#ifdef __cplusplus\n}\n#endif"
  },
  {
    "path": "third-party/nvfbc/NvFBC.h",
    "content": "/*!\n * \\file\n *\n * This file contains the interface constants, structure definitions and\n * function prototypes defining the NvFBC API for Linux.\n *\n * Copyright (c) 2013-2020, NVIDIA CORPORATION. All rights reserved.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a\n * copy of this software and associated documentation files (the \"Software\"),\n * to deal in the Software without restriction, including without limitation\n * the rights to use, copy, modify, merge, publish, distribute, sublicense,\n * and/or sell copies of the Software, and to permit persons to whom the\n * Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL\n * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n * DEALINGS IN THE SOFTWARE.\n */\n\n#ifndef _NVFBC_H_\n#define _NVFBC_H_\n\n#include <stdint.h>\n\n/*!\n * \\mainpage NVIDIA Framebuffer Capture (NvFBC) for Linux.\n *\n * NvFBC is a high performance, low latency API to capture the framebuffer of\n * an X server screen.\n *\n * The output from NvFBC captures everything that would be visible if we were\n * directly looking at the monitor.  This includes window manager decoration,\n * mouse cursor, overlay, etc.\n *\n * It is ideally suited to desktop or fullscreen application capture and\n * remoting.\n */\n\n/*!\n * \\defgroup FBC_REQ Requirements\n *\n * The following requirements are provided by the regular NVIDIA Display Driver\n * package:\n *\n * - OpenGL core >= 4.2:\n *   Required.  NvFBC relies on OpenGL to perform frame capture and\n *   post-processing.\n *\n * - Vulkan 1.1:\n *   Required.\n *\n * - libcuda.so.1 >= 5.5:\n *   Optional. Used for capture to video memory with CUDA interop.\n *\n * The following requirements must be installed separately depending on the\n * Linux distribution being used:\n *\n * - XRandR extension >= 1.2:\n *   Optional.  Used for RandR output tracking.\n *\n * - libX11-xcb.so.1 >= 1.2:\n *   Required.  NvFBC uses a mix of Xlib and XCB.  Xlib is needed to use GLX,\n *   XCB is needed to make NvFBC more resilient against X server terminations\n *   while a capture session is active.\n *\n * - libxcb.so.1 >= 1.3:\n *   Required.  See above.\n *\n * - xorg-server >= 1.3:\n *   Optional.  Required for push model to work properly.\n *\n * Note that all optional dependencies are dlopen()'d at runtime.  Failure to\n * load an optional library is not fatal.\n */\n\n/*!\n * \\defgroup FBC_CHANGES ChangeLog\n *\n * NvFBC Linux API version 0.1\n * - Initial BETA release.\n *\n * NvFBC Linux API version 0.2\n * - Added 'bEnableMSE' field to NVFBC_H264_HW_ENC_CONFIG.\n * - Added 'dwMSE' field to NVFBC_TOH264_GRAB_FRAME_PARAMS.\n * - Added 'bEnableAQ' field to NVFBC_H264_HW_ENC_CONFIG.\n * - Added 'NVFBC_H264_PRESET_LOSSLESS_HP' enum to NVFBC_H264_PRESET.\n * - Added 'NVFBC_BUFFER_FORMAT_YUV444P' enum to NVFBC_BUFFER_FORMAT.\n * - Added 'eInputBufferFormat' field to NVFBC_H264_HW_ENC_CONFIG.\n * - Added '0' and '244' values for NVFBC_H264_HW_ENC_CONFIG::dwProfile.\n *\n * NvFBC Linux API version 0.3\n * - Improved multi-threaded support by implementing an API locking mechanism.\n * - Added 'nvFBCBindContext' API entry point.\n * - Added 'nvFBCReleaseContext' API entry point.\n *\n * NvFBC Linux API version 1.0\n * - Added codec agnostic interface for HW encoding.\n * - Deprecated H.264 interface.\n * - Added support for H.265/HEVC HW encoding.\n *\n * NvFBC Linux API version 1.1\n * - Added 'nvFBCToHwGetCaps' API entry point.\n * - Added 'dwDiffMapScalingFactor' field to NVFBC_TOSYS_SETUP_PARAMS.\n *\n * NvFBC Linux API version 1.2\n * - Deprecated ToHwEnc interface.\n * - Added ToGL interface that captures frames to an OpenGL texture in video\n *   memory.\n * - Added 'bDisableAutoModesetRecovery' field to\n *   NVFBC_CREATE_CAPTURE_SESSION_PARAMS.\n * - Added 'bExternallyManagedContext' field to NVFBC_CREATE_HANDLE_PARAMS.\n *\n * NvFBC Linux API version 1.3\n * - Added NVFBC_BUFFER_FORMAT_RGBA\n * - Added 'dwTimeoutMs' field to NVFBC_TOSYS_GRAB_FRAME_PARAMS,\n *   NVFBC_TOCUDA_GRAB_FRAME_PARAMS, and NVFBC_TOGL_GRAB_FRAME_PARAMS.\n *\n * NvFBC Linux API version 1.4\n * - Clarified that NVFBC_BUFFER_FORMAT_{ARGB,RGB,RGBA} are byte-order formats.\n * - Renamed NVFBC_BUFFER_FORMAT_YUV420P to NVFBC_BUFFER_FORMAT_NV12.\n * - Added new requirements.\n * - Made NvFBC more resilient against the X server terminating during an active\n *   capture session.  See new comments for ::NVFBC_ERR_X.\n * - Relaxed requirement that 'frameSize' must have a width being a multiple of\n *   4 and a height being a multiple of 2.\n * - Added 'bRoundFrameSize' field to NVFBC_CREATE_CAPTURE_SESSION_PARAMS.\n * - Relaxed requirement that the scaling factor for differential maps must be\n *   a multiple of the size of the frame.\n * - Added 'diffMapSize' field to NVFBC_TOSYS_SETUP_PARAMS and\n *   NVFBC_TOGL_SETUP_PARAMS.\n *\n * NvFBC Linux API version 1.5\n * - Added NVFBC_BUFFER_FORMAT_BGRA\n *\n * NvFBC Linux API version 1.6\n * - Added the 'NVFBC_TOSYS_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY',\n *   'NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY', and\n *   'NVFBC_TOGL_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY' capture flags.\n * - Exposed debug and performance logs through the NVFBC_LOG_LEVEL environment\n *   variable.  Setting it to \"1\" enables performance logs, setting it to \"2\"\n *   enables debugging logs, setting it to \"3\" enables both.\n * - Logs are printed to stdout or to the file pointed by the NVFBC_LOG_FILE\n *   environment variable.\n * - Added 'ulTimestampUs' to NVFBC_FRAME_GRAB_INFO.\n * - Added 'dwSamplingRateMs' to NVFBC_CREATE_CAPTURE_SESSION_PARAMS.\n * - Added 'bPushModel' to NVFBC_CREATE_CAPTURE_SESSION_PARAMS.\n *\n * NvFBC Linux API version 1.7\n * - Retired the NVFBC_CAPTURE_TO_HW_ENCODER interface.\n *   This interface has been deprecated since NvFBC 1.2 and has received no\n *   updates or new features since. We recommend using the NVIDIA Video Codec\n *   SDK to encode NvFBC frames.\n *   See: https://developer.nvidia.com/nvidia-video-codec-sdk\n * - Added a 'Capture Modes' section to those headers.\n * - Added a 'Post Processing' section to those headers.\n * - Added an 'Environment Variables' section to those headers.\n * - Added 'bInModeset' to NVFBC_GET_STATUS_PARAMS.\n * - Added 'bAllowDirectCapture' to NVFBC_CREATE_CAPTURE_SESSION_PARAMS.\n * - Added 'bDirectCaptured' to NVFBC_FRAME_GRAB_INFO.\n * - Added 'bRequiredPostProcessing' to NVFBC_FRAME_GRAB_INFO.\n */\n\n/*!\n * \\defgroup FBC_MODES Capture Modes\n *\n * When creating a capture session, NvFBC instantiates a capture subsystem\n * living in the NVIDIA X driver.\n *\n * This subsystem listens for damage events coming from applications then\n * generates (composites) frames for NvFBC when new content is available.\n *\n * This capture server can operate on a timer where it periodically checks if\n * there are any pending damage events, or it can generate frames as soon as it\n * receives a new damage event.\n * See NVFBC_CREATE_CAPTURE_SESSION_PARAMS::dwSamplingRateMs,\n * and NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bPushModel.\n *\n * NvFBC can also attach itself to a fullscreen unoccluded application and have\n * it copy its frames directly into a buffer owned by NvFBC upon present. This\n * mode bypasses the X server.\n * See NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bAllowDirectCapture.\n *\n * NvFBC is designed to capture frames with as few copies as possible. The\n * NVIDIA X driver composites frames directly into the NvFBC buffers, and\n * direct capture copies frames directly into these buffers as well.\n *\n * Depending on the configuration of a capture session, an extra copy (rendering\n * pass) may be needed. See the 'Post Processing' section.\n */\n\n/*!\n * \\defgroup FBC_PP Post Processing\n *\n * Depending on the configuration of a capture session, NvFBC might require to\n * do post processing on frames.\n *\n * Post processing is required for the following reasons:\n * - NvFBC needs to do a pixel format conversion.\n * - Diffmaps are requested.\n * - Capture to system memory is requested.\n *\n * NvFBC needs to do a conversion if the requested pixel format does not match\n * the native format. The native format is NVFBC_BUFFER_FORMAT_BGRA.\n *\n * Note: post processing is *not* required for frame scaling and frame cropping.\n *\n * Skipping post processing can reduce capture latency. An application can know\n * whether post processing was required by checking\n * NVFBC_FRAME_GRAB_INFO::bRequiredPostProcessing.\n */\n\n/*!\n * \\defgroup FBC_ENVVAR Environment Variables\n *\n * Below are the environment variables supported by NvFBC:\n *\n * - NVFBC_LOG_LEVEL\n *   Bitfield where the first bit enables debug logs and the second bit enables\n *   performance logs. Both can be enabled by setting this envvar to 3.\n *\n * - NVFBC_LOG_FILE\n *   Write all NvFBC logs to the given file.\n *\n * - NVFBC_FORCE_ALLOW_DIRECT_CAPTURE\n *   Used to override NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bAllowDirectCapture.\n *\n * - NVFBC_FORCE_POST_PROCESSING\n *   Used to force the post processing step, even if it could be skipped.\n *   See the 'Post Processing' section.\n */\n\n/*!\n * \\defgroup FBC_STRUCT Structure Definition\n *\n * @{\n */\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n/*!\n * Calling convention.\n */\n#define NVFBCAPI\n\n/*!\n * NvFBC API major version.\n */\n#define NVFBC_VERSION_MAJOR 1\n\n/*!\n * NvFBC API minor version.\n */\n#define NVFBC_VERSION_MINOR 7\n\n/*!\n * NvFBC API version.\n */\n#define NVFBC_VERSION (uint32_t)(NVFBC_VERSION_MINOR | (NVFBC_VERSION_MAJOR << 8))\n\n/*!\n * Creates a version number for structure parameters.\n */\n#define NVFBC_STRUCT_VERSION(typeName, ver) \\\n  (uint32_t)(sizeof(typeName) | ((ver) << 16) | (NVFBC_VERSION << 24))\n\n/*!\n * Defines error codes.\n *\n * \\see NvFBCGetLastErrorStr\n */\ntypedef enum _NVFBCSTATUS {\n  /*!\n   * This indicates that the API call returned with no errors.\n   */\n  NVFBC_SUCCESS = 0,\n  /*!\n   * This indicates that the API version between the client and the library\n   * is not compatible.\n   */\n  NVFBC_ERR_API_VERSION = 1,\n  /*!\n   * An internal error occurred.\n   */\n  NVFBC_ERR_INTERNAL = 2,\n  /*!\n   * This indicates that one or more of the parameter passed to the API call\n   * is invalid.\n   */\n  NVFBC_ERR_INVALID_PARAM = 3,\n  /*!\n   * This indicates that one or more of the pointers passed to the API call\n   * is invalid.\n   */\n  NVFBC_ERR_INVALID_PTR = 4,\n  /*!\n   * This indicates that the handle passed to the API call to identify the\n   * client is invalid.\n   */\n  NVFBC_ERR_INVALID_HANDLE = 5,\n  /*!\n   * This indicates that the maximum number of threaded clients of the same\n   * process has been reached.  The limit is 10 threads per process.\n   * There is no limit on the number of process.\n   */\n  NVFBC_ERR_MAX_CLIENTS = 6,\n  /*!\n   * This indicates that the requested feature is not currently supported\n   * by the library.\n   */\n  NVFBC_ERR_UNSUPPORTED = 7,\n  /*!\n   * This indicates that the API call failed because it was unable to allocate\n   * enough memory to perform the requested operation.\n   */\n  NVFBC_ERR_OUT_OF_MEMORY = 8,\n  /*!\n   * This indicates that the API call was not expected.  This happens when\n   * API calls are performed in a wrong order, such as trying to capture\n   * a frame prior to creating a new capture session; or trying to set up\n   * a capture to video memory although a capture session to system memory\n   * was created.\n   */\n  NVFBC_ERR_BAD_REQUEST = 9,\n  /*!\n   * This indicates an X error, most likely meaning that the X server has\n   * been terminated.  When this error is returned, the only resort is to\n   * create another FBC handle using NvFBCCreateHandle().\n   *\n   * The previous handle should still be freed with NvFBCDestroyHandle(), but\n   * it might leak resources, in particular X, GLX, and GL resources since\n   * it is no longer possible to communicate with an X server to free them\n   * through the driver.\n   *\n   * The best course of action to eliminate this potential leak is to close\n   * the OpenGL driver, close the forked process running the capture, or\n   * restart the application.\n   */\n  NVFBC_ERR_X = 10,\n  /*!\n   * This indicates a GLX error.\n   */\n  NVFBC_ERR_GLX = 11,\n  /*!\n   * This indicates an OpenGL error.\n   */\n  NVFBC_ERR_GL = 12,\n  /*!\n   * This indicates a CUDA error.\n   */\n  NVFBC_ERR_CUDA = 13,\n  /*!\n   * This indicates a HW encoder error.\n   */\n  NVFBC_ERR_ENCODER = 14,\n  /*!\n   * This indicates an NvFBC context error.\n   */\n  NVFBC_ERR_CONTEXT = 15,\n  /*!\n   * This indicates that the application must recreate the capture session.\n   *\n   * This error can be returned if a modeset event occurred while capturing\n   * frames, and NVFBC_CREATE_HANDLE_PARAMS::bDisableAutoModesetRecovery\n   * was set to NVFBC_TRUE.\n   */\n  NVFBC_ERR_MUST_RECREATE = 16,\n  /*!\n   * This indicates a Vulkan error.\n   */\n  NVFBC_ERR_VULKAN = 17,\n} NVFBCSTATUS;\n\n/*!\n * Defines boolean values.\n */\ntypedef enum _NVFBC_BOOL {\n  /*!\n   * False value.\n   */\n  NVFBC_FALSE = 0,\n  /*!\n   * True value.\n   */\n  NVFBC_TRUE,\n} NVFBC_BOOL;\n\n/*!\n * Maximum size in bytes of an error string.\n */\n#define NVFBC_ERR_STR_LEN 512\n\n/*!\n * Capture type.\n */\ntypedef enum _NVFBC_CAPTURE_TYPE {\n  /*!\n   * Capture frames to a buffer in system memory.\n   */\n  NVFBC_CAPTURE_TO_SYS = 0,\n  /*!\n   * Capture frames to a CUDA device in video memory.\n   *\n   * Specifying this will dlopen() libcuda.so.1 and fail if not available.\n   */\n  NVFBC_CAPTURE_SHARED_CUDA,\n  /*!\n   * Retired. Do not use.\n   */\n  /* NVFBC_CAPTURE_TO_HW_ENCODER, */\n  /*!\n   * Capture frames to an OpenGL buffer in video memory.\n   */\n  NVFBC_CAPTURE_TO_GL = 3,\n} NVFBC_CAPTURE_TYPE;\n\n/*!\n * Tracking type.\n *\n * NvFBC can track a specific region of the framebuffer to capture.\n *\n * An X screen corresponds to the entire framebuffer.\n *\n * An RandR CRTC is a component of the GPU that reads pixels from a region of\n * the X screen and sends them through a pipeline to an RandR output.\n * A physical monitor can be connected to an RandR output.  Tracking an RandR\n * output captures the region of the X screen that the RandR CRTC is sending to\n * the RandR output.\n */\ntypedef enum {\n  /*!\n   * By default, NvFBC tries to track a connected primary output.  If none is\n   * found, then it tries to track the first connected output.  If none is\n   * found then it tracks the entire X screen.\n   *\n   * If the XRandR extension is not available, this option has the same effect\n   * as ::NVFBC_TRACKING_SCREEN.\n   *\n   * This default behavior might be subject to changes in the future.\n   */\n  NVFBC_TRACKING_DEFAULT = 0,\n  /*!\n   * Track an RandR output specified by its ID in the appropriate field.\n   *\n   * The list of connected outputs can be queried via NvFBCGetStatus().\n   * This list can also be obtained using e.g., xrandr(1).\n   *\n   * If the XRandR extension is not available, setting this option returns an\n   * error.\n   */\n  NVFBC_TRACKING_OUTPUT,\n  /*!\n   * Track the entire X screen.\n   */\n  NVFBC_TRACKING_SCREEN,\n} NVFBC_TRACKING_TYPE;\n\n/*!\n * Buffer format.\n */\ntypedef enum _NVFBC_BUFFER_FORMAT {\n  /*!\n   * Data will be converted to ARGB8888 byte-order format. 32 bpp.\n   */\n  NVFBC_BUFFER_FORMAT_ARGB = 0,\n  /*!\n   * Data will be converted to RGB888 byte-order format. 24 bpp.\n   */\n  NVFBC_BUFFER_FORMAT_RGB,\n  /*!\n   * Data will be converted to NV12 format using HDTV weights\n   * according to ITU-R BT.709.  12 bpp.\n   */\n  NVFBC_BUFFER_FORMAT_NV12,\n  /*!\n   * Data will be converted to YUV 444 planar format using HDTV weights\n   * according to ITU-R BT.709.  24 bpp\n   */\n  NVFBC_BUFFER_FORMAT_YUV444P,\n  /*!\n   * Data will be converted to RGBA8888 byte-order format. 32 bpp.\n   */\n  NVFBC_BUFFER_FORMAT_RGBA,\n  /*!\n   * Native format. No pixel conversion needed.\n   * BGRA8888 byte-order format. 32 bpp.\n   */\n  NVFBC_BUFFER_FORMAT_BGRA,\n} NVFBC_BUFFER_FORMAT;\n\n#define NVFBC_BUFFER_FORMAT_YUV420P NVFBC_BUFFER_FORMAT_NV12\n\n/*!\n * Handle used to identify an NvFBC session.\n */\ntypedef uint64_t NVFBC_SESSION_HANDLE;\n\n/*!\n * Box used to describe an area of the tracked region to capture.\n *\n * The coordinates are relative to the tracked region.\n *\n * E.g., if the size of the X screen is 3520x1200 and the tracked RandR output\n * scans a region of 1600x1200+1920+0, then setting a capture box of\n * 800x600+100+50 effectively captures a region of 800x600+2020+50 relative to\n * the X screen.\n */\ntypedef struct _NVFBC_BOX {\n  /*!\n   * [in] X offset of the box.\n   */\n  uint32_t x;\n  /*!\n   * [in] Y offset of the box.\n   */\n  uint32_t y;\n  /*!\n   * [in] Width of the box.\n   */\n  uint32_t w;\n  /*!\n   * [in] Height of the box.\n   */\n  uint32_t h;\n} NVFBC_BOX;\n\n/*!\n * Size used to describe the size of a frame.\n */\ntypedef struct _NVFBC_SIZE {\n  /*!\n   * [in] Width.\n   */\n  uint32_t w;\n  /*!\n   * [in] Height.\n   */\n  uint32_t h;\n} NVFBC_SIZE;\n\n/*!\n * Describes information about a captured frame.\n */\ntypedef struct _NVFBC_FRAME_GRAB_INFO {\n  /*!\n   * [out] Width of the captured frame.\n   */\n  uint32_t dwWidth;\n  /*!\n   * [out] Height of the captured frame.\n   */\n  uint32_t dwHeight;\n  /*!\n   * [out] Size of the frame in bytes.\n   */\n  uint32_t dwByteSize;\n  /*!\n   * [out] Incremental ID of the current frame.\n   *\n   * This can be used to identify a frame.\n   */\n  uint32_t dwCurrentFrame;\n  /*!\n   * [out] Whether the captured frame is a new frame.\n   *\n   * When using non blocking calls it is possible to capture a frame\n   * that was already captured before if the display server did not\n   * render a new frame in the meantime.  In that case, this flag\n   * will be set to NVFBC_FALSE.\n   *\n   * When using blocking calls each captured frame will have\n   * this flag set to NVFBC_TRUE since the blocking mechanism waits for\n   * the display server to render a new frame.\n   *\n   * Note that this flag does not guarantee that the content of\n   * the frame will be different compared to the previous captured frame.\n   *\n   * In particular, some compositing managers report the entire\n   * framebuffer as damaged when an application refreshes its content.\n   *\n   * Consider a single X screen spanned across physical displays A and B\n   * and an NvFBC application tracking display A.  Depending on the\n   * compositing manager, it is possible that an application refreshing\n   * itself on display B will trigger a frame capture on display A.\n   *\n   * Workarounds include:\n   * - Using separate X screens\n   * - Disabling the composite extension\n   * - Using a compositing manager that properly reports what regions\n   *   are damaged\n   * - Using NvFBC's diffmaps to find out if the frame changed\n   */\n  NVFBC_BOOL bIsNewFrame;\n  /*!\n   * [out] Frame timestamp\n   *\n   * Time in micro seconds when the display server started rendering the\n   * frame.\n   *\n   * This does not account for when the frame was captured.  If capturing an\n   * old frame (e.g., bIsNewFrame is NVFBC_FALSE) the reported timestamp\n   * will reflect the time when the old frame was rendered by the display\n   * server.\n   */\n  uint64_t ulTimestampUs;\n  /*\n   * [out] Number of frames generated since the last capture.\n   *\n   * This can help applications tell whether they missed frames or there\n   * were no frames generated by the server since the last capture.\n   */\n  uint32_t dwMissedFrames;\n  /*\n   * [out] Whether the captured frame required post processing.\n   *\n   * See the 'Post Processing' section.\n   */\n  NVFBC_BOOL bRequiredPostProcessing;\n  /*\n   * [out] Whether this frame was obtained via direct capture.\n   *\n   * See NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bAllowDirectCapture.\n   */\n  NVFBC_BOOL bDirectCapture;\n} NVFBC_FRAME_GRAB_INFO;\n\n/*!\n * Defines parameters for the CreateHandle() API call.\n */\ntypedef struct _NVFBC_CREATE_HANDLE_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_CREATE_HANDLE_PARAMS_VER\n   */\n  uint32_t dwVersion;\n  /*!\n   * [in] Application specific private information passed to the NvFBC\n   * session.\n   */\n  const void *privateData;\n  /*!\n   * [in] Size of the application specific private information passed to the\n   * NvFBC session.\n   */\n  uint32_t privateDataSize;\n  /*!\n   * [in] Whether NvFBC should not create and manage its own graphics context\n   *\n   * NvFBC internally uses OpenGL to perform graphics operations on the\n   * captured frames.  By default, NvFBC will create and manage (e.g., make\n   * current, detect new threads, etc.) its own OpenGL context.\n   *\n   * If set to NVFBC_TRUE, NvFBC will use the application's context.  It will\n   * be the application's responsibility to make sure that a context is\n   * current on the thread calling into the NvFBC API.\n   */\n  NVFBC_BOOL bExternallyManagedContext;\n  /*!\n   * [in] GLX context\n   *\n   * GLX context that NvFBC should use internally to create pixmaps and\n   * make them current when creating a new capture session.\n   *\n   * Note: NvFBC expects a context created against a GLX_RGBA_TYPE render\n   * type.\n   */\n  void *glxCtx;\n  /*!\n   * [in] GLX framebuffer configuration\n   *\n   * Framebuffer configuration that was used to create the GLX context, and\n   * that will be used to create pixmaps internally.\n   *\n   * Note: NvFBC expects a configuration having at least the following\n   * attributes:\n   *  GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT\n   *  GLX_BIND_TO_TEXTURE_RGBA_EXT, 1\n   *  GLX_BIND_TO_TEXTURE_TARGETS_EXT, GLX_TEXTURE_2D_BIT_EXT\n   */\n  void *glxFBConfig;\n} NVFBC_CREATE_HANDLE_PARAMS;\n\n/*!\n * NVFBC_CREATE_HANDLE_PARAMS structure version.\n */\n#define NVFBC_CREATE_HANDLE_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_CREATE_HANDLE_PARAMS, 2)\n\n/*!\n * Defines parameters for the ::NvFBCDestroyHandle() API call.\n */\ntypedef struct _NVFBC_DESTROY_HANDLE_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_DESTROY_HANDLE_PARAMS_VER\n   */\n  uint32_t dwVersion;\n} NVFBC_DESTROY_HANDLE_PARAMS;\n\n/*!\n * NVFBC_DESTROY_HANDLE_PARAMS structure version.\n */\n#define NVFBC_DESTROY_HANDLE_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_DESTROY_HANDLE_PARAMS, 1)\n\n/*!\n * Maximum number of connected RandR outputs to an X screen.\n */\n#define NVFBC_OUTPUT_MAX 5\n\n/*!\n * Maximum size in bytes of an RandR output name.\n */\n#define NVFBC_OUTPUT_NAME_LEN 128\n\n/*!\n * Describes an RandR output.\n *\n * Filling this structure relies on the XRandR extension.  This feature cannot\n * be used if the extension is missing or its version is below the requirements.\n *\n * \\see Requirements\n */\ntypedef struct _NVFBC_OUTPUT {\n  /*!\n   * Identifier of the RandR output.\n   */\n  uint32_t dwId;\n  /*!\n   * Name of the RandR output, as reported by tools such as xrandr(1).\n   *\n   * Example: \"DVI-I-0\"\n   */\n  char name[NVFBC_OUTPUT_NAME_LEN];\n  /*!\n   * Region of the X screen tracked by the RandR CRTC driving this RandR\n   * output.\n   */\n  NVFBC_BOX trackedBox;\n} NVFBC_RANDR_OUTPUT_INFO;\n\n/*!\n * Defines parameters for the ::NvFBCGetStatus() API call.\n */\ntypedef struct _NVFBC_GET_STATUS_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_GET_STATUS_PARAMS_VER\n   */\n  uint32_t dwVersion;\n  /*!\n   * [out] Whether or not framebuffer capture is supported by the graphics\n   * driver.\n   */\n  NVFBC_BOOL bIsCapturePossible;\n  /*!\n   * [out] Whether or not there is already a capture session on this system.\n   */\n  NVFBC_BOOL bCurrentlyCapturing;\n  /*!\n   * [out] Whether or not it is possible to create a capture session on this\n   * system.\n   */\n  NVFBC_BOOL bCanCreateNow;\n  /*!\n   * [out] Size of the X screen (framebuffer).\n   */\n  NVFBC_SIZE screenSize;\n  /*!\n   * [out] Whether the XRandR extension is available.\n   *\n   * If this extension is not available then it is not possible to have\n   * information about RandR outputs.\n   */\n  NVFBC_BOOL bXRandRAvailable;\n  /*!\n   * [out] Array of outputs connected to the X screen.\n   *\n   * An application can track a specific output by specifying its ID when\n   * creating a capture session.\n   *\n   * Only if XRandR is available.\n   */\n  NVFBC_RANDR_OUTPUT_INFO outputs[NVFBC_OUTPUT_MAX];\n  /*!\n   * [out] Number of outputs connected to the X screen.\n   *\n   * This must be used to parse the array of connected outputs.\n   *\n   * Only if XRandR is available.\n   */\n  uint32_t dwOutputNum;\n  /*!\n   * [out] Version of the NvFBC library running on this system.\n   */\n  uint32_t dwNvFBCVersion;\n  /*!\n   * [out] Whether the X server is currently in modeset.\n   *\n   * When the X server is in modeset, it must give up all its video\n   * memory allocations. It is not possible to create a capture\n   * session until the modeset is over.\n   *\n   * Note that VT-switches are considered modesets.\n   */\n  NVFBC_BOOL bInModeset;\n} NVFBC_GET_STATUS_PARAMS;\n\n/*!\n * NVFBC_GET_STATUS_PARAMS structure version.\n */\n#define NVFBC_GET_STATUS_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_GET_STATUS_PARAMS, 2)\n\n/*!\n * Defines parameters for the ::NvFBCCreateCaptureSession() API call.\n */\ntypedef struct _NVFBC_CREATE_CAPTURE_SESSION_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER\n   */\n  uint32_t dwVersion;\n  /*!\n   * [in] Desired capture type.\n   *\n   * Note that when specyfing ::NVFBC_CAPTURE_SHARED_CUDA NvFBC will try to\n   * dlopen() the corresponding libraries.  This means that NvFBC can run on\n   * a system without the CUDA library since it does not link against them.\n   */\n  NVFBC_CAPTURE_TYPE eCaptureType;\n  /*!\n   * [in] What region of the framebuffer should be tracked.\n   */\n  NVFBC_TRACKING_TYPE eTrackingType;\n  /*!\n   * [in] ID of the output to track if eTrackingType is set to\n   * ::NVFBC_TRACKING_OUTPUT.\n   */\n  uint32_t dwOutputId;\n  /*!\n   * [in] Crop the tracked region.\n   *\n   * The coordinates are relative to the tracked region.\n   *\n   * It can be set to 0 to capture the entire tracked region.\n   */\n  NVFBC_BOX captureBox;\n  /*!\n   * [in] Desired size of the captured frame.\n   *\n   * This parameter allow to scale the captured frame.\n   *\n   * It can be set to 0 to disable frame resizing.\n   */\n  NVFBC_SIZE frameSize;\n  /*!\n   * [in] Whether the mouse cursor should be composited to the frame.\n   *\n   * Disabling the cursor will not generate new frames when only the cursor\n   * is moved.\n   */\n  NVFBC_BOOL bWithCursor;\n  /*!\n   * [in] Whether NvFBC should not attempt to recover from modesets.\n   *\n   * NvFBC is able to detect when a modeset event occurred and can automatically\n   * re-create a capture session with the same settings as before, then resume\n   * its frame capture session transparently.\n   *\n   * This option allows to disable this behavior.  NVFBC_ERR_MUST_RECREATE\n   * will be returned in that case.\n   *\n   * It can be useful in the cases when an application needs to do some work\n   * between setting up a capture and grabbing the first frame.\n   *\n   * For example: an application using the ToGL interface needs to register\n   * resources with EncodeAPI prior to encoding frames.\n   *\n   * Note that during modeset recovery, NvFBC will try to re-create the\n   * capture session every second until it succeeds.\n   */\n  NVFBC_BOOL bDisableAutoModesetRecovery;\n  /*!\n   * [in] Whether NvFBC should round the requested frameSize.\n   *\n   * When disabled, NvFBC will not attempt to round the requested resolution.\n   *\n   * However, some pixel formats have resolution requirements.  E.g., YUV/NV\n   * formats must have a width being a multiple of 4, and a height being a\n   * multiple of 2.  RGB formats don't have such requirements.\n   *\n   * If the resolution doesn't meet the requirements of the format, then NvFBC\n   * will fail at setup time.\n   *\n   * When enabled, NvFBC will round the requested width to the next multiple\n   * of 4 and the requested height to the next multiple of 2.\n   *\n   * In this case, requesting any resolution will always work with every\n   * format.  However, an NvFBC client must be prepared to handle the case\n   * where the requested resolution is different than the captured resolution.\n   *\n   * NVFBC_FRAME_GRAB_INFO::dwWidth and NVFBC_FRAME_GRAB_INFO::dwHeight should\n   * always be used for getting information about captured frames.\n   */\n  NVFBC_BOOL bRoundFrameSize;\n  /*!\n   * [in] Rate in ms at which the display server generates new frames\n   *\n   * This controls the frequency at which the display server will generate\n   * new frames if new content is available.  This effectively controls the\n   * capture rate when using blocking calls.\n   *\n   * Note that lower values will increase the CPU and GPU loads.\n   *\n   * The default value is 16ms (~ 60 Hz).\n   */\n  uint32_t dwSamplingRateMs;\n  /*!\n   * [in] Enable push model for frame capture\n   *\n   * When set to NVFBC_TRUE, the display server will generate frames whenever\n   * it receives a damage event from applications.\n   *\n   * Setting this to NVFBC_TRUE will ignore ::dwSamplingRateMs.\n   *\n   * Using push model with the NVFBC_*_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY\n   * capture flag should guarantee the shortest amount of time between an\n   * application rendering a frame and an NvFBC client capturing it, provided\n   * that the NvFBC client is able to process the frames quickly enough.\n   *\n   * Note that applications running at high frame rates will increase CPU and\n   * GPU loads.\n   */\n  NVFBC_BOOL bPushModel;\n  /*!\n   * [in] Allow direct capture\n   *\n   * Direct capture allows NvFBC to attach itself to a fullscreen graphics\n   * application. Whenever that application presents a frame, it makes a copy\n   * of it directly into a buffer owned by NvFBC thus bypassing the X server.\n   *\n   * When direct capture is *not* enabled, the NVIDIA X driver generates a\n   * frame for NvFBC when it receives a damage event from an application if push\n   * model is enabled, or periodically checks if there are any pending damage\n   * events otherwise (see NVFBC_CREATE_CAPTURE_SESSION_PARAMS::dwSamplingRateMs).\n   *\n   * Direct capture is possible under the following conditions:\n   * - Direct capture is allowed\n   * - Push model is enabled (see NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bPushModel)\n   * - The mouse cursor is not composited (see NVFBC_CREATE_CAPTURE_SESSION_PARAMS::bWithCursor)\n   * - No viewport transformation is required. This happens when the remote\n   *   desktop is e.g. rotated.\n   *\n   * When direct capture is possible, NvFBC will automatically attach itself\n   * to a fullscreen unoccluded application, if such exists.\n   *\n   * Notes:\n   * - This includes compositing desktops such as GNOME (e.g., gnome-shell\n   *   is the fullscreen unoccluded application).\n   * - There can be only one fullscreen unoccluded application at a time.\n   * - The NVIDIA X driver monitors which application qualifies or no\n   *   longer qualifies.\n   *\n   * For example, if a fullscreen application is launched in GNOME, NvFBC will\n   * detach from gnome-shell and attach to that application.\n   *\n   * Attaching and detaching happens automatically from the perspective of an\n   * NvFBC client. When detaching from an application, the X driver will\n   * transparently resume generating frames for NvFBC.\n   *\n   * An application can know whether a given frame was obtained through\n   * direct capture by checking NVFBC_FRAME_GRAB_INFO::bDirectCapture.\n   */\n  NVFBC_BOOL bAllowDirectCapture;\n} NVFBC_CREATE_CAPTURE_SESSION_PARAMS;\n\n/*!\n * NVFBC_CREATE_CAPTURE_SESSION_PARAMS structure version.\n */\n#define NVFBC_CREATE_CAPTURE_SESSION_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_CREATE_CAPTURE_SESSION_PARAMS, 6)\n\n/*!\n * Defines parameters for the ::NvFBCDestroyCaptureSession() API call.\n */\ntypedef struct _NVFBC_DESTROY_CAPTURE_SESSION_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER\n   */\n  uint32_t dwVersion;\n} NVFBC_DESTROY_CAPTURE_SESSION_PARAMS;\n\n/*!\n * NVFBC_DESTROY_CAPTURE_SESSION_PARAMS structure version.\n */\n#define NVFBC_DESTROY_CAPTURE_SESSION_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_DESTROY_CAPTURE_SESSION_PARAMS, 1)\n\n/*!\n * Defines parameters for the ::NvFBCBindContext() API call.\n */\ntypedef struct _NVFBC_BIND_CONTEXT_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_BIND_CONTEXT_PARAMS_VER\n   */\n  uint32_t dwVersion;\n} NVFBC_BIND_CONTEXT_PARAMS;\n\n/*!\n * NVFBC_BIND_CONTEXT_PARAMS structure version.\n */\n#define NVFBC_BIND_CONTEXT_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_BIND_CONTEXT_PARAMS, 1)\n\n/*!\n * Defines parameters for the ::NvFBCReleaseContext() API call.\n */\ntypedef struct _NVFBC_RELEASE_CONTEXT_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_RELEASE_CONTEXT_PARAMS_VER\n   */\n  uint32_t dwVersion;\n} NVFBC_RELEASE_CONTEXT_PARAMS;\n\n/*!\n * NVFBC_RELEASE_CONTEXT_PARAMS structure version.\n */\n#define NVFBC_RELEASE_CONTEXT_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_RELEASE_CONTEXT_PARAMS, 1)\n\n/*!\n * Defines flags that can be used when capturing to system memory.\n */\ntypedef enum {\n  /*!\n   * Default, capturing waits for a new frame or mouse move.\n   *\n   * The default behavior of blocking grabs is to wait for a new frame until\n   * after the call was made.  But it's possible that there is a frame already\n   * ready that the client hasn't seen.\n   * \\see NVFBC_TOSYS_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY\n   */\n  NVFBC_TOSYS_GRAB_FLAGS_NOFLAGS = 0,\n  /*!\n   * Capturing does not wait for a new frame nor a mouse move.\n   *\n   * It is therefore possible to capture the same frame multiple times.\n   * When this occurs, the dwCurrentFrame parameter of the\n   * NVFBC_FRAME_GRAB_INFO structure is not incremented.\n   */\n  NVFBC_TOSYS_GRAB_FLAGS_NOWAIT = (1 << 0),\n  /*!\n   * Forces the destination buffer to be refreshed even if the frame has not\n   * changed since previous capture.\n   *\n   * By default, if the captured frame is identical to the previous one, NvFBC\n   * will omit one copy and not update the destination buffer.\n   *\n   * Setting that flag will prevent this behavior.  This can be useful e.g.,\n   * if the application has modified the buffer in the meantime.\n   */\n  NVFBC_TOSYS_GRAB_FLAGS_FORCE_REFRESH = (1 << 1),\n  /*!\n   * Similar to NVFBC_TOSYS_GRAB_FLAGS_NOFLAGS, except that the capture will\n   * not wait if there is already a frame available that the client has\n   * never seen yet.\n   */\n  NVFBC_TOSYS_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY = (1 << 2),\n} NVFBC_TOSYS_GRAB_FLAGS;\n\n/*!\n * Defines parameters for the ::NvFBCToSysSetUp() API call.\n */\ntypedef struct _NVFBC_TOSYS_SETUP_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_TOSYS_SETUP_PARAMS_VER\n   */\n  uint32_t dwVersion;\n  /*!\n   * [in] Desired buffer format.\n   */\n  NVFBC_BUFFER_FORMAT eBufferFormat;\n  /*!\n   * [out] Pointer to a pointer to a buffer in system memory.\n   *\n   * This buffer contains the pixel value of the requested format.  Refer to\n   * the description of the buffer formats to understand the memory layout.\n   *\n   * The application does not need to allocate memory for this buffer.  It\n   * should not free this buffer either.  This buffer is automatically\n   * re-allocated when needed (e.g., when the resolution changes).\n   *\n   * This buffer is allocated by the NvFBC library to the proper size.  This\n   * size is returned in the dwByteSize field of the\n   * ::NVFBC_FRAME_GRAB_INFO structure.\n   */\n  void **ppBuffer;\n  /*!\n   * [in] Whether differential maps should be generated.\n   */\n  NVFBC_BOOL bWithDiffMap;\n  /*!\n   * [out] Pointer to a pointer to a buffer in system memory.\n   *\n   * This buffer contains the differential map of two frames.  It must be read\n   * as an array of unsigned char.  Each unsigned char is either 0 or\n   * non-zero.  0 means that the pixel value at the given location has not\n   * changed since the previous captured frame.  Non-zero means that the pixel\n   * value has changed.\n   *\n   * The application does not need to allocate memory for this buffer.  It\n   * should not free this buffer either.  This buffer is automatically\n   * re-allocated when needed (e.g., when the resolution changes).\n   *\n   * This buffer is allocated by the NvFBC library to the proper size.  The\n   * size of the differential map is returned in ::diffMapSize.\n   *\n   * This option is not compatible with the ::NVFBC_BUFFER_FORMAT_YUV420P and\n   * ::NVFBC_BUFFER_FORMAT_YUV444P buffer formats.\n   */\n  void **ppDiffMap;\n  /*!\n   * [in] Scaling factor of the differential maps.\n   *\n   * For example, a scaling factor of 16 means that one pixel of the diffmap\n   * will represent 16x16 pixels of the original frames.\n   *\n   * If any of these 16x16 pixels is different between the current and the\n   * previous frame, then the corresponding pixel in the diffmap will be set\n   * to non-zero.\n   *\n   * The default scaling factor is 1.  A dwDiffMapScalingFactor of 0 will be\n   * set to 1.\n   */\n  uint32_t dwDiffMapScalingFactor;\n  /*!\n   * [out] Size of the differential map.\n   *\n   * Only set if bWithDiffMap is set to NVFBC_TRUE.\n   */\n  NVFBC_SIZE diffMapSize;\n} NVFBC_TOSYS_SETUP_PARAMS;\n\n/*!\n * NVFBC_TOSYS_SETUP_PARAMS structure version.\n */\n#define NVFBC_TOSYS_SETUP_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOSYS_SETUP_PARAMS, 3)\n\n/*!\n * Defines parameters for the ::NvFBCToSysGrabFrame() API call.\n */\ntypedef struct _NVFBC_TOSYS_GRAB_FRAME_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER\n   */\n  uint32_t dwVersion;\n  /*!\n   * [in] Flags defining the behavior of this frame capture.\n   */\n  uint32_t dwFlags;\n  /*!\n   * [out] Information about the captured frame.\n   *\n   * Can be NULL.\n   */\n  NVFBC_FRAME_GRAB_INFO *pFrameGrabInfo;\n  /*!\n   * [in] Wait timeout in milliseconds.\n   *\n   * When capturing frames with the NVFBC_TOSYS_GRAB_FLAGS_NOFLAGS or\n   * NVFBC_TOSYS_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY flags,\n   * NvFBC will wait for a new frame or mouse move until the below timer\n   * expires.\n   *\n   * When timing out, the last captured frame will be returned.  Note that as\n   * long as the NVFBC_TOSYS_GRAB_FLAGS_FORCE_REFRESH flag is not set,\n   * returning an old frame will incur no performance penalty.\n   *\n   * NvFBC clients can use the return value of the grab frame operation to\n   * find out whether a new frame was captured, or the timer expired.\n   *\n   * Note that the behavior of blocking calls is to wait for a new frame\n   * *after* the call has been made.  When using timeouts, it is possible\n   * that NvFBC will return a new frame (e.g., it has never been captured\n   * before) even though no new frame was generated after the grab call.\n   *\n   * For the precise definition of what constitutes a new frame, see\n   * ::bIsNewFrame.\n   *\n   * Set to 0 to disable timeouts.\n   */\n  uint32_t dwTimeoutMs;\n} NVFBC_TOSYS_GRAB_FRAME_PARAMS;\n\n/*!\n * NVFBC_TOSYS_GRAB_FRAME_PARAMS structure version.\n */\n#define NVFBC_TOSYS_GRAB_FRAME_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOSYS_GRAB_FRAME_PARAMS, 2)\n\n/*!\n * Defines flags that can be used when capturing to a CUDA buffer in video memory.\n */\ntypedef enum {\n  /*!\n   * Default, capturing waits for a new frame or mouse move.\n   *\n   * The default behavior of blocking grabs is to wait for a new frame until\n   * after the call was made.  But it's possible that there is a frame already\n   * ready that the client hasn't seen.\n   * \\see NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY\n   */\n  NVFBC_TOCUDA_GRAB_FLAGS_NOFLAGS = 0,\n  /*!\n   * Capturing does not wait for a new frame nor a mouse move.\n   *\n   * It is therefore possible to capture the same frame multiple times.\n   * When this occurs, the dwCurrentFrame parameter of the\n   * NVFBC_FRAME_GRAB_INFO structure is not incremented.\n   */\n  NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT = (1 << 0),\n  /*!\n   * [in] Forces the destination buffer to be refreshed even if the frame\n   * has not changed since previous capture.\n   *\n   * By default, if the captured frame is identical to the previous one, NvFBC\n   * will omit one copy and not update the destination buffer.\n   *\n   * Setting that flag will prevent this behavior.  This can be useful e.g.,\n   * if the application has modified the buffer in the meantime.\n   */\n  NVFBC_TOCUDA_GRAB_FLAGS_FORCE_REFRESH = (1 << 1),\n  /*!\n   * Similar to NVFBC_TOCUDA_GRAB_FLAGS_NOFLAGS, except that the capture will\n   * not wait if there is already a frame available that the client has\n   * never seen yet.\n   */\n  NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY = (1 << 2),\n} NVFBC_TOCUDA_FLAGS;\n\n/*!\n * Defines parameters for the ::NvFBCToCudaSetUp() API call.\n */\ntypedef struct _NVFBC_TOCUDA_SETUP_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_TOCUDA_SETUP_PARAMS_VER\n   */\n  uint32_t dwVersion;\n  /*!\n   * [in] Desired buffer format.\n   */\n  NVFBC_BUFFER_FORMAT eBufferFormat;\n} NVFBC_TOCUDA_SETUP_PARAMS;\n\n/*!\n * NVFBC_TOCUDA_SETUP_PARAMS structure version.\n */\n#define NVFBC_TOCUDA_SETUP_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOCUDA_SETUP_PARAMS, 1)\n\n/*!\n * Defines parameters for the ::NvFBCToCudaGrabFrame() API call.\n */\ntypedef struct _NVFBC_TOCUDA_GRAB_FRAME_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER.\n   */\n  uint32_t dwVersion;\n  /*!\n   * [in] Flags defining the behavior of this frame capture.\n   */\n  uint32_t dwFlags;\n  /*!\n   * [out] Pointer to a ::CUdeviceptr\n   *\n   * The application does not need to allocate memory for this CUDA device.\n   *\n   * The application does need to create its own CUDA context to use this\n   * CUDA device.\n   *\n   * This ::CUdeviceptr will be mapped to a segment in video memory containing\n   * the frame.  It is not possible to process a CUDA device while capturing\n   * a new frame.  If the application wants to do so, it must copy the CUDA\n   * device using ::cuMemcpyDtoD or ::cuMemcpyDtoH beforehand.\n   */\n  void *pCUDADeviceBuffer;\n  /*!\n   * [out] Information about the captured frame.\n   *\n   * Can be NULL.\n   */\n  NVFBC_FRAME_GRAB_INFO *pFrameGrabInfo;\n  /*!\n   * [in] Wait timeout in milliseconds.\n   *\n   * When capturing frames with the NVFBC_TOCUDA_GRAB_FLAGS_NOFLAGS or\n   * NVFBC_TOCUDA_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY flags,\n   * NvFBC will wait for a new frame or mouse move until the below timer\n   * expires.\n   *\n   * When timing out, the last captured frame will be returned.  Note that as\n   * long as the NVFBC_TOCUDA_GRAB_FLAGS_FORCE_REFRESH flag is not set,\n   * returning an old frame will incur no performance penalty.\n   *\n   * NvFBC clients can use the return value of the grab frame operation to\n   * find out whether a new frame was captured, or the timer expired.\n   *\n   * Note that the behavior of blocking calls is to wait for a new frame\n   * *after* the call has been made.  When using timeouts, it is possible\n   * that NvFBC will return a new frame (e.g., it has never been captured\n   * before) even though no new frame was generated after the grab call.\n   *\n   * For the precise definition of what constitutes a new frame, see\n   * ::bIsNewFrame.\n   *\n   * Set to 0 to disable timeouts.\n   */\n  uint32_t dwTimeoutMs;\n} NVFBC_TOCUDA_GRAB_FRAME_PARAMS;\n\n/*!\n * NVFBC_TOCUDA_GRAB_FRAME_PARAMS structure version.\n */\n#define NVFBC_TOCUDA_GRAB_FRAME_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOCUDA_GRAB_FRAME_PARAMS, 2)\n\n/*!\n * Defines flags that can be used when capturing to an OpenGL buffer in video memory.\n */\ntypedef enum {\n  /*!\n   * Default, capturing waits for a new frame or mouse move.\n   *\n   * The default behavior of blocking grabs is to wait for a new frame until\n   * after the call was made.  But it's possible that there is a frame already\n   * ready that the client hasn't seen.\n   * \\see NVFBC_TOGL_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY\n   */\n  NVFBC_TOGL_GRAB_FLAGS_NOFLAGS = 0,\n  /*!\n   * Capturing does not wait for a new frame nor a mouse move.\n   *\n   * It is therefore possible to capture the same frame multiple times.\n   * When this occurs, the dwCurrentFrame parameter of the\n   * NVFBC_FRAME_GRAB_INFO structure is not incremented.\n   */\n  NVFBC_TOGL_GRAB_FLAGS_NOWAIT = (1 << 0),\n  /*!\n   * [in] Forces the destination buffer to be refreshed even if the frame\n   * has not changed since previous capture.\n   *\n   * By default, if the captured frame is identical to the previous one, NvFBC\n   * will omit one copy and not update the destination buffer.\n   *\n   * Setting that flag will prevent this behavior.  This can be useful e.g.,\n   * if the application has modified the buffer in the meantime.\n   */\n  NVFBC_TOGL_GRAB_FLAGS_FORCE_REFRESH = (1 << 1),\n  /*!\n   * Similar to NVFBC_TOGL_GRAB_FLAGS_NOFLAGS, except that the capture will\n   * not wait if there is already a frame available that the client has\n   * never seen yet.\n   */\n  NVFBC_TOGL_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY = (1 << 2),\n} NVFBC_TOGL_FLAGS;\n\n/*!\n * Maximum number of GL textures that can be used to store frames.\n */\n#define NVFBC_TOGL_TEXTURES_MAX 2\n\n/*!\n * Defines parameters for the ::NvFBCToGLSetUp() API call.\n */\ntypedef struct _NVFBC_TOGL_SETUP_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_TOGL_SETUP_PARAMS_VER\n   */\n  uint32_t dwVersion;\n  /*!\n   * [in] Desired buffer format.\n   */\n  NVFBC_BUFFER_FORMAT eBufferFormat;\n  /*!\n   * [in] Whether differential maps should be generated.\n   */\n  NVFBC_BOOL bWithDiffMap;\n  /*!\n   * [out] Pointer to a pointer to a buffer in system memory.\n   *\n   * \\see NVFBC_TOSYS_SETUP_PARAMS::ppDiffMap\n   */\n  void **ppDiffMap;\n  /*!\n   * [in] Scaling factor of the differential maps.\n   *\n   * \\see NVFBC_TOSYS_SETUP_PARAMS::dwDiffMapScalingFactor\n   */\n  uint32_t dwDiffMapScalingFactor;\n  /*!\n   * [out] List of GL textures that will store the captured frames.\n   *\n   * This array is 0 terminated.  The number of textures varies depending on\n   * the capture settings (such as whether diffmaps are enabled).\n   *\n   * An application wishing to interop with, for example, EncodeAPI will need\n   * to register these textures prior to start encoding frames.\n   *\n   * After each frame capture, the texture holding the current frame will be\n   * returned in NVFBC_TOGL_GRAB_FRAME_PARAMS::dwTexture.\n   */\n  uint32_t dwTextures[NVFBC_TOGL_TEXTURES_MAX];\n  /*!\n   * [out] GL target to which the texture should be bound.\n   */\n  uint32_t dwTexTarget;\n  /*!\n   * [out] GL format of the textures.\n   */\n  uint32_t dwTexFormat;\n  /*!\n   * [out] GL type of the textures.\n   */\n  uint32_t dwTexType;\n  /*!\n   * [out] Size of the differential map.\n   *\n   * Only set if bWithDiffMap is set to NVFBC_TRUE.\n   */\n  NVFBC_SIZE diffMapSize;\n} NVFBC_TOGL_SETUP_PARAMS;\n\n/*!\n * NVFBC_TOGL_SETUP_PARAMS structure version.\n */\n#define NVFBC_TOGL_SETUP_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOGL_SETUP_PARAMS, 2)\n\n/*!\n * Defines parameters for the ::NvFBCToGLGrabFrame() API call.\n */\ntypedef struct _NVFBC_TOGL_GRAB_FRAME_PARAMS {\n  /*!\n   * [in] Must be set to NVFBC_TOGL_GRAB_FRAME_PARAMS_VER.\n   */\n  uint32_t dwVersion;\n  /*!\n   * [in] Flags defining the behavior of this frame capture.\n   */\n  uint32_t dwFlags;\n  /*!\n   * [out] Index of the texture storing the current frame.\n   *\n   * This is an index in the NVFBC_TOGL_SETUP_PARAMS::dwTextures array.\n   */\n  uint32_t dwTextureIndex;\n  /*!\n   * [out] Information about the captured frame.\n   *\n   * Can be NULL.\n   */\n  NVFBC_FRAME_GRAB_INFO *pFrameGrabInfo;\n  /*!\n   * [in] Wait timeout in milliseconds.\n   *\n   * When capturing frames with the NVFBC_TOGL_GRAB_FLAGS_NOFLAGS or\n   * NVFBC_TOGL_GRAB_FLAGS_NOWAIT_IF_NEW_FRAME_READY flags,\n   * NvFBC will wait for a new frame or mouse move until the below timer\n   * expires.\n   *\n   * When timing out, the last captured frame will be returned.  Note that as\n   * long as the NVFBC_TOGL_GRAB_FLAGS_FORCE_REFRESH flag is not set,\n   * returning an old frame will incur no performance penalty.\n   *\n   * NvFBC clients can use the return value of the grab frame operation to\n   * find out whether a new frame was captured, or the timer expired.\n   *\n   * Note that the behavior of blocking calls is to wait for a new frame\n   * *after* the call has been made.  When using timeouts, it is possible\n   * that NvFBC will return a new frame (e.g., it has never been captured\n   * before) even though no new frame was generated after the grab call.\n   *\n   * For the precise definition of what constitutes a new frame, see\n   * ::bIsNewFrame.\n   *\n   * Set to 0 to disable timeouts.\n   */\n  uint32_t dwTimeoutMs;\n} NVFBC_TOGL_GRAB_FRAME_PARAMS;\n\n/*!\n * NVFBC_TOGL_GRAB_FRAME_PARAMS structure version.\n */\n#define NVFBC_TOGL_GRAB_FRAME_PARAMS_VER NVFBC_STRUCT_VERSION(NVFBC_TOGL_GRAB_FRAME_PARAMS, 2)\n\n/*! @} FBC_STRUCT */\n\n/*!\n * \\defgroup FBC_FUNC API Entry Points\n *\n * Entry points are thread-safe and can be called concurrently.\n *\n * The locking model includes a global lock that protects session handle\n * management (\\see NvFBCCreateHandle, \\see NvFBCDestroyHandle).\n *\n * Each NvFBC session uses a local lock to protect other entry points.  Note\n * that in certain cases, a thread can hold the local lock for an undefined\n * amount of time, such as grabbing a frame using a blocking call.\n *\n * Note that a context is associated with each session.  NvFBC clients wishing\n * to share a session between different threads are expected to release and\n * bind the context appropriately (\\see NvFBCBindContext,\n * \\see NvFBCReleaseContext).  This is not required when each thread uses its\n * own NvFBC session.\n *\n * @{\n */\n\n/*!\n * Gets the last error message that got recorded for a client.\n *\n * When NvFBC returns an error, it will save an error message that can be\n * queried through this API call.  Only the last message is saved.\n * The message and the return code should give enough information about\n * what went wrong.\n *\n * \\param [in] sessionHandle\n *   Handle to the NvFBC client.\n * \\return\n *   A NULL terminated error message, or an empty string.  Its maximum length\n *   is NVFBC_ERROR_STR_LEN.\n */\nconst char *NVFBCAPI\nNvFBCGetLastErrorStr(const NVFBC_SESSION_HANDLE sessionHandle);\n\n/*!\n * \\brief Allocates a new handle for an NvFBC client.\n *\n * This function allocates a session handle used to identify an FBC client.\n *\n * This function implicitly calls NvFBCBindContext().\n *\n * \\param [out] pSessionHandle\n *   Pointer that will hold the allocated session handle.\n * \\param [in] pParams\n *   ::NVFBC_CREATE_HANDLE_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_PTR \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_OUT_OF_MEMORY \\n\n *   ::NVFBC_ERR_MAX_CLIENTS \\n\n *   ::NVFBC_ERR_X \\n\n *   ::NVFBC_ERR_GLX \\n\n *   ::NVFBC_ERR_GL\n *\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCCreateHandle(NVFBC_SESSION_HANDLE *pSessionHandle, NVFBC_CREATE_HANDLE_PARAMS *pParams);\n\n/*!\n * \\brief Destroys the handle of an NvFBC client.\n *\n * This function uninitializes an FBC client.\n *\n * This function implicitly calls NvFBCReleaseContext().\n *\n * After this function returns, it is not possible to use this session handle\n * for any further API call.\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n * \\param [in] pParams\n *   ::NVFBC_DESTROY_HANDLE_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_X\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCDestroyHandle(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_DESTROY_HANDLE_PARAMS *pParams);\n\n/*!\n * \\brief Gets the current status of the display driver.\n *\n * This function queries the display driver for various information.\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n * \\param [in] pParams\n *   ::NVFBC_GET_STATUS_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_X\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCGetStatus(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_GET_STATUS_PARAMS *pParams);\n\n/*!\n * \\brief Binds the FBC context to the calling thread.\n *\n * The NvFBC library internally relies on objects that must be bound to a\n * thread.  Such objects are OpenGL contexts and CUDA contexts.\n *\n * This function binds these objects to the calling thread.\n *\n * The FBC context must be bound to the calling thread for most NvFBC entry\n * points, otherwise ::NVFBC_ERR_CONTEXT is returned.\n *\n * If the FBC context is already bound to a different thread,\n * ::NVFBC_ERR_CONTEXT is returned.  The other thread must release the context\n * first by calling the ReleaseContext() entry point.\n *\n * If the FBC context is already bound to the current thread, this function has\n * no effects.\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n * \\param [in] pParams\n *   ::NVFBC_DESTROY_CAPTURE_SESSION_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_X\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCBindContext(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_BIND_CONTEXT_PARAMS *pParams);\n\n/*!\n * \\brief Releases the FBC context from the calling thread.\n *\n * If the FBC context is bound to a different thread, ::NVFBC_ERR_CONTEXT is\n * returned.\n *\n * If the FBC context is already released, this function has no effects.\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n * \\param [in] pParams\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_X\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCReleaseContext(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_RELEASE_CONTEXT_PARAMS *pParams);\n\n/*!\n * \\brief Creates a capture session for an FBC client.\n *\n * This function starts a capture session of the desired type (system memory,\n * video memory with CUDA interop, or H.264 compressed frames in system memory).\n *\n * Not all types are supported on all systems.  Also, it is possible to use\n * NvFBC without having the CUDA library.  In this case, requesting a capture\n * session of the concerned type will return an error.\n *\n * After this function returns, the display driver will start generating frames\n * that can be captured using the corresponding API call.\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n * \\param [in] pParams\n *   ::NVFBC_CREATE_CAPTURE_SESSION_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_INVALID_PARAM \\n\n *   ::NVFBC_ERR_OUT_OF_MEMORY \\n\n *   ::NVFBC_ERR_X \\n\n *   ::NVFBC_ERR_GLX \\n\n *   ::NVFBC_ERR_GL \\n\n *   ::NVFBC_ERR_CUDA \\n\n *   ::NVFBC_ERR_MUST_RECREATE \\n\n *   ::NVFBC_ERR_INTERNAL\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCCreateCaptureSession(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_CREATE_CAPTURE_SESSION_PARAMS *pParams);\n\n/*!\n * \\brief Destroys a capture session for an FBC client.\n *\n * This function stops a capture session and frees allocated objects.\n *\n * After this function returns, it is possible to create another capture\n * session using the corresponding API call.\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n * \\param [in] pParams\n *   ::NVFBC_DESTROY_CAPTURE_SESSION_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_X\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCDestroyCaptureSession(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_DESTROY_CAPTURE_SESSION_PARAMS *pParams);\n\n/*!\n * \\brief Sets up a capture to system memory session.\n *\n * This function configures how the capture to system memory should behave. It\n * can be called anytime and several times after the capture session has been\n * created.  However, it must be called at least once prior to start capturing\n * frames.\n *\n * This function allocates the buffer that will contain the captured frame.\n * The application does not need to free this buffer.  The size of this buffer\n * is returned in the ::NVFBC_FRAME_GRAB_INFO structure.\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n * \\param [in] pParams\n *   ::NVFBC_TOSYS_SETUP_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_UNSUPPORTED \\n\n *   ::NVFBC_ERR_INVALID_PTR \\n\n *   ::NVFBC_ERR_INVALID_PARAM \\n\n *   ::NVFBC_ERR_OUT_OF_MEMORY \\n\n *   ::NVFBC_ERR_X\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCToSysSetUp(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOSYS_SETUP_PARAMS *pParams);\n\n/*!\n * \\brief Captures a frame to a buffer in system memory.\n *\n * This function triggers a frame capture to a buffer in system memory that was\n * registered with the ToSysSetUp() API call.\n *\n * Note that it is possible that the resolution of the desktop changes while\n * capturing frames. This should be transparent for the application.\n *\n * When the resolution changes, the capture session is recreated using the same\n * parameters, and necessary buffers are re-allocated. The frame counter is not\n * reset.\n *\n * An application can detect that the resolution changed by comparing the\n * dwByteSize member of the ::NVFBC_FRAME_GRAB_INFO against a previous\n * frame and/or dwWidth and dwHeight.\n *\n * During a change of resolution the capture is paused even in asynchronous\n * mode.\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n * \\param [in] pParams\n *   ::NVFBC_TOSYS_GRAB_FRAME_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_INVALID_PTR \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_X \\n\n *   ::NVFBC_ERR_MUST_RECREATE \\n\n *   \\see NvFBCCreateCaptureSession \\n\n *   \\see NvFBCToSysSetUp\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCToSysGrabFrame(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOSYS_GRAB_FRAME_PARAMS *pParams);\n\n/*!\n * \\brief Sets up a capture to video memory session.\n *\n * This function configures how the capture to video memory with CUDA interop\n * should behave.  It can be called anytime and several times after the capture\n * session has been created.  However, it must be called at least once prior\n * to start capturing frames.\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n *\n * \\param [in] pParams\n *   ::NVFBC_TOCUDA_SETUP_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_UNSUPPORTED \\n\n *   ::NVFBC_ERR_GL \\n\n *   ::NVFBC_ERR_X\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCToCudaSetUp(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOCUDA_SETUP_PARAMS *pParams);\n\n/*!\n * \\brief Captures a frame to a CUDA device in video memory.\n *\n * This function triggers a frame capture to a CUDA device in video memory.\n *\n * Note about changes of resolution: \\see NvFBCToSysGrabFrame\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n *\n * \\param [in] pParams\n *   ::NVFBC_TOCUDA_GRAB_FRAME_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_INVALID_PTR \\n\n *   ::NVFBC_ERR_CUDA \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_X \\n\n *   ::NVFBC_ERR_MUST_RECREATE \\n\n *   \\see NvFBCCreateCaptureSession \\n\n *   \\see NvFBCToCudaSetUp\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCToCudaGrabFrame(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOCUDA_GRAB_FRAME_PARAMS *pParams);\n\n/*!\n * \\brief Sets up a capture to OpenGL buffer in video memory session.\n *\n * This function configures how the capture to video memory should behave.\n * It can be called anytime and several times after the capture session has been\n * created.  However, it must be called at least once prior to start capturing\n * frames.\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n *\n * \\param [in] pParams\n *   ::NVFBC_TOGL_SETUP_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_UNSUPPORTED \\n\n *   ::NVFBC_ERR_GL \\n\n *   ::NVFBC_ERR_X\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCToGLSetUp(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOGL_SETUP_PARAMS *pParams);\n\n/*!\n * \\brief Captures a frame to an OpenGL buffer in video memory.\n *\n * This function triggers a frame capture to a selected resource in video memory.\n *\n * Note about changes of resolution: \\see NvFBCToSysGrabFrame\n *\n * \\param [in] sessionHandle\n *   FBC session handle.\n *\n * \\param [in] pParams\n *   ::NVFBC_TOGL_GRAB_FRAME_PARAMS\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_HANDLE \\n\n *   ::NVFBC_ERR_API_VERSION \\n\n *   ::NVFBC_ERR_BAD_REQUEST \\n\n *   ::NVFBC_ERR_CONTEXT \\n\n *   ::NVFBC_ERR_INVALID_PTR \\n\n *   ::NVFBC_ERR_INTERNAL \\n\n *   ::NVFBC_ERR_X \\n\n *   ::NVFBC_ERR_MUST_RECREATE \\n\n *   \\see NvFBCCreateCaptureSession \\n\n *   \\see NvFBCToCudaSetUp\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCToGLGrabFrame(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOGL_GRAB_FRAME_PARAMS *pParams);\n\n/*!\n * \\cond FBC_PFN\n *\n * Defines API function pointers\n */\ntypedef const char *(NVFBCAPI *PNVFBCGETLASTERRORSTR)(const NVFBC_SESSION_HANDLE sessionHandle);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCCREATEHANDLE)(NVFBC_SESSION_HANDLE *pSessionHandle, NVFBC_CREATE_HANDLE_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCDESTROYHANDLE)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_DESTROY_HANDLE_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCBINDCONTEXT)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_BIND_CONTEXT_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCRELEASECONTEXT)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_RELEASE_CONTEXT_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCGETSTATUS)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_GET_STATUS_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCCREATECAPTURESESSION)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_CREATE_CAPTURE_SESSION_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCDESTROYCAPTURESESSION)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_DESTROY_CAPTURE_SESSION_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOSYSSETUP)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOSYS_SETUP_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOSYSGRABFRAME)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOSYS_GRAB_FRAME_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOCUDASETUP)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOCUDA_SETUP_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOCUDAGRABFRAME)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOCUDA_GRAB_FRAME_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOGLSETUP)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOGL_SETUP_PARAMS *pParams);\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCTOGLGRABFRAME)(const NVFBC_SESSION_HANDLE sessionHandle, NVFBC_TOGL_GRAB_FRAME_PARAMS *pParams);\n\n/// \\endcond\n\n/*! @} FBC_FUNC */\n\n/*!\n * \\ingroup FBC_STRUCT\n *\n * Structure populated with API function pointers.\n */\ntypedef struct\n{\n  uint32_t dwVersion;  //!< [in] Must be set to NVFBC_VERSION.\n  PNVFBCGETLASTERRORSTR nvFBCGetLastErrorStr;  //!< [out] Pointer to ::NvFBCGetLastErrorStr().\n  PNVFBCCREATEHANDLE nvFBCCreateHandle;  //!< [out] Pointer to ::NvFBCCreateHandle().\n  PNVFBCDESTROYHANDLE nvFBCDestroyHandle;  //!< [out] Pointer to ::NvFBCDestroyHandle().\n  PNVFBCGETSTATUS nvFBCGetStatus;  //!< [out] Pointer to ::NvFBCGetStatus().\n  PNVFBCCREATECAPTURESESSION nvFBCCreateCaptureSession;  //!< [out] Pointer to ::NvFBCCreateCaptureSession().\n  PNVFBCDESTROYCAPTURESESSION nvFBCDestroyCaptureSession;  //!< [out] Pointer to ::NvFBCDestroyCaptureSession().\n  PNVFBCTOSYSSETUP nvFBCToSysSetUp;  //!< [out] Pointer to ::NvFBCToSysSetUp().\n  PNVFBCTOSYSGRABFRAME nvFBCToSysGrabFrame;  //!< [out] Pointer to ::NvFBCToSysGrabFrame().\n  PNVFBCTOCUDASETUP nvFBCToCudaSetUp;  //!< [out] Pointer to ::NvFBCToCudaSetUp().\n  PNVFBCTOCUDAGRABFRAME nvFBCToCudaGrabFrame;  //!< [out] Pointer to ::NvFBCToCudaGrabFrame().\n  void *pad1;  //!< [out] Retired. Do not use.\n  void *pad2;  //!< [out] Retired. Do not use.\n  void *pad3;  //!< [out] Retired. Do not use.\n  PNVFBCBINDCONTEXT nvFBCBindContext;  //!< [out] Pointer to ::NvFBCBindContext().\n  PNVFBCRELEASECONTEXT nvFBCReleaseContext;  //!< [out] Pointer to ::NvFBCReleaseContext().\n  void *pad4;  //!< [out] Retired. Do not use.\n  void *pad5;  //!< [out] Retired. Do not use.\n  void *pad6;  //!< [out] Retired. Do not use.\n  void *pad7;  //!< [out] Retired. Do not use.\n  PNVFBCTOGLSETUP nvFBCToGLSetUp;  //!< [out] Pointer to ::nvFBCToGLSetup().\n  PNVFBCTOGLGRABFRAME nvFBCToGLGrabFrame;  //!< [out] Pointer to ::nvFBCToGLGrabFrame().\n} NVFBC_API_FUNCTION_LIST;\n\n/*!\n * \\ingroup FBC_FUNC\n *\n * \\brief Entry Points to the NvFBC interface.\n *\n * Creates an instance of the NvFBC interface, and populates the\n * pFunctionList with function pointers to the API routines implemented by\n * the NvFBC interface.\n *\n * \\param [out] pFunctionList\n *\n * \\return\n *   ::NVFBC_SUCCESS \\n\n *   ::NVFBC_ERR_INVALID_PTR \\n\n *   ::NVFBC_ERR_API_VERSION\n */\nNVFBCSTATUS NVFBCAPI\nNvFBCCreateInstance(NVFBC_API_FUNCTION_LIST *pFunctionList);\n/*!\n * \\ingroup FBC_FUNC\n *\n * Defines function pointer for the ::NvFBCCreateInstance() API call.\n */\ntypedef NVFBCSTATUS(NVFBCAPI *PNVFBCCREATEINSTANCE)(NVFBC_API_FUNCTION_LIST *pFunctionList);\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif  // _NVFBC_H_\n"
  },
  {
    "path": "third-party/nvfbc/helper_math.h",
    "content": "/* Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n *  * Redistributions of source code must retain the above copyright\n *    notice, this list of conditions and the following disclaimer.\n *  * Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in the\n *    documentation and/or other materials provided with the distribution.\n *  * Neither the name of NVIDIA CORPORATION nor the names of its\n *    contributors may be used to endorse or promote products derived\n *    from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY\n * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR\n * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\n * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY\n * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n\n/*\n *  This file implements common mathematical operations on vector types\n *  (float3, float4 etc.) since these are not provided as standard by CUDA.\n *\n *  The syntax is modeled on the Cg standard library.\n *\n *  This is part of the Helper library includes\n *\n *    Thanks to Linh Hah for additions and fixes.\n */\n\n#ifndef HELPER_MATH_H\n#define HELPER_MATH_H\n\n#include \"cuda_runtime.h\"\n\ntypedef unsigned int uint;\ntypedef unsigned short ushort;\n\n#ifndef EXIT_WAIVED\n  #define EXIT_WAIVED 2\n#endif\n\n#ifndef __CUDACC__\n  #include <math.h>\n\n////////////////////////////////////////////////////////////////////////////////\n// host implementations of CUDA functions\n////////////////////////////////////////////////////////////////////////////////\n\ninline float\nfminf(float a, float b) {\n  return a < b ? a : b;\n}\n\ninline float\nfmaxf(float a, float b) {\n  return a > b ? a : b;\n}\n\ninline int\nmax(int a, int b) {\n  return a > b ? a : b;\n}\n\ninline int\nmin(int a, int b) {\n  return a < b ? a : b;\n}\n\ninline float\nrsqrtf(float x) {\n  return 1.0f / sqrtf(x);\n}\n#endif\n\n////////////////////////////////////////////////////////////////////////////////\n// constructors\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\nmake_float2(float s) {\n  return make_float2(s, s);\n}\ninline __host__ __device__ float2\nmake_float2(float3 a) {\n  return make_float2(a.x, a.y);\n}\ninline __host__ __device__ float2\nmake_float2(int2 a) {\n  return make_float2(float(a.x), float(a.y));\n}\ninline __host__ __device__ float2\nmake_float2(uint2 a) {\n  return make_float2(float(a.x), float(a.y));\n}\n\ninline __host__ __device__ int2\nmake_int2(int s) {\n  return make_int2(s, s);\n}\ninline __host__ __device__ int2\nmake_int2(int3 a) {\n  return make_int2(a.x, a.y);\n}\ninline __host__ __device__ int2\nmake_int2(uint2 a) {\n  return make_int2(int(a.x), int(a.y));\n}\ninline __host__ __device__ int2\nmake_int2(float2 a) {\n  return make_int2(int(a.x), int(a.y));\n}\n\ninline __host__ __device__ uint2\nmake_uint2(uint s) {\n  return make_uint2(s, s);\n}\ninline __host__ __device__ uint2\nmake_uint2(uint3 a) {\n  return make_uint2(a.x, a.y);\n}\ninline __host__ __device__ uint2\nmake_uint2(int2 a) {\n  return make_uint2(uint(a.x), uint(a.y));\n}\n\ninline __host__ __device__ float3\nmake_float3(float s) {\n  return make_float3(s, s, s);\n}\ninline __host__ __device__ float3\nmake_float3(float2 a) {\n  return make_float3(a.x, a.y, 0.0f);\n}\ninline __host__ __device__ float3\nmake_float3(float2 a, float s) {\n  return make_float3(a.x, a.y, s);\n}\ninline __host__ __device__ float3\nmake_float3(float4 a) {\n  return make_float3(a.x, a.y, a.z);\n}\ninline __host__ __device__ float3\nmake_float3(int3 a) {\n  return make_float3(float(a.x), float(a.y), float(a.z));\n}\ninline __host__ __device__ float3\nmake_float3(uint3 a) {\n  return make_float3(float(a.x), float(a.y), float(a.z));\n}\n\ninline __host__ __device__ int3\nmake_int3(int s) {\n  return make_int3(s, s, s);\n}\ninline __host__ __device__ int3\nmake_int3(int2 a) {\n  return make_int3(a.x, a.y, 0);\n}\ninline __host__ __device__ int3\nmake_int3(int2 a, int s) {\n  return make_int3(a.x, a.y, s);\n}\ninline __host__ __device__ int3\nmake_int3(uint3 a) {\n  return make_int3(int(a.x), int(a.y), int(a.z));\n}\ninline __host__ __device__ int3\nmake_int3(float3 a) {\n  return make_int3(int(a.x), int(a.y), int(a.z));\n}\n\ninline __host__ __device__ uint3\nmake_uint3(uint s) {\n  return make_uint3(s, s, s);\n}\ninline __host__ __device__ uint3\nmake_uint3(uint2 a) {\n  return make_uint3(a.x, a.y, 0);\n}\ninline __host__ __device__ uint3\nmake_uint3(uint2 a, uint s) {\n  return make_uint3(a.x, a.y, s);\n}\ninline __host__ __device__ uint3\nmake_uint3(uint4 a) {\n  return make_uint3(a.x, a.y, a.z);\n}\ninline __host__ __device__ uint3\nmake_uint3(int3 a) {\n  return make_uint3(uint(a.x), uint(a.y), uint(a.z));\n}\n\ninline __host__ __device__ float4\nmake_float4(float s) {\n  return make_float4(s, s, s, s);\n}\ninline __host__ __device__ float4\nmake_float4(float3 a) {\n  return make_float4(a.x, a.y, a.z, 0.0f);\n}\ninline __host__ __device__ float4\nmake_float4(float3 a, float w) {\n  return make_float4(a.x, a.y, a.z, w);\n}\ninline __host__ __device__ float4\nmake_float4(int4 a) {\n  return make_float4(float(a.x), float(a.y), float(a.z), float(a.w));\n}\ninline __host__ __device__ float4\nmake_float4(uint4 a) {\n  return make_float4(float(a.x), float(a.y), float(a.z), float(a.w));\n}\n\ninline __host__ __device__ int4\nmake_int4(int s) {\n  return make_int4(s, s, s, s);\n}\ninline __host__ __device__ int4\nmake_int4(int3 a) {\n  return make_int4(a.x, a.y, a.z, 0);\n}\ninline __host__ __device__ int4\nmake_int4(int3 a, int w) {\n  return make_int4(a.x, a.y, a.z, w);\n}\ninline __host__ __device__ int4\nmake_int4(uint4 a) {\n  return make_int4(int(a.x), int(a.y), int(a.z), int(a.w));\n}\ninline __host__ __device__ int4\nmake_int4(float4 a) {\n  return make_int4(int(a.x), int(a.y), int(a.z), int(a.w));\n}\n\ninline __host__ __device__ uint4\nmake_uint4(uint s) {\n  return make_uint4(s, s, s, s);\n}\ninline __host__ __device__ uint4\nmake_uint4(uint3 a) {\n  return make_uint4(a.x, a.y, a.z, 0);\n}\ninline __host__ __device__ uint4\nmake_uint4(uint3 a, uint w) {\n  return make_uint4(a.x, a.y, a.z, w);\n}\ninline __host__ __device__ uint4\nmake_uint4(int4 a) {\n  return make_uint4(uint(a.x), uint(a.y), uint(a.z), uint(a.w));\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// negate\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\noperator-(float2 &a) {\n  return make_float2(-a.x, -a.y);\n}\ninline __host__ __device__ int2\noperator-(int2 &a) {\n  return make_int2(-a.x, -a.y);\n}\ninline __host__ __device__ float3\noperator-(float3 &a) {\n  return make_float3(-a.x, -a.y, -a.z);\n}\ninline __host__ __device__ int3\noperator-(int3 &a) {\n  return make_int3(-a.x, -a.y, -a.z);\n}\ninline __host__ __device__ float4\noperator-(float4 &a) {\n  return make_float4(-a.x, -a.y, -a.z, -a.w);\n}\ninline __host__ __device__ int4\noperator-(int4 &a) {\n  return make_int4(-a.x, -a.y, -a.z, -a.w);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// addition\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\noperator+(float2 a, float2 b) {\n  return make_float2(a.x + b.x, a.y + b.y);\n}\ninline __host__ __device__ void\noperator+=(float2 &a, float2 b) {\n  a.x += b.x;\n  a.y += b.y;\n}\ninline __host__ __device__ float2\noperator+(float2 a, float b) {\n  return make_float2(a.x + b, a.y + b);\n}\ninline __host__ __device__ float2\noperator+(float b, float2 a) {\n  return make_float2(a.x + b, a.y + b);\n}\ninline __host__ __device__ void\noperator+=(float2 &a, float b) {\n  a.x += b;\n  a.y += b;\n}\n\ninline __host__ __device__ int2\noperator+(int2 a, int2 b) {\n  return make_int2(a.x + b.x, a.y + b.y);\n}\ninline __host__ __device__ void\noperator+=(int2 &a, int2 b) {\n  a.x += b.x;\n  a.y += b.y;\n}\ninline __host__ __device__ int2\noperator+(int2 a, int b) {\n  return make_int2(a.x + b, a.y + b);\n}\ninline __host__ __device__ int2\noperator+(int b, int2 a) {\n  return make_int2(a.x + b, a.y + b);\n}\ninline __host__ __device__ void\noperator+=(int2 &a, int b) {\n  a.x += b;\n  a.y += b;\n}\n\ninline __host__ __device__ uint2\noperator+(uint2 a, uint2 b) {\n  return make_uint2(a.x + b.x, a.y + b.y);\n}\ninline __host__ __device__ void\noperator+=(uint2 &a, uint2 b) {\n  a.x += b.x;\n  a.y += b.y;\n}\ninline __host__ __device__ uint2\noperator+(uint2 a, uint b) {\n  return make_uint2(a.x + b, a.y + b);\n}\ninline __host__ __device__ uint2\noperator+(uint b, uint2 a) {\n  return make_uint2(a.x + b, a.y + b);\n}\ninline __host__ __device__ void\noperator+=(uint2 &a, uint b) {\n  a.x += b;\n  a.y += b;\n}\n\ninline __host__ __device__ float3\noperator+(float3 a, float3 b) {\n  return make_float3(a.x + b.x, a.y + b.y, a.z + b.z);\n}\ninline __host__ __device__ void\noperator+=(float3 &a, float3 b) {\n  a.x += b.x;\n  a.y += b.y;\n  a.z += b.z;\n}\ninline __host__ __device__ float3\noperator+(float3 a, float b) {\n  return make_float3(a.x + b, a.y + b, a.z + b);\n}\ninline __host__ __device__ void\noperator+=(float3 &a, float b) {\n  a.x += b;\n  a.y += b;\n  a.z += b;\n}\n\ninline __host__ __device__ int3\noperator+(int3 a, int3 b) {\n  return make_int3(a.x + b.x, a.y + b.y, a.z + b.z);\n}\ninline __host__ __device__ void\noperator+=(int3 &a, int3 b) {\n  a.x += b.x;\n  a.y += b.y;\n  a.z += b.z;\n}\ninline __host__ __device__ int3\noperator+(int3 a, int b) {\n  return make_int3(a.x + b, a.y + b, a.z + b);\n}\ninline __host__ __device__ void\noperator+=(int3 &a, int b) {\n  a.x += b;\n  a.y += b;\n  a.z += b;\n}\n\ninline __host__ __device__ uint3\noperator+(uint3 a, uint3 b) {\n  return make_uint3(a.x + b.x, a.y + b.y, a.z + b.z);\n}\ninline __host__ __device__ void\noperator+=(uint3 &a, uint3 b) {\n  a.x += b.x;\n  a.y += b.y;\n  a.z += b.z;\n}\ninline __host__ __device__ uint3\noperator+(uint3 a, uint b) {\n  return make_uint3(a.x + b, a.y + b, a.z + b);\n}\ninline __host__ __device__ void\noperator+=(uint3 &a, uint b) {\n  a.x += b;\n  a.y += b;\n  a.z += b;\n}\n\ninline __host__ __device__ int3\noperator+(int b, int3 a) {\n  return make_int3(a.x + b, a.y + b, a.z + b);\n}\ninline __host__ __device__ uint3\noperator+(uint b, uint3 a) {\n  return make_uint3(a.x + b, a.y + b, a.z + b);\n}\ninline __host__ __device__ float3\noperator+(float b, float3 a) {\n  return make_float3(a.x + b, a.y + b, a.z + b);\n}\n\ninline __host__ __device__ float4\noperator+(float4 a, float4 b) {\n  return make_float4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);\n}\ninline __host__ __device__ void\noperator+=(float4 &a, float4 b) {\n  a.x += b.x;\n  a.y += b.y;\n  a.z += b.z;\n  a.w += b.w;\n}\ninline __host__ __device__ float4\noperator+(float4 a, float b) {\n  return make_float4(a.x + b, a.y + b, a.z + b, a.w + b);\n}\ninline __host__ __device__ float4\noperator+(float b, float4 a) {\n  return make_float4(a.x + b, a.y + b, a.z + b, a.w + b);\n}\ninline __host__ __device__ void\noperator+=(float4 &a, float b) {\n  a.x += b;\n  a.y += b;\n  a.z += b;\n  a.w += b;\n}\n\ninline __host__ __device__ int4\noperator+(int4 a, int4 b) {\n  return make_int4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);\n}\ninline __host__ __device__ void\noperator+=(int4 &a, int4 b) {\n  a.x += b.x;\n  a.y += b.y;\n  a.z += b.z;\n  a.w += b.w;\n}\ninline __host__ __device__ int4\noperator+(int4 a, int b) {\n  return make_int4(a.x + b, a.y + b, a.z + b, a.w + b);\n}\ninline __host__ __device__ int4\noperator+(int b, int4 a) {\n  return make_int4(a.x + b, a.y + b, a.z + b, a.w + b);\n}\ninline __host__ __device__ void\noperator+=(int4 &a, int b) {\n  a.x += b;\n  a.y += b;\n  a.z += b;\n  a.w += b;\n}\n\ninline __host__ __device__ uint4\noperator+(uint4 a, uint4 b) {\n  return make_uint4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);\n}\ninline __host__ __device__ void\noperator+=(uint4 &a, uint4 b) {\n  a.x += b.x;\n  a.y += b.y;\n  a.z += b.z;\n  a.w += b.w;\n}\ninline __host__ __device__ uint4\noperator+(uint4 a, uint b) {\n  return make_uint4(a.x + b, a.y + b, a.z + b, a.w + b);\n}\ninline __host__ __device__ uint4\noperator+(uint b, uint4 a) {\n  return make_uint4(a.x + b, a.y + b, a.z + b, a.w + b);\n}\ninline __host__ __device__ void\noperator+=(uint4 &a, uint b) {\n  a.x += b;\n  a.y += b;\n  a.z += b;\n  a.w += b;\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// subtract\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\noperator-(float2 a, float2 b) {\n  return make_float2(a.x - b.x, a.y - b.y);\n}\ninline __host__ __device__ void\noperator-=(float2 &a, float2 b) {\n  a.x -= b.x;\n  a.y -= b.y;\n}\ninline __host__ __device__ float2\noperator-(float2 a, float b) {\n  return make_float2(a.x - b, a.y - b);\n}\ninline __host__ __device__ float2\noperator-(float b, float2 a) {\n  return make_float2(b - a.x, b - a.y);\n}\ninline __host__ __device__ void\noperator-=(float2 &a, float b) {\n  a.x -= b;\n  a.y -= b;\n}\n\ninline __host__ __device__ int2\noperator-(int2 a, int2 b) {\n  return make_int2(a.x - b.x, a.y - b.y);\n}\ninline __host__ __device__ void\noperator-=(int2 &a, int2 b) {\n  a.x -= b.x;\n  a.y -= b.y;\n}\ninline __host__ __device__ int2\noperator-(int2 a, int b) {\n  return make_int2(a.x - b, a.y - b);\n}\ninline __host__ __device__ int2\noperator-(int b, int2 a) {\n  return make_int2(b - a.x, b - a.y);\n}\ninline __host__ __device__ void\noperator-=(int2 &a, int b) {\n  a.x -= b;\n  a.y -= b;\n}\n\ninline __host__ __device__ uint2\noperator-(uint2 a, uint2 b) {\n  return make_uint2(a.x - b.x, a.y - b.y);\n}\ninline __host__ __device__ void\noperator-=(uint2 &a, uint2 b) {\n  a.x -= b.x;\n  a.y -= b.y;\n}\ninline __host__ __device__ uint2\noperator-(uint2 a, uint b) {\n  return make_uint2(a.x - b, a.y - b);\n}\ninline __host__ __device__ uint2\noperator-(uint b, uint2 a) {\n  return make_uint2(b - a.x, b - a.y);\n}\ninline __host__ __device__ void\noperator-=(uint2 &a, uint b) {\n  a.x -= b;\n  a.y -= b;\n}\n\ninline __host__ __device__ float3\noperator-(float3 a, float3 b) {\n  return make_float3(a.x - b.x, a.y - b.y, a.z - b.z);\n}\ninline __host__ __device__ void\noperator-=(float3 &a, float3 b) {\n  a.x -= b.x;\n  a.y -= b.y;\n  a.z -= b.z;\n}\ninline __host__ __device__ float3\noperator-(float3 a, float b) {\n  return make_float3(a.x - b, a.y - b, a.z - b);\n}\ninline __host__ __device__ float3\noperator-(float b, float3 a) {\n  return make_float3(b - a.x, b - a.y, b - a.z);\n}\ninline __host__ __device__ void\noperator-=(float3 &a, float b) {\n  a.x -= b;\n  a.y -= b;\n  a.z -= b;\n}\n\ninline __host__ __device__ int3\noperator-(int3 a, int3 b) {\n  return make_int3(a.x - b.x, a.y - b.y, a.z - b.z);\n}\ninline __host__ __device__ void\noperator-=(int3 &a, int3 b) {\n  a.x -= b.x;\n  a.y -= b.y;\n  a.z -= b.z;\n}\ninline __host__ __device__ int3\noperator-(int3 a, int b) {\n  return make_int3(a.x - b, a.y - b, a.z - b);\n}\ninline __host__ __device__ int3\noperator-(int b, int3 a) {\n  return make_int3(b - a.x, b - a.y, b - a.z);\n}\ninline __host__ __device__ void\noperator-=(int3 &a, int b) {\n  a.x -= b;\n  a.y -= b;\n  a.z -= b;\n}\n\ninline __host__ __device__ uint3\noperator-(uint3 a, uint3 b) {\n  return make_uint3(a.x - b.x, a.y - b.y, a.z - b.z);\n}\ninline __host__ __device__ void\noperator-=(uint3 &a, uint3 b) {\n  a.x -= b.x;\n  a.y -= b.y;\n  a.z -= b.z;\n}\ninline __host__ __device__ uint3\noperator-(uint3 a, uint b) {\n  return make_uint3(a.x - b, a.y - b, a.z - b);\n}\ninline __host__ __device__ uint3\noperator-(uint b, uint3 a) {\n  return make_uint3(b - a.x, b - a.y, b - a.z);\n}\ninline __host__ __device__ void\noperator-=(uint3 &a, uint b) {\n  a.x -= b;\n  a.y -= b;\n  a.z -= b;\n}\n\ninline __host__ __device__ float4\noperator-(float4 a, float4 b) {\n  return make_float4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);\n}\ninline __host__ __device__ void\noperator-=(float4 &a, float4 b) {\n  a.x -= b.x;\n  a.y -= b.y;\n  a.z -= b.z;\n  a.w -= b.w;\n}\ninline __host__ __device__ float4\noperator-(float4 a, float b) {\n  return make_float4(a.x - b, a.y - b, a.z - b, a.w - b);\n}\ninline __host__ __device__ void\noperator-=(float4 &a, float b) {\n  a.x -= b;\n  a.y -= b;\n  a.z -= b;\n  a.w -= b;\n}\n\ninline __host__ __device__ int4\noperator-(int4 a, int4 b) {\n  return make_int4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);\n}\ninline __host__ __device__ void\noperator-=(int4 &a, int4 b) {\n  a.x -= b.x;\n  a.y -= b.y;\n  a.z -= b.z;\n  a.w -= b.w;\n}\ninline __host__ __device__ int4\noperator-(int4 a, int b) {\n  return make_int4(a.x - b, a.y - b, a.z - b, a.w - b);\n}\ninline __host__ __device__ int4\noperator-(int b, int4 a) {\n  return make_int4(b - a.x, b - a.y, b - a.z, b - a.w);\n}\ninline __host__ __device__ void\noperator-=(int4 &a, int b) {\n  a.x -= b;\n  a.y -= b;\n  a.z -= b;\n  a.w -= b;\n}\n\ninline __host__ __device__ uint4\noperator-(uint4 a, uint4 b) {\n  return make_uint4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);\n}\ninline __host__ __device__ void\noperator-=(uint4 &a, uint4 b) {\n  a.x -= b.x;\n  a.y -= b.y;\n  a.z -= b.z;\n  a.w -= b.w;\n}\ninline __host__ __device__ uint4\noperator-(uint4 a, uint b) {\n  return make_uint4(a.x - b, a.y - b, a.z - b, a.w - b);\n}\ninline __host__ __device__ uint4\noperator-(uint b, uint4 a) {\n  return make_uint4(b - a.x, b - a.y, b - a.z, b - a.w);\n}\ninline __host__ __device__ void\noperator-=(uint4 &a, uint b) {\n  a.x -= b;\n  a.y -= b;\n  a.z -= b;\n  a.w -= b;\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// multiply\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\noperator*(float2 a, float2 b) {\n  return make_float2(a.x * b.x, a.y * b.y);\n}\ninline __host__ __device__ void\noperator*=(float2 &a, float2 b) {\n  a.x *= b.x;\n  a.y *= b.y;\n}\ninline __host__ __device__ float2\noperator*(float2 a, float b) {\n  return make_float2(a.x * b, a.y * b);\n}\ninline __host__ __device__ float2\noperator*(float b, float2 a) {\n  return make_float2(b * a.x, b * a.y);\n}\ninline __host__ __device__ void\noperator*=(float2 &a, float b) {\n  a.x *= b;\n  a.y *= b;\n}\n\ninline __host__ __device__ int2\noperator*(int2 a, int2 b) {\n  return make_int2(a.x * b.x, a.y * b.y);\n}\ninline __host__ __device__ void\noperator*=(int2 &a, int2 b) {\n  a.x *= b.x;\n  a.y *= b.y;\n}\ninline __host__ __device__ int2\noperator*(int2 a, int b) {\n  return make_int2(a.x * b, a.y * b);\n}\ninline __host__ __device__ int2\noperator*(int b, int2 a) {\n  return make_int2(b * a.x, b * a.y);\n}\ninline __host__ __device__ void\noperator*=(int2 &a, int b) {\n  a.x *= b;\n  a.y *= b;\n}\n\ninline __host__ __device__ uint2\noperator*(uint2 a, uint2 b) {\n  return make_uint2(a.x * b.x, a.y * b.y);\n}\ninline __host__ __device__ void\noperator*=(uint2 &a, uint2 b) {\n  a.x *= b.x;\n  a.y *= b.y;\n}\ninline __host__ __device__ uint2\noperator*(uint2 a, uint b) {\n  return make_uint2(a.x * b, a.y * b);\n}\ninline __host__ __device__ uint2\noperator*(uint b, uint2 a) {\n  return make_uint2(b * a.x, b * a.y);\n}\ninline __host__ __device__ void\noperator*=(uint2 &a, uint b) {\n  a.x *= b;\n  a.y *= b;\n}\n\ninline __host__ __device__ float3\noperator*(float3 a, float3 b) {\n  return make_float3(a.x * b.x, a.y * b.y, a.z * b.z);\n}\ninline __host__ __device__ void\noperator*=(float3 &a, float3 b) {\n  a.x *= b.x;\n  a.y *= b.y;\n  a.z *= b.z;\n}\ninline __host__ __device__ float3\noperator*(float3 a, float b) {\n  return make_float3(a.x * b, a.y * b, a.z * b);\n}\ninline __host__ __device__ float3\noperator*(float b, float3 a) {\n  return make_float3(b * a.x, b * a.y, b * a.z);\n}\ninline __host__ __device__ void\noperator*=(float3 &a, float b) {\n  a.x *= b;\n  a.y *= b;\n  a.z *= b;\n}\n\ninline __host__ __device__ int3\noperator*(int3 a, int3 b) {\n  return make_int3(a.x * b.x, a.y * b.y, a.z * b.z);\n}\ninline __host__ __device__ void\noperator*=(int3 &a, int3 b) {\n  a.x *= b.x;\n  a.y *= b.y;\n  a.z *= b.z;\n}\ninline __host__ __device__ int3\noperator*(int3 a, int b) {\n  return make_int3(a.x * b, a.y * b, a.z * b);\n}\ninline __host__ __device__ int3\noperator*(int b, int3 a) {\n  return make_int3(b * a.x, b * a.y, b * a.z);\n}\ninline __host__ __device__ void\noperator*=(int3 &a, int b) {\n  a.x *= b;\n  a.y *= b;\n  a.z *= b;\n}\n\ninline __host__ __device__ uint3\noperator*(uint3 a, uint3 b) {\n  return make_uint3(a.x * b.x, a.y * b.y, a.z * b.z);\n}\ninline __host__ __device__ void\noperator*=(uint3 &a, uint3 b) {\n  a.x *= b.x;\n  a.y *= b.y;\n  a.z *= b.z;\n}\ninline __host__ __device__ uint3\noperator*(uint3 a, uint b) {\n  return make_uint3(a.x * b, a.y * b, a.z * b);\n}\ninline __host__ __device__ uint3\noperator*(uint b, uint3 a) {\n  return make_uint3(b * a.x, b * a.y, b * a.z);\n}\ninline __host__ __device__ void\noperator*=(uint3 &a, uint b) {\n  a.x *= b;\n  a.y *= b;\n  a.z *= b;\n}\n\ninline __host__ __device__ float4\noperator*(float4 a, float4 b) {\n  return make_float4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);\n}\ninline __host__ __device__ void\noperator*=(float4 &a, float4 b) {\n  a.x *= b.x;\n  a.y *= b.y;\n  a.z *= b.z;\n  a.w *= b.w;\n}\ninline __host__ __device__ float4\noperator*(float4 a, float b) {\n  return make_float4(a.x * b, a.y * b, a.z * b, a.w * b);\n}\ninline __host__ __device__ float4\noperator*(float b, float4 a) {\n  return make_float4(b * a.x, b * a.y, b * a.z, b * a.w);\n}\ninline __host__ __device__ void\noperator*=(float4 &a, float b) {\n  a.x *= b;\n  a.y *= b;\n  a.z *= b;\n  a.w *= b;\n}\n\ninline __host__ __device__ int4\noperator*(int4 a, int4 b) {\n  return make_int4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);\n}\ninline __host__ __device__ void\noperator*=(int4 &a, int4 b) {\n  a.x *= b.x;\n  a.y *= b.y;\n  a.z *= b.z;\n  a.w *= b.w;\n}\ninline __host__ __device__ int4\noperator*(int4 a, int b) {\n  return make_int4(a.x * b, a.y * b, a.z * b, a.w * b);\n}\ninline __host__ __device__ int4\noperator*(int b, int4 a) {\n  return make_int4(b * a.x, b * a.y, b * a.z, b * a.w);\n}\ninline __host__ __device__ void\noperator*=(int4 &a, int b) {\n  a.x *= b;\n  a.y *= b;\n  a.z *= b;\n  a.w *= b;\n}\n\ninline __host__ __device__ uint4\noperator*(uint4 a, uint4 b) {\n  return make_uint4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);\n}\ninline __host__ __device__ void\noperator*=(uint4 &a, uint4 b) {\n  a.x *= b.x;\n  a.y *= b.y;\n  a.z *= b.z;\n  a.w *= b.w;\n}\ninline __host__ __device__ uint4\noperator*(uint4 a, uint b) {\n  return make_uint4(a.x * b, a.y * b, a.z * b, a.w * b);\n}\ninline __host__ __device__ uint4\noperator*(uint b, uint4 a) {\n  return make_uint4(b * a.x, b * a.y, b * a.z, b * a.w);\n}\ninline __host__ __device__ void\noperator*=(uint4 &a, uint b) {\n  a.x *= b;\n  a.y *= b;\n  a.z *= b;\n  a.w *= b;\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// divide\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\noperator/(float2 a, float2 b) {\n  return make_float2(a.x / b.x, a.y / b.y);\n}\ninline __host__ __device__ void\noperator/=(float2 &a, float2 b) {\n  a.x /= b.x;\n  a.y /= b.y;\n}\ninline __host__ __device__ float2\noperator/(float2 a, float b) {\n  return make_float2(a.x / b, a.y / b);\n}\ninline __host__ __device__ void\noperator/=(float2 &a, float b) {\n  a.x /= b;\n  a.y /= b;\n}\ninline __host__ __device__ float2\noperator/(float b, float2 a) {\n  return make_float2(b / a.x, b / a.y);\n}\n\ninline __host__ __device__ float3\noperator/(float3 a, float3 b) {\n  return make_float3(a.x / b.x, a.y / b.y, a.z / b.z);\n}\ninline __host__ __device__ void\noperator/=(float3 &a, float3 b) {\n  a.x /= b.x;\n  a.y /= b.y;\n  a.z /= b.z;\n}\ninline __host__ __device__ float3\noperator/(float3 a, float b) {\n  return make_float3(a.x / b, a.y / b, a.z / b);\n}\ninline __host__ __device__ void\noperator/=(float3 &a, float b) {\n  a.x /= b;\n  a.y /= b;\n  a.z /= b;\n}\ninline __host__ __device__ float3\noperator/(float b, float3 a) {\n  return make_float3(b / a.x, b / a.y, b / a.z);\n}\n\ninline __host__ __device__ float4\noperator/(float4 a, float4 b) {\n  return make_float4(a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w);\n}\ninline __host__ __device__ void\noperator/=(float4 &a, float4 b) {\n  a.x /= b.x;\n  a.y /= b.y;\n  a.z /= b.z;\n  a.w /= b.w;\n}\ninline __host__ __device__ float4\noperator/(float4 a, float b) {\n  return make_float4(a.x / b, a.y / b, a.z / b, a.w / b);\n}\ninline __host__ __device__ void\noperator/=(float4 &a, float b) {\n  a.x /= b;\n  a.y /= b;\n  a.z /= b;\n  a.w /= b;\n}\ninline __host__ __device__ float4\noperator/(float b, float4 a) {\n  return make_float4(b / a.x, b / a.y, b / a.z, b / a.w);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// min\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\nfminf(float2 a, float2 b) {\n  return make_float2(fminf(a.x, b.x), fminf(a.y, b.y));\n}\ninline __host__ __device__ float3\nfminf(float3 a, float3 b) {\n  return make_float3(fminf(a.x, b.x), fminf(a.y, b.y), fminf(a.z, b.z));\n}\ninline __host__ __device__ float4\nfminf(float4 a, float4 b) {\n  return make_float4(fminf(a.x, b.x), fminf(a.y, b.y), fminf(a.z, b.z), fminf(a.w, b.w));\n}\n\ninline __host__ __device__ int2\nmin(int2 a, int2 b) {\n  return make_int2(min(a.x, b.x), min(a.y, b.y));\n}\ninline __host__ __device__ int3\nmin(int3 a, int3 b) {\n  return make_int3(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z));\n}\ninline __host__ __device__ int4\nmin(int4 a, int4 b) {\n  return make_int4(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z), min(a.w, b.w));\n}\n\ninline __host__ __device__ uint2\nmin(uint2 a, uint2 b) {\n  return make_uint2(min(a.x, b.x), min(a.y, b.y));\n}\ninline __host__ __device__ uint3\nmin(uint3 a, uint3 b) {\n  return make_uint3(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z));\n}\ninline __host__ __device__ uint4\nmin(uint4 a, uint4 b) {\n  return make_uint4(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z), min(a.w, b.w));\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// max\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\nfmaxf(float2 a, float2 b) {\n  return make_float2(fmaxf(a.x, b.x), fmaxf(a.y, b.y));\n}\ninline __host__ __device__ float3\nfmaxf(float3 a, float3 b) {\n  return make_float3(fmaxf(a.x, b.x), fmaxf(a.y, b.y), fmaxf(a.z, b.z));\n}\ninline __host__ __device__ float4\nfmaxf(float4 a, float4 b) {\n  return make_float4(fmaxf(a.x, b.x), fmaxf(a.y, b.y), fmaxf(a.z, b.z), fmaxf(a.w, b.w));\n}\n\ninline __host__ __device__ int2\nmax(int2 a, int2 b) {\n  return make_int2(max(a.x, b.x), max(a.y, b.y));\n}\ninline __host__ __device__ int3\nmax(int3 a, int3 b) {\n  return make_int3(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z));\n}\ninline __host__ __device__ int4\nmax(int4 a, int4 b) {\n  return make_int4(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z), max(a.w, b.w));\n}\n\ninline __host__ __device__ uint2\nmax(uint2 a, uint2 b) {\n  return make_uint2(max(a.x, b.x), max(a.y, b.y));\n}\ninline __host__ __device__ uint3\nmax(uint3 a, uint3 b) {\n  return make_uint3(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z));\n}\ninline __host__ __device__ uint4\nmax(uint4 a, uint4 b) {\n  return make_uint4(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z), max(a.w, b.w));\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// lerp\n// - linear interpolation between a and b, based on value t in [0, 1] range\n////////////////////////////////////////////////////////////////////////////////\n\ninline __device__ __host__ float\nlerp(float a, float b, float t) {\n  return a + t * (b - a);\n}\ninline __device__ __host__ float2\nlerp(float2 a, float2 b, float t) {\n  return a + t * (b - a);\n}\ninline __device__ __host__ float3\nlerp(float3 a, float3 b, float t) {\n  return a + t * (b - a);\n}\ninline __device__ __host__ float4\nlerp(float4 a, float4 b, float t) {\n  return a + t * (b - a);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// clamp\n// - clamp the value v to be in the range [a, b]\n////////////////////////////////////////////////////////////////////////////////\n\ninline __device__ __host__ float\nclamp(float f, float a, float b) {\n  return fmaxf(a, fminf(f, b));\n}\ninline __device__ __host__ int\nclamp(int f, int a, int b) {\n  return max(a, min(f, b));\n}\ninline __device__ __host__ uint\nclamp(uint f, uint a, uint b) {\n  return max(a, min(f, b));\n}\n\ninline __device__ __host__ float2\nclamp(float2 v, float a, float b) {\n  return make_float2(clamp(v.x, a, b), clamp(v.y, a, b));\n}\ninline __device__ __host__ float2\nclamp(float2 v, float2 a, float2 b) {\n  return make_float2(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y));\n}\ninline __device__ __host__ float3\nclamp(float3 v, float a, float b) {\n  return make_float3(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b));\n}\ninline __device__ __host__ float3\nclamp(float3 v, float3 a, float3 b) {\n  return make_float3(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z));\n}\ninline __device__ __host__ float4\nclamp(float4 v, float a, float b) {\n  return make_float4(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b), clamp(v.w, a, b));\n}\ninline __device__ __host__ float4\nclamp(float4 v, float4 a, float4 b) {\n  return make_float4(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z), clamp(v.w, a.w, b.w));\n}\n\ninline __device__ __host__ int2\nclamp(int2 v, int a, int b) {\n  return make_int2(clamp(v.x, a, b), clamp(v.y, a, b));\n}\ninline __device__ __host__ int2\nclamp(int2 v, int2 a, int2 b) {\n  return make_int2(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y));\n}\ninline __device__ __host__ int3\nclamp(int3 v, int a, int b) {\n  return make_int3(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b));\n}\ninline __device__ __host__ int3\nclamp(int3 v, int3 a, int3 b) {\n  return make_int3(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z));\n}\ninline __device__ __host__ int4\nclamp(int4 v, int a, int b) {\n  return make_int4(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b), clamp(v.w, a, b));\n}\ninline __device__ __host__ int4\nclamp(int4 v, int4 a, int4 b) {\n  return make_int4(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z), clamp(v.w, a.w, b.w));\n}\n\ninline __device__ __host__ uint2\nclamp(uint2 v, uint a, uint b) {\n  return make_uint2(clamp(v.x, a, b), clamp(v.y, a, b));\n}\ninline __device__ __host__ uint2\nclamp(uint2 v, uint2 a, uint2 b) {\n  return make_uint2(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y));\n}\ninline __device__ __host__ uint3\nclamp(uint3 v, uint a, uint b) {\n  return make_uint3(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b));\n}\ninline __device__ __host__ uint3\nclamp(uint3 v, uint3 a, uint3 b) {\n  return make_uint3(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z));\n}\ninline __device__ __host__ uint4\nclamp(uint4 v, uint a, uint b) {\n  return make_uint4(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b), clamp(v.w, a, b));\n}\ninline __device__ __host__ uint4\nclamp(uint4 v, uint4 a, uint4 b) {\n  return make_uint4(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z), clamp(v.w, a.w, b.w));\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// dot product\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float\ndot(float2 a, float2 b) {\n  return a.x * b.x + a.y * b.y;\n}\ninline __host__ __device__ float\ndot(float3 a, float3 b) {\n  return a.x * b.x + a.y * b.y + a.z * b.z;\n}\ninline __host__ __device__ float\ndot(float4 a, float4 b) {\n  return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;\n}\n\ninline __host__ __device__ int\ndot(int2 a, int2 b) {\n  return a.x * b.x + a.y * b.y;\n}\ninline __host__ __device__ int\ndot(int3 a, int3 b) {\n  return a.x * b.x + a.y * b.y + a.z * b.z;\n}\ninline __host__ __device__ int\ndot(int4 a, int4 b) {\n  return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;\n}\n\ninline __host__ __device__ uint\ndot(uint2 a, uint2 b) {\n  return a.x * b.x + a.y * b.y;\n}\ninline __host__ __device__ uint\ndot(uint3 a, uint3 b) {\n  return a.x * b.x + a.y * b.y + a.z * b.z;\n}\ninline __host__ __device__ uint\ndot(uint4 a, uint4 b) {\n  return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// length\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float\nlength(float2 v) {\n  return sqrtf(dot(v, v));\n}\ninline __host__ __device__ float\nlength(float3 v) {\n  return sqrtf(dot(v, v));\n}\ninline __host__ __device__ float\nlength(float4 v) {\n  return sqrtf(dot(v, v));\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// normalize\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\nnormalize(float2 v) {\n  float invLen = rsqrtf(dot(v, v));\n  return v * invLen;\n}\ninline __host__ __device__ float3\nnormalize(float3 v) {\n  float invLen = rsqrtf(dot(v, v));\n  return v * invLen;\n}\ninline __host__ __device__ float4\nnormalize(float4 v) {\n  float invLen = rsqrtf(dot(v, v));\n  return v * invLen;\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// floor\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\nfloorf(float2 v) {\n  return make_float2(floorf(v.x), floorf(v.y));\n}\ninline __host__ __device__ float3\nfloorf(float3 v) {\n  return make_float3(floorf(v.x), floorf(v.y), floorf(v.z));\n}\ninline __host__ __device__ float4\nfloorf(float4 v) {\n  return make_float4(floorf(v.x), floorf(v.y), floorf(v.z), floorf(v.w));\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// frac - returns the fractional portion of a scalar or each vector component\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float\nfracf(float v) {\n  return v - floorf(v);\n}\ninline __host__ __device__ float2\nfracf(float2 v) {\n  return make_float2(fracf(v.x), fracf(v.y));\n}\ninline __host__ __device__ float3\nfracf(float3 v) {\n  return make_float3(fracf(v.x), fracf(v.y), fracf(v.z));\n}\ninline __host__ __device__ float4\nfracf(float4 v) {\n  return make_float4(fracf(v.x), fracf(v.y), fracf(v.z), fracf(v.w));\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// fmod\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\nfmodf(float2 a, float2 b) {\n  return make_float2(fmodf(a.x, b.x), fmodf(a.y, b.y));\n}\ninline __host__ __device__ float3\nfmodf(float3 a, float3 b) {\n  return make_float3(fmodf(a.x, b.x), fmodf(a.y, b.y), fmodf(a.z, b.z));\n}\ninline __host__ __device__ float4\nfmodf(float4 a, float4 b) {\n  return make_float4(fmodf(a.x, b.x), fmodf(a.y, b.y), fmodf(a.z, b.z), fmodf(a.w, b.w));\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// absolute value\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float2\nfabs(float2 v) {\n  return make_float2(fabs(v.x), fabs(v.y));\n}\ninline __host__ __device__ float3\nfabs(float3 v) {\n  return make_float3(fabs(v.x), fabs(v.y), fabs(v.z));\n}\ninline __host__ __device__ float4\nfabs(float4 v) {\n  return make_float4(fabs(v.x), fabs(v.y), fabs(v.z), fabs(v.w));\n}\n\ninline __host__ __device__ int2\nabs(int2 v) {\n  return make_int2(abs(v.x), abs(v.y));\n}\ninline __host__ __device__ int3\nabs(int3 v) {\n  return make_int3(abs(v.x), abs(v.y), abs(v.z));\n}\ninline __host__ __device__ int4\nabs(int4 v) {\n  return make_int4(abs(v.x), abs(v.y), abs(v.z), abs(v.w));\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// reflect\n// - returns reflection of incident ray I around surface normal N\n// - N should be normalized, reflected vector's length is equal to length of I\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float3\nreflect(float3 i, float3 n) {\n  return i - 2.0f * n * dot(n, i);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// cross product\n////////////////////////////////////////////////////////////////////////////////\n\ninline __host__ __device__ float3\ncross(float3 a, float3 b) {\n  return make_float3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// smoothstep\n// - returns 0 if x < a\n// - returns 1 if x > b\n// - otherwise returns smooth interpolation between 0 and 1 based on x\n////////////////////////////////////////////////////////////////////////////////\n\ninline __device__ __host__ float\nsmoothstep(float a, float b, float x) {\n  float y = clamp((x - a) / (b - a), 0.0f, 1.0f);\n  return (y * y * (3.0f - (2.0f * y)));\n}\ninline __device__ __host__ float2\nsmoothstep(float2 a, float2 b, float2 x) {\n  float2 y = clamp((x - a) / (b - a), 0.0f, 1.0f);\n  return (y * y * (make_float2(3.0f) - (make_float2(2.0f) * y)));\n}\ninline __device__ __host__ float3\nsmoothstep(float3 a, float3 b, float3 x) {\n  float3 y = clamp((x - a) / (b - a), 0.0f, 1.0f);\n  return (y * y * (make_float3(3.0f) - (make_float3(2.0f) * y)));\n}\ninline __device__ __host__ float4\nsmoothstep(float4 a, float4 b, float4 x) {\n  float4 y = clamp((x - a) / (b - a), 0.0f, 1.0f);\n  return (y * y * (make_float4(3.0f) - (make_float4(2.0f) * y)));\n}\n\n#endif\n"
  },
  {
    "path": "tools/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.20)\n\nproject(sunshine_tools)\n\ninclude_directories(\"${CMAKE_SOURCE_DIR}\")\n\nadd_executable(dxgi-info dxgi.cpp)\nset_target_properties(dxgi-info PROPERTIES CXX_STANDARD 23)\ntarget_link_libraries(dxgi-info\n        ${CMAKE_THREAD_LIBS_INIT}\n        dxgi\n        ${PLATFORM_LIBRARIES})\ntarget_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})\n\nadd_executable(audio-info audio.cpp)\nset_target_properties(audio-info PROPERTIES CXX_STANDARD 23)\ntarget_link_libraries(audio-info\n        ${Boost_LIBRARIES}\n        ${CMAKE_THREAD_LIBS_INIT}\n        ksuser\n        ${PLATFORM_LIBRARIES})\ntarget_compile_options(audio-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS})\n\nadd_executable(sunshinesvc sunshinesvc.cpp)\nset_target_properties(sunshinesvc PROPERTIES CXX_STANDARD 23)\ntarget_link_libraries(sunshinesvc\n        ${CMAKE_THREAD_LIBS_INIT}\n        wtsapi32\n        ${PLATFORM_LIBRARIES})\ntarget_compile_options(sunshinesvc PRIVATE ${SUNSHINE_COMPILE_OPTIONS})\n\nadd_executable(qiin-tabtip qiin-tabtip.cpp)\nset_target_properties(qiin-tabtip PROPERTIES CXX_STANDARD 23)\ntarget_compile_definitions(qiin-tabtip PRIVATE UNICODE _UNICODE)\ntarget_link_libraries(qiin-tabtip\n        ${CMAKE_THREAD_LIBS_INIT}\n        shell32\n        advapi32\n        ole32\n        oleaut32\n        uuid\n        ${PLATFORM_LIBRARIES})\ntarget_compile_options(qiin-tabtip PRIVATE ${SUNSHINE_COMPILE_OPTIONS})\n# 为 MinGW 设置正确的入口点和子系统\nif(MINGW)\n    # 优化大小：-Os 优化大小, -s 移除调试符号\n    target_compile_options(qiin-tabtip PRIVATE -Os)\n    target_link_options(qiin-tabtip PRIVATE \n        -municode \n        -s  # Strip symbols\n        -Wl,--gc-sections  # 移除未使用的section\n    )\nendif()\n\n# 测试工具：test_args - 仅用于开发测试，不会被打包\nadd_executable(test_args test_args.cpp)\nset_target_properties(test_args PROPERTIES CXX_STANDARD 23)\ntarget_link_libraries(test_args\n        ${CMAKE_THREAD_LIBS_INIT}\n        ${PLATFORM_LIBRARIES})\nif(WIN32)\n    target_link_libraries(test_args advapi32 userenv)\nendif()\ntarget_compile_options(test_args PRIVATE ${SUNSHINE_COMPILE_OPTIONS})\n# 明确标记为不安装（开发测试工具），但可以正常构建\n# 注意：不会在 cmake/packaging/windows.cmake 中安装，所以不会被打包"
  },
  {
    "path": "tools/audio.cpp",
    "content": "/**\n * @file tools/audio.cpp\n * @brief Handles collecting audio device information from Windows.\n */\n#define INITGUID\n\n// platform includes\n#include <audioclient.h>\n#include <codecvt>\n#include <iostream>\n#include <locale>\n#include <mmdeviceapi.h>\n#include <roapi.h>\n#include <synchapi.h>\n\n// lib includes\n#include <boost/locale.hpp>\n\n// local includes\n#include \"src/utility.h\"\n\nDEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2);  // DEVPROP_TYPE_STRING\nDEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);  // DEVPROP_TYPE_STRING\nDEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2);\n\nusing namespace std::literals;\n\nint device_state_filter = DEVICE_STATE_ACTIVE;\n\nnamespace audio {\n  template<class T>\n  void Release(T *p) {\n    p->Release();\n  }\n\n  template<class T>\n  void co_task_free(T *p) {\n    CoTaskMemFree((LPVOID) p);\n  }\n\n  using device_enum_t = util::safe_ptr<IMMDeviceEnumerator, Release<IMMDeviceEnumerator>>;\n  using collection_t = util::safe_ptr<IMMDeviceCollection, Release<IMMDeviceCollection>>;\n  using prop_t = util::safe_ptr<IPropertyStore, Release<IPropertyStore>>;\n  using device_t = util::safe_ptr<IMMDevice, Release<IMMDevice>>;\n  using audio_client_t = util::safe_ptr<IAudioClient, Release<IAudioClient>>;\n  using audio_capture_t = util::safe_ptr<IAudioCaptureClient, Release<IAudioCaptureClient>>;\n  using wave_format_t = util::safe_ptr<WAVEFORMATEX, co_task_free<WAVEFORMATEX>>;\n\n  using wstring_t = util::safe_ptr<WCHAR, co_task_free<WCHAR>>;\n\n  using handle_t = util::safe_ptr_v2<void, BOOL, CloseHandle>;\n\n  class prop_var_t {\n  public:\n    prop_var_t() {\n      PropVariantInit(&prop);\n    }\n\n    ~prop_var_t() {\n      PropVariantClear(&prop);\n    }\n\n    PROPVARIANT prop;\n  };\n\n  const wchar_t *no_null(const wchar_t *str) {\n    return str ? str : L\"Unknown\";\n  }\n\n  struct format_t {\n    std::string_view name;\n    int channels;\n    int channel_mask;\n  } formats[] {\n    {\"Mono\"sv,\n     1,\n     SPEAKER_FRONT_CENTER},\n    {\"Stereo\"sv,\n     2,\n     SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT},\n    {\"Quadraphonic\"sv,\n     4,\n     SPEAKER_FRONT_LEFT |\n       SPEAKER_FRONT_RIGHT |\n       SPEAKER_BACK_LEFT |\n       SPEAKER_BACK_RIGHT},\n    {\"Surround 5.1 (Side)\"sv,\n     6,\n     SPEAKER_FRONT_LEFT |\n       SPEAKER_FRONT_RIGHT |\n       SPEAKER_FRONT_CENTER |\n       SPEAKER_LOW_FREQUENCY |\n       SPEAKER_SIDE_LEFT |\n       SPEAKER_SIDE_RIGHT},\n    {\"Surround 5.1 (Back)\"sv,\n     6,\n     SPEAKER_FRONT_LEFT |\n       SPEAKER_FRONT_RIGHT |\n       SPEAKER_FRONT_CENTER |\n       SPEAKER_LOW_FREQUENCY |\n       SPEAKER_BACK_LEFT |\n       SPEAKER_BACK_RIGHT},\n    {\"Surround 7.1\"sv,\n     8,\n     SPEAKER_FRONT_LEFT |\n       SPEAKER_FRONT_RIGHT |\n       SPEAKER_FRONT_CENTER |\n       SPEAKER_LOW_FREQUENCY |\n       SPEAKER_BACK_LEFT |\n       SPEAKER_BACK_RIGHT |\n       SPEAKER_SIDE_LEFT |\n       SPEAKER_SIDE_RIGHT}\n  };\n\n  void set_wave_format(audio::wave_format_t &wave_format, const format_t &format) {\n    wave_format->nChannels = format.channels;\n    wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8;\n    wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign;\n\n    if (wave_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {\n      ((PWAVEFORMATEXTENSIBLE) wave_format.get())->dwChannelMask = format.channel_mask;\n    }\n  }\n\n  audio_client_t make_audio_client(device_t &device, const format_t &format) {\n    audio_client_t audio_client;\n    auto status = device->Activate(\n      IID_IAudioClient,\n      CLSCTX_ALL,\n      nullptr,\n      (void **) &audio_client\n    );\n\n    if (FAILED(status)) {\n      std::cout << \"Couldn't activate Device: [0x\"sv << util::hex(status).to_string_view() << ']' << std::endl;\n\n      return nullptr;\n    }\n\n    wave_format_t wave_format;\n    status = audio_client->GetMixFormat(&wave_format);\n\n    if (FAILED(status)) {\n      std::cout << \"Couldn't acquire Wave Format [0x\"sv << util::hex(status).to_string_view() << ']' << std::endl;\n\n      return nullptr;\n    }\n\n    set_wave_format(wave_format, format);\n\n    status = audio_client->Initialize(\n      AUDCLNT_SHAREMODE_SHARED,\n      AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,\n      0,\n      0,\n      wave_format.get(),\n      nullptr\n    );\n\n    if (status) {\n      return nullptr;\n    }\n\n    return audio_client;\n  }\n\n  void print_device(device_t &device) {\n    audio::wstring_t wstring;\n    DWORD device_state;\n\n    device->GetState(&device_state);\n    device->GetId(&wstring);\n\n    audio::prop_t prop;\n    device->OpenPropertyStore(STGM_READ, &prop);\n\n    prop_var_t adapter_friendly_name;\n    prop_var_t device_friendly_name;\n    prop_var_t device_desc;\n\n    prop->GetValue(PKEY_Device_FriendlyName, &device_friendly_name.prop);\n    prop->GetValue(PKEY_DeviceInterface_FriendlyName, &adapter_friendly_name.prop);\n    prop->GetValue(PKEY_Device_DeviceDesc, &device_desc.prop);\n\n    if (!(device_state & device_state_filter)) {\n      return;\n    }\n\n    std::wstring device_state_string = L\"Unknown\"s;\n    switch (device_state) {\n      case DEVICE_STATE_ACTIVE:\n        device_state_string = L\"Active\"s;\n        break;\n      case DEVICE_STATE_DISABLED:\n        device_state_string = L\"Disabled\"s;\n        break;\n      case DEVICE_STATE_UNPLUGGED:\n        device_state_string = L\"Unplugged\"s;\n        break;\n      case DEVICE_STATE_NOTPRESENT:\n        device_state_string = L\"Not present\"s;\n        break;\n    }\n\n    std::wstring current_format = L\"Unknown\"s;\n    for (const auto &format : formats) {\n      // This will fail for any format that's not the mix format for this device,\n      // so we can take the first match as the current format to display.\n      auto audio_client = make_audio_client(device, format);\n      if (audio_client) {\n        current_format = boost::locale::conv::utf_to_utf<wchar_t>(format.name.data());\n        break;\n      }\n    }\n\n    std::wcout\n      << L\"===== Device =====\"sv << std::endl\n      << L\"Device ID          : \"sv << wstring.get() << std::endl\n      << L\"Device name        : \"sv << no_null((LPWSTR) device_friendly_name.prop.pszVal) << std::endl\n      << L\"Adapter name       : \"sv << no_null((LPWSTR) adapter_friendly_name.prop.pszVal) << std::endl\n      << L\"Device description : \"sv << no_null((LPWSTR) device_desc.prop.pszVal) << std::endl\n      << L\"Device state       : \"sv << device_state_string << std::endl\n      << L\"Current format     : \"sv << current_format << std::endl\n      << std::endl;\n  }\n}  // namespace audio\n\nvoid print_help() {\n  std::cout\n    << \"==== Help ====\"sv << std::endl\n    << \"Usage:\"sv << std::endl\n    << \"    audio-info [Active|Disabled|Unplugged|Not-Present]\" << std::endl;\n}\n\nint main(int argc, char *argv[]) {\n  CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY);\n\n  auto fg = util::fail_guard([]() {\n    CoUninitialize();\n  });\n\n  if (argc > 1) {\n    device_state_filter = 0;\n  }\n\n  for (auto x = 1; x < argc; ++x) {\n    for (auto p = argv[x]; *p != '\\0'; ++p) {\n      if (*p == ' ') {\n        *p = '-';\n\n        continue;\n      }\n\n      *p = std::tolower(*p);\n    }\n\n    if (argv[x] == \"active\"sv) {\n      device_state_filter |= DEVICE_STATE_ACTIVE;\n    } else if (argv[x] == \"disabled\"sv) {\n      device_state_filter |= DEVICE_STATE_DISABLED;\n    } else if (argv[x] == \"unplugged\"sv) {\n      device_state_filter |= DEVICE_STATE_UNPLUGGED;\n    } else if (argv[x] == \"not-present\"sv) {\n      device_state_filter |= DEVICE_STATE_NOTPRESENT;\n    } else {\n      print_help();\n      return 2;\n    }\n  }\n\n  HRESULT status;\n\n  audio::device_enum_t device_enum;\n  status = CoCreateInstance(\n    CLSID_MMDeviceEnumerator,\n    nullptr,\n    CLSCTX_ALL,\n    IID_IMMDeviceEnumerator,\n    (void **) &device_enum\n  );\n\n  if (FAILED(status)) {\n    std::cout << \"Couldn't create Device Enumerator: [0x\"sv << util::hex(status).to_string_view() << ']' << std::endl;\n\n    return -1;\n  }\n\n  audio::collection_t collection;\n  status = device_enum->EnumAudioEndpoints(eRender, device_state_filter, &collection);\n\n  if (FAILED(status)) {\n    std::cout << \"Couldn't enumerate: [0x\"sv << util::hex(status).to_string_view() << ']' << std::endl;\n\n    return -1;\n  }\n\n  UINT count;\n  collection->GetCount(&count);\n\n  std::cout << \"====== Found \"sv << count << \" audio devices ======\"sv << std::endl;\n  for (auto x = 0; x < count; ++x) {\n    audio::device_t device;\n    collection->Item(x, &device);\n\n    audio::print_device(device);\n  }\n\n  return 0;\n}"
  },
  {
    "path": "tools/build-qiin-tabtip.sh",
    "content": "#!/bin/bash\n# qiin-tabtip.exe 快速编译脚本\n\nset -e  # 遇到错误立即退出\n\necho \"========================================\"\necho \"qiin-tabtip 编译脚本\"\necho \"========================================\"\necho \"\"\n\n# 检查源文件\nif [ ! -f \"qiin-tabtip.cpp\" ]; then\n    echo \"❌ 错误: 找不到 qiin-tabtip.cpp\"\n    exit 1\nfi\n\n# 检测编译器\nCOMPILER=\"\"\nCOMPILE_CMD=\"\"\n\nif command -v cl.exe &> /dev/null; then\n    COMPILER=\"MSVC\"\n    COMPILE_CMD=\"cl.exe /EHsc /std:c++17 /DUNICODE /D_UNICODE /Os /Fe:qiin-tabtip.exe qiin-tabtip.cpp shell32.lib user32.lib advapi32.lib ole32.lib oleaut32.lib uuid.lib /link /SUBSYSTEM:CONSOLE /nologo\"\nelif command -v g++ &> /dev/null; then\n    COMPILER=\"MinGW/GCC\"\n    COMPILE_CMD=\"g++ -std=c++17 -DUNICODE -D_UNICODE -Os -s -o qiin-tabtip.exe qiin-tabtip.cpp -lshell32 -luser32 -ladvapi32 -lole32 -loleaut32 -luuid -static-libgcc -static-libstdc++ -Wl,--gc-sections -municode\"\nelif command -v clang++ &> /dev/null; then\n    COMPILER=\"Clang\"\n    COMPILE_CMD=\"clang++ -std=c++17 -DUNICODE -D_UNICODE -Os -s -o qiin-tabtip.exe qiin-tabtip.cpp -lshell32 -luser32 -ladvapi32 -lole32 -loleaut32 -luuid -static-libgcc -static-libstdc++ -Wl,--gc-sections -municode\"\nelse\n    echo \"❌ 未找到 C++ 编译器\"\n    echo \"\"\n    echo \"请安装以下之一:\"\n    echo \"  - Visual Studio (MSVC)\"\n    echo \"  - MinGW-w64\"\n    echo \"  - Clang\"\n    exit 1\nfi\n\necho \"编译器: $COMPILER\"\necho \"\"\necho \"开始编译...\"\necho \"========================================\"\n\n# 执行编译\neval $COMPILE_CMD\n\nif [ $? -eq 0 ]; then\n    echo \"\"\n    echo \"========================================\"\n    echo \"✓ 编译成功！\"\n    echo \"========================================\"\n    echo \"\"\n    \n    # 显示文件信息\n    if [ -f \"qiin-tabtip.exe\" ]; then\n        FILE_SIZE=$(stat -c%s \"qiin-tabtip.exe\" 2>/dev/null || stat -f%z \"qiin-tabtip.exe\" 2>/dev/null || echo \"unknown\")\n        if [ \"$FILE_SIZE\" != \"unknown\" ]; then\n            SIZE_KB=$((FILE_SIZE / 1024))\n            echo \"文件: qiin-tabtip.exe\"\n            echo \"大小: ${SIZE_KB} KB\"\n            echo \"\"\n        fi\n    fi\n    \n    echo \"使用方法:\"\n    echo \"  ./qiin-tabtip.exe        # 切换键盘\"\n    echo \"  ./qiin-tabtip.exe show   # 显示键盘\"\n    echo \"  ./qiin-tabtip.exe osk    # 屏幕键盘\"\n    echo \"  ./qiin-tabtip.exe help   # 查看帮助\"\n    echo \"\"\nelse\n    echo \"\"\n    echo \"========================================\"\n    echo \"❌ 编译失败\"\n    echo \"========================================\"\n    exit 1\nfi\n\n"
  },
  {
    "path": "tools/dxgi.cpp",
    "content": "/**\n * @file tools/dxgi.cpp\n * @brief Displays information about connected displays and GPUs\n */\n#define WINVER 0x0A00\n#include <d3dcommon.h>\n#include <dxgi.h>\n\n#include <iostream>\n\n#include \"src/utility.h\"\n\nusing namespace std::literals;\nnamespace dxgi {\n  template <class T>\n  void\n  Release(T *dxgi) {\n    dxgi->Release();\n  }\n\n  using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory1>>;\n  using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;\n  using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;\n\n}  // namespace dxgi\n\nint\nmain(int argc, char *argv[]) {\n  HRESULT status;\n\n  // Set ourselves as per-monitor DPI aware for accurate resolution values on High DPI systems\n  SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);\n\n  dxgi::factory1_t::pointer factory_p {};\n  status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory_p);\n  dxgi::factory1_t factory { factory_p };\n  if (FAILED(status)) {\n    std::cout << \"Failed to create DXGIFactory1 [0x\"sv << util::hex(status).to_string_view() << ']' << std::endl;\n    return -1;\n  }\n\n  dxgi::adapter_t::pointer adapter_p {};\n  for (int x = 0; factory->EnumAdapters1(x, &adapter_p) != DXGI_ERROR_NOT_FOUND; ++x) {\n    dxgi::adapter_t adapter { adapter_p };\n\n    DXGI_ADAPTER_DESC1 adapter_desc;\n    adapter->GetDesc1(&adapter_desc);\n\n    std::cout\n      << \"====== ADAPTER =====\"sv << std::endl;\n    std::wcout\n      << L\"Device Name      : \"sv << adapter_desc.Description << std::endl;\n    std::cout\n      << \"Device Vendor ID : 0x\"sv << util::hex(adapter_desc.VendorId).to_string_view() << std::endl\n      << \"Device Device ID : 0x\"sv << util::hex(adapter_desc.DeviceId).to_string_view() << std::endl\n      << \"Device Video Mem : \"sv << adapter_desc.DedicatedVideoMemory / 1048576 << \" MiB\"sv << std::endl\n      << \"Device Sys Mem   : \"sv << adapter_desc.DedicatedSystemMemory / 1048576 << \" MiB\"sv << std::endl\n      << \"Share Sys Mem    : \"sv << adapter_desc.SharedSystemMemory / 1048576 << \" MiB\"sv << std::endl\n      << std::endl\n      << \"    ====== OUTPUT ======\"sv << std::endl;\n\n    dxgi::output_t::pointer output_p {};\n    for (int y = 0; adapter->EnumOutputs(y, &output_p) != DXGI_ERROR_NOT_FOUND; ++y) {\n      dxgi::output_t output { output_p };\n\n      DXGI_OUTPUT_DESC desc;\n      output->GetDesc(&desc);\n\n      auto width = desc.DesktopCoordinates.right - desc.DesktopCoordinates.left;\n      auto height = desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top;\n\n      std::wcout\n        << L\"    Output Name       : \"sv << desc.DeviceName << std::endl;\n      std::cout\n        << \"    AttachedToDesktop : \"sv << (desc.AttachedToDesktop ? \"yes\"sv : \"no\"sv) << std::endl\n        << \"    Resolution        : \"sv << width << 'x' << height << std::endl\n        << std::endl;\n    }\n  }\n\n  return 0;\n}\n"
  },
  {
    "path": "tools/qiin-tabtip.cpp",
    "content": "/**\n * @file tools/qiin-tabtip.cpp\n * @brief 调出或隐藏 Windows 触摸虚拟键盘的工具\n * @note 优化版本 - 不使用 C++ 标准库以减小文件大小\n */\n#ifndef UNICODE\n#define UNICODE\n#endif\n#ifndef _UNICODE\n#define _UNICODE\n#endif\n#define WIN32_LEAN_AND_MEAN\n#include <Windows.h>\n#include <shellapi.h>\n#include <initguid.h>\n#include <Objbase.h>\n\n// 简单的控制台输出函数（替代 iostream）\nstatic void Print(const wchar_t* msg) {\n  DWORD written;\n  HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);\n  WriteConsoleW(hConsole, msg, lstrlenW(msg), &written, NULL);\n  WriteConsoleW(hConsole, L\"\\r\\n\", 2, &written, NULL);\n}\n\nstatic void PrintError(const wchar_t* msg) {\n  DWORD written;\n  HANDLE hConsole = GetStdHandle(STD_ERROR_HANDLE);\n  WriteConsoleW(hConsole, msg, lstrlenW(msg), &written, NULL);\n  WriteConsoleW(hConsole, L\"\\r\\n\", 2, &written, NULL);\n}\n\n// 字符串比较（不区分大小写）\nstatic bool StrEqualI(const wchar_t* str1, const wchar_t* str2) {\n  return lstrcmpiW(str1, str2) == 0;\n}\n\n// ITipInvocation COM 接口 - 这是微软官方的触摸键盘 API\n// CLSID for UIHostNoLaunch\nDEFINE_GUID(CLSID_UIHostNoLaunch,\n    0x4CE576FA, 0x83DC, 0x4f88, 0x95, 0x1C, 0x9D, 0x07, 0x82, 0xB4, 0xE3, 0x76);\n\n// IID for ITipInvocation\nDEFINE_GUID(IID_ITipInvocation,\n    0x37c994e7, 0x432b, 0x4834, 0xa2, 0xf7, 0xdc, 0xe1, 0xf1, 0x3b, 0x83, 0x4b);\n\n// ITipInvocation 接口定义\nstruct ITipInvocation : IUnknown {\n    virtual HRESULT STDMETHODCALLTYPE Toggle(HWND wnd) = 0;\n};\n\n// Windows 10/11 触摸键盘路径\nconst wchar_t* TABTIP_PATH = L\"C:\\\\Program Files\\\\Common Files\\\\microsoft shared\\\\ink\\\\TabTip.exe\";\n\n/**\n * 检查触摸键盘是否正在运行\n */\nbool IsKeyboardVisible() {\n  HWND hwnd = FindWindow(L\"IPTip_Main_Window\", NULL);\n  if (hwnd == NULL) {\n    // Windows 11 可能使用不同的类名\n    hwnd = FindWindow(L\"ApplicationFrameWindow\", L\"Microsoft Text Input Application\");\n  }\n  \n  if (hwnd != NULL) {\n    // 检查窗口是否可见\n    return IsWindowVisible(hwnd);\n  }\n  return false;\n}\n\n/**\n * 检查 TabTip.exe 是否存在\n */\nbool CheckTabTipExists() {\n  DWORD dwAttrib = GetFileAttributes(TABTIP_PATH);\n  return (dwAttrib != INVALID_FILE_ATTRIBUTES && !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY));\n}\n\n/**\n * 启用触摸键盘的桌面模式自动调用\n * 这是 Windows 10/11 中显示 TabTip 的关键设置\n */\nbool EnableDesktopModeAutoInvoke() {\n  HKEY hKey;\n  LONG result = RegOpenKeyEx(\n    HKEY_CURRENT_USER,\n    L\"SOFTWARE\\\\Microsoft\\\\TabletTip\\\\1.7\",\n    0,\n    KEY_READ | KEY_WRITE,\n    &hKey\n  );\n\n  if (result != ERROR_SUCCESS) {\n    // 如果键不存在，尝试创建\n    result = RegCreateKeyEx(\n      HKEY_CURRENT_USER,\n      L\"SOFTWARE\\\\Microsoft\\\\TabletTip\\\\1.7\",\n      0,\n      NULL,\n      REG_OPTION_NON_VOLATILE,\n      KEY_READ | KEY_WRITE,\n      NULL,\n      &hKey,\n      NULL\n    );\n    \n    if (result != ERROR_SUCCESS) {\n      return false;\n    }\n  }\n\n  // 设置 EnableDesktopModeAutoInvoke 为 1\n  DWORD value = 1;\n  result = RegSetValueEx(\n    hKey,\n    L\"EnableDesktopModeAutoInvoke\",\n    0,\n    REG_DWORD,\n    (BYTE*)&value,\n    sizeof(DWORD)\n  );\n\n  RegCloseKey(hKey);\n  return result == ERROR_SUCCESS;\n}\n\n/**\n * 检查是否已启用桌面模式自动调用\n */\nbool IsDesktopModeAutoInvokeEnabled() {\n  HKEY hKey;\n  LONG result = RegOpenKeyEx(\n    HKEY_CURRENT_USER,\n    L\"SOFTWARE\\\\Microsoft\\\\TabletTip\\\\1.7\",\n    0,\n    KEY_READ,\n    &hKey\n  );\n\n  if (result != ERROR_SUCCESS) {\n    return false;\n  }\n\n  DWORD value = 0;\n  DWORD size = sizeof(DWORD);\n  result = RegQueryValueEx(\n    hKey,\n    L\"EnableDesktopModeAutoInvoke\",\n    NULL,\n    NULL,\n    (BYTE*)&value,\n    &size\n  );\n\n  RegCloseKey(hKey);\n  return (result == ERROR_SUCCESS && value == 1);\n}\n\n/**\n * 强制显示已存在的键盘窗口\n */\nbool ForceShowKeyboardWindow() {\n  // 查找键盘窗口\n  HWND hwnd = FindWindow(L\"IPTip_Main_Window\", NULL);\n  \n  if (hwnd == NULL) {\n    // Windows 11 可能使用不同的类名\n    hwnd = FindWindow(L\"ApplicationFrameWindow\", L\"Microsoft Text Input Application\");\n  }\n\n  if (hwnd != NULL) {\n    // 显示窗口\n    ShowWindow(hwnd, SW_SHOW);\n    SetForegroundWindow(hwnd);\n    \n    // 确保窗口位置在屏幕内\n    RECT rect;\n    GetWindowRect(hwnd, &rect);\n    int keyboardHeight = rect.bottom - rect.top;\n    \n    if (keyboardHeight > 0) {\n      int screenWidth = GetSystemMetrics(SM_CXSCREEN);\n      int screenHeight = GetSystemMetrics(SM_CYSCREEN);\n      int keyboardWidth = rect.right - rect.left;\n      int x = (screenWidth - keyboardWidth) / 2;\n      int y = screenHeight - keyboardHeight - 50;\n      \n      SetWindowPos(hwnd, HWND_TOPMOST, x, y, 0, 0, SWP_NOSIZE | SWP_SHOWWINDOW);\n      Sleep(50);\n      SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);\n    }\n    \n    return IsWindowVisible(hwnd);\n  }\n  \n  return false;\n}\n\n/**\n * 使用 COM 接口显示触摸键盘（推荐方法）\n * 这是 Microsoft 官方的 ITipInvocation 接口\n */\nbool ShowKeyboardViaCOM() {\n  HRESULT hr = CoInitialize(NULL);\n  bool needsUninit = SUCCEEDED(hr);\n  \n  ITipInvocation* pTipInvocation = NULL;\n  hr = CoCreateInstance(\n    CLSID_UIHostNoLaunch,\n    NULL,\n    CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER,\n    IID_ITipInvocation,\n    (void**)&pTipInvocation\n  );\n  \n  bool success = false;\n  if (SUCCEEDED(hr) && pTipInvocation) {\n    hr = pTipInvocation->Toggle(GetDesktopWindow());\n    success = SUCCEEDED(hr);\n    pTipInvocation->Release();\n  }\n  \n  if (needsUninit) {\n    CoUninitialize();\n  }\n  \n  return success;\n}\n\n/**\n * 显示触摸键盘（综合方法）\n */\nbool ShowKeyboard() {\n  // 方法 1: 使用 COM 接口（最可靠的方法）\n  if (ShowKeyboardViaCOM()) {\n    Print(L\"✓ 触摸键盘已显示\");\n    return true;\n  }\n  \n  // 方法 2: 传统方法作为备选\n  if (!CheckTabTipExists()) {\n    PrintError(L\"✗ 找不到 TabTip.exe\");\n    return false;\n  }\n\n  // 确保注册表设置正确\n  if (!IsDesktopModeAutoInvokeEnabled()) {\n    EnableDesktopModeAutoInvoke();\n  }\n\n  // 检查窗口是否已存在\n  HWND existingWnd = FindWindow(L\"IPTip_Main_Window\", NULL);\n  if (existingWnd == NULL) {\n    existingWnd = FindWindow(L\"ApplicationFrameWindow\", L\"Microsoft Text Input Application\");\n  }\n\n  if (existingWnd != NULL) {\n    if (ForceShowKeyboardWindow()) {\n      Print(L\"✓ 触摸键盘已显示\");\n      return true;\n    }\n  }\n\n  // 启动 TabTip.exe\n  SHELLEXECUTEINFO sei = { 0 };\n  sei.cbSize = sizeof(SHELLEXECUTEINFO);\n  sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;\n  sei.lpVerb = L\"open\";\n  sei.lpFile = TABTIP_PATH;\n  sei.nShow = SW_SHOW;\n\n  if (ShellExecuteEx(&sei)) {\n    if (sei.hProcess) {\n      WaitForSingleObject(sei.hProcess, 1000);\n      CloseHandle(sei.hProcess);\n    }\n    Sleep(500);\n    \n    if (ForceShowKeyboardWindow()) {\n      Print(L\"✓ 触摸键盘已显示\");\n      return true;\n    }\n  }\n\n  // 最后备选：OSK\n  HINSTANCE result = ShellExecute(NULL, L\"open\", L\"osk.exe\", NULL, NULL, SW_SHOW);\n  if ((INT_PTR)result > 32) {\n    Print(L\"✓ 屏幕键盘已显示\");\n    return true;\n  }\n  \n  PrintError(L\"✗ 无法显示键盘\");\n  return false;\n}\n\n/**\n * 隐藏触摸键盘\n */\nbool HideKeyboard() {\n  // 查找键盘窗口\n  HWND hwnd = FindWindow(L\"IPTip_Main_Window\", NULL);\n  if (hwnd == NULL) {\n    // Windows 11 可能使用不同的类名\n    hwnd = FindWindow(L\"ApplicationFrameWindow\", L\"Microsoft Text Input Application\");\n  }\n\n  if (hwnd != NULL && IsWindowVisible(hwnd)) {\n    PostMessage(hwnd, WM_SYSCOMMAND, SC_CLOSE, 0);\n    Print(L\"✓ 触摸键盘已隐藏\");\n    return true;\n  }\n  \n  Print(L\"触摸键盘未运行或已隐藏\");\n  return false;\n}\n\n/**\n * 切换触摸键盘状态\n */\nbool ToggleKeyboard() {\n  if (IsKeyboardVisible()) {\n    return HideKeyboard();\n  } else {\n    return ShowKeyboard();\n  }\n}\n\n/**\n * 诊断系统环境\n */\nvoid Diagnose() {\n  wchar_t buffer[256];\n  \n  Print(L\"=== 系统诊断信息 ===\");\n  Print(L\"\");\n  \n  // 检查 Windows 版本\n  OSVERSIONINFOEX osvi = { 0 };\n  osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);\n  #if defined(_MSC_VER)\n  #pragma warning(push)\n  #pragma warning(disable: 4996)\n  #endif\n  GetVersionEx((LPOSVERSIONINFO)&osvi);\n  #if defined(_MSC_VER)\n  #pragma warning(pop)\n  #endif\n  wsprintfW(buffer, L\"Windows 版本: %d.%d\", osvi.dwMajorVersion, osvi.dwMinorVersion);\n  Print(buffer);\n  \n  // 检查 TabTip.exe\n  Print(L\"\");\n  wsprintfW(buffer, L\"TabTip 路径: %s\", TABTIP_PATH);\n  Print(buffer);\n  Print(CheckTabTipExists() ? L\"TabTip.exe: ✓ 存在\" : L\"TabTip.exe: ✗ 不存在\");\n  \n  // 检查注册表设置\n  Print(L\"\");\n  Print(L\"注册表设置:\");\n  Print(IsDesktopModeAutoInvokeEnabled() ? \n        L\"  EnableDesktopModeAutoInvoke: ✓ 已启用\" : \n        L\"  EnableDesktopModeAutoInvoke: ✗ 未启用\");\n  \n  // 检查键盘窗口\n  Print(L\"\");\n  Print(L\"检查键盘窗口:\");\n  HWND hwnd = FindWindow(L\"IPTip_Main_Window\", NULL);\n  if (hwnd) {\n    Print(L\"  IPTip_Main_Window: ✓ 找到 (Windows 10)\");\n    Print(IsWindowVisible(hwnd) ? L\"  可见性: 可见\" : L\"  可见性: 隐藏\");\n  } else {\n    Print(L\"  IPTip_Main_Window: ✗ 未找到\");\n  }\n  \n  hwnd = FindWindow(L\"ApplicationFrameWindow\", L\"Microsoft Text Input Application\");\n  if (hwnd) {\n    Print(L\"  ApplicationFrameWindow: ✓ 找到 (Windows 11)\");\n    Print(IsWindowVisible(hwnd) ? L\"  可见性: 可见\" : L\"  可见性: 隐藏\");\n  } else {\n    Print(L\"  ApplicationFrameWindow: ✗ 未找到\");\n  }\n  \n  Print(L\"\");\n  Print(IsKeyboardVisible() ? L\"当前键盘状态: 可见\" : L\"当前键盘状态: 隐藏\");\n}\n\n/**\n * 显示屏幕键盘 (OSK)\n */\nbool ShowOSK() {\n  HINSTANCE result = ShellExecute(NULL, L\"open\", L\"osk.exe\", NULL, NULL, SW_SHOW);\n  if ((INT_PTR)result > 32) {\n    Print(L\"✓ 屏幕键盘已显示\");\n    return true;\n  }\n  PrintError(L\"✗ 无法显示屏幕键盘\");\n  return false;\n}\n\n/**\n * 显示使用帮助\n */\nvoid ShowHelp() {\n  Print(L\"Windows 虚拟触摸键盘工具\");\n  Print(L\"\");\n  Print(L\"用法:\");\n  Print(L\"  qiin-tabtip [选项]\");\n  Print(L\"\");\n  Print(L\"选项:\");\n  Print(L\"  show      - 显示触摸键盘 (TabTip)\");\n  Print(L\"  hide      - 隐藏触摸键盘\");\n  Print(L\"  toggle    - 切换键盘显示状态 (默认)\");\n  Print(L\"  osk       - 显示屏幕键盘 (OSK)\");\n  Print(L\"  status    - 检查键盘是否可见\");\n  Print(L\"  diagnose  - 诊断系统环境\");\n  Print(L\"  help      - 显示此帮助信息\");\n  Print(L\"\");\n  Print(L\"示例:\");\n  Print(L\"  qiin-tabtip              # 切换键盘状态\");\n  Print(L\"  qiin-tabtip show         # 显示触摸键盘\");\n  Print(L\"  qiin-tabtip osk          # 显示屏幕键盘\");\n  Print(L\"  qiin-tabtip diagnose     # 诊断问题\");\n}\n\nint wmain(int argc, wchar_t* argv[]) {\n  // 设置控制台 UTF-8 输出\n  SetConsoleOutputCP(CP_UTF8);\n\n  const wchar_t* command = L\"toggle\";\n  wchar_t cmdLower[256] = {0};\n  \n  if (argc > 1) {\n    command = argv[1];\n    // 转换为小写用于比较\n    lstrcpynW(cmdLower, command, 255);\n    CharLowerW(cmdLower);\n    command = cmdLower;\n  }\n\n  if (StrEqualI(command, L\"show\")) {\n    return ShowKeyboard() ? 0 : 1;\n  }\n  else if (StrEqualI(command, L\"hide\")) {\n    return HideKeyboard() ? 0 : 1;\n  }\n  else if (StrEqualI(command, L\"toggle\")) {\n    return ToggleKeyboard() ? 0 : 1;\n  }\n  else if (StrEqualI(command, L\"osk\")) {\n    return ShowOSK() ? 0 : 1;\n  }\n  else if (StrEqualI(command, L\"status\")) {\n    if (IsKeyboardVisible()) {\n      Print(L\"触摸键盘当前: 可见\");\n      return 0;\n    } else {\n      Print(L\"触摸键盘当前: 隐藏\");\n      return 1;\n    }\n  }\n  else if (StrEqualI(command, L\"diagnose\") || StrEqualI(command, L\"diag\")) {\n    Diagnose();\n    return 0;\n  }\n  else if (StrEqualI(command, L\"help\") || StrEqualI(command, L\"--help\") || \n           StrEqualI(command, L\"-h\") || StrEqualI(command, L\"/?\")) {\n    ShowHelp();\n    return 0;\n  }\n  else {\n    wchar_t errMsg[512];\n    wsprintfW(errMsg, L\"未知命令: %s\", command);\n    PrintError(errMsg);\n    PrintError(L\"使用 'qiin-tabtip help' 查看帮助\");\n    return 1;\n  }\n\n  return 0;\n}\n\n// 如果没有 wmain 支持，使用普通的 main 函数\n#ifndef _UNICODE\nint main(int argc, char* argv[]) {\n  // 获取命令行参数的宽字符版本\n  LPWSTR* szArglist;\n  int nArgs;\n  \n  szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs);\n  if (szArglist == NULL) {\n    PrintError(L\"CommandLineToArgvW failed\");\n    return 1;\n  }\n  \n  int result = wmain(nArgs, szArglist);\n  \n  LocalFree(szArglist);\n  return result;\n}\n#endif\n\n"
  },
  {
    "path": "tools/sunshinesvc.cpp",
    "content": "/**\n * @file tools/sunshinesvc.cpp\n * @brief Handles launching Sunshine.exe into user sessions as SYSTEM\n */\n#define WIN32_LEAN_AND_MEAN\n#include <Windows.h>\n#include <wtsapi32.h>\n\n#include <string>\n\n// PROC_THREAD_ATTRIBUTE_JOB_LIST is currently missing from MinGW headers\n#ifndef PROC_THREAD_ATTRIBUTE_JOB_LIST\n  #define PROC_THREAD_ATTRIBUTE_JOB_LIST ProcThreadAttributeValue(13, FALSE, TRUE, FALSE)\n#endif\n\nSERVICE_STATUS_HANDLE service_status_handle;\nSERVICE_STATUS service_status;\nHANDLE stop_event;\nHANDLE session_change_event;\n\n#define SERVICE_NAME \"SunshineService\"\n\nDWORD WINAPI\nHandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) {\n  switch (dwControl) {\n    case SERVICE_CONTROL_INTERROGATE:\n      return NO_ERROR;\n\n    case SERVICE_CONTROL_SESSIONCHANGE:\n      // If a new session connects to the console, restart Sunshine\n      // to allow it to spawn inside the new console session.\n      if (dwEventType == WTS_CONSOLE_CONNECT) {\n        SetEvent(session_change_event);\n      }\n      return NO_ERROR;\n\n    case SERVICE_CONTROL_PRESHUTDOWN:\n      // The system is shutting down\n    case SERVICE_CONTROL_STOP:\n      // Let SCM know we're stopping in up to 30 seconds\n      service_status.dwCurrentState = SERVICE_STOP_PENDING;\n      service_status.dwControlsAccepted = 0;\n      service_status.dwWaitHint = 30 * 1000;\n      SetServiceStatus(service_status_handle, &service_status);\n\n      // Trigger ServiceMain() to start cleanup\n      SetEvent(stop_event);\n      return NO_ERROR;\n\n    default:\n      return ERROR_CALL_NOT_IMPLEMENTED;\n  }\n}\n\nHANDLE\nCreateJobObjectForChildProcess() {\n  HANDLE job_handle = CreateJobObjectW(NULL, NULL);\n  if (!job_handle) {\n    return NULL;\n  }\n\n  JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_limit_info = {};\n\n  // Kill Sunshine.exe when the final job object handle is closed (which will happen if we terminate unexpectedly).\n  // This ensures we don't leave an orphaned Sunshine.exe running with an inherited handle to our log file.\n  job_limit_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;\n\n  // Allow Sunshine.exe to use CREATE_BREAKAWAY_FROM_JOB when spawning processes to ensure they can to live beyond\n  // the lifetime of SunshineSvc.exe. This avoids unexpected user data loss if we crash or are killed.\n  job_limit_info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_BREAKAWAY_OK;\n\n  if (!SetInformationJobObject(job_handle, JobObjectExtendedLimitInformation, &job_limit_info, sizeof(job_limit_info))) {\n    CloseHandle(job_handle);\n    return NULL;\n  }\n\n  return job_handle;\n}\n\nLPPROC_THREAD_ATTRIBUTE_LIST\nAllocateProcThreadAttributeList(DWORD attribute_count) {\n  SIZE_T size;\n  InitializeProcThreadAttributeList(NULL, attribute_count, 0, &size);\n\n  auto list = (LPPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc(GetProcessHeap(), 0, size);\n  if (list == NULL) {\n    return NULL;\n  }\n\n  if (!InitializeProcThreadAttributeList(list, attribute_count, 0, &size)) {\n    HeapFree(GetProcessHeap(), 0, list);\n    return NULL;\n  }\n\n  return list;\n}\n\nHANDLE\nDuplicateTokenForSession(DWORD console_session_id) {\n  HANDLE current_token;\n  if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, &current_token)) {\n    return NULL;\n  }\n\n  // Duplicate our own LocalSystem token\n  HANDLE new_token;\n  if (!DuplicateTokenEx(current_token, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &new_token)) {\n    CloseHandle(current_token);\n    return NULL;\n  }\n\n  CloseHandle(current_token);\n\n  // Change the duplicated token to the console session ID\n  if (!SetTokenInformation(new_token, TokenSessionId, &console_session_id, sizeof(console_session_id))) {\n    CloseHandle(new_token);\n    return NULL;\n  }\n\n  return new_token;\n}\n\nHANDLE\nOpenLogFileHandle() {\n  WCHAR log_file_name[MAX_PATH];\n\n  // Create sunshine.log in the Temp folder (usually %SYSTEMROOT%\\Temp)\n  GetTempPathW(_countof(log_file_name), log_file_name);\n  wcscat_s(log_file_name, L\"sunshine.log\");\n\n  // The file handle must be inheritable for our child process to use it\n  SECURITY_ATTRIBUTES security_attributes = { sizeof(security_attributes), NULL, TRUE };\n\n  // Overwrite the old sunshine.log\n  return CreateFileW(log_file_name,\n    GENERIC_WRITE,\n    FILE_SHARE_READ,\n    &security_attributes,\n    CREATE_ALWAYS,\n    0,\n    NULL);\n}\n\nbool\nRunTerminationHelper(HANDLE console_token, DWORD pid) {\n  WCHAR module_path[MAX_PATH];\n  GetModuleFileNameW(NULL, module_path, _countof(module_path));\n  std::wstring command;\n\n  command += L'\"';\n  command += module_path;\n  command += L'\"';\n  command += L\" --terminate \" + std::to_wstring(pid);\n\n  STARTUPINFOW startup_info = {};\n  startup_info.cb = sizeof(startup_info);\n  startup_info.lpDesktop = (LPWSTR) L\"winsta0\\\\default\";\n\n  // Execute ourselves as a detached process in the user session with the --terminate argument.\n  // This will allow us to attach to Sunshine's console and send it a Ctrl-C event.\n  PROCESS_INFORMATION process_info;\n  if (!CreateProcessAsUserW(console_token,\n        module_path,\n        (LPWSTR) command.c_str(),\n        NULL,\n        NULL,\n        FALSE,\n        CREATE_UNICODE_ENVIRONMENT | DETACHED_PROCESS,\n        NULL,\n        NULL,\n        &startup_info,\n        &process_info)) {\n    return false;\n  }\n\n  // Wait for the termination helper to complete\n  WaitForSingleObject(process_info.hProcess, INFINITE);\n\n  // Check the exit status of the helper process\n  DWORD exit_code;\n  GetExitCodeProcess(process_info.hProcess, &exit_code);\n\n  // Cleanup handles\n  CloseHandle(process_info.hProcess);\n  CloseHandle(process_info.hThread);\n\n  // If the helper process returned 0, it succeeded\n  return exit_code == 0;\n}\n\nVOID WINAPI\nServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) {\n  service_status_handle = RegisterServiceCtrlHandlerEx(SERVICE_NAME, HandlerEx, NULL);\n  if (service_status_handle == NULL) {\n    // Nothing we can really do here but terminate ourselves\n    ExitProcess(GetLastError());\n    return;\n  }\n\n  // Tell SCM we're starting\n  service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;\n  service_status.dwServiceSpecificExitCode = 0;\n  service_status.dwWin32ExitCode = NO_ERROR;\n  service_status.dwWaitHint = 0;\n  service_status.dwControlsAccepted = 0;\n  service_status.dwCheckPoint = 0;\n  service_status.dwCurrentState = SERVICE_START_PENDING;\n  SetServiceStatus(service_status_handle, &service_status);\n\n  // Create a manual-reset stop event\n  stop_event = CreateEventA(NULL, TRUE, FALSE, NULL);\n  if (stop_event == NULL) {\n    // Tell SCM we failed to start\n    service_status.dwWin32ExitCode = GetLastError();\n    service_status.dwCurrentState = SERVICE_STOPPED;\n    SetServiceStatus(service_status_handle, &service_status);\n    return;\n  }\n\n  // Create an auto-reset session change event\n  session_change_event = CreateEventA(NULL, FALSE, FALSE, NULL);\n  if (session_change_event == NULL) {\n    // Tell SCM we failed to start\n    service_status.dwWin32ExitCode = GetLastError();\n    service_status.dwCurrentState = SERVICE_STOPPED;\n    SetServiceStatus(service_status_handle, &service_status);\n    return;\n  }\n\n  auto log_file_handle = OpenLogFileHandle();\n  if (log_file_handle == INVALID_HANDLE_VALUE) {\n    // Tell SCM we failed to start\n    service_status.dwWin32ExitCode = GetLastError();\n    service_status.dwCurrentState = SERVICE_STOPPED;\n    SetServiceStatus(service_status_handle, &service_status);\n    return;\n  }\n\n  // We can use a single STARTUPINFOEXW for all the processes that we launch\n  STARTUPINFOEXW startup_info = {};\n  startup_info.StartupInfo.cb = sizeof(startup_info);\n  startup_info.StartupInfo.lpDesktop = (LPWSTR) L\"winsta0\\\\default\";\n  startup_info.StartupInfo.dwFlags = STARTF_USESTDHANDLES;\n  startup_info.StartupInfo.hStdInput = NULL;\n  startup_info.StartupInfo.hStdOutput = log_file_handle;\n  startup_info.StartupInfo.hStdError = log_file_handle;\n\n  // Allocate an attribute list with space for 2 entries\n  startup_info.lpAttributeList = AllocateProcThreadAttributeList(2);\n  if (startup_info.lpAttributeList == NULL) {\n    // Tell SCM we failed to start\n    service_status.dwWin32ExitCode = GetLastError();\n    service_status.dwCurrentState = SERVICE_STOPPED;\n    SetServiceStatus(service_status_handle, &service_status);\n    return;\n  }\n\n  // Only allow Sunshine.exe to inherit the log file handle, not all inheritable handles\n  UpdateProcThreadAttribute(startup_info.lpAttributeList,\n    0,\n    PROC_THREAD_ATTRIBUTE_HANDLE_LIST,\n    &log_file_handle,\n    sizeof(log_file_handle),\n    NULL,\n    NULL);\n\n  // Tell SCM we're running (and stoppable now)\n  service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN | SERVICE_ACCEPT_SESSIONCHANGE;\n  service_status.dwCurrentState = SERVICE_RUNNING;\n  SetServiceStatus(service_status_handle, &service_status);\n\n  // Loop every 3 seconds until the stop event is set or Sunshine.exe is running\n  while (WaitForSingleObject(stop_event, 3000) != WAIT_OBJECT_0) {\n    auto console_session_id = WTSGetActiveConsoleSessionId();\n    if (console_session_id == 0xFFFFFFFF) {\n      // No console session yet\n      continue;\n    }\n\n    auto console_token = DuplicateTokenForSession(console_session_id);\n    if (console_token == NULL) {\n      continue;\n    }\n\n    // Job objects cannot span sessions, so we must create one for each process\n    auto job_handle = CreateJobObjectForChildProcess();\n    if (job_handle == NULL) {\n      CloseHandle(console_token);\n      continue;\n    }\n\n    // Start Sunshine.exe inside our job object\n    UpdateProcThreadAttribute(startup_info.lpAttributeList,\n      0,\n      PROC_THREAD_ATTRIBUTE_JOB_LIST,\n      &job_handle,\n      sizeof(job_handle),\n      NULL,\n      NULL);\n\n    PROCESS_INFORMATION process_info;\n    if (!CreateProcessAsUserW(console_token,\n          L\"Sunshine.exe\",\n          NULL,\n          NULL,\n          NULL,\n          TRUE,\n          CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT,\n          NULL,\n          NULL,\n          (LPSTARTUPINFOW) &startup_info,\n          &process_info)) {\n      CloseHandle(console_token);\n      CloseHandle(job_handle);\n      continue;\n    }\n\n    bool still_running;\n    do {\n      // Wait for the stop event to be set, Sunshine.exe to terminate, or the console session to change\n      const HANDLE wait_objects[] = { stop_event, process_info.hProcess, session_change_event };\n      switch (WaitForMultipleObjects(_countof(wait_objects), wait_objects, FALSE, INFINITE)) {\n        case WAIT_OBJECT_0 + 2:\n          if (WTSGetActiveConsoleSessionId() == console_session_id) {\n            // The active console session didn't actually change. Let Sunshine keep running.\n            still_running = true;\n            continue;\n          }\n          // Fall-through to terminate Sunshine.exe and start it again.\n        case WAIT_OBJECT_0:\n          // The service is shutting down, so try to gracefully terminate Sunshine.exe.\n          // If it doesn't terminate in 20 seconds, we will forcefully terminate it.\n          if (!RunTerminationHelper(console_token, process_info.dwProcessId) ||\n              WaitForSingleObject(process_info.hProcess, 20000) != WAIT_OBJECT_0) {\n            // If it won't terminate gracefully, kill it now\n            TerminateProcess(process_info.hProcess, ERROR_PROCESS_ABORTED);\n          }\n          still_running = false;\n          break;\n\n        case WAIT_OBJECT_0 + 1: {\n          // Sunshine terminated itself.\n\n          DWORD exit_code;\n          if (GetExitCodeProcess(process_info.hProcess, &exit_code) && exit_code == ERROR_SHUTDOWN_IN_PROGRESS) {\n            // Sunshine is asking for us to shut down, so gracefully stop ourselves.\n            SetEvent(stop_event);\n          }\n          still_running = false;\n          break;\n        }\n      }\n    } while (still_running);\n\n    CloseHandle(process_info.hThread);\n    CloseHandle(process_info.hProcess);\n    CloseHandle(console_token);\n    CloseHandle(job_handle);\n  }\n\n  // Let SCM know we've stopped\n  service_status.dwCurrentState = SERVICE_STOPPED;\n  SetServiceStatus(service_status_handle, &service_status);\n}\n\n// This will run in a child process in the user session\nint\nDoGracefulTermination(DWORD pid) {\n  // Attach to Sunshine's console\n  if (!AttachConsole(pid)) {\n    return GetLastError();\n  }\n\n  // Disable our own Ctrl-C handling\n  SetConsoleCtrlHandler(NULL, TRUE);\n\n  // Send a Ctrl-C event to Sunshine\n  if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)) {\n    return GetLastError();\n  }\n\n  return 0;\n}\n\nint\nmain(int argc, char *argv[]) {\n  static const SERVICE_TABLE_ENTRY service_table[] = {\n    { (LPSTR) SERVICE_NAME, ServiceMain },\n    { NULL, NULL }\n  };\n\n  // Check if this is a reinvocation of ourselves to send Ctrl-C to Sunshine.exe\n  if (argc == 3 && strcmp(argv[1], \"--terminate\") == 0) {\n    return DoGracefulTermination(atol(argv[2]));\n  }\n\n  // By default, services have their current directory set to %SYSTEMROOT%\\System32.\n  // We want to use the directory where Sunshine.exe is located instead of system32.\n  // This requires stripping off 2 path components: the file name and the last folder\n  WCHAR module_path[MAX_PATH];\n  GetModuleFileNameW(NULL, module_path, _countof(module_path));\n  for (auto i = 0; i < 2; i++) {\n    auto last_sep = wcsrchr(module_path, '\\\\');\n    if (last_sep) {\n      *last_sep = 0;\n    }\n  }\n  SetCurrentDirectoryW(module_path);\n\n  // Trigger our ServiceMain()\n  return StartServiceCtrlDispatcher(service_table);\n}\n"
  },
  {
    "path": "tools/test_args.cpp",
    "content": "/**\n * @file tools/test_args.cpp\n * @brief 测试程序：输出所有命令行参数到日志文件\n * @note 此程序仅用于开发测试，不会被打包到发布版本中\n */\n\n#include <iostream>\n#include <fstream>\n#include <string>\n#include <ctime>\n#include <iomanip>\n#include <sstream>\n#include <vector>\n\n#ifdef _WIN32\n#include <windows.h>\n#include <io.h>\n#include <sddl.h>\n#include <userenv.h>\n#include <lmcons.h>  // For UNLEN, MAX_COMPUTERNAME_LENGTH\n#include <cstring>\n#ifndef _MSC_VER\n// MinGW doesn't support #pragma comment, link libraries in CMakeLists.txt instead\n#else\n#pragma comment(lib, \"advapi32.lib\")\n#pragma comment(lib, \"userenv.lib\")\n#endif\n#else\n#include <unistd.h>\n#include <pwd.h>\n#include <limits.h>\n#include <cstring>\n#endif\n\nstd::string get_current_time() {\n    auto now = std::time(nullptr);\n    auto tm = *std::localtime(&now);\n    std::ostringstream oss;\n    oss << std::put_time(&tm, \"%Y-%m-%d %H:%M:%S\");\n    return oss.str();\n}\n\n#ifdef _WIN32\nvoid print_user_info(std::ofstream& log) {\n    log << \"----------------------------------------\\n\";\n    log << \"User Information (用户信息):\\n\";\n    \n    // 获取用户名\n    char username[UNLEN + 1];\n    DWORD username_len = UNLEN + 1;\n    if (GetUserNameA(username, &username_len)) {\n        log << \"  Username (用户名): \" << username << \"\\n\";\n    } else {\n        log << \"  Username (用户名): <Failed to get (获取失败)>\\n\";\n    }\n    \n    // 获取计算机名\n    char computer_name[MAX_COMPUTERNAME_LENGTH + 1];\n    DWORD computer_name_len = MAX_COMPUTERNAME_LENGTH + 1;\n    if (GetComputerNameA(computer_name, &computer_name_len)) {\n        log << \"  Computer (计算机名): \" << computer_name << \"\\n\";\n    }\n    \n    // 检查是否是管理员\n    BOOL is_admin = FALSE;\n    PSID admin_group = NULL;\n    SID_IDENTIFIER_AUTHORITY nt_authority = SECURITY_NT_AUTHORITY;\n    if (AllocateAndInitializeSid(&nt_authority, 2, SECURITY_BUILTIN_DOMAIN_RID,\n                                  DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &admin_group)) {\n        CheckTokenMembership(NULL, admin_group, &is_admin);\n        FreeSid(admin_group);\n    }\n    log << \"  Is Admin (是否管理员): \" << (is_admin ? \"Yes (是)\" : \"No (否)\") << \"\\n\";\n    \n    // 获取当前进程的令牌信息\n    HANDLE token = NULL;\n    if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {\n        // 获取用户 SID\n        DWORD token_user_size = 0;\n        GetTokenInformation(token, TokenUser, NULL, 0, &token_user_size);\n        if (token_user_size > 0) {\n            std::vector<BYTE> token_user_buf(token_user_size);\n            PTOKEN_USER token_user = reinterpret_cast<PTOKEN_USER>(token_user_buf.data());\n            if (GetTokenInformation(token, TokenUser, token_user, token_user_size, &token_user_size)) {\n                LPSTR sid_string = NULL;\n                if (ConvertSidToStringSidA(token_user->User.Sid, &sid_string)) {\n                    log << \"  User SID (用户 SID): \" << sid_string << \"\\n\";\n                    LocalFree(sid_string);\n                }\n            }\n        }\n        \n        // 获取权限级别\n        DWORD elevation_type_size = sizeof(TOKEN_ELEVATION_TYPE);\n        TOKEN_ELEVATION_TYPE elevation_type;\n        if (GetTokenInformation(token, TokenElevationType, &elevation_type,\n                                elevation_type_size, &elevation_type_size)) {\n            const char* elevation_str = \"Unknown (未知)\";\n            switch (elevation_type) {\n                case TokenElevationTypeDefault:\n                    elevation_str = \"Default (默认)\";\n                    break;\n                case TokenElevationTypeFull:\n                    elevation_str = \"Full (Elevated) (完全/已提升)\";\n                    break;\n                case TokenElevationTypeLimited:\n                    elevation_str = \"Limited (受限)\";\n                    break;\n            }\n            log << \"  Elevation Type (权限提升类型): \" << elevation_str << \"\\n\";\n        }\n        \n        // 检查是否以管理员身份运行\n        BOOL is_elevated = FALSE;\n        DWORD is_elevated_size = sizeof(BOOL);\n        if (GetTokenInformation(token, TokenElevation, &is_elevated,\n                               is_elevated_size, &is_elevated_size)) {\n            log << \"  Is Elevated (是否已提升权限): \" << (is_elevated ? \"Yes (是)\" : \"No (否)\") << \"\\n\";\n        }\n        \n        CloseHandle(token);\n    }\n    \n    // 获取进程 ID\n    log << \"  Process ID (进程 ID): \" << GetCurrentProcessId() << \"\\n\";\n    log << \"  Thread ID (线程 ID): \" << GetCurrentThreadId() << \"\\n\";\n    \n    // 获取会话 ID\n    DWORD session_id = 0;\n    if (ProcessIdToSessionId(GetCurrentProcessId(), &session_id)) {\n        log << \"  Session ID (会话 ID): \" << session_id << \"\\n\";\n    }\n}\n#else\nvoid print_user_info(std::ofstream& log) {\n    log << \"----------------------------------------\\n\";\n    log << \"User Information (用户信息):\\n\";\n    \n    // 获取用户 ID 和组 ID\n    uid_t uid = getuid();\n    gid_t gid = getgid();\n    log << \"  UID (用户 ID): \" << uid << \"\\n\";\n    log << \"  GID (组 ID): \" << gid << \"\\n\";\n    \n    // 获取用户名\n    struct passwd* pw = getpwuid(uid);\n    if (pw) {\n        log << \"  Username (用户名): \" << pw->pw_name << \"\\n\";\n        log << \"  Home (主目录): \" << pw->pw_dir << \"\\n\";\n    }\n    \n    // 检查是否是 root\n    log << \"  Is Root (是否 Root): \" << (uid == 0 ? \"Yes (是)\" : \"No (否)\") << \"\\n\";\n    \n    // 获取进程 ID\n    log << \"  Process ID (进程 ID): \" << getpid() << \"\\n\";\n}\n#endif\n\nint main(int argc, char* argv[]) {\n    // 获取可执行文件所在目录\n    std::string log_file;\n#ifdef _WIN32\n    char exe_path[MAX_PATH];\n    DWORD path_len = GetModuleFileNameA(NULL, exe_path, MAX_PATH);\n    if (path_len > 0 && path_len < MAX_PATH) {\n        // 找到最后一个反斜杠\n        char* last_slash = strrchr(exe_path, '\\\\');\n        if (last_slash) {\n            *last_slash = '\\0';  // 截断到目录\n            log_file = std::string(exe_path) + \"\\\\sunshine_test_args.log\";\n        } else {\n            log_file = \"sunshine_test_args.log\";  // 回退到当前目录\n        }\n    } else {\n        log_file = \"sunshine_test_args.log\";  // 回退到当前目录\n    }\n#else\n    char exe_path[PATH_MAX];\n    ssize_t path_len = readlink(\"/proc/self/exe\", exe_path, PATH_MAX - 1);\n    if (path_len > 0) {\n        exe_path[path_len] = '\\0';\n        char* last_slash = strrchr(exe_path, '/');\n        if (last_slash) {\n            *last_slash = '\\0';  // 截断到目录\n            log_file = std::string(exe_path) + \"/sunshine_test_args.log\";\n        } else {\n            log_file = \"sunshine_test_args.log\";  // 回退到当前目录\n        }\n    } else {\n        log_file = \"sunshine_test_args.log\";  // 回退到当前目录\n    }\n#endif\n\n    std::ofstream log(log_file, std::ios::app);\n    if (!log.is_open()) {\n        std::cerr << \"Failed to open log file: \" << log_file << std::endl;\n        return 1;\n    }\n\n    // 写入分隔符和时间戳\n    log << \"\\n\";\n    log << \"========================================\\n\";\n    log << \"Test Time (测试时间): \" << get_current_time() << \"\\n\";\n    log << \"========================================\\n\";\n    log << \"Total Arguments (参数总数): \" << argc << \"\\n\";\n    log << \"Executable (可执行文件): \" << (argc > 0 ? argv[0] : \"unknown\") << \"\\n\";\n    \n    // 打印用户权限信息\n    print_user_info(log);\n    \n    log << \"----------------------------------------\\n\";\n\n    // 输出所有参数\n    for (int i = 0; i < argc; i++) {\n        log << \"Arg[\" << i << \"] (参数[\" << i << \"]): \\\"\" << argv[i] << \"\\\"\\n\";\n    }\n\n    log << \"----------------------------------------\\n\";\n    log << \"Argument Analysis (参数分析):\\n\";\n\n    // 检查是否有环境变量相关的参数\n    bool found_env_vars = false;\n    for (int i = 1; i < argc; i++) {\n        std::string arg = argv[i];\n        if (arg.find(\"%SUNSHINE_\") != std::string::npos) {\n            log << \"  WARNING (警告): Found unexpanded environment variable in arg[\" << i << \"] (在参数[\" << i << \"] 中发现未展开的环境变量): \" << arg << \"\\n\";\n            found_env_vars = true;\n        }\n    }\n\n    if (!found_env_vars) {\n        log << \"  ✓ All environment variables appear to be expanded (所有环境变量已正确展开)\\n\";\n    }\n\n    log << \"========================================\\n\";\n    log << \"\\n\";\n\n    log.close();\n\n    // 同时输出到控制台（如果可用）\n    std::cout << \"Arguments logged to (参数已记录到): \" << log_file << std::endl;\n    std::cout << \"Total arguments (参数总数): \" << argc << std::endl;\n    for (int i = 0; i < argc; i++) {\n        std::cout << \"  [\" << i << \"] \" << argv[i] << std::endl;\n    }\n\n    return 0;\n}\n\n"
  },
  {
    "path": "translate_simple.py",
    "content": "import os\nimport re\nimport requests\n\n# 项目名和术语保护列表\nPROTECTED_TERMS = [\n    'Sunshine', 'README', 'GitHub', 'CI', 'API', 'Markdown', 'OpenAI', 'DeepL', 'Google Translate',\n    # 可在此添加更多术语\n]\n\ndef mask_terms(text):\n    for term in PROTECTED_TERMS:\n        text = re.sub(rf'(?<![`\\w]){re.escape(term)}(?![`\\w])', f'@@@{term}@@@', text)\n    return text\n\ndef unmask_terms(text):\n    for term in PROTECTED_TERMS:\n        text = text.replace(f'@@@{term}@@@', term)\n    return text\n\ndef translate_with_deepseek(text, target_lang):\n    # 使用 DeepSeek API 进行翻译\n    api_key = os.getenv('DEEPSEEK_API_KEY')\n    if not api_key:\n        raise Exception('DEEPSEEK_API_KEY 环境变量未设置')\n    url = 'https://api.deepseek.com/v1/chat/completions'\n    prompt = f\"请将以下 Markdown 内容翻译为{target_lang}，但不要翻译项目名和术语：{', '.join(PROTECTED_TERMS)}。保持原有格式、链接和图片。\\n\\n{text}\"\n    headers = {\n        'Authorization': f'Bearer {api_key}',\n        'Content-Type': 'application/json'\n    }\n    payload = {\n        \"model\": \"deepseek-chat\",\n        \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n        \"temperature\": 0.2\n    }\n    resp = requests.post(url, headers=headers, json=payload)\n    resp.raise_for_status()\n    result = resp.json()\n    return result['choices'][0]['message']['content']\n\ndef translate_readme():\n    with open('README.md', 'r', encoding='utf-8') as f:\n        content = f.read()\n\n    languages = [\n        ('en', 'English'),\n        ('fr', 'French'),\n        ('de', 'German'),\n        ('ja', 'Japanese')\n    ]\n\n    for lang_code, lang_name in languages:\n        try:\n            if lang_code == 'zh_CN':\n                translated_content = content\n            else:\n                masked = mask_terms(content)\n                translated = translate_with_deepseek(masked, lang_name)\n                translated = unmask_terms(translated)\n                # 去除 DeepSeek 返回的多余提示，只保留第一个 Markdown 标题及后面内容\n                lines = translated.splitlines()\n                for idx, line in enumerate(lines):\n                    if line.strip().startswith('#'):\n                      translated_content = '\\n'.join(lines[idx:])\n                      break\n                else:\n                  translated_content = translated.strip()\n\n            filename = f'README.{lang_code}.md'\n            with open(filename, 'w', encoding='utf-8') as f:\n                f.write(translated_content)\n            print(f\"✓ Translated to {lang_name} ({lang_code})\")\n        except Exception as e:\n            print(f\"✗ Failed to translate to {lang_name}: {e}\")\n\nif __name__ == \"__main__\":\n    translate_readme()\n"
  },
  {
    "path": "vite-plugin-ejs-v7.js",
    "content": "import ejs from 'ejs'\n\n/**\n * Vite EJS Plugin for Vite 7\n * Compatible replacement for vite-plugin-ejs that works with Vite 7's new transformIndexHtml API\n * @param {Record<string, any> | ((config: any) => Record<string, any>)} data - Data to pass to EJS template\n * @param {object} options - Optional EJS options\n * @returns {import('vite').Plugin}\n */\nexport function ViteEjsPlugin(data = {}, options = {}) {\n  let config\n  \n  return {\n    name: 'vite-plugin-ejs-v7',\n    // Get Resolved config\n    configResolved(resolvedConfig) {\n      config = resolvedConfig\n    },\n    transformIndexHtml: {\n      // Use 'pre' order to ensure EJS is processed before other HTML transformations\n      order: 'pre',\n      // Vite 7 uses 'handler' instead of 'transform'\n      handler(html) {\n        // Resolve data if it's a function\n        const resolvedData = typeof data === 'function' ? data(config) : data\n        \n        // Resolve EJS options if it's a function\n        let ejsOptions = options && options.ejs ? options.ejs : {}\n        if (typeof ejsOptions === 'function') {\n          ejsOptions = ejsOptions(config)\n        }\n        \n        // Render EJS template with data\n        const rendered = ejs.render(\n          html,\n          Object.assign(\n            {\n              NODE_ENV: config.mode,\n              isDev: config.mode === 'development',\n            },\n            resolvedData\n          ),\n          Object.assign(\n            {\n              // Setting views enables includes support\n              views: [config.root],\n            },\n            ejsOptions,\n            {\n              async: false, // Force sync\n            }\n          )\n        )\n        \n        return rendered\n      },\n    },\n  }\n}\n"
  },
  {
    "path": "vite.config.js",
    "content": "import fs from 'fs'\nimport { resolve } from 'path'\nimport { defineConfig } from 'vite'\nimport { ViteEjsPlugin } from './vite-plugin-ejs-v7.js'\nimport vue from '@vitejs/plugin-vue'\nimport process from 'process'\n\n/**\n * Before actually building the pages with Vite, we do an intermediate build step using ejs\n * Importing this separately and joining them using ejs\n * allows us to split some repeating HTML that cannot be added\n * by Vue itself (e.g. style/script loading, common meta head tags, Widgetbot)\n * The vite-plugin-ejs handles this automatically\n */\nlet assetsSrcPath = 'src_assets/common/assets/web'\nlet assetsDstPath = 'build/assets/web'\n\nif (process.env.SUNSHINE_BUILD_HOMEBREW) {\n  console.log('Building for homebrew, using default paths')\n} else {\n  if (process.env.SUNSHINE_SOURCE_ASSETS_DIR) {\n    console.log('Using srcdir from Cmake: ' + resolve(process.env.SUNSHINE_SOURCE_ASSETS_DIR, 'common/assets/web'))\n    assetsSrcPath = resolve(process.env.SUNSHINE_SOURCE_ASSETS_DIR, 'common/assets/web')\n  }\n  if (process.env.SUNSHINE_ASSETS_DIR) {\n    console.log('Using destdir from Cmake: ' + resolve(process.env.SUNSHINE_ASSETS_DIR, 'assets/web'))\n    assetsDstPath = resolve(process.env.SUNSHINE_ASSETS_DIR, 'assets/web')\n  }\n}\n\nconst header = fs.readFileSync(resolve(assetsSrcPath, 'template_header.html'))\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  resolve: {\n    alias: {\n      vue: 'vue/dist/vue.esm-bundler.js',\n    },\n  },\n  plugins: [vue(), ViteEjsPlugin({ header })],\n  root: resolve(assetsSrcPath),\n  preview: {\n    port: 3000,\n    host: '0.0.0.0',\n    open: false,\n  },\n  build: {\n    outDir: resolve(assetsDstPath),\n    emptyOutDir: true,\n    chunkSizeWarningLimit: 1000,\n    // Vite 8 使用 rolldown 内核：多入口 HTML 必须放在 rolldownOptions.input\n    // 放到 rollupOptions.input 会被忽略，导致除 index.html 外其它页面 404\n    rolldownOptions: {\n      input: {\n        apps: resolve(assetsSrcPath, 'apps.html'),\n        config: resolve(assetsSrcPath, 'config.html'),\n        index: resolve(assetsSrcPath, 'index.html'),\n        password: resolve(assetsSrcPath, 'password.html'),\n        pin: resolve(assetsSrcPath, 'pin.html'),\n        troubleshooting: resolve(assetsSrcPath, 'troubleshooting.html'),\n        welcome: resolve(assetsSrcPath, 'welcome.html'),\n      },\n      output: {\n        // 优化chunk命名\n        chunkFileNames: 'assets/[name]-[hash].js',\n        // 优化入口文件命名（只影响 JS 文件）\n        entryFileNames: 'assets/[name]-[hash].js',\n        // 优化资源文件命名\n        assetFileNames: (assetInfo) => {\n          const name = assetInfo.name || ''\n          const ext = name.split('.').pop()\n          \n          if (/\\.(css)$/.test(name)) {\n            return 'assets/[name]-[hash].[ext]'\n          }\n          if (/\\.(woff2?|eot|ttf|otf)$/.test(name)) {\n            return 'assets/fonts/[name]-[hash].[ext]'\n          }\n          if (/\\.(png|jpe?g|gif|svg|webp|avif)$/.test(name)) {\n            return 'assets/images/[name]-[hash].[ext]'\n          }\n          return 'assets/[name]-[hash].[ext]'\n        },\n      },\n    },\n    // 启用CSS代码分割\n    cssCodeSplit: true,\n    // 启用源码映射（生产环境可选）\n    sourcemap: false,\n    // 优化依赖预构建\n    commonjsOptions: {\n      include: [/node_modules/],\n    },\n  },\n  // 优化依赖预构建\n  optimizeDeps: {\n    include: ['vue', 'vue-i18n', 'bootstrap', '@popperjs/core', 'marked', 'nanoid', 'vuedraggable'],\n  },\n})\n"
  },
  {
    "path": "vite.dev.config.js",
    "content": "import fs from 'fs'\nimport { resolve } from 'path'\nimport { defineConfig } from 'vite'\nimport { ViteEjsPlugin } from './vite-plugin-ejs-v7.js'\nimport vue from '@vitejs/plugin-vue'\nimport mkcert from 'vite-plugin-mkcert'\n\n// 静态资源路径\nconst assetsSrcPath = 'src_assets/common/assets/web'\n// 读取开发环境模板头文件\nconst header = fs.readFileSync(resolve(assetsSrcPath, 'template_header_dev.html'))\n\n// 支持无.html后缀访问的中间件\nfunction htmlExtensionMiddleware(htmlFiles) {\n  return (req, res, next) => {\n    if (req.method !== 'GET') return next()\n    const url = req.url.split('?')[0]\n    if (url.endsWith('/') || url.includes('.')) return next()\n    const page = url.replace(/^\\//, '')\n    if (htmlFiles.includes(page)) {\n      res.writeHead(302, { Location: `${url}.html` })\n      res.end()\n      return\n    }\n    next()\n  }\n}\n\n// 需要支持的html页面\nconst htmlPages = ['apps', 'config', 'index', 'password', 'pin', 'troubleshooting', 'welcome']\n\n// 代理配置复用函数\nfunction createProxyLogger(prefix, target, rewritePath) {\n  return {\n    target,\n    changeOrigin: true,\n    secure: true,\n    rewrite: (path) => path.replace(rewritePath, ''),\n    configure(proxy) {\n      proxy.on('proxyReq', (proxyReq, req) => {\n        console.log(`${prefix}请求:`, req.method, req.url, '-> ' + target + req.url.replace(rewritePath, ''))\n      })\n      proxy.on('proxyRes', (proxyRes, req) => {\n        console.log(`✅ ${prefix}响应:`, req.url, '状态码:', proxyRes.statusCode)\n      })\n    },\n  }\n}\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      vue: 'vue/dist/vue.esm-bundler.js',\n      '@fortawesome/fontawesome-free': resolve('node_modules/@fortawesome/fontawesome-free'),\n      bootstrap: resolve('node_modules/bootstrap'),\n    },\n  },\n  plugins: [\n    vue(),\n    mkcert(),\n    ViteEjsPlugin({\n      header,\n      sunshineVersion: {\n        version: '0.21.0-dev',\n        release: 'development',\n        commit: 'dev-build',\n      },\n    }),\n    {\n      name: 'html-extension-middleware',\n      configureServer(server) {\n        server.middlewares.use(htmlExtensionMiddleware(htmlPages))\n      },\n    },\n  ],\n  root: resolve(assetsSrcPath),\n  server: {\n    https: true,\n    port: 3000,\n    host: '0.0.0.0',\n    open: true,\n    cors: true,\n    // HMR 配置：确保 WebSocket 直接连接到 Vite 服务器，而不是通过代理\n    hmr: {\n      protocol: 'wss',\n      host: 'localhost',\n      port: 3000,\n    },\n    proxy: {\n      '/steam-api': createProxyLogger('🎮 Steam API', 'https://api.steampowered.com', /^\\/steam-api/),\n      '/steam-store': createProxyLogger('🛒 Steam Store', 'https://store.steampowered.com', /^\\/steam-store/),\n      '/boxart': {\n        target: 'https://localhost:47990',\n        changeOrigin: true,\n        secure: false,\n        configure(proxy) {\n          proxy.on('error', (err, req, res) => {\n            console.log('❌ Boxart 代理错误:', err.message)\n            if (!res.headersSent) {\n              res.writeHead(500, { 'Content-Type': 'text/plain' })\n            }\n            res.end('Boxart proxy error: ' + err.message)\n          })\n          proxy.on('proxyReq', (proxyReq, req) => {\n            console.log('🖼️  Boxart 请求:', req.method, req.url, '-> https://localhost:47990' + req.url)\n          })\n          proxy.on('proxyRes', (proxyRes, req) => {\n            console.log('✅ Boxart 响应:', req.url, '状态码:', proxyRes.statusCode)\n            // 清理可能有问题的响应头\n            delete proxyRes.headers['content-encoding']\n          })\n        },\n      },\n      '/api': {\n        target: 'https://localhost:47990',\n        changeOrigin: true,\n        secure: false,\n        configure(proxy) {\n          proxy.on('error', (err, req, res) => {\n            console.log('API proxy error:', err.message)\n            // 如果响应头已发送，不能再次发送\n            if (res.headersSent) {\n              return\n            }\n\n            const mockResponses = {\n              '/api/config': {\n                platform: 'windows',\n                version: '0.21.0-dev',\n                notify_pre_releases: true,\n                locale: 'zh_CN',\n                sunshine_name: 'Sunshine Development Server',\n                min_log_level: 2,\n                port: 47990,\n                upnp: true,\n                enable_ipv6: false,\n                origin_web_ui_allowed: 'pc',\n              },\n              '/api/apps': {\n                apps: [\n                  {\n                    name: 'Steam',\n                    output: 'steam-output',\n                    cmd: 'steam.exe',\n                    'exclude-global-prep-cmd': false,\n                    elevated: false,\n                    'auto-detach': true,\n                    'wait-all': true,\n                    'exit-timeout': 5,\n                    'prep-cmd': [],\n                    'menu-cmd': [],\n                    detached: [],\n                    'image-path': '',\n                    'working-dir': '',\n                  },\n                  {\n                    name: 'Notepad',\n                    output: 'notepad-output',\n                    cmd: 'notepad.exe',\n                    'exclude-global-prep-cmd': false,\n                    elevated: false,\n                    'auto-detach': true,\n                    'wait-all': true,\n                    'exit-timeout': 5,\n                    'prep-cmd': [],\n                    'menu-cmd': [],\n                    detached: [],\n                    'image-path': '',\n                    'working-dir': '',\n                  },\n                ],\n              },\n              '/api/logs':\n                'Sunshine Development Server - Mock Logs\\n[INFO] Server started\\n[INFO] Development mode enabled\\n',\n              '/api/restart': { status: 'ok', message: 'Restart initiated (mock)' },\n            }\n\n            // 处理特殊端点\n            if (req.url === '/api/logs') {\n              const mockData = mockResponses[req.url] || 'No logs available (mock)'\n              res.writeHead(200, { 'Content-Type': 'text/plain' })\n              res.end(typeof mockData === 'string' ? mockData : String(mockData))\n            } else {\n              const mockData = mockResponses[req.url] || { error: 'Mock endpoint not found' }\n              res.writeHead(200, { 'Content-Type': 'application/json' })\n              res.end(JSON.stringify(mockData))\n            }\n          })\n          proxy.on('proxyReq', (proxyReq, req) => {\n            console.log('🔗 代理请求:', req.method, req.url, '-> https://localhost:47990' + req.url)\n          })\n          proxy.on('proxyRes', (proxyRes, req) => {\n            console.log('✅ 代理响应:', req.url, '状态码:', proxyRes.statusCode)\n          })\n        },\n      },\n    },\n    fs: {\n      allow: [resolve('node_modules'), resolve(assetsSrcPath), resolve('.')],\n    },\n  },\n  build: {\n    chunkSizeWarningLimit: 1000, // 提高警告阈值到1MB\n    rolldownOptions: {\n      input: htmlPages.reduce((acc, name) => {\n        acc[name] = resolve(assetsSrcPath, `${name}.html`)\n        return acc\n      }, {}),\n      output: {\n        advancedChunks: {\n          groups: [\n            // 将Vue相关库分离到单独的chunk\n            { name: 'vue-vendor', test: /[\\\\/]node_modules[\\\\/](vue|vue-i18n)[\\\\/]/ },\n            // 将Bootstrap和FontAwesome分离\n            { name: 'ui-vendor', test: /[\\\\/]node_modules[\\\\/](bootstrap|@fortawesome|@popperjs)[\\\\/]/ },\n            // 将其他第三方库分离\n            { name: 'utils-vendor', test: /[\\\\/]node_modules[\\\\/](marked|nanoid|vuedraggable)[\\\\/]/ },\n          ],\n        },\n        // 优化chunk命名\n        chunkFileNames: (chunkInfo) => {\n          const facadeModuleId = chunkInfo.facadeModuleId\n          if (facadeModuleId) {\n            const fileName = facadeModuleId\n              .split('/')\n              .pop()\n              .replace(/\\.[^/.]+$/, '')\n            return `assets/${fileName}-[hash].js`\n          }\n          return 'assets/[name]-[hash].js'\n        },\n        // 优化资源文件命名\n        assetFileNames: (assetInfo) => {\n          const info = assetInfo.name.split('.')\n          const ext = info[info.length - 1]\n          if (/\\.(css)$/.test(assetInfo.name)) {\n            return `assets/[name]-[hash].${ext}`\n          }\n          if (/\\.(woff2?|eot|ttf|otf)$/.test(assetInfo.name)) {\n            return `assets/fonts/[name]-[hash].${ext}`\n          }\n          if (/\\.(png|jpe?g|gif|svg|webp|avif)$/.test(assetInfo.name)) {\n            return `assets/images/[name]-[hash].${ext}`\n          }\n          return `assets/[name]-[hash].${ext}`\n        },\n      },\n    },\n  },\n  define: {\n    __DEV__: true,\n    __PROD__: false,\n    __SUNSHINE_VERSION__: JSON.stringify({\n      version: '0.21.0-dev',\n      release: 'development',\n      commit: 'dev-build',\n    }),\n  },\n  css: {\n    devSourcemap: true,\n  },\n})\n"
  }
]